DEV Community

Dexterlung
Dexterlung

Posted on • Originally published at coffeeshooters.com

Defensive Trace Lock, engineering edition — 5 artifacts in detail

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.

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.


The 5 artifacts at a glance

# 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.


Artifact 1: Registry markdown format

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
Enter fullscreen mode Exit fullscreen mode

Why fields are sliced this way

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."

Design tradeoff: why not YAML or JSON

I considered YAML but rejected it. Reasons:

  1. Markdown links and code references are built-in. YAML needs an extra schema to express "this is a file path."
  2. Human maintenance cost. YAML's strict indentation is unfriendly for small edits. Markdown can wrap and annotate freely.
  3. GitHub and VS Code rendering. Markdown shows links inline; YAML shows raw text.

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.


Artifact 2: Trace test 5-section structure

The T-001 trace test contains 5 describe blocks. Each section pins one semantic facet of the trace:

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 */ })
Enter fullscreen mode Exit fullscreen mode

What each section pins

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.

Why importing the anchor is mandatory

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.


Artifacts 3 & 4: governance rule code walkthrough

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:

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
}
Enter fullscreen mode Exit fullscreen mode

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).

Rule A: every trace has a corresponding test file

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`,
      })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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).

Rule B: trace test imports its declared anchor

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`,
      })
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.


Artifact 5: the AI reminder skill

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."
---
Enter fullscreen mode Exit fullscreen mode

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.).

What each of the 5 steps enforces

  1. Confirm which trace the file belongs to (grep the registry)
  2. List the trace's full chain to the user (even if AI already knows, the user might not remember)
  3. Run the trace test before editing (record the baseline)
  4. Re-run the trace test after editing (confirm nothing broke)
  5. Update the Last edited field in the registry

Step 2 matters. AI should not assume the user remembers trace details. Listing the chain every time forces user and AI to sync mental models.


Engineering considerations

Parser tolerance

Regex parsing has 3 common failure modes:

  1. Markdown links and plain text coexist. Both **Anchor SSOT**: [\path](url) and **Anchor SSOT**: path need to be recognized. My regex uses \[? / \]? to accept either.
  2. Annotations after field values. Field names can be followed by emoji or markers. /\*\*Type\*\*:\s* plus a ? accepts cases like **Type**: data-flow ⚠️.
  3. Empty fields. When a trace omits a field, don't throw. Return null and let the governance rule emit "please fill this field" with a clear message for the user.

Cross-platform paths

The Node.js script runs on Windows, Mac, and Linux:

const repoRoot = path.dirname(fileURLToPath(import.meta.url)).replace(/scripts$/, '')
const registryPath = path.join(repoRoot, '文檔/data-source-registry.md')
Enter fullscreen mode Exit fullscreen mode

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).

CI integration

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
Enter fullscreen mode Exit fullscreen mode

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.

Incremental cost

Each new trace (not the first one, but a new one added with the framework already in place):

  • Write the registry block: ~10 minutes
  • Write the trace test (5 sections): 20-30 minutes
  • Run the two governance rules to verify: 30 seconds
  • Update Last edited: 30 seconds

Total 30-45 minutes per trace. Consistent with the estimate from A1.


Cross-project portability

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.

Detailed cross-project reuse analysis is in C2 Cross-project reuse matrix (next post in the same series branch).


When this doesn't pay off

Lifting these 5 artifacts wholesale isn't worth it when:

  1. Only 1-2 cross-layer relationships exist. Just write them into the commit message or PR description. Building a registry is overengineering.
  2. No markdown-friendly editing workflow. If the team lives in Notion or Confluence, double-maintaining git and wiki is a drag.
  3. CI can't run custom scripts. Without CI enforcement, governance rules get ignored over time.
  4. No AI pair programming. The AI reminder skill is an integration point for Claude Code / Cursor / similar tools. Without one, the skill has nowhere to hook.
  5. Business contracts change very fast. If traces need re-locking weekly, the registry maintenance cost exceeds the benefit.

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.


Related posts


About this post

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:

  • Terms used in this post (5 artifacts / fuse test / AI reminder / Trace Lock / Anchor SSOT / sql-only-trace) are working names I gave them myself, not industry-standard terminology
  • My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context
  • I'm not a software engineer — just a barista who pairs with AI to write code

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

Top comments (0)