{"slug": "defensive-trace-lock-engineering-edition-5-artifacts-in-detail", "title": "Defensive Trace Lock, engineering edition — 5 artifacts in detail", "summary": "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.", "body_md": "**May 2026** · Series \"Trace Lock — Governance notes from pairing with AI to write code\" · Post 6 of 9\n\nThis post is the engineering version of [A1 Defensive Trace Lock](https://./trace-lock-a1-defense-en.md).\n\nA1 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.\n\nWritten 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.\n\nMy 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.\n\n| # | Artifact | Role | File type |\n|---|---|---|---|\n| 1 | Registry | The trace directory, hand-written markdown | `文檔/data-source-registry.md` |\n| 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` |\n| 3 | Governance rule A | Checks every registry trace has a corresponding test file | `scripts/governance-guard.mjs` |\n| 4 | Governance rule B | Checks the trace test actually imports the declared anchor | `scripts/governance-guard.mjs` |\n| 5 | AI reminder skill | When AI touches trace nodes, auto-triggers the 5-step checklist | `.claude/skills/trace-lock-modify/SKILL.md` |\n\nTotal ~600 lines of code (registry parser + 2 governance rules + skill markdown). First setup takes about 4 hours. Each subsequent trace costs 30-45 minutes.\n\nThe 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.\n\nEach trace is a `### T-{id}: title`\n\nblock. Fields use the `**Field Name**: value`\n\nconvention, which makes them easy to grab with regex. Here is a trimmed version of the real T-001:\n\n```\n### T-001: Roasted bean stock grams → POS variant orderable count\n\n- **Type**: data-flow trace\n- **Status**: locked (2026-05-25)\n- **Anchor SSOT**: [`frontend-app/src/lib/business-rules/variantInventory.js`](../path)\n- **Trace test**: [`frontend-app/src/__tests__/traces/T001-variant-inventory.trace.test.js`](../path)\n- **Trace nodes** (write side to display side):\n  1. **DB column**: `roasted_batches.remaining_weight`\n  2. **DB trigger**: `trg_sync_products_stock_virtual_from_batch_iu`\n  3. **DB column**: `products.stock_virtual_grams`\n  4. **Frontend fetch**: useProducts.js fetchProducts\n  5. **SSOT helper**: variantInventory.getVariantAvailableQuantity\n  6. **Entry A (modal)**: ProductVariantModal.variantInventoryLimit\n  7. **Entry B (checkout)**: AdminPOS.cartStockSafety\n- **Related incident**: San Agustin drip-bag-only-1-pack issue\n- **Last edited**: 2026-05-25\n```\n\n**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.\n\n**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.\"\n\n**Entry points** are the multiple entry pathways to the same anchor SSOT. For example, T-001's anchor `variantInventory.js`\n\nis 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?\"\n\n**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.\"\n\nI considered YAML but rejected it. Reasons:\n\nThe cost is that parsing is ~30% trickier than YAML (regex is more error-prone than `yaml.parse`\n\n). I accept the tradeoff because the registry has a practical upper bound of ~30 traces per project, so parsing complexity is also bounded.\n\nThe T-001 trace test contains 5 `describe`\n\nblocks. Each section pins one semantic facet of the trace:\n\n``` js\nimport { describe, expect, it } from 'vitest'\nimport {\n  getVariantWeightPerPack,\n  getVariantAvailableQuantity,\n  resolveCartItemVariantFormat,\n} from '../../lib/business-rules/variantInventory'  // ← import the anchor SSOT\n\ndescribe('T-001 trace: variant weight contract', () => { /* Section 1 */ })\ndescribe('T-001 trace: variant available quantity (incident pinning)', () => { /* Section 2 */ })\ndescribe('T-001 trace: edge cases', () => { /* Section 3 */ })\ndescribe('T-001 trace: cart item variant format resolution', () => { /* Section 4 */ })\ndescribe('T-001 trace: SSOT cross-entry consistency', () => { /* Section 5 */ })\n```\n\n**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.\n\n**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.\n\n**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.\n\n**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`\n\nderives variant format from a cart item object.\n\n**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.\"\n\nNot 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.\n\nArtifact 4 (governance rule B) checks that the test file's first `import`\n\nactually points at the anchor path declared in the registry.\n\nReason: 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.\n\nThis 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.\n\nThe two governance rules live in `scripts/governance-guard.mjs`\n\nand run on every pre-push or CI cycle. The core is `parseTraceRegistry()`\n\n, which extracts markdown into an object array:\n\n``` js\nfunction parseTraceRegistry() {\n  const registryPath = path.join(repoRoot, '文檔/data-source-registry.md')\n  if (!fs.existsSync(registryPath)) return []\n\n  const content = fs.readFileSync(registryPath, 'utf8')\n  // Only parse the content between ## Critical Traces and the next ## heading\n  const sectionMatch = content.match(/##\\s*Critical Traces[\\s\\S]*?(?=\\n##\\s|$)/)\n  if (!sectionMatch) return []\n\n  const section = sectionMatch[0]\n  const traceBlocks = section.split(/\\n###\\s+(?=T-\\d+:)/).slice(1)\n\n  for (const block of traceBlocks) {\n    const idMatch = block.match(/^T-(\\d+):\\s*(.+?)$/m)\n    if (!idMatch) continue\n    // ... extract Anchor SSOT / Trace test / Type fields\n    traces.push({ id, title, anchorPath, testPath, isSqlOnly })\n  }\n  return traces\n}\n```\n\nA few engineering details:\n\n**Use [\\s\\S]*?, not .*?.** JS regex doesn't match newlines with\n\n`.`\n\nby default. Markdown blocks span multiple lines, so `[\\s\\S]`\n\nis required.**Use (?=\\n##\\s|$) as the terminator.** The lookahead ensures parsing stops at the next\n\n`##`\n\nheading or end-of-file. Without it, the parser would scoop up unrelated sections.** .slice(1) skips the prelude.** The first chunk from\n\n`split`\n\nis the content between the `## Critical Traces`\n\nheading and the first `###`\n\n. That's the section preamble, not a trace.**isSqlOnly carveout.** T-021 (FIFO consumption trace) is sql-only. Its test is a `.sql`\n\nfile, not `.js`\n\n. The Type field is marked `sql-only-trace`\n\nand rule B skips the import check (because SQL tests have no import concept).\n\n``` js\nfunction checkTraceRegistryTestCoverage(violations) {\n  const traces = parseTraceRegistry()\n  for (const trace of traces) {\n    if (!trace.testPath) {\n      violations.push({\n        message: `T-${trace.id} registry has no Trace test field`,\n      })\n      continue\n    }\n    const absTestPath = path.join(repoRoot, trace.testPath)\n    if (!fs.existsSync(absTestPath)) {\n      violations.push({\n        message: `T-${trace.id} declared Trace test file does not exist`,\n      })\n    }\n  }\n}\n```\n\nBlocks 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).\n\n``` js\nfunction checkTraceTestImportsAnchor(violations) {\n  for (const trace of traces) {\n    if (!trace.testPath || !trace.anchorPath) continue\n    if (trace.isSqlOnly) continue\n\n    const testSource = fs.readFileSync(absTestPath, 'utf8')\n    const anchorBasename = path.basename(trace.anchorPath, path.extname(trace.anchorPath))\n\n    const importPattern = new RegExp(\n      String.raw`(?:from|require\\s*\\()\\s*['\"\\`][^'\"\\`]*${anchorBasename}(?:\\.[jt]s)?['\"\\`]`,\n      'g',\n    )\n    if (!importPattern.test(testSource)) {\n      violations.push({\n        message: `T-${trace.id} trace test must import its declared Anchor SSOT`,\n      })\n    }\n  }\n}\n```\n\nLoose match on the anchor basename (path-relative-flexible) so any of `import { ... } from '...variantInventory(.js)'`\n\nor `require(...)`\n\npasses. `String.raw`\n\navoids backslash escape hell.\n\nBoth rules combined: roughly 60 lines of code.\n\nThe skill is a markdown file. The frontmatter `description`\n\ndeclares the trigger condition. The body holds the 5-step audit checklist. Claude Code auto-loads it when a task description matches:\n\n```\n---\nname: trace-lock-modify\ndescription: \"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.\"\n---\n```\n\nA few engineering details:\n\n**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).\n\n**List specific file paths.** The description directly lists the three categories (`Anchor SSOT / Trace nodes / Entry points`\n\n). When AI sees a task description that mentions these paths, the match rate is high.\n\n**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.).\n\n`Last edited`\n\nfield 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.\n\nRegex parsing has 3 common failure modes:\n\n`**Anchor SSOT**: [\\`\n\npath`](url)`\n\nand `**Anchor SSOT**: path`\n\nneed to be recognized. My regex uses `\\[?`\n\n/ `\\]?`\n\nto accept either.`/\\*\\*Type\\*\\*:\\s*`\n\nplus a `?`\n\naccepts cases like `**Type**: data-flow ⚠️`\n\n.The Node.js script runs on Windows, Mac, and Linux:\n\n``` js\nconst repoRoot = path.dirname(fileURLToPath(import.meta.url)).replace(/scripts$/, '')\nconst registryPath = path.join(repoRoot, '文檔/data-source-registry.md')\n```\n\nNever hardcode `'/'`\n\nor `'\\\\'`\n\n. Always use `path.join`\n\n. 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`\n\n+ `LC_ALL=C.UTF-8`\n\n).\n\n`scripts/governance-guard.mjs`\n\nis written as a standalone entry. Pre-push hooks and GitHub Actions both run it:\n\n```\n# pre-push hook\nnode scripts/governance-guard.mjs || exit 1\n```\n\nA `violations`\n\narray 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.\n\nEach new trace (not the first one, but a new one added with the framework already in place):\n\n`Last edited`\n\n: 30 secondsTotal 30-45 minutes per trace. Consistent with the estimate from A1.\n\nHow portable each artifact is when copied to a different project:\n\n| Artifact | Portability | Why |\n|---|---|---|\n| Registry markdown format | ★★★★★ | Pure convention, project-independent |\n| Trace test 5-section structure | ★★★★☆ | Vitest / Jest / Mocha all work; only the import syntax changes |\n| Governance rule code | ★★★★☆ | Node.js script, replace the registry path |\n| AI reminder skill format | ★★★★☆ | Claude Code skill format; other AI tools use different syntax |\n| Specific trace contents | ★☆☆☆☆ | 100% business-specific; reinvented per project |\n\nIn 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).\n\nDetailed 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).\n\nLifting these 5 artifacts wholesale **isn't worth it** when:\n\nMy 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.\n\nThis post is an organized record of conversations I had with Claude (an AI pair-programming tool)\n\nduring May 2026. I noticed some patterns worth keeping for my own future reference,\n\nso I asked Claude to help structure them into writing.\n\nA few things I'm **not** claiming:\n\nIf a professional engineer spots misuse, or there's already a more standard name for any of these concepts, **I genuinely welcome corrections**.\n\n*本文原載於我的部落格： Defensive Trace Lock, engineering edition — 5 artifacts in detail*", "url": "https://wpnews.pro/news/defensive-trace-lock-engineering-edition-5-artifacts-in-detail", "canonical_source": "https://dev.to/dexterlung/defensive-trace-lock-engineering-edition-5-artifacts-in-detail-4bie", "published_at": "2026-07-28 15:24:33+00:00", "updated_at": "2026-07-28 15:35:00.642441+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-safety"], "entities": ["Vue 3", "Vite", "Vitest", "Supabase", "PostgreSQL", "Node.js"], "alternates": {"html": "https://wpnews.pro/news/defensive-trace-lock-engineering-edition-5-artifacts-in-detail", "markdown": "https://wpnews.pro/news/defensive-trace-lock-engineering-edition-5-artifacts-in-detail.md", "text": "https://wpnews.pro/news/defensive-trace-lock-engineering-edition-5-artifacts-in-detail.txt", "jsonld": "https://wpnews.pro/news/defensive-trace-lock-engineering-edition-5-artifacts-in-detail.jsonld"}}