Close Menu
ToolTechBlogToolTechBlog

    Subscribe to Updates

    Get the latest creative news from FooBar about art, design and business.

    What's Hot

    Apple adds more Siri AI tools in its 27.2 system updates, out today

    October 26, 2026

    Uh oh, it sounds like the iPhone Duo keeps cracking and users aren’t happy about it

    October 26, 2026

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026
    Facebook X (Twitter) Instagram
    ToolTechBlogToolTechBlog
    • Home
    • AI Tools
    • Web Hosting
    • Tech
    • Digital Marketing
    • Business Software
    • VPN & Cybersecurity
    ToolTechBlogToolTechBlog
    Home»Web Hosting»Invariants via Types & CI
    Web Hosting

    Invariants via Types & CI

    Tool Tech TeamBy Tool Tech TeamOctober 21, 2026No Comments14 Mins Read
    Facebook Twitter Pinterest Telegram LinkedIn Tumblr WhatsApp Email
    Invariants via Types & CI
    Share
    Facebook Twitter LinkedIn Pinterest Telegram Email

    Codebase Design for AI Agents: Types, Lint Rules, and CI Gates

    SitePoint Team

    SitePoint TeamPublished inAI·Programming·JavaScript·
    October 20, 2026

    The AI briefing for Developers

    Stay up to date with AI tools, model releases, and developer workflows that matter.

    Weekly. Free. One click to leave.

    SitePoint Premium
    Stay Relevant and Grow Your Career in Tech

    • Premium Results
    • Publish articles on SitePoint
    • Daily curated jobs
    • Learning Paths
    • Discounts to dev tools

    7 Day Free Trial. Cancel Anytime.

    AI coding agents generate production code, shifting the developer’s role from writer to reviewer. That shift exposes a specific failure mode: the constraints a codebase enforces at compile time and CI time determine agent output quality, not natural-language prompt quality.

    How to Structure a TypeScript Codebase for AI Coding Agents

    1. Define branded types for every domain identifier so the compiler rejects raw primitives at generation time.
    2. Create constructor functions with runtime validation as the sole way to instantiate branded values.
    3. Write custom AST-based ESLint rules to enforce architectural boundaries like service-layer imports.
    4. Add RuleTester fixtures so agents see concrete valid and invalid examples in their context window.
    5. Configure a GitHub Actions CI workflow that runs type-check, lint, and tests in fail-early sequential order.
    6. Enable branch protection requiring all CI checks to pass before any PR merges to main.
    7. Place an AGENTS.md file at the repository root documenting conventions, boundaries, and type constructors.
    8. Review AGENTS.md each sprint to keep guidance aligned with new types, rules, and boundaries.

    Table of Contents

    • Why Your Codebase Architecture Is Now an AI Prompt
    • Prerequisites
    • The Guardrail Hierarchy: Where to Invest for Agent-Friendly Codebases
    • Layer 1: Enforce Domain Invariants with TypeScript Branded Types
    • Layer 2: Catch Convention Violations with a Custom ESLint Rule
    • Layer 3: Gate Merges with a GitHub Actions CI Workflow
    • Layer 4: Guide Agent Behaviour with AGENTS.md
    • Putting It All Together: The Runnable Scaffold Repository
    • Checklist: Is Your Codebase Agent-Ready?
    • Make the Right Path the Only Path

    Why Your Codebase Architecture Is Now an AI Prompt

    AI coding agents — Copilot, Cursor, Codex, Devin, and their successors — are now standard tooling in many engineering teams. They generate production code, shifting the developer’s role from writer to reviewer. That shift exposes a specific failure mode: natural-language prompt quality does not primarily determine agent output quality. The constraints a codebase enforces at compile time and CI time determine it.

    Engineers who run AI coding agents in production, including Lauren Tan (@poteto), have argued publicly that traditional codebase hygiene is the highest-leverage investment for agent correctness. The core argument: making the correct path the easiest one, through types, invariants, and architectural boundaries, beats prompt engineering or post-hoc code review. When a compiler rejects invalid code before a pull request is even opened, no amount of conversational finesse matters.

    When a compiler rejects invalid code before a pull request is even opened, no amount of conversational finesse matters.

    This tutorial builds a minimal, runnable repository scaffold that demonstrates four complementary guardrails: branded types that enforce domain invariants at compile time, a custom ESLint rule that catches convention violations through static analysis, a GitHub Actions CI gate that prevents non-conforming code from merging, and an AGENTS.md guidance file that provides soft contextual nudges agents can read. These layers enforce constraints at four different points between code generation and merge, and each layer reinforces the others.

    Prerequisites

    Before following this tutorial, ensure you have the following installed locally:

    Node.js 20 or later is required. The project assumes TypeScript 4.5+ (the branded type pattern uses unique symbol, available since TS 2.7, but modern TS is expected). You will also need ESLint v9, since this tutorial uses the flat config API and context.filename, both v9 features. Install @typescript-eslint/parser for the RuleTester examples, and have a test runner available — Vitest, Jest, or the built-in Node.js test runner all work.

    Install the core dev dependencies:

    npminstall --save-dev typescript eslint @typescript-eslint/parser

    The Guardrail Hierarchy: Where to Invest for Agent-Friendly Codebases

    Why Agents Default to the Loosest Valid Pattern

    AI coding agents generate code that satisfies the immediate request. They lack whole-codebase awareness of unwritten conventions, team norms, or architectural decisions that exist only in developers’ heads. Without machine-enforceable constraints, an agent defaults to the loosest valid pattern. Asked to fetch a user, it will accept a raw string parameter rather than seeking out a branded UserId type. It will import the database client directly rather than routing through a service layer. The agent is not being careless. It optimizes for the shortest path to syntactically valid code.

    The Leverage Ranking

    A useful way to organize guardrails is as a hierarchy ordered by enforcement strength and feedback speed:

    1. Types and compiler checks. Zero runtime overhead. The editor surfaces type errors before the agent completes its generation pass — forcing correction before any code leaves the editor. This is the highest-leverage layer.
    2. Lint rules and static analysis. Custom ESLint rules catch convention violations the type system cannot express: import boundaries, naming patterns, forbidden API usage. They run fast and produce actionable error messages that point the agent toward the correct pattern.
    3. CI gates and branch protection. A GitHub Actions workflow runs type-checking, linting, and tests on every pull request. Branch protection rules ensure no code — human-authored or agent-generated — reaches main without passing. This is the final automated checkpoint.
    4. Documentation and agent-specific guidance (AGENTS.md). A structured Markdown file at the repository root provides natural-language context that agent frameworks can ingest as system-level instructions, when the framework supports it. The softest layer, but it fills gaps the other three cannot cover.

    Each layer compounds the effectiveness of the others. Types catch the majority of structural errors. Lint rules catch convention drift that types miss. CI gates catch anything that slips past local tooling. Documentation explains the why behind constraints, helping agents make better choices in ambiguous situations.

    Layer 1: Enforce Domain Invariants with TypeScript Branded Types

    What Are Branded Types and Why Agents Need Them

    Consider a common function signature: function getUser(id: string). That parameter accepts any string. An email address, a raw UUID, an order ID, an empty string. An AI agent generating a call to getUser will happily pass whatever string-shaped value is closest at hand, with no compiler objection.

    Branded types solve this by intersecting a base type with a unique symbol tag, creating a compile-time-only distinction between structurally identical types. A UserId and an OrderId are both strings at runtime, but the compiler treats them as incompatible types. The technique carries zero runtime overhead because the compiler erases the brand during compilation.

    Important: Any direct cast such as "value" as UserId anywhere in the codebase bypasses the brand guarantee entirely. The TypeScript compiler will not prevent this. To enforce constructor-only creation, add an ESLint rule (e.g., @typescript-eslint/consistent-type-assertions) configured to ban as casts to branded types.

    Code: Creating UserId and OrderId Branded Types

    declareconst __brand: unique symbol;typeBrand<T,Bextendsstring>=T&{readonly[__brand]:B};exporttypeUserId= Brand<string,"UserId">;exporttypeOrderId= Brand<string,"OrderId">;exportfunctioncreateUserId(raw:string): UserId {const uuidRegex =/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;if(!uuidRegex.test(raw)){const safe =String(raw).replace(/[x00-x1fx7f]/g,"").slice(0,64);thrownewError(`Invalid UserId format: "${safe}"`);}return raw as UserId;}exportfunctioncreateOrderId(raw:string): OrderId {const orderIdRegex =/^ORD-[A-Z0-9]{8}$/;if(!orderIdRegex.test(raw)){const safe =String(raw).replace(/[x00-x1fx7f]/g,"").slice(0,64);thrownewError(`Invalid OrderId format: "${safe}"`);}return raw as OrderId;}exportfunctiongetUser(id: UserId):{ id: UserId; name:string}{return{ id, name:"Alice"};}

    How This Constrains an AI Agent

    When an agent generates a call to getUser, the TypeScript compiler immediately rejects a raw string argument. The error message in the agent’s context window points to the type mismatch, forcing the agent to locate or create a UserId through the createUserId constructor function, which automatically enforces format validation. The agent does not need to know the convention. The type system prevents the wrong path. Every domain identifier that uses a branded type becomes a self-documenting, self-enforcing contract that agents must satisfy to produce code that compiles.

    The agent does not need to know the convention. The type system prevents the wrong path.

    Layer 2: Catch Convention Violations with a Custom ESLint Rule

    Choosing What to Lint

    Not every team convention belongs in a lint rule. Good candidates share a common trait: they can be detectedtive. Banning direct imports from internal modules, enforcing barrel-file usage, and preventing any in specific directories are all strong fits. Rules that require judgment calls or contextual understanding are poor candidates

    This tutorial focuses on a rule that forbids importing from @/db outside the @/services directory, enforcing a service-layer boundary. Without this rule, an agent scaffolding a new HTTP handler would likely import the database client directly, bypassing the service layer and scattering data-access logic across the codebase.

    Note: This rule requires ESLint v9 with flat config. The context.filename property is the ESLint v9 flat-config API. The legacy context.getFilename() is deprecated in v9.

    Code: Writing the AST-Based ESLint Rule

    importpathfrom"path";const rule ={meta:{type:"problem",docs:{description:"Disallow importing from @/db outside of the src/services directory",},messages:{noDirectDbImport:"Do not import from '@/db' outside of src/services/. "+"Use a service function from @/services instead.",},schema:[],},create(context){return{ImportDeclaration(node){const importSource =typeof node.source.value==="string"? node.source.value:"";if(importSource !=="@/db"&&!importSource.startsWith("@/db/")){return;}const filename = context.filename??"";if(filename ==="<input>"|| filename ===""){return;}const normalized = filename.replace(/\/g,"/");if(!normalized.includes("src/services/")){context.report({node,messageId:"noDirectDbImport",});}},};},};exportdefault rule;

    Note for Windows users: The filename.replace(/\/g, "/") normalization handles Windows backslash separators and mixed-separator strings reliably.

    Code: Validating the Rule with RuleTester

    import{RuleTester}from"eslint";importparserfrom"@typescript-eslint/parser";importrulefrom"../no-direct-db-import.js";const ruleTester =newRuleTester({languageOptions:{parser: parser,},});ruleTester.run("no-direct-db-import", rule,{valid:[{code:`import { findUser } from "@/db";`,filename:"src/services/userService.ts",},{code:`import { client } from "@/db/client";`,filename:"src/services/orderService.ts",},{code:`import { connect } from "@/database";`,filename:"src/handlers/userHandler.ts",},],invalid:[{code:`import { findUser } from "@/db";`,filename:"src/handlers/userHandler.ts",errors:[{messageId:"noDirectDbImport"}],},{code:`import { pool } from "@/db/client";`,filename:"src/handlers/orderHandler.ts",errors:[{messageId:"noDirectDbImport"}],},{code:`import { findUser } from "@/db";`,filename:"src/index.ts",errors:[{messageId:"noDirectDbImport"}],},],});

    When an agent’s context window includes the test file, RuleTester fixtures serve as living documentation: the agent can see concrete examples of what is permitted and what is not. This doubles as both verification and guidance.

    An agent scaffolding a new handler tries to query the database directly by importing from @/db. The lint step fails with: “Use a service function from @/services instead.” That message points the agent toward the correct architectural pattern — immediate, actionable, and unambiguous. The agent does not need to have read a wiki page or remembered a Slack conversation.

    Layer 3: Gate Merges with a GitHub Actions CI Workflow

    Designing the Workflow for Agent-Generated PRs

    Agents frequently push code to feature branches and open pull requests automatically. CI must run before any human review to surface issues cheaply and quickly. The workflow follows a fail-early sequential ordering: type-check first, then lint, then test. If the type-check fails, there is no point running linting or tests on code that does not compile. Because steps in a single job are sequential, any failing step halts the job immediately.

    Code: The GitHub Actions Workflow File

    name: Agent Guardrailson:pull_request:branches:[main]jobs:guardrails:runs-on: ubuntu-latesttimeout-minutes:10steps:-name: Checkout codeuses: actions/checkout@v4-name: Setup Node.jsuses: actions/setup-node@v4with:node-version:20cache: npm-name: Install dependenciesrun: npm ci-name: Type-checkrun: npx tsc --noEmit-name: Lintrun: npx eslint .-name: Testrun: npm test

    Branch Protection as the Final Lock

    The CI workflow alone is not sufficient. Without branch protection, a determined agent or developer can merge a pull request despite failures. In the GitHub repository settings under Branches, enable branch protection for main: require status checks to pass before merging, specifically require the guardrails job from the Agent Guardrails workflow, and disable bypass permissions for bot accounts. This combination ensures that no code reaches the default branch without satisfying every automated check. The workflow becomes a hard gate rather than an advisory signal.

    Note: Some branch protection features (such as restricting bypass permissions) may require a paid GitHub plan for private repositories.

    Layer 4: Guide Agent Behaviour with AGENTS.md

    What Is AGENTS.md and Which Agents Read It

    AGENTS.md is a convention for placing natural-language guidance at the repository root that agent frameworks can ingest as system-level context. Variations of this convention include CLAUDE.md for Claude Code and .cursorrules for Cursor. OpenAI Codex CLI also reads AGENTS.md from the repository root. The agents and tools that currently support reading repository-level guidance files include OpenAI Codex CLI, Claude Code, Cursor, and Windsurf. The agent framework loads the content into the agent’s context window alongside the codebase when supported, making it available during code generation without requiring explicit prompting.

    Code: A Practical AGENTS.md File

    # AGENTS.md — Agent Guidance for agent-guardrails-scaffold## Project OverviewTypeScript Node.js service with strict domain boundaries.All code must pass `tsc --noEmit`, `eslint .`, and `npm test` before merge.## Architecture Boundaries- Database access (`@/db`) is ONLY permitted inside `src/services/`.- Handlers in `src/handlers/` must call service functions, never query the DB directly.- The ESLint rule `no-direct-db-import` enforces this boundary.## Type Conventions- Domain identifiers use branded types defined in `src/types/brand.ts`.- NEVER pass raw strings where a branded type is expected.- Use `createUserId()` and `createOrderId()` constructor functions to create branded values.- These constructors validate format; do not cast strings directly.## Testing Requirements- Every service function must have at least one unit test.- ESLint rule fixtures in `eslint-rules/__tests__/` must cover valid and invalid cases.## PR Checklist- [ ] No `tsc` errors- [ ] No ESLint violations- [ ] All tests pass- [ ] New domain identifiers use branded types- [ ] New data access goes through src/services/

    AGENTS.md Is a Soft Guardrail: Don’t Rely on It Alone

    Agents may ignore, truncate, or misinterpret documentation. For example, Claude Code truncates files that exceed its context window limit, and instructions that work with one model version may produce different behavior after an update. AGENTS.md works best as a complement to compiler and linter enforcement, not a replacement. It fills gaps that hard enforcement cannot cover: explaining why a boundary exists, pointing to specific file paths, and providing checklists that aid both agents and human reviewers.

    A practical maintenance habit: review and update AGENTS.md during sprint retrospectives. As new branded types, lint rules, or architectural boundaries are added, the guidance file must reflect them, or agents will lack the context to follow new conventions.

    Putting It All Together: The Runnable Scaffold Repository

    The complete scaffold follows this directory structure:

    agent-guardrails-scaffold/├── src/│   ├── types/brand.ts│   ├── services/userService.ts│   └── handlers/userHandler.ts├── eslint-rules/no-direct-db-import.js├── eslint-rules/__tests__/no-direct-db-import.test.js├── .github/workflows/agent-guardrails.yml├── AGENTS.md├── tsconfig.json├── eslint.config.mjs└── package.json

    The ESLint flat config file wires the custom rule into the project:

    importnoDirectDbImportfrom"./eslint-rules/no-direct-db-import.js";importtsParserfrom"@typescript-eslint/parser";exportdefault[{ignores:["node_modules/**","dist/**"],},{files:["src/**/*.ts"],languageOptions:{parser: tsParser,ecmaVersion:"latest",sourceType:"module",},plugins:{"custom-rules":{rules:{"no-direct-db-import": noDirectDbImport,},},},rules:{"custom-rules/no-direct-db-import":"error",},},{files:["eslint-rules/**/*.js"],languageOptions:{ecmaVersion:"latest",sourceType:"module",},rules:{"no-undef":"error","no-unused-vars":"warn",},},];

    Your package.json should include at minimum the following scripts and dev dependencies:

    {"scripts":{"test":"node --test 'eslint-rules/__tests__/**/*.test.js'"},"devDependencies":{"typescript":"^5.0.0","eslint":"^9.0.0","@typescript-eslint/parser":"^8.0.0"}}

    Note: The "test" script above uses the built-in Node.js test runner (Node 20+) with an explicit glob pattern to ensure test file discovery works reliably across Node versions. Replace with your preferred test runner (e.g., vitest run or jest) if needed.

    Your tsconfig.json should include path aliases to support the @/ import convention used in the codebase:

    {"compilerOptions":{"strict":true,"noEmit":true,"baseUrl":".","paths":{"@/*":["./src/*"]}},"include":["src"]}

    To run this scaffold locally, create the directory structure above, add each file from the code blocks in this tutorial, install the dev dependencies, and run npm install && npm test to verify all guardrails pass. From there, adapt the branded types and lint rules to match your domain.

    Checklist: Is Your Codebase Agent-Ready?

    • ☐ Domain identifiers use branded types, not raw primitives.
    • ☐ Direct as casts to branded types are banned by a lint rule.
    • ☐ Architectural boundaries are enforced by custom lint rules with RuleTester fixtures.
    • ☐ CI runs type-check, lint, and tests on every PR in fail-early sequential order.
    • ☐ Branch protection requires CI to pass before merge.
    • ☐ An AGENTS.md file documents conventions, boundaries, and type constructors.
    • ☐ AGENTS.md is reviewed and updated at least once per sprint.

    Make the Right Path the Only Path

    Prompt instructions that work on GPT-4o may produce different results on Claude or on the next model update. Natural-language instructions degrade across context windows, model updates, and agent implementations. Structural enforcement through types, lint rules, and CI gates is durable. It works regardless of which agent generates the code, which model version powers it, or whether the human reviewer catches every subtlety.

    Structural enforcement through types, lint rules, and CI gates is durable. It works regardless of which agent generates the code, which model version powers it, or whether the human reviewer catches every subtlety.

    The practical path forward is incremental. Start with one branded type for a critical domain identifier and one lint rule for the most frequently violated architectural boundary. Expand coverage as agent usage grows and new patterns of convention drift emerge. Clone the scaffold, adapt it to your domain, and let the compiler do the reviewing.

    Sharing our passion for building incredible internet things.

    Invariants Types
    Share. Facebook Twitter Pinterest LinkedIn Tumblr Email
    Tool Tech Team
    • Website

    Related Posts

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026

    7 Engineering Patterns for Measuring Traffic From AI Search and Chat Tools

    October 26, 2026

    9 Checks Before Shipping a Product Email: A Workflow for Developers and Designers

    October 25, 2026

    7 Checks Developers Should Make So AI Search Can Read Their Site

    October 25, 2026

    Best AI Recruiting Tools for Agencies and Staffing Firms in 2026

    October 24, 2026

    A Distributed Systems Problem

    October 24, 2026
    Leave A Reply Cancel Reply

    Top posts
    Tech

    Apple adds more Siri AI tools in its 27.2 system updates, out today

    By Tool Tech Team
    Business Software

    Uh oh, it sounds like the iPhone Duo keeps cracking and users aren’t happy about it

    By Tool Tech Team
    Web Hosting

    Best AI Recruiting Agents in 2026: 6 Compared

    By Tool Tech Team
    Editors Picks

    Apple adds more Siri AI tools in its 27.2 system updates, out today

    October 26, 2026

    Uh oh, it sounds like the iPhone Duo keeps cracking and users aren’t happy about it

    October 26, 2026

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026

    Ubersuggest Now Lives Inside Claude. Here’s What It Can Actually Do

    October 26, 2026
    About Us

    Welcome to ToolTechBlog, your trusted source for the latest insights, reviews, and practical guides on AI tools, business software, cybersecurity, web hosting, and consumer technology.
    Our mission is simple: to help individuals, entrepreneurs, freelancers, students, and businesses discover the right digital tools to improve productivity, streamline workflows, and make informed technology decisions.

    Our Picks

    Apple adds more Siri AI tools in its 27.2 system updates, out today

    October 26, 2026

    Uh oh, it sounds like the iPhone Duo keeps cracking and users aren’t happy about it

    October 26, 2026

    Best AI Recruiting Agents in 2026: 6 Compared

    October 26, 2026
    Top Reviews

    The AI Hype Index: Unsexy AI

    July 29, 2026

    What it is and How to Fix it

    July 29, 2026

    LG to Ban Residential Proxies from Smart TV Apps

    July 29, 2026

    © 2026 tooltechblog.com. All rights reserved. Designed by DD.

    • About Us
    • Contact Us
    • Terms and Conditions
    • Privacy Policy
    • Disclaimer

    Type above and press Enter to search. Press Esc to cancel.