cd /news/developer-tools/defensive-trace-lock-engineering-edi… · home topics developer-tools article
[ARTICLE · art-77180] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read10 min views2 publishedJul 28, 2026

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.

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

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:

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

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

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:

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:

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.

Detailed cross-project reuse analysis is in C2 Cross-project reuse matrix (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

── more in #developer-tools 4 stories · sorted by recency
── more on @vue 3 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/defensive-trace-lock…] indexed:0 read:10min 2026-07-28 ·