Defensive Trace Lock, engineering edition — 5 artifacts in detail A developer detailed five artifacts for a 'Defensive Trace Lock' governance pattern that pairs with AI-assisted coding. The artifacts include a hand-written markdown registry, a pinning test, two governance rules, and an AI reminder skill, totaling ~600 lines of code. The pattern is framework-agnostic and relies on filesystem conventions and regex parsing. May 2026 · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 6 of 9 This post is the engineering version of A1 Defensive Trace Lock https://./trace-lock-a1-defense-en.md . A1 covered "why" and "what the 5 artifacts look like at a high level." This post unpacks each artifact: code shape, why it was designed that way, what you can tune. Written for readers who already know what governance rules, pinning tests, and config-as-code are. If you have a non-engineering background, A1 is more suitable. My stack: Vue 3 + Vite + Vitest + Supabase PostgreSQL + Node.js scripts. But the pattern in the 5 artifacts below is independent of the frontend framework. It depends mainly on filesystem conventions and regex parsing. | | Artifact | Role | File type | |---|---|---|---| | 1 | Registry | The trace directory, hand-written markdown | 文檔/data-source-registry.md | | 2 | Trace test fuse test | A pure-function test that pins the trace's current correct behavior | frontend-app/src/ tests /traces/T{id}- .trace.test.js | | 3 | Governance rule A | Checks every registry trace has a corresponding test file | scripts/governance-guard.mjs | | 4 | Governance rule B | Checks the trace test actually imports the declared anchor | scripts/governance-guard.mjs | | 5 | AI reminder skill | When AI touches trace nodes, auto-triggers the 5-step checklist | .claude/skills/trace-lock-modify/SKILL.md | Total ~600 lines of code registry parser + 2 governance rules + skill markdown . First setup takes about 4 hours. Each subsequent trace costs 30-45 minutes. The registry is a markdown file. Not YAML, not JSON, not a database. The reason: it needs to be maintainable by humans AND parseable by scripts. Markdown is acceptable in both directions. Each trace is a T-{id}: title block. Fields use the Field Name : value convention, which makes them easy to grab with regex. Here is a trimmed version of the real T-001: T-001: Roasted bean stock grams → POS variant orderable count - Type : data-flow trace - Status : locked 2026-05-25 - Anchor SSOT : frontend-app/src/lib/business-rules/variantInventory.js ../path - Trace test : frontend-app/src/ tests /traces/T001-variant-inventory.trace.test.js ../path - Trace nodes write side to display side : 1. DB column : roasted batches.remaining weight 2. DB trigger : trg sync products stock virtual from batch iu 3. DB column : products.stock virtual grams 4. Frontend fetch : useProducts.js fetchProducts 5. SSOT helper : variantInventory.getVariantAvailableQuantity 6. Entry A modal : ProductVariantModal.variantInventoryLimit 7. Entry B checkout : AdminPOS.cartStockSafety - Related incident : San Agustin drip-bag-only-1-pack issue - Last edited : 2026-05-25 Anchor SSOT is the one trustworthy compute entry for this trace. All Entry points must converge on the anchor. If the anchor changes, the trace contract changes. That counts as a major change. Trace nodes lists every node from write side DB column, trigger to display side UI component , in order. Each node is a candidate file where "touching this might affect trace behavior." Entry points are the multiple entry pathways to the same anchor SSOT. For example, T-001's anchor variantInventory.js is called by both ProductVariantModal when the user picks a variant in POS and AdminPOS the cart safety check before checkout . Entry points form the audit checklist for "is this SSOT actually used everywhere it should be used?" Last edited is for AI to read. After each trace-node edit, the human bumps this field. The next time AI looks, it sees "someone touched this recently, context might have drifted." I considered YAML but rejected it. Reasons: The cost is that parsing is ~30% trickier than YAML regex is more error-prone than yaml.parse . I accept the tradeoff because the registry has a practical upper bound of ~30 traces per project, so parsing complexity is also bounded. The T-001 trace test contains 5 describe blocks. Each section pins one semantic facet of the trace: js import { describe, expect, it } from 'vitest' import { getVariantWeightPerPack, getVariantAvailableQuantity, resolveCartItemVariantFormat, } from '../../lib/business-rules/variantInventory' // ← import the anchor SSOT describe 'T-001 trace: variant weight contract', = { / Section 1 / } describe 'T-001 trace: variant available quantity incident pinning ', = { / Section 2 / } describe 'T-001 trace: edge cases', = { / Section 3 / } describe 'T-001 trace: cart item variant format resolution', = { / Section 4 / } describe 'T-001 trace: SSOT cross-entry consistency', = { / Section 5 / } Section 1, Contract. Declares the "base contract" the trace covers. For T-001, the contract is "each variant format maps to a specific net weight per pack." This section tests pure spec mapping, no state. Section 2, Incident pinning. Writes the customer-event that triggered creating this trace as a test case. For T-001, that's the San Agustin drip-bag incident 244g barrel → 1 half-pound or 2 drip-bag packs . This section is the historical memory: without it, the trace can rot back to broken. Section 3, Edge cases. null, undefined, 0, negative numbers, missing fields. None should throw; all should fall through to a safe fallback. This section exists so future AI edits to the helper don't regress edge cases. Section 4, Reverse resolution. If the trace requires "reversing from an entry point back to a format the anchor understands," this section tests reverse resolution. For T-001, resolveCartItemVariantFormat derives variant format from a cart item object. Section 5, Cross-entry consistency. The same input passed to N entry points must return the same answer. This section tests that "entries actually share one path, not duplicated implementations." Not every trace needs all 5 sections. Minimum is Section 1 + Section 2 contract + incident . Sections 3-5 are added based on the trace's shape. Artifact 4 governance rule B checks that the test file's first import actually points at the anchor path declared in the registry. Reason: if the trace test doesn't import the anchor, what it's testing is " a copy-pasted logic snippet inside the test file ," not the SSOT. When the anchor later changes, the test can't see it. The trace rots while the test stays green. This check is simple but load-bearing. I had one trace it never made it to the registry during a debugging detour that rotted exactly this way. The test was green, but the actual logic was rewritten by a different PR. The two governance rules live in scripts/governance-guard.mjs and run on every pre-push or CI cycle. The core is parseTraceRegistry , which extracts markdown into an object array: js function parseTraceRegistry { const registryPath = path.join repoRoot, '文檔/data-source-registry.md' if fs.existsSync registryPath return const content = fs.readFileSync registryPath, 'utf8' // Only parse the content between Critical Traces and the next heading const sectionMatch = content.match / \s Critical Traces \s\S ? ?=\n \s|$ / if sectionMatch return const section = sectionMatch 0 const traceBlocks = section.split /\n \s+ ?=T-\d+: / .slice 1 for const block of traceBlocks { const idMatch = block.match /^T- \d+ :\s .+? $/m if idMatch continue // ... extract Anchor SSOT / Trace test / Type fields traces.push { id, title, anchorPath, testPath, isSqlOnly } } return traces } A few engineering details: Use \s\S ?, not . ?. JS regex doesn't match newlines with . by default. Markdown blocks span multiple lines, so \s\S is required. Use ?=\n \s|$ as the terminator. The lookahead ensures parsing stops at the next heading or end-of-file. Without it, the parser would scoop up unrelated sections. .slice 1 skips the prelude. The first chunk from split is the content between the Critical Traces heading and the first . That's the section preamble, not a trace. isSqlOnly carveout. T-021 FIFO consumption trace is sql-only. Its test is a .sql file, not .js . The Type field is marked sql-only-trace and rule B skips the import check because SQL tests have no import concept . js function checkTraceRegistryTestCoverage violations { const traces = parseTraceRegistry for const trace of traces { if trace.testPath { violations.push { message: T-${trace.id} registry has no Trace test field , } continue } const absTestPath = path.join repoRoot, trace.testPath if fs.existsSync absTestPath { violations.push { message: T-${trace.id} declared Trace test file does not exist , } } } } Blocks two failure modes: a a trace declared in registry without a test path field; b test path declared but the file doesn't exist path typo, or not created yet . js function checkTraceTestImportsAnchor violations { for const trace of traces { if trace.testPath || trace.anchorPath continue if trace.isSqlOnly continue const testSource = fs.readFileSync absTestPath, 'utf8' const anchorBasename = path.basename trace.anchorPath, path.extname trace.anchorPath const importPattern = new RegExp String.raw ?:from|require\s \ \s '"\ ^'"\ ${anchorBasename} ?:\. jt s ? '"\ , 'g', if importPattern.test testSource { violations.push { message: T-${trace.id} trace test must import its declared Anchor SSOT , } } } } Loose match on the anchor basename path-relative-flexible so any of import { ... } from '...variantInventory .js ' or require ... passes. String.raw avoids backslash escape hell. Both rules combined: roughly 60 lines of code. The skill is a markdown file. The frontmatter description declares the trigger condition. The body holds the 5-step audit checklist. Claude Code auto-loads it when a task description matches: --- name: trace-lock-modify description: "Use when modifying any file listed in 文檔/data-source-registry.md Critical Traces 的 Anchor SSOT / Trace nodes / Entry points. Cross-layer trace guardrail. AI must list the trace's full chain + run the trace test to pin baseline + re-run after the edit + remind user to bump Last edited." --- A few engineering details: The description must start with "Use when ...". Claude Code uses this phrasing to judge trigger conditions. Other phrasings have measurably lower trigger rates observed across 4 skill rewrites . List specific file paths. The description directly lists the three categories Anchor SSOT / Trace nodes / Entry points . When AI sees a task description that mentions these paths, the match rate is high. The 5-step checklist lives in the body. After triggering, AI actually reads the SKILL.md body. Steps in the body need to be runnable grep command, vitest command, "re-run after edit," etc. . Last edited field in the registryStep 2 matters. AI should not assume the user remembers trace details. Listing the chain every time forces user and AI to sync mental models. Regex parsing has 3 common failure modes: Anchor SSOT : \ path url and Anchor SSOT : path need to be recognized. My regex uses \ ? / \ ? to accept either. /\ \ Type\ \ :\s plus a ? accepts cases like Type : data-flow ⚠️ .The Node.js script runs on Windows, Mac, and Linux: js const repoRoot = path.dirname fileURLToPath import.meta.url .replace /scripts$/, '' const registryPath = path.join repoRoot, '文檔/data-source-registry.md' Never hardcode '/' or '\\' . Always use path.join . CJK folder names work on Windows under UTF-8 Node.js fs handles it automatically , but console output on Windows requires care with cp950 encoding chcp 65001 + LC ALL=C.UTF-8 . scripts/governance-guard.mjs is written as a standalone entry. Pre-push hooks and GitHub Actions both run it: pre-push hook node scripts/governance-guard.mjs || exit 1 A violations array collects all violation messages. At the end, it prints them and exits with code 1. Key point: all trace rules are BLOCKER tier not advisory . They block the push. Reason: traces are by design "relationships that should already be stable." Advisory equals no enforcement. Each new trace not the first one, but a new one added with the framework already in place : Last edited : 30 secondsTotal 30-45 minutes per trace. Consistent with the estimate from A1. How portable each artifact is when copied to a different project: | Artifact | Portability | Why | |---|---|---| | Registry markdown format | ★★★★★ | Pure convention, project-independent | | Trace test 5-section structure | ★★★★☆ | Vitest / Jest / Mocha all work; only the import syntax changes | | Governance rule code | ★★★★☆ | Node.js script, replace the registry path | | AI reminder skill format | ★★★★☆ | Claude Code skill format; other AI tools use different syntax | | Specific trace contents | ★☆☆☆☆ | 100% business-specific; reinvented per project | In other words: the framework layer is ~80% portable; the content layer is 0% portable. This ratio is consistent with the "80% framework portable, 20% content reinvented" estimate from C1 Offense + Defense combined https://./trace-lock-c1-combo-en.md . Detailed cross-project reuse analysis is in C2 Cross-project reuse matrix https://./trace-lock-c2-combo-engineering-en.md next post in the same series branch . Lifting these 5 artifacts wholesale isn't worth it when: My setup solo maintainer, many cross-layer dependencies, relatively stable business contracts, Claude Code pairing happens to hit all the conditions. Your situation needs its own judgment. This post is an organized record of conversations I had with Claude an AI pair-programming tool during May 2026. I noticed some patterns worth keeping for my own future reference, so I asked Claude to help structure them into writing. A few things I'm not claiming: If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, I genuinely welcome corrections . 本文原載於我的部落格: Defensive Trace Lock, engineering edition — 5 artifacts in detail