lity β enforce named exports, reject default exports
- File organization β restrict module paths to
src/billing/
Step 2: Map to a Verification Class
Static analysis and verification tools operate on specific surface types. Rules must align with one of these classes to be mechanically checkable:
- AST-level: Function signatures, type annotations, import/export patterns, forbidden tokens
- Filesystem: Path patterns, file size limits, naming conventions, directory structure
- Regex: Literal strings, log formats, commit message patterns, configuration values
- Tooling: Presence of linters, formatters, package managers, test runners
- Config-file: Contents of
tsconfig.json, .eslintrc, package.json
- Git-history: Branch naming, commit prefixes, merge strategies
- Preference ratios: "Prefer X over Y" with a compliance threshold instead of a binary mandate
Constraints must be binary or threshold-based. They should read like test assertions, not style recommendations.
// Unenforceable
// "Use proper error handling in the billing module."
// Enforceable
// "All async functions in src/billing/ must return Result<T, BillingError> or use explicit try/catch blocks."
// "Forbidden token: `any` within src/billing/ directory."
// "Export pattern: Named exports only. Default exports prohibited."
Step 4: Define Scope and Exceptions
Global rules generate false positives in legacy code, interop layers, and third-party adapters. Scope constraints make rules narrower and more accurate without reducing enforceability.
// Scope-aware constraint
// "No `any` types in src/billing/, except in src/billing/interop/legacy-adapter.ts"
// "Named exports required for all modules outside src/billing/index.ts"
Step 5: Implement a Verification Interface
To operationalize these rules, teams should build a lightweight verification layer that parses instruction files and evaluates them against the codebase. Below is a TypeScript implementation demonstrating how deterministic rules map to checkable surfaces.
interface VerificationSurface {
type: 'ast' | 'regex' | 'filesystem' | 'preference';
pattern: string | RegExp;
scope: string[];
threshold?: number; // For preference ratios
}
interface RuleDefinition {
id: string;
description: string;
surface: VerificationSurface;
severity: 'error' | 'warning';
}
class RuleEngine {
private rules: Map<string, RuleDefinition> = new Map();
register(rule: RuleDefinition): void {
this.rules.set(rule.id, rule);
}
async evaluate(targetPath: string, source: string): Promise<RuleResult[]> {
const results: RuleResult[] = [];
for (const [, rule] of this.rules) {
const inScope = rule.surface.scope.some(s => targetPath.includes(s));
if (!inScope) continue;
const passed = await this.checkSurface(rule.surface, source, targetPath);
results.push({
ruleId: rule.id,
passed,
severity: rule.severity,
message: passed ? 'Compliant' : `Violation: ${rule.description}`
});
}
return results;
}
private async checkSurface(surface: VerificationSurface, source: string, path: string): Promise<boolean> {
switch (surface.type) {
case 'regex':
return surface.pattern instanceof RegExp
? !surface.pattern.test(source)
: !new RegExp(surface.pattern).test(source);
case 'filesystem':
return new RegExp(surface.pattern).test(path);
case 'preference':
const matchCount = (source.match(new RegExp(surface.pattern, 'g')) || []).length;
const totalBlocks = (source.match(/(async\s+\w+|function\s+\w+)/g) || []).length;
const ratio = totalBlocks > 0 ? matchCount / totalBlocks : 1;
return ratio >= (surface.threshold || 0.8);
default:
return true; // AST checks require external parser integration
}
}
}
interface RuleResult {
ruleId: string;
passed: boolean;
severity: 'error' | 'warning';
message: string;
}
Architecture Decisions and Rationale
Why separate scope from pattern? Scoping prevents rule collision in monorepos and legacy directories. It allows teams to enforce strict standards in new code while maintaining backward compatibility in established modules.
Why use preference ratios instead of absolute bans? Real-world codebases contain pragmatic exceptions. A 100% ban on try/catch blocks breaks error boundary implementations. A threshold-based approach (e.g., "β₯80% compliance") captures intent without forcing artificial refactors.
Why abstract verification surfaces? Different tools specialize in different checks. ESLint handles AST, find/grep handle filesystem, and custom scripts handle git history. A unified interface allows teams to swap verification backends without rewriting rule definitions.
Pitfall Guide
1. The Adjective Trap
Explanation: Rules containing subjective qualifiers like "clean," "efficient," "modern," or "careful" lack a verifiable surface. Language models interpret these differently across sessions, leading to inconsistent output.
Fix: Replace adjectives with measurable properties. Instead of "write efficient queries," use "database calls must include explicit column selection; SELECT * is prohibited."
2. Scope Blindness
Explanation: Applying rules globally without path constraints generates false positives in test fixtures, generated code, and third-party adapters. This erodes trust in the verification system.
Fix: Always pair rules with glob patterns or directory constraints. Use except clauses for interop layers. Example: "No console.log in src/, except src/debug/profiling.ts."
3. Context Window Cannibalization
Explanation: Instruction files often contain rules the model already follows natively. These lines consume tokens without adding verification value, reducing the context available for project-specific constraints.
Fix: Audit instruction files against model baseline behavior. Remove directives that match default training distributions. Keep only rules that diverge from standard conventions or enforce project-specific patterns.
4. The Absolute Ban Fallacy
Explanation: Mandating 100% compliance on complex structural patterns forces artificial code transformations. This increases technical debt and reduces developer adoption of the verification system.
Fix: Implement threshold-based compliance for non-critical patterns. Use preference surfaces with configurable ratios. Reserve absolute bans for security-critical or architecture-breaking violations.
5. Check Class Mismatch
Explanation: Writing filesystem constraints as AST rules, or regex patterns as structural checks, causes verification failures. The parser cannot evaluate the rule against the intended surface.
Fix: Map each rule to its native verification class before implementation. AST for syntax/structure, regex for literals/formatting, filesystem for paths/naming, git for history/branching.
6. Documentation/Rule Conflation
Explanation: Mixing onboarding prose, architectural context, and deterministic constraints in a single file makes parsing impossible. Verification tools cannot distinguish between guidance and enforcement.
Fix: Separate concerns using explicit delimiters or distinct files. Use context.md for architecture and constraints.md for machine-extractable rules. Alternatively, use metadata tags like [ENFORCE] and [CONTEXT].
7. Ignoring Verification Feedback Loops
Explanation: Teams write rules but never integrate them into CI/CD or pre-commit hooks. Rules become static documentation rather than active controls.
Fix: Wire verification engines into pull request checks. Fail builds on error severity violations. Warn on warning severity. Track compliance trends over time to identify drift.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| New microservice | Strict deterministic rules with 100% compliance | No legacy debt; enforces standards from day one | Low (minimal refactoring) |
| Legacy monolith | Scoped rules with preference ratios and exception lists | Avoids breaking existing patterns while guiding new development | Medium (requires careful scoping) |
| Team onboarding | Hybrid context + constraint files | Provides architectural grounding without consuming verification budget | Low (improves agent accuracy) |
| Security-critical paths | Absolute bans with AST-level verification | Zero tolerance for vulnerable patterns; mechanical enforcement required | Low (prevents costly breaches) |
| Experimental features | Regex-based formatting rules only | Allows structural flexibility while maintaining consistency | Low (minimal overhead) |
Configuration Template
# agent-constraints.yaml
version: "1.0"
verification_engine: "static"
rules:
- id: "TS-NO-ANY-BILLING"
description: "Prohibit implicit any types in billing module"
surface:
type: "ast"
pattern: "TypeAnnotation: AnyKeyword"
scope: ["src/billing/"]
exceptions: ["src/billing/interop/"]
severity: "error"
- id: "FS-KEBAB-NAMING"
description: "Enforce kebab-case for all source files"
surface:
type: "filesystem"
pattern: "^[a-z0-9]+(-[a-z0-9]+)*\\.(ts|tsx|js|jsx)$"
scope: ["src/"]
severity: "warning"
- id: "LOG-CONSOLE-BAN"
description: "Replace console.log with structured logger"
surface:
type: "regex"
pattern: "console\\.(log|debug|info)"
scope: ["src/"]
exceptions: ["src/debug/", "tests/"]
severity: "error"
- id: "COMP-PREFERENCE-FUNCTIONAL"
description: "Prefer functional components over class components"
surface:
type: "preference"
pattern: "class\\s+\\w+\\s+extends\\s+React\\.Component"
scope: ["src/ui/"]
threshold: 0.15 # Max 15% class components allowed
severity: "warning"
- id: "GIT-COMMIT-FORMAT"
description: "Enforce conventional commit prefixes"
surface:
type: "regex"
pattern: "^(feat|fix|chore|docs|refactor|test|ci|build)(\\(.+\\))?:\\s.+"
scope: ["git-history"]
severity: "error"
Quick Start Guide
- Extract existing rules: Open your instruction file and copy all lines that resemble directives. Strip context, examples, and prose.
- Classify verification surfaces: Tag each rule as
ast, regex, filesystem, preference, or tooling. Discard rules that cannot be mapped.
- Apply scope constraints: Add directory paths and exception lists to prevent false positives. Use glob patterns for flexibility.
- Configure verification: Load the rules into a static analysis parser or custom verification engine. Run against your codebase to establish a baseline.
- Wire to CI/CD: Add a pre-commit hook or pull request check that fails on
error severity violations. Monitor compliance trends and adjust thresholds quarterly.
Enforceable instructions are not about restricting agent creativity; they are about engineering predictability. By translating intent into deterministic constraints, teams transform AI coding assistants from experimental tools into reliable production partners. The verification surface is the bridge between human expectation and machine execution. Build it deliberately, and the agent will follow.