<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dexterlung</title>
    <description>The latest articles on DEV Community by Dexterlung (@dexterlung).</description>
    <link>https://dev.to/dexterlung</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4029426%2F0d3f03e5-03d2-46ff-975f-c565c23e82ce.jpg</url>
      <title>DEV Community: Dexterlung</title>
      <link>https://dev.to/dexterlung</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dexterlung"/>
    <language>en</language>
    <item>
      <title>spec-sonar: The Complete Design Record (Part 1) — From One Idea to an Open-Source Toolchain</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Wed, 05 Aug 2026 13:05:13 +0000</pubDate>
      <link>https://dev.to/dexterlung/spec-sonar-the-complete-design-record-part-1-from-one-idea-to-an-open-source-toolchain-4k7h</link>
      <guid>https://dev.to/dexterlung/spec-sonar-the-complete-design-record-part-1-from-one-idea-to-an-open-source-toolchain-4k7h</guid>
      <description>&lt;p&gt;This is one long conversation — about 40+ exchanges — in which I went from a very vague idea to the initial design of an open-source toolchain called spec-sonar. Part 1 ends where I hand 13 review questions to Fable 5.&lt;/p&gt;

&lt;p&gt;Part 2 covers the next 24 hours: the review results, 25 files shipped, and a plot twist I never saw coming — the tool catching conflicts between my own two open-source repos.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Read on: &lt;a href="https://dev.to/content/spec-sonar-design-journal-part-2-en"&gt;Part 2 — Fable's review and a real-world conflict&lt;/a&gt; · &lt;a href="https://dev.to/content/spec-sonar-design-journal-part-1"&gt;繁體中文版&lt;/a&gt; · this tool is listed in my &lt;a href="https://dev.to/content/dev-toolkit"&gt;open-source toolkit index&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. The starting point: an intuition
&lt;/h2&gt;

&lt;p&gt;The first thing I typed into Claude was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I'm wondering whether I can use the standard body of software-engineering knowledge, Claude Code's round-based questioning, and a well-designed skill to help people new to AI-assisted development turn ideas into reality — confirm the safety boundaries, set design limits to prevent scope creep, settle the architecture, and finally produce a series of execution playbooks or goals…"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Several key intuitions were already in that sentence:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The concept of &lt;strong&gt;dark zones and bright zones&lt;/strong&gt; — use set difference to find the questions worth asking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convergence, not generation&lt;/strong&gt; — the goal is fewer rework cycles, not faster output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity loaded on demand&lt;/strong&gt; — I decide how deep to go, instead of getting everything at full depth.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. First turning point: interrogate before designing
&lt;/h2&gt;

&lt;p&gt;I forced myself to answer one question first: &lt;strong&gt;is this reinventing the wheel?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What the search turned up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requirements-management tools for PMs/BAs exist, but they assume you already understand User Stories.&lt;/li&gt;
&lt;li&gt;Spec-driven tools for developers exist, but they assume a spec draft already exists.&lt;/li&gt;
&lt;li&gt;Claude Code's CLAUDE.md ecosystem exists, but nobody generates specs &lt;em&gt;for non-technical users&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion: not a reinvented wheel — but the differentiation has to be explicit.&lt;/strong&gt; The real difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Existing tools' route: make the AI smarter so it tolerates vague input
spec-sonar's route:    make the user's input itself precise
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latter is sturdier, because it doesn't depend on a model's tolerance for ambiguity — and that tolerance is exactly where hallucination comes from.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Second turning point: reframing the value proposition
&lt;/h2&gt;

&lt;p&gt;I first wrote the value proposition as "reduce design rework." &lt;strong&gt;That was wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The problem: a non-technical user can't perceive that value — they don't know how many detours they would otherwise have taken, so they can't feel the detours they avoided.&lt;/p&gt;

&lt;p&gt;The corrected value proposition: &lt;strong&gt;"reduce the anxiety of landing something vague."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's trust, not efficiency. What the user actually feels is: &lt;em&gt;"I told it what I wanted, it told me the problems I hadn't thought of, and what got built matches what I had in mind."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This reframe shaped every later decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The tone of the Q&amp;amp;A — assistance, not interrogation.&lt;/li&gt;
&lt;li&gt;How infeasibility is handled — not rejection, but shrinking to a feasible version.&lt;/li&gt;
&lt;li&gt;Showing bright/dark-zone progress every round — the core trust-building mechanism.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Core system-design decisions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 A behavioral contract, not roleplay
&lt;/h3&gt;

&lt;p&gt;My first idea was to inject a "you are a senior architect" persona prompt.&lt;/p&gt;

&lt;p&gt;The problem: &lt;strong&gt;an identity is not a set of behavior rules.&lt;/strong&gt; Claude plays the architect but doesn't know what this architect must do, must not do, or when to stop.&lt;/p&gt;

&lt;p&gt;So I switched to injecting a &lt;strong&gt;behavioral contract&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inject a behavioral contract, not an identity:
"Your job is to converge a software product idea into an executable spec.
 You are not a roleplaying architect.
 You are a system with fixed rules of behavior."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This made the system's behavior predictable, testable, and iterable.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 STATE: externalizing conversational memory
&lt;/h3&gt;

&lt;p&gt;The problem: there is no persistent memory between rounds; by round 4 the model has forgotten what round 1 established.&lt;/p&gt;

&lt;p&gt;My fix was to &lt;strong&gt;externalize the conversation memory&lt;/strong&gt; into a portable JSON block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;STATE&amp;gt;&lt;/span&gt;
{
  "round": 2,
  "bright": ["established items"],
  "dark": ["unconfirmed items"],
  "next_focus": "next round's topic"
}
&lt;span class="nt"&gt;&amp;lt;/STATE&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrap it in an XML tag — more stable than plain text; Claude parses its own XML output more reliably.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;next_focus&lt;/code&gt; field forces each round onto a single topic, preventing scattershot questioning.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STATE_FINAL.json&lt;/code&gt; is portable — you can leave mid-process and resume later with full state.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4.3 The baseline problem of the set-difference method
&lt;/h3&gt;

&lt;p&gt;Doing gap analysis, I found a hole:&lt;/p&gt;

&lt;p&gt;Set difference only works if I already hold a standard template of "what a complete system looks like" to diff against. No baseline, no difference.&lt;/p&gt;

&lt;p&gt;The solution is a &lt;code&gt;dark-zone-baseline.md&lt;/code&gt;: 10 standard dimensions that seed every new session's dark list — target users, core feature boundary, data model, auth &amp;amp; permissions, third-party integrations, deployment platform, performance &amp;amp; scale, maintenance &amp;amp; updates, budget &amp;amp; timeline, success criteria.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The most important detail: the Session-vs-Persistent fork.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I only discovered this after running the real test case (a math tower-defense game). The "do we need an account system?" decision had been &lt;em&gt;back-derived&lt;/em&gt; from "does the data persist?", rather than asked proactively. The fork has enormous architectural impact, so I promoted it to a mandatory proactive question.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The real test: a math tower-defense game
&lt;/h2&gt;

&lt;p&gt;Why this case: a non-technical user (with an engineer partner) wanted to land "Plants-vs-Zombies tower defense × middle-school math × in-class team battle." I used it to stress-test my tool.&lt;/p&gt;

&lt;p&gt;What ran: the full 5-round convergence, from "I don't know what to build" all the way to a complete README.md + CLAUDE.md + STATE_FINAL.json.&lt;/p&gt;

&lt;p&gt;Key observations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The resource mechanic was the highest-value discussion.&lt;/strong&gt; That user raised the real classroom pain point — "a team-captain system creates conflict between teammates." The final design: &lt;em&gt;personal earn, personal spend, plus per-unit cooldowns&lt;/em&gt; — individual effort converts directly into individual agency, with no middleman to skim.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wheel-reinvention detection confirmed the differentiation.&lt;/strong&gt; Single-player math tower-defense games exist; "whole class online + team battle + teacher console + realtime multiplayer" does not.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The dark zones cleared after 6 rounds — but one was never proactively probed.&lt;/strong&gt; The Session-vs-Persistent fork emerged by inference from the "no accounts" answer, not from the baseline. That bug directly drove the baseline fix.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test case's role: the math game is &lt;strong&gt;not&lt;/strong&gt; spec-sonar's target scenario. It's a deliberately hard &lt;strong&gt;stress test&lt;/strong&gt; — WebSocket realtime sync, a game engine, multiple dependency types, ephemeral-session design. Only that level of complexity genuinely probes the system.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Third turning point: from a tool to a toolchain
&lt;/h2&gt;

&lt;p&gt;The original design was a single requirements-convergence skill (idea-to-spec). It kept growing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;idea-to-spec (requirements convergence)
    ↓ problem found: CLAUDE.md outputs modules, but no dependencies
goal-decomposer (goal decomposition)
    ↓ problem found: only works from zero; can't diagnose existing projects
Audit Mode (existing-project diagnosis)
    ↓ problem found: large complex systems (ERP) have different needs
Complex System Mode
    ↓ problem found: Claude-Code-only is too narrow
Adapters layer (platform-agnostic universal format)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The most consequential evolution: from Claude-specific to platform-agnostic.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Core spec layer (universal)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;goal-graph.json + goals/*.md&lt;/span&gt;
&lt;span class="na"&gt;Adapter layer (per platform)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;adapters/CLAUDE.md               ← Claude Code&lt;/span&gt;
  &lt;span class="s"&gt;adapters/.cursor/rules           ← Cursor&lt;/span&gt;
  &lt;span class="s"&gt;adapters/copilot-instructions.md ← Copilot&lt;/span&gt;
  &lt;span class="s"&gt;adapters/system-prompt.md        ← any AI agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That decision turned spec-sonar from "a Claude Code tool" into "a general requirements-engineering tool for the whole AI-coding ecosystem."&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The philosophy: one expensive deep design → cheap structured execution
&lt;/h2&gt;

&lt;p&gt;The core philosophy of the whole toolchain, surfaced while I was working out how goal-decomposer integrates with goal-workflow-designer.&lt;/p&gt;

&lt;p&gt;The background problem is concrete: &lt;strong&gt;not everyone can afford Fable-tier model costs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My insight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Fable (expensive) thinks deeply once
    → the thinking is frozen into a goal graph and individual goal files
    → any model (Haiku, Sonnet) executes by following the graph
    → Fable-tier reasoning is no longer required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The analogy is compilation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fable = the compiler that does the complex reasoning&lt;/li&gt;
&lt;li&gt;goal-graph.json = the compiled executable&lt;/li&gt;
&lt;li&gt;any model = the CPU that runs it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The smartest (and most expensive) model appears only once, at design time, to settle all the hard decisions; everything after that runs literally on cheap models.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Naming: spec-sonar
&lt;/h2&gt;

&lt;p&gt;I considered SpecForge, DarkMap, ConvergeKit, and ReqSonar. I picked &lt;strong&gt;spec-sonar&lt;/strong&gt; because it passed three tests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Searchability&lt;/strong&gt;: no same-name repo on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanation cost&lt;/strong&gt;: "like sonar, it finds what's submerged in your spec" — one sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Works in both languages&lt;/strong&gt;: "I sonar-scanned the requirements" reads naturally.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logic behind the name: sonar's core mechanism is &lt;em&gt;using waves you can't see to find things you can't see&lt;/em&gt;. spec-sonar's core mechanism is &lt;em&gt;using questions you didn't ask to find requirements you didn't think of&lt;/em&gt;. Form matches content.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Compatibility principles (the last major decisions)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility with existing CLAUDE.md files.&lt;/strong&gt; spec-sonar is a design-time tool; CLAUDE.md is a build-time artifact — naturally on different timelines. So spec-sonar installs non-destructively: it never overwrites your existing CLAUDE.md, it only appends one reference line.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conflict detection across existing skills.&lt;/strong&gt; I upgraded the goal from "avoid conflicts" to &lt;strong&gt;"conflict as a feature"&lt;/strong&gt;: spec-sonar's Conflict Analysis Mode reads all your installed skills and outputs a conflict map. That turns it from "yet another skill" into "a governance tool for your skill ecosystem."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load on demand: three install tiers&lt;/strong&gt; — Lite (1 skill) for small projects/MVPs/anything finishable in a week; Standard (2 skills) for most projects under 3 months; Pro (full suite) for enterprise/ERP/long-term maintenance.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  10. The role of Fable 5
&lt;/h2&gt;

&lt;p&gt;On 2026-06-09 Anthropic released Claude Fable 5 — the first public model of the Mythos family. Its edge: "the longer and more complex the task, the wider the lead," with a particular strength in one-shotting a complete design.&lt;/p&gt;

&lt;p&gt;My prompt strategy wasn't "what do you think of this design?" — it was &lt;strong&gt;"under what conditions does this design break, and fix it."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I asked Fable to do five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Review the CLAUDE.md format from an &lt;em&gt;executor's&lt;/em&gt; perspective ("could I start work from this spec?").&lt;/li&gt;
&lt;li&gt;Find the dark-zone baseline's systematic omissions per product type.&lt;/li&gt;
&lt;li&gt;Design STATE's conflict handling (retracted bright items, contradictions, late additions).&lt;/li&gt;
&lt;li&gt;Design the complete goal-decomposer SKILL.md (dependency inference, model-tier assignment).&lt;/li&gt;
&lt;li&gt;Design the adapter format for every platform.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  11. This design was designed with spec-sonar
&lt;/h2&gt;

&lt;p&gt;The most meta observation worth recording: this very design started from "I have a vague idea" and went through scope judgment, bright/dark separation, set-difference questioning, infeasibility detection (none found), complexity calibration, and wheel-reinvention detection — and that whole flow &lt;strong&gt;is spec-sonar's own flow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tool's first user was the person designing the tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Final output list
&lt;/h2&gt;

&lt;p&gt;By the end of the conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;idea-to-spec-v1.1.zip
├── SKILL.md                      the convergence engine
├── references/
│   ├── dark-zone-baseline.md     10 dimensions (with the Session-vs-Persistent fix)
│   └── output-templates.md       three output format templates
├── examples/
│   ├── README.md                 math tower-defense case (human-readable)
│   ├── CLAUDE.md                 math tower-defense case (executable spec)
│   └── STATE_FINAL.json          final convergence state
└── docs/
    ├── fable-review-prompt.md    the full Fable 5 review prompt (13 questions, 5 jobs)
    └── spec-sonar-README.md      open-source README draft
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pending Fable 5's reply: goal-decomposer/SKILL.md, project-scanner.py, the adapters/ directory (4 platforms), audit-mode, Conflict Analysis Mode, the tiered INSTALL.md.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Every item on that list ships in Part 2 — plus three spec holes, one schema self-audit, and a real-world conflict analysis spanning two open-source repos.)&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Reusable thinking patterns (Part 1)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Interrogate before designing.&lt;/strong&gt; Every new idea first answers "is this a reinvented wheel?" — with search results, not gut feeling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark zones matter more than bright zones.&lt;/strong&gt; Stated requirements are rarely the problem; unstated assumptions are the root of failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral contracts beat roleplay.&lt;/strong&gt; Tell the AI its rules of behavior for this session, not who it is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Externalized state solves context loss.&lt;/strong&gt; Multi-round tools must carry state explicitly every round.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find design gaps with real cases.&lt;/strong&gt; After the theory, run one real case and let the gaps surface (that's how the Session-vs-Persistent fork was found).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expensive thinking once → cheap structured execution.&lt;/strong&gt; The cost-optimal AI strategy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Universal core + per-platform adapters.&lt;/strong&gt; The more universal the core format, the longer the tool lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load on demand.&lt;/strong&gt; Tiered installation lets users choose their complexity instead of paying full cost by default.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Coming in Part 2
&lt;/h2&gt;

&lt;p&gt;Part 1 ends with 13 questions packed into a review prompt and handed to Fable 5. Part 2 records what happened next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fable reviews the test case &lt;em&gt;as an executor&lt;/em&gt; and finds &lt;strong&gt;three day-one landmines&lt;/strong&gt; in a seemingly complete spec.&lt;/li&gt;
&lt;li&gt;The tool audits its own output and catches &lt;strong&gt;its own schema drift&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The full goal-decomposer design: dependency inference, contract freezing, pre-adjudication, the cold-start test.&lt;/li&gt;
&lt;li&gt;25 files shipped as a complete open-source package.&lt;/li&gt;
&lt;li&gt;Then the real plot twist: installed into a production workspace, the Conflict Analysis Mode &lt;strong&gt;finds 5 conflicts between my own two open-source repos&lt;/strong&gt; — and one structural problem nobody had seen.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;→ &lt;a href="https://dev.to/content/spec-sonar-design-journal-part-2-en"&gt;Continue to Part 2&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/spec-sonar-design-journal-part-1-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-spec-sonar-design-journal-part-1-en" rel="noopener noreferrer"&gt;spec-sonar: The Complete Design Record (Part 1) — From One Idea to an Open-Source Toolchain&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Audio Plays on Desktop but Not on iPhone / iPad — The Culprit Is the MP4 moov Atom</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Sun, 02 Aug 2026 13:05:25 +0000</pubDate>
      <link>https://dev.to/dexterlung/audio-plays-on-desktop-but-not-on-iphone-ipad-the-culprit-is-the-mp4-moov-atom-1j01</link>
      <guid>https://dev.to/dexterlung/audio-plays-on-desktop-but-not-on-iphone-ipad-the-culprit-is-the-mp4-moov-atom-1j01</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Series: Engineering gotchas&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I put a batch of podcast audio files (&lt;code&gt;.m4a&lt;/code&gt;) on Cloudflare R2 and embedded them with a plain &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; tag. Desktop Chrome and Firefox played them perfectly. But on &lt;strong&gt;iPhone and iPad, they wouldn't play at all&lt;/strong&gt; — tap the button, nothing happens, no error message, console completely clean.&lt;/p&gt;

&lt;p&gt;The most maddening part: &lt;strong&gt;it throws no error.&lt;/strong&gt; You have no idea where to even start looking.&lt;/p&gt;

&lt;p&gt;This is how I tracked it down to the root cause, and the one-line fix at the end. If you googled "audio plays on desktop but not iphone" or "m4a not playing ios safari" and landed here, skip to section three.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, rule out all the usual suspects
&lt;/h2&gt;

&lt;p&gt;iOS Safari is picky about audio, and the three most common answers online are: wrong MIME type, no HTTP Range support, or bad codec. I checked them one by one — and they were &lt;strong&gt;all correct.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1) Is the Content-Type right?&lt;/span&gt;
curl &lt;span class="nt"&gt;-sI&lt;/span&gt; &lt;span class="s2"&gt;"https://your-domain/xxx.m4a"&lt;/span&gt;
&lt;span class="c"&gt;# → Content-Type: audio/mp4   ✅ correct (m4a is audio/mp4)&lt;/span&gt;
&lt;span class="c"&gt;# → Accept-Ranges: bytes      ✅ Range supported&lt;/span&gt;

&lt;span class="c"&gt;# 2) Does a Range request actually return 206? (iOS strictly requires this)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Range: bytes=0-99"&lt;/span&gt; &lt;span class="s2"&gt;"https://your-domain/xxx.m4a"&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;HTTP
&lt;span class="c"&gt;# → HTTP/1.1 206 Partial Content   ✅ correct&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server side is &lt;strong&gt;entirely correct.&lt;/strong&gt; Content-Type is &lt;code&gt;audio/mp4&lt;/code&gt;, Range is supported, it returns &lt;code&gt;206 Partial Content&lt;/code&gt;. R2 is fine. CORS isn't the issue either (an &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; without the &lt;code&gt;crossorigin&lt;/code&gt; attribute plays cross-origin without needing CORS).&lt;/p&gt;

&lt;p&gt;And the codec is &lt;code&gt;.m4a&lt;/code&gt; (AAC) — the format iOS supports most natively. All three common culprits ruled out. So what is it?&lt;/p&gt;

&lt;h2&gt;
  
  
  The real culprit: the moov atom is at the end of the file (no faststart)
&lt;/h2&gt;

&lt;p&gt;An MP4 / m4a file is made of boxes (also called atoms). Two matter most here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mdat&lt;/code&gt;&lt;/strong&gt;: the actual audio / video data (large — 11 MB in my file)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;moov&lt;/code&gt;&lt;/strong&gt;: the playback index / metadata (timeline, sample tables — the player must read this &lt;em&gt;first&lt;/em&gt; to know how to decode)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the problem: &lt;strong&gt;ffmpeg's default output puts &lt;code&gt;mdat&lt;/code&gt; first and &lt;code&gt;moov&lt;/code&gt; at the very end of the file.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop browsers (Chrome / Firefox)&lt;/strong&gt; are smarter / more aggressive: they fire a Range request for the &lt;strong&gt;end&lt;/strong&gt; of the file to grab &lt;code&gt;moov&lt;/code&gt;, find the index, and play.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS Safari / WebKit&lt;/strong&gt;, with &lt;code&gt;&amp;lt;audio preload="metadata"&amp;gt;&lt;/code&gt;, only fetches the &lt;strong&gt;beginning&lt;/strong&gt; of the file. No &lt;code&gt;moov&lt;/code&gt; at the start → it can't find the index → &lt;strong&gt;it silently gives up and won't play.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why it plays on desktop but not on iPhone / iPad. Both iPhone and iPad use WebKit, so they fail together.&lt;/p&gt;

&lt;p&gt;Moving &lt;code&gt;moov&lt;/code&gt; to the front of the file is called &lt;strong&gt;faststart&lt;/strong&gt; — the standard trick that lets video / audio "play while downloading."&lt;/p&gt;

&lt;h2&gt;
  
  
  How to confirm it (30 seconds)
&lt;/h2&gt;

&lt;p&gt;Grab the first 48 bytes and check whether &lt;code&gt;ftyp&lt;/code&gt; is followed by &lt;code&gt;moov&lt;/code&gt; or &lt;code&gt;mdat&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Range: bytes=0-47"&lt;/span&gt; &lt;span class="s2"&gt;"https://your-domain/xxx.m4a"&lt;/span&gt; | xxd | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Broken (no faststart)&lt;/strong&gt; — &lt;code&gt;mdat&lt;/code&gt; right after &lt;code&gt;ftyp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000: 0000 001c 6674 7970 4d34 4120 ...  ....ftypM4A
00000020: 6672 6565 00a8 3b91 6d64 6174 ...  free..;.mdat   ← mdat first, moov at the end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Good (faststart)&lt;/strong&gt; — &lt;code&gt;moov&lt;/code&gt; right after &lt;code&gt;ftyp&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;00000000: 0000 001c 6674 7970 4d34 4120 ...  ....ftypM4A
00000020: 6d6f 6f76 0000 006c 6d76 6864 ...  moov...lmvhd   ← moov right after ftyp ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;mdat&lt;/code&gt; comes before &lt;code&gt;moov&lt;/code&gt;, that's your bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one line of ffmpeg, lossless, no re-encode
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-i&lt;/span&gt; input.m4a &lt;span class="nt"&gt;-c&lt;/span&gt; copy &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart output.m4a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is &lt;strong&gt;&lt;code&gt;-c copy&lt;/code&gt;&lt;/strong&gt; — it &lt;strong&gt;does not re-encode&lt;/strong&gt;; it just copies the existing stream as-is and relocates &lt;code&gt;moov&lt;/code&gt; to the front. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lossless&lt;/strong&gt;: audio quality is untouched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same size&lt;/strong&gt; (mine was 11,258,702 bytes before and after — not a single byte different)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant&lt;/strong&gt;: 11 MB in under a second&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then &lt;strong&gt;re-upload to overwrite&lt;/strong&gt; the original. If your files are on R2 / S3, keep &lt;code&gt;Content-Type: audio/mp4&lt;/code&gt; on the upload.&lt;/p&gt;

&lt;p&gt;(If a CDN like Cloudflare sits in front, check whether the edge cached the old file. Mine was &lt;code&gt;cf-cache-status: DYNAMIC&lt;/code&gt; — not cached — so the overwrite took effect immediately. The only remaining cache is the phone browser's &lt;strong&gt;local&lt;/strong&gt; cache — test in a private tab for a clean result.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How to stop it from happening again
&lt;/h2&gt;

&lt;p&gt;If you have a "generate audio → upload" pipeline, &lt;strong&gt;always run it through faststart before uploading&lt;/strong&gt; — never push the raw ffmpeg / converter output straight up. I baked it into the upload script as a mandatory step: remux faststart → upload → auto-verify with curl that &lt;code&gt;moov&lt;/code&gt; really is at the front, all three together. New audio never hits this bug again.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This bug is a good reminder: &lt;strong&gt;"works on my machine" and "works on the user's device" are two different things.&lt;/strong&gt; The server config was all correct, the file was there, desktop played fine — every "obvious" place was clean, and the defect hid in the order of the atoms &lt;em&gt;inside&lt;/em&gt; the MP4. Problems that only surface on real devices (especially iOS) slip right past automated tests and desktop development. Next time you hit "fine on desktop, broken on mobile," don't just stare at your code — think &lt;strong&gt;file format / platform differences.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;







&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/m4a-audio-not-playing-on-ios-faststart-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-m4a-audio-not-playing-on-ios-faststart-en" rel="noopener noreferrer"&gt;Audio Plays on Desktop but Not on iPhone / iPad — The Culprit Is the MP4 moov Atom&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Series reading guide — How to pick which of these 9 Trace Lock posts to read</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Fri, 31 Jul 2026 14:34:55 +0000</pubDate>
      <link>https://dev.to/dexterlung/series-reading-guide-how-to-pick-which-of-these-9-trace-lock-posts-to-read-48jm</link>
      <guid>https://dev.to/dexterlung/series-reading-guide-how-to-pick-which-of-these-9-trace-lock-posts-to-read-48jm</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 5 of 9&lt;/p&gt;




&lt;p&gt;This series has 9 posts total. They're an organized record of conversations I had with Claude between late April and end of May 2026.&lt;/p&gt;

&lt;p&gt;After writing the first 4, I re-read them and noticed a problem: &lt;strong&gt;readers won't know where to start&lt;/strong&gt;. The difficulty range goes from beginner to advanced. The content jumps from personal narrative to engineering implementation details. Reading from post 1 to post 9 in order will probably make you give up by post 3.&lt;/p&gt;

&lt;p&gt;So this post is a map, not content. If you only read this one and leave, that's fine with me.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this series is about
&lt;/h2&gt;

&lt;p&gt;In one sentence: &lt;strong&gt;when pairing with AI to write code, cross-layer bugs kept happening, so Claude and I worked out two governance patterns that pair together&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Both patterns are working names I gave them myself (not industry-standard terminology):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Defensive Trace Lock&lt;/strong&gt;: after tripping on a cross-layer bug once, lock down that relationship so it doesn't rot later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offensive audit&lt;/strong&gt;: proactively audit a business flow (e.g. "order to shipment"), find all unprotected chain nodes, fix them in one pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Combined, they form what I call the "dual-blade" approach. The 9 posts spread these 2 patterns across 3 layers: plain-language version, engineering version, and meta reflection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Map of the 9 posts
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Post&lt;/th&gt;
&lt;th&gt;Topic&lt;/th&gt;
&lt;th&gt;Series category&lt;/th&gt;
&lt;th&gt;Difficulty&lt;/th&gt;
&lt;th&gt;Word count&lt;/th&gt;
&lt;th&gt;Who it's for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Meta: how the methodology emerged from conversations with AI&lt;/td&gt;
&lt;td&gt;Cross-field diary&lt;/td&gt;
&lt;td&gt;beginner&lt;/td&gt;
&lt;td&gt;EN 1300 / ZH 1700&lt;/td&gt;
&lt;td&gt;Wants to know how the whole thing came together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;This post&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Series reading guide&lt;/td&gt;
&lt;td&gt;Cross-field diary&lt;/td&gt;
&lt;td&gt;beginner&lt;/td&gt;
&lt;td&gt;EN 700 / ZH 900&lt;/td&gt;
&lt;td&gt;Wants to decide which posts to read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;Defensive Trace Lock plain-language intro&lt;/td&gt;
&lt;td&gt;Indie dev notes&lt;/td&gt;
&lt;td&gt;intermediate&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;Already hit cross-layer bugs, wants to see how to lock them down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B1&lt;/td&gt;
&lt;td&gt;Offensive audit plain-language intro&lt;/td&gt;
&lt;td&gt;Indie dev notes&lt;/td&gt;
&lt;td&gt;intermediate&lt;/td&gt;
&lt;td&gt;EN 1600 / ZH 2100&lt;/td&gt;
&lt;td&gt;Wants to know how to audit a whole business flow in one pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;td&gt;Offense + Defense combined&lt;/td&gt;
&lt;td&gt;Indie dev notes&lt;/td&gt;
&lt;td&gt;intermediate&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;Wants to see how the two pair up + where this doesn't apply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;Defensive engineering version&lt;/td&gt;
&lt;td&gt;Engineer diary&lt;/td&gt;
&lt;td&gt;advanced&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;Wants the 5-artifact implementation details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;Offensive engineering version&lt;/td&gt;
&lt;td&gt;Engineer diary&lt;/td&gt;
&lt;td&gt;advanced&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;Wants the 6-piece fix pattern template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C2&lt;/td&gt;
&lt;td&gt;Cross-project reuse matrix&lt;/td&gt;
&lt;td&gt;Engineer diary&lt;/td&gt;
&lt;td&gt;advanced&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;Wants to port this to their own project, needs to know what transfers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;sql-only-trace&lt;/td&gt;
&lt;td&gt;Engineer diary&lt;/td&gt;
&lt;td&gt;advanced&lt;/td&gt;
&lt;td&gt;EN 2000 / ZH 2500&lt;/td&gt;
&lt;td&gt;How to test pure-DB logic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Three reading paths
&lt;/h2&gt;

&lt;h3&gt;
  
  
  "I only have 30 minutes"
&lt;/h3&gt;

&lt;p&gt;Read &lt;strong&gt;E meta&lt;/strong&gt; (how the whole thing came together) plus this guide. That's enough.&lt;/p&gt;

&lt;p&gt;You don't need to look at the specific patterns. E meta compresses the whole journey into one post. After reading it you'll know "this kind of thing exists, here's the context it fits." That's fine.&lt;/p&gt;

&lt;h3&gt;
  
  
  "I'm not a software engineer but I'm curious about how AI pair programming gets governed"
&lt;/h3&gt;

&lt;p&gt;Order: &lt;strong&gt;E meta → A1 → B1 → C1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Skip the 4 engineering posts. The 3 plain-language posts cover the full thinking, real time costs, and where the approach doesn't apply. After reading you'll have a sense of "is there an analogue to this in my own work." If you're a designer, product manager, or like me a self-taught coder, this path fits best.&lt;/p&gt;

&lt;h3&gt;
  
  
  "I'm an engineer, I want implementation details"
&lt;/h3&gt;

&lt;p&gt;Order: &lt;strong&gt;A1 (shared baseline) → A2 → B2 → C2 → D&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A1 is required reading because the engineering posts assume you know what the 5 artifacts are. A2/B2/C2/D are where the code examples, governance rule templates, and pure-SQL test gotchas live.&lt;/p&gt;

&lt;p&gt;If you only want one technical angle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"How is the protection rule written" → A2&lt;/li&gt;
&lt;li&gt;"How does the cross-layer audit actually run" → B2&lt;/li&gt;
&lt;li&gt;"Can I port this to my project" → C2&lt;/li&gt;
&lt;li&gt;"How do I test pure-DB logic" → D&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Who this series is not for
&lt;/h2&gt;

&lt;p&gt;In C1 I listed 5 scenarios where trace lock doesn't apply. Extending that to "who can skip this whole series":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your project is pure read-only reporting / pure frontend without DB. The whole approach doesn't fit&lt;/li&gt;
&lt;li&gt;You don't pair with AI to write code. The series premise doesn't hold&lt;/li&gt;
&lt;li&gt;You're on a 5+ person team with strong code review culture. Defensive may overlap with human review, value drops&lt;/li&gt;
&lt;li&gt;You're looking for "best practices" or "industry standards." This series doesn't offer those. I name things myself, estimate time myself, list where it doesn't apply myself. No authority claims&lt;/li&gt;
&lt;li&gt;You want to read just one post and leave. Apart from this guide, the others assume context, so cherry-picking only works in the order I suggested above&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Shared vocabulary: the working names
&lt;/h2&gt;

&lt;p&gt;Names that recur across the series (none of them are industry-standard terminology):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;First appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trace Lock&lt;/td&gt;
&lt;td&gt;Locking a cross-layer relationship with 5 artifacts&lt;/td&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offensive / Defensive&lt;/td&gt;
&lt;td&gt;The two audit modes&lt;/td&gt;
&lt;td&gt;A1 / B1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-blade&lt;/td&gt;
&lt;td&gt;The closed loop where both modes pair up&lt;/td&gt;
&lt;td&gt;C1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Decision Pinning (business contract freeze)&lt;/td&gt;
&lt;td&gt;Writing the rule down explicitly before fixing a BLOCKER&lt;/td&gt;
&lt;td&gt;B1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6-piece fix pattern&lt;/td&gt;
&lt;td&gt;The 6 steps for fixing one BLOCKER&lt;/td&gt;
&lt;td&gt;B1 / B2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sql-only-trace&lt;/td&gt;
&lt;td&gt;The trace category for pure-DB logic&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 artifacts&lt;/td&gt;
&lt;td&gt;The 5 things every defensive trace must contain&lt;/td&gt;
&lt;td&gt;A1 / A2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you hit an unfamiliar term in any post, come back here.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I wrote these 9 posts
&lt;/h2&gt;

&lt;p&gt;Not because I think the patterns "need to be known by the industry." Because I tripped on the same shape of bug for half a year, the conversations with Claude when sorting it out got long, and I was afraid of forgetting. The blog version is partly for my own future re-reading, partly so that if a professional engineer sees me misusing something they can correct me.&lt;/p&gt;

&lt;p&gt;There's no "teaching" stance when I was writing. I noticed I'd written 9 posts only after they were done, so I laid them out and put them on the blog.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;E · Meta: how the methodology emerged from conversations with AI (series starting point)&lt;/li&gt;
&lt;li&gt;C1 · Offense + Defense combined (dual-blade overview, read this before deciding whether to go into A2/B2/C2/D)&lt;/li&gt;
&lt;li&gt;中文版&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (Trace Lock / Offensive / Defensive / Dual-blade / Decision Pinning / 6-piece fix pattern / sql-only-trace / 5 artifacts) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer, just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-index-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-index-en" rel="noopener noreferrer"&gt;Series reading guide — How to pick which of these 9 Trace Lock posts to read&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>I built a vertically-integrated commerce system with AI — and accidentally discovered 4 governance patterns</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:05:11 +0000</pubDate>
      <link>https://dev.to/dexterlung/i-built-a-vertically-integrated-commerce-system-with-ai-and-accidentally-discovered-4-governance-hdk</link>
      <guid>https://dev.to/dexterlung/i-built-a-vertically-integrated-commerce-system-with-ai-and-accidentally-discovered-4-governance-hdk</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 1 of 9 (meta)&lt;/p&gt;




&lt;p&gt;In May 2026, Claude (my AI pair-programming tool) and I did something I genuinely didn't expect:&lt;/p&gt;

&lt;p&gt;We slowly extracted 4 methodology patterns (working names — I'll explain why in a moment) from a 6-month loop of "fix a bug → another one appears → fix that one → another one appears."&lt;/p&gt;

&lt;p&gt;I'm a barista. I maintain a self-built, vertically-integrated commerce system by pair-programming with AI. I'm writing this 9-post series for 3 reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I'll forget why I designed things this way in 3 months&lt;/li&gt;
&lt;li&gt;Every new AI session starts from zero — writing patterns as markdown that lives in the project means the next conversation auto-onboards (the AI reads them before we start)&lt;/li&gt;
&lt;li&gt;There should be more and more of us — non-CS-background, pair-with-AI, solo-maintaining a medium-complexity system. If this series saves one person a year of wall-banging, it was worth writing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is the series' meta entry — context, why these patterns surfaced, what the 4 patterns are. The "how to actually do it" goes into the next 8 posts.&lt;/p&gt;




&lt;h2&gt;
  
  
  What my system is
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;For the past 7 years I cobbled together a bunch of off-the-shelf SaaS to run my business (a POS, a member system, a pile of spreadsheets)&lt;/li&gt;
&lt;li&gt;In 2024 I decided to build a vertically-integrated system myself — products / orders / inventory / members / roasting station / packaging station / shipping all wired together&lt;/li&gt;
&lt;li&gt;Stack: Vue 3 frontend, Supabase backend, Claude Code for pair-programming&lt;/li&gt;
&lt;li&gt;Scale: 14,000+ files, 40,000+ code relationships (numbers from the indexing tool)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The pattern I kept noticing
&lt;/h2&gt;

&lt;p&gt;For 6 months I kept hitting the same class of bug:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Data flows from a write-side to a render-side, and somewhere in the middle a layer forgot to sync. Then something breaks."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concrete examples:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Order payment status didn't sync with order status. Customer thinks they paid, backend shows unpaid.&lt;/li&gt;
&lt;li&gt;Stored-value top-up plans got accidentally pushed into the packaging worksheet. The roaster was confused: "How do I package store credit?"&lt;/li&gt;
&lt;li&gt;13 coffee products showed "out of stock" to consumers on the storefront, but the admin backend showed them fine (admin / anon view discrepancy).&lt;/li&gt;
&lt;li&gt;A VVIP customer topped up $5,000 and got the wrong bonus amount.&lt;/li&gt;
&lt;li&gt;After adding a "medium-dark" roast tier, the UI showed the English enum code (no translation).&lt;/li&gt;
&lt;li&gt;A customer ordered a "drip-bag variant of a half-pound product" and could only order 1 pack (the system calculated it as half-pound grams).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each fix took 3-5 hours. Over 6 months that's 5-6 fixes for the same class.&lt;/p&gt;

&lt;p&gt;The painful part isn't the fix itself. It's that a month after fixing it, a sibling bug appears, because the root cause wasn't addressed. A team has QA / SRE / PM / code review to catch these. I don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  The conversation that changed things
&lt;/h2&gt;

&lt;p&gt;On May 25th I hit another one of these. Customer screenshot: "The 'complete roasting' button is frozen, showing 'roast level mismatch'."&lt;/p&gt;

&lt;p&gt;I was about to fix it the usual way: comment out one RAISE statement and call it done. But I asked Claude one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I've fixed 5 of these in 6 months. Should I stop and look for &lt;strong&gt;all leaks of this same shape&lt;/strong&gt;, fix them all at once?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude proposed: &lt;strong&gt;Spend 1 hour auditing the whole business flow first. List which chain nodes are unprotected. Then decide what to fix first.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I spent 1 hour with Claude mapping it out. We found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;11 chain nodes in the business flow&lt;/li&gt;
&lt;li&gt;5 already well-protected&lt;/li&gt;
&lt;li&gt;2 partially protected&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 completely unprotected BLOCKERs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those 4 BLOCKERs included: "order cancel / refund flow," "roasting completion stage," "FIFO ingredient consumption," "packaging station task state." Each one was a "next customer to trip on this costs me 3-5 hours" landmine.&lt;/p&gt;

&lt;p&gt;Then Claude and I spent 7 hours fixing all 4 BLOCKERs at once. Plus building a mechanism that auto-reminds me when I edit related code in the future.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 4 patterns that surfaced during the process
&lt;/h2&gt;

&lt;p&gt;These 4 patterns came to me &lt;strong&gt;gradually during those 7 hours&lt;/strong&gt;. They weren't known in advance. They crystallized in retrospect:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: "Fix a bug" vs "audit a chain" are fundamentally different mental modes
&lt;/h3&gt;

&lt;p&gt;Fix a bug = start from the specific symptom the customer reported, trace outward, fix the root cause.&lt;br&gt;
Audit a chain = walk from the business flow's entry to its exit, listing whether each node is protected.&lt;/p&gt;

&lt;p&gt;For 6 months I'd only been doing the first kind. The problem with the first kind: &lt;strong&gt;it only fixes reported bugs. The unreported ones keep lurking.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second kind looks like more work (1 hour of audit), but the ROI is much higher, because it surfaces &lt;strong&gt;problems that haven't shown up yet&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I gave this pair of mental modes a working name: "&lt;strong&gt;outside-in (looking at the whole chain from outside) vs inside-out (tracing from a specific bug outward) debugging.&lt;/strong&gt;" But this is just my own naming. Formal software engineering probably has more precise terms already, I'm not sure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 2: Decisions made with a customer present are forgotten 3 months later
&lt;/h3&gt;

&lt;p&gt;These 4 BLOCKER fixes all involved a business decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should a refund go back to cash first or bonus first? (I chose cash-first. The original logic deducted bonus first, so a cash-first reversal keeps things balanced.)&lt;/li&gt;
&lt;li&gt;Should "roast level mismatch" block the operator or write a warning and let them continue? (I chose the latter, to avoid the operator station freezing.)&lt;/li&gt;
&lt;li&gt;Should packaging tasks support a "skipped" state? (I chose no, to avoid state-machine explosion.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These decisions feel obvious to me in the moment, but in 3 months me or another engineer (or AI) reading the code might think "this logic is weird, let me clean it up."&lt;/p&gt;

&lt;p&gt;Claude's suggestion: write those decisions as &lt;strong&gt;test cases with incident-pinning comments&lt;/strong&gt; (tests that include a "why" comment). Six months later, when anyone edits the code, the test fails red, forcing them to see the original decision context.&lt;/p&gt;

&lt;p&gt;I gave this practice a working name: "&lt;strong&gt;Decision Pinning.&lt;/strong&gt;" Same thing, just my working name. I don't know if there's a more standard industry term.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 3: Cross-layer dependencies should be managed as assets
&lt;/h3&gt;

&lt;p&gt;Software systems are full of "if I change A, I must also change B" cross-layer dependencies. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Change an RPC return shape → must sync the normalize function's whitelist → must sync the UI render side&lt;/li&gt;
&lt;li&gt;Change an SSOT field → must sync every caller&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Historically these relationships &lt;strong&gt;only existed in my head&lt;/strong&gt;. Three months later, forgetting means "I changed A but forgot B."&lt;/p&gt;

&lt;p&gt;Claude and I designed a thing we call &lt;strong&gt;"Trace Lock"&lt;/strong&gt; (working name again):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Register each cross-layer relationship in a markdown table&lt;/li&gt;
&lt;li&gt;Pair it with a "fuse test" that pins down the currently-correct behavior&lt;/li&gt;
&lt;li&gt;Pair it with two "auto-inspectors" (governance rules) that block regression&lt;/li&gt;
&lt;li&gt;Pair it with an "AI reminder" (Claude Code auto-reminds me when I'm about to edit an affected file)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup is working well so far. Claude and I extended it during this audit with a new sub-category called &lt;code&gt;sql-only-trace&lt;/code&gt; (a testing approach for pure DB logic).&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 4: Offense + Defense as a dual mode
&lt;/h3&gt;

&lt;p&gt;Combining Pattern 1 and Pattern 3 surfaces a larger pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offense mode&lt;/strong&gt; (audit + fix N items at once) = proactively attacking, building new protection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defense mode&lt;/strong&gt; (trace lock + auto trigger) = passively defending, blocking regression&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither alone is enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No offense → protection coverage doesn't expand, bugs keep accumulating&lt;/li&gt;
&lt;li&gt;No defense → existing protection rots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both together&lt;/strong&gt; → the system's "entropy" moves toward decreasing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In physics, entropy (you can think of it as "disorder") increasing monotonically is "natural". To decrease entropy you need to continuously inject energy. Software bug count follows the same logic, I think (this is just how I personally make sense of it. I'm not claiming it's a rigorous analogy).&lt;/p&gt;




&lt;h2&gt;
  
  
  What this series will cover
&lt;/h2&gt;

&lt;p&gt;I plan to write:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A1 Defensive Trace Lock&lt;/strong&gt;: How to lock down a single cross-layer relationship (for future me; readable without a CS degree)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B1 Offensive audit&lt;/strong&gt;: How to periodically audit a whole business flow (for future me; readable without a CS degree)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C1 Offense + Defense combined&lt;/strong&gt;: How the two pair up (for future me)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D sql-only-trace&lt;/strong&gt;: How to test pure DB logic (engineer-facing technical detail)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A2 / B2 / C2 Engineer versions&lt;/strong&gt;: Engineering-grade versions of the 3 above (for engineers who might be curious, with code samples + cross-project reuse matrix)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index post&lt;/strong&gt;: A "which post should you read first" map for readers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only read one, I'd recommend &lt;strong&gt;A1&lt;/strong&gt; (defensive). That's where this whole thing started.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A1 · Defensive Trace Lock (next in series)&lt;/li&gt;
&lt;li&gt;Series index (reading map)&lt;/li&gt;
&lt;li&gt;中文版&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (Decision Pinning / Trace Lock / outside-in vs inside-out / offense vs defense / sql-only-trace) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies,
ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer, just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any&lt;br&gt;
of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-e-meta-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-e-meta-en" rel="noopener noreferrer"&gt;I built a vertically-integrated commerce system with AI — and accidentally discovered 4 governance patterns&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>sql-only-trace Engineering Edition — Testing DB-Side Pure Logic</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Thu, 30 Jul 2026 13:05:14 +0000</pubDate>
      <link>https://dev.to/dexterlung/sql-only-trace-engineering-edition-testing-db-side-pure-logic-f9j</link>
      <guid>https://dev.to/dexterlung/sql-only-trace-engineering-edition-testing-db-side-pure-logic-f9j</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance Notes from AI Pair-Programming" · Post 9 of 9 (series finale)&lt;/p&gt;




&lt;p&gt;This is the final post in the series. It expands the "sql-only-trace variant" briefly mentioned in &lt;a href="//./trace-lock-b2-offense-engineering-en.md"&gt;B2 Offense Engineering Edition&lt;/a&gt; at line 485, and fills in the engineering details that &lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 Combined Engineering Edition&lt;/a&gt;'s cross-database mapping section left out.&lt;/p&gt;

&lt;p&gt;Written for engineers who already know the 11-piece matrix from C2 and the sql-only-trace variant concept from B2 line 485. If you are a non-technical reader, &lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 Combined Offense + Defense (plain version)&lt;/a&gt; is the post you want.&lt;/p&gt;

&lt;p&gt;My environment: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js scripts. The DO block, &lt;code&gt;set_config&lt;/code&gt;, and &lt;code&gt;RAISE EXCEPTION&lt;/code&gt; mechanics discussed below are PostgreSQL-specific. Equivalents for MySQL / SQLite / MongoDB are covered in the "when not to use" section at the end.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Unit Test Frameworks Are Not Enough
&lt;/h2&gt;

&lt;p&gt;In the 11-piece matrix, piece 2 ("Trace test 5-section structure") assumes Vitest / Jest / Pytest or similar application-layer unit test frameworks. But some business logic lives &lt;strong&gt;purely inside the database&lt;/strong&gt;, and application-layer tests cannot reach the core.&lt;/p&gt;

&lt;p&gt;Four scenarios where unit tests fall short:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Why frontend / app-layer tests cannot reach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Complex PL/pgSQL stored procedures&lt;/td&gt;
&lt;td&gt;Logic lives inside &lt;code&gt;DO $$ ... $$&lt;/code&gt; or &lt;code&gt;CREATE FUNCTION&lt;/code&gt;. App layer only sees "input → output" as a black box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FIFO / inventory invariants&lt;/td&gt;
&lt;td&gt;Multi-table interactions (&lt;code&gt;green_beans&lt;/code&gt; + &lt;code&gt;opened_beans&lt;/code&gt; + &lt;code&gt;roasting_input_items&lt;/code&gt;) plus trigger cascades. App layer mocks cannot reproduce them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RLS policy boundaries&lt;/td&gt;
&lt;td&gt;RLS conditions use &lt;code&gt;current_setting('request.jwt.claims')&lt;/code&gt;. You need to simulate JWT to test them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Function overload behavior&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;CREATE OR REPLACE&lt;/code&gt; vs &lt;code&gt;DROP FUNCTION&lt;/code&gt; behave differently. When multiple overloads exist, the app layer cannot predict which one gets called&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My environment hits all four. &lt;code&gt;fn_roaster_consume_beans_fifo&lt;/code&gt; is a FIFO consumption RPC (scenarios 1, 2, 3). &lt;code&gt;fn_issue_order_action_token&lt;/code&gt; had an overload incident (scenario 4; see &lt;a href="//../../CLAUDE.md"&gt;CLAUDE.md Rule 27&lt;/a&gt; for the incident pinning case).&lt;/p&gt;

&lt;p&gt;Testing these RPCs from the Vue component layer only verifies "call succeeded vs failed". It cannot verify "is FIFO ordering correct", "is the inventory invariant maintained", "does a bad JWT return permission denied instead of silent return".&lt;/p&gt;

&lt;p&gt;sql-only-trace replaces piece 2's trace test from &lt;code&gt;.trace.test.js&lt;/code&gt; with &lt;code&gt;.trace.test.sql&lt;/code&gt;, letting the trace test run inside the database and use the database's own assertion mechanism.&lt;/p&gt;




&lt;h2&gt;
  
  
  SQL DO Block Structure Anatomy
&lt;/h2&gt;

&lt;p&gt;Expand the skeleton from B2 line 485-549 into 6 sections plus an EXCEPTION fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- scripts/integration-test/T021-fifo-consume-test.sql&lt;/span&gt;

&lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
&lt;span class="c1"&gt;-- 0. JWT config (let RLS / has_operator_capability pass)&lt;/span&gt;
&lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;set_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'request.jwt.claims'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{"sub":"t021-test-user","system_role":"super_admin","tenant_id":"coffeeshooters"}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jwt_set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;T021&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. Variable declarations (seed IDs, result holders, errors array)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="n"&gt;v_kind_id&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="n"&gt;v_gb1_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'T021-GB1-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;v_ro_id&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'T021-RO-'&lt;/span&gt;  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;v_consume_result&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_gb1_stock_after&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_errors&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[]::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 2. SEED (insert fake data with unique prefix to avoid pollution)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'=== T021 SEED ==='&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coffee_bean_kinds&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tenant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green_beans&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;

  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 3. RUN (call the RPC under test)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'=== T021 RUN ==='&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_consume_result&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_roaster_consume_beans_fifo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'consume_result = %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_consume_result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 4. ASSERTIONS (append failures to v_errors, do not abort)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;stock_grams&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_gb1_stock_after&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green_beans&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_gb1_stock_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;v_errors&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;array_append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'A1 GB1.stock_grams expected 0, got %s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_gb1_stock_after&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;-- ... other assertions&lt;/span&gt;

  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 5. CLEANUP (explicit DELETE, no transaction rollback available)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_input_items&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;roasting_order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green_beans&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coffee_bean_kinds&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="c1"&gt;-- 6. Conclusion (green or red)&lt;/span&gt;
  &lt;span class="c1"&gt;-- ════════════════════════════════════════════════════════════&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'✅ T-021 ALL ASSERTIONS PASSED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'❌ T-021 FAILED — %s errors:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;%'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;  - '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="c1"&gt;-- 7. EXCEPTION fallback (best-effort cleanup + re-raise)&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_input_items&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;roasting_order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green_beans&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coffee_bean_kinds&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;T021&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Final SELECT lets the PASS message show in CLI output&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'T-021 PASSED'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;executed_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seven sections map to a typical unit test as follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SQL section&lt;/th&gt;
&lt;th&gt;Vitest equivalent&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0 JWT config&lt;/td&gt;
&lt;td&gt;&lt;code&gt;beforeEach(setupAuth)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RLS simulation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 Variable declarations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;let v = ...&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;seed IDs + result holders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 SEED&lt;/td&gt;
&lt;td&gt;&lt;code&gt;beforeAll(seed)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No transaction; need prefix to avoid pollution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 RUN&lt;/td&gt;
&lt;td&gt;&lt;code&gt;result = await fn()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Call the RPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 ASSERTIONS&lt;/td&gt;
&lt;td&gt;&lt;code&gt;expect().toBe()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accumulating, non-aborting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 CLEANUP&lt;/td&gt;
&lt;td&gt;&lt;code&gt;afterAll(cleanup)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must use explicit DELETE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 Conclusion&lt;/td&gt;
&lt;td&gt;test runner decides&lt;/td&gt;
&lt;td&gt;RAISE EXCEPTION = red&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 EXCEPTION fallback&lt;/td&gt;
&lt;td&gt;no equivalent&lt;/td&gt;
&lt;td&gt;Unit tests get auto-rollback from transactions; SQL does not&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two most critical sections are &lt;strong&gt;0 JWT config&lt;/strong&gt; and &lt;strong&gt;7 EXCEPTION fallback&lt;/strong&gt;. Without them sql-only-trace cannot run (blocked by RLS) or leaves seed pollution when it crashes (no cleanup).&lt;/p&gt;




&lt;h2&gt;
  
  
  Supabase CLI Gotchas When Running SQL Tests
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npx supabase db query --linked -f&lt;/code&gt; behaves differently from a normal unit test runner in four ways. Each one bit me.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 1 (no transaction)
&lt;/h3&gt;

&lt;p&gt;Supabase CLI treats each statement as an auto-committing independent transaction. You cannot wrap the entire DO block in &lt;code&gt;BEGIN; ... ROLLBACK;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Implication: All SEED data stays in the database. CLEANUP must use explicit DELETE; you cannot rely on rollback.&lt;/p&gt;

&lt;p&gt;Mitigation: Section 5 and section 7 both need DELETE. Best-effort cleanup inside EXCEPTION needs its own copy. The original error is already in flight, so even if cleanup itself fails, swallow that failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 2 (only the last SELECT is returned)
&lt;/h3&gt;

&lt;p&gt;By default, the CLI only prints the rows from the last SELECT. RAISE NOTICE is hidden. SELECT statements in the middle of the DO block are hidden.&lt;/p&gt;

&lt;p&gt;Implication: To show PASS / FAIL, put the conclusion in the last SELECT. On failure, RAISE EXCEPTION carries the error string out (the CLI shows the EXCEPTION message).&lt;/p&gt;

&lt;p&gt;Mitigation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Always end with this&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'T-021 PASSED'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;executed_at&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure, RAISE EXCEPTION aborts before this SELECT runs, so it only shows on PASS. On FAIL, the CLI prints the EXCEPTION message (including the v_errors array string).&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 3 (RAISE NOTICE is hidden)
&lt;/h3&gt;

&lt;p&gt;The CLI defaults &lt;code&gt;client_min_messages&lt;/code&gt; to &lt;code&gt;WARNING&lt;/code&gt;, so &lt;code&gt;NOTICE&lt;/code&gt;-level RAISE NOTICE gets swallowed.&lt;/p&gt;

&lt;p&gt;When debugging, add &lt;code&gt;SET client_min_messages = NOTICE;&lt;/code&gt; at the top of the file to expose RAISE NOTICE for inspecting SEED IDs or RUN intermediate state. Do not leave it on for daily test runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gotcha 4 (no retry, no parallel)
&lt;/h3&gt;

&lt;p&gt;The CLI runs a file sequentially top to bottom with no retry. A network blip or DB connection drop is an immediate fail.&lt;/p&gt;

&lt;p&gt;Mitigation: Write sql-only-trace tests as idempotent (use SEED prefix to ensure multiple runs do not conflict). For CI, run them in both the pre-push hook and the main branch CI (two layers of defense).&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Run Command
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase db query &lt;span class="nt"&gt;--linked&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; scripts/integration-test/T021-fifo-consume-test.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On PASS the CLI prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; status        | assertions | executed_at
---------------+------------+------------------------------
 T-021 PASSED  |         16 | 2026-05-26 14:30:00+00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On FAIL the CLI prints (RAISE EXCEPTION message):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR:  ❌ T-021 FAILED — 2 errors:
  - A1 GB1.stock_grams expected 0, got 600
  - A7 inventory invariant violated: deducted 1800 but RPC asked for 1500
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit code is non-zero, so the pre-push hook blocks the push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extending the Registry for sql-only-trace
&lt;/h2&gt;

&lt;p&gt;Piece 1 (Registry markdown) and pieces 3-4 (Governance rules) originally assume trace tests are &lt;code&gt;.trace.test.js&lt;/code&gt; running on Vitest. Adding sql-only-trace requires the registry parser to recognize the new category.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry Entry Format (with sql-only-trace type)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### T-021: FIFO inventory invariant for roasting consumption&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Type**&lt;/span&gt;: sql-only-trace
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Anchor SSOT**&lt;/span&gt;: &lt;span class="sb"&gt;`supabase/migrations/N_fn_roaster_consume_beans_fifo.sql`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace test (SQL)**&lt;/span&gt;: &lt;span class="sb"&gt;`scripts/integration-test/T021-fifo-consume-test.sql`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Business contract**&lt;/span&gt;: FIFO consumption ordered by created_at ASC within the same kind. opened_beans remainder takes priority. weighted_avg_cost = SUM(grams × cost) / total_grams
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Last edited**&lt;/span&gt;: 2026-05-25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compared to a frontend trace entry, the differences are &lt;code&gt;Type: sql-only-trace&lt;/code&gt; and &lt;code&gt;Trace test (SQL)&lt;/code&gt; instead of &lt;code&gt;Trace test: src/__tests__/traces/T-NN.trace.test.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parser isSqlOnly Detection
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scripts/parse-trace-registry.mjs&lt;/code&gt; adds ~10 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseTraceEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;Type&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isSqlOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sql-only-trace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;isSqlOnly&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;Trace test &lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="sr"&gt;SQL&lt;/span&gt;&lt;span class="se"&gt;\)\*\*&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*`&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;`&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;Trace test&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*`&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;`&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/### &lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;T-&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;isSqlOnly&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;Anchor SSOT&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*`&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;`&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;lastEdited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;Last edited&lt;/span&gt;&lt;span class="se"&gt;\*\*&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(\S&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)?.[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Governance Rule B Skips Import Check
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;checkTraceTestImportsAnchor()&lt;/code&gt; originally enforces that "trace test must &lt;code&gt;import&lt;/code&gt; the anchor SSOT it declares" (catches the rot where a trace test was written but no longer actually references the anchor). SQL has no &lt;code&gt;import&lt;/code&gt; concept, so it needs a carveout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkTraceTestImportsAnchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// sql-only-trace skips import check (SQL has no import concept)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSqlOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchorBasename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;testContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;anchorBasename&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`trace test &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not import anchor &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Governance Rule A Still Applies (trace-registry-test-coverage)
&lt;/h3&gt;

&lt;p&gt;The "every trace in the registry must have a corresponding test file that exists" rule applies regardless of frontend or SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkTraceRegistryTestCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;traceId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; does not exist (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSqlOnly&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SQL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;JS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; test)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The total parser plus governance rule extension is about 15-20 lines, done in the same sprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  T-021 Full Case Study
&lt;/h2&gt;

&lt;p&gt;T-021 is the trace test for Gap-3 ("FIFO inventory invariant for roasting consumption") in the sprint's 4 BLOCKERs. The actual file lives at &lt;code&gt;scripts/integration-test/T021-fifo-consume-test.sql&lt;/code&gt;, 282 lines, 16 assertions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Business Contract
&lt;/h3&gt;

&lt;p&gt;Frozen 2026-05-25 (the day the sprint ended):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Within the same kind, deduct in &lt;code&gt;green_beans.created_at&lt;/code&gt; ASC order (FIFO)&lt;/li&gt;
&lt;li&gt;For each bean batch, deduct from &lt;code&gt;opened_beans&lt;/code&gt; remainder first, then break a fresh pack from &lt;code&gt;stock_grams&lt;/code&gt; into &lt;code&gt;opened_beans&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;After breaking a pack, leftover opened remainder stays in &lt;code&gt;opened_beans&lt;/code&gt; for the next FIFO round&lt;/li&gt;
&lt;li&gt;&lt;code&gt;weighted_avg_cost_per_g = SUM(grams × cost_per_g) / total_grams&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;roasting_orders.is_fifo_resolved = true&lt;/code&gt; with &lt;code&gt;green_bean_id/name&lt;/code&gt; carrying the primary source&lt;/li&gt;
&lt;li&gt;Inventory invariant: (pre-deduction stock+opened total) - (post-deduction stock+opened total) = consumed total_grams&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Six business contract clauses, each backed by 2-3 assertions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Scenario Design
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GB1 (earliest, spec=600g): stock=1200g, cost=400/kg, created_at=-3 days
GB2 (next,     spec=500g): stock=500g,  cost=500/kg, created_at=-2 days
GB3 (latest,   spec=600g): stock=600g,  cost=600/kg, created_at=-1 day

Roasting order consumes 1500g
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected FIFO behavior:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GB1 breaks 2 fresh packs and consumes 1200g (stock=0, opened=0)&lt;/li&gt;
&lt;li&gt;GB2 breaks 1 pack and consumes 300g (stock=0, opened=200 remainder)&lt;/li&gt;
&lt;li&gt;GB3 untouched (v_remaining=0 triggers EXIT WHEN)&lt;/li&gt;
&lt;li&gt;200g remainder stays in &lt;code&gt;opened_beans&lt;/code&gt; for the next FIFO round&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The 16 Assertions
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;GB1.stock_grams&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;GB1.opened.remaining_grams&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;GB2.stock_grams&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A4&lt;/td&gt;
&lt;td&gt;GB2.opened.remaining_grams&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A5&lt;/td&gt;
&lt;td&gt;GB3.stock_grams (untouched)&lt;/td&gt;
&lt;td&gt;600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A6&lt;/td&gt;
&lt;td&gt;GB3.opened.remaining_grams (untouched)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A7&lt;/td&gt;
&lt;td&gt;Inventory invariant&lt;/td&gt;
&lt;td&gt;1500g consumed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A8&lt;/td&gt;
&lt;td&gt;roasting_input_items row count&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A9&lt;/td&gt;
&lt;td&gt;roasting_input_items total grams&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A10&lt;/td&gt;
&lt;td&gt;seq=1 is GB1&lt;/td&gt;
&lt;td&gt;GB1 id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A11&lt;/td&gt;
&lt;td&gt;seq=1 grams&lt;/td&gt;
&lt;td&gt;1200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A12&lt;/td&gt;
&lt;td&gt;seq=2 is GB2&lt;/td&gt;
&lt;td&gt;GB2 id&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A13&lt;/td&gt;
&lt;td&gt;seq=2 grams&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A14&lt;/td&gt;
&lt;td&gt;roasting_orders.is_fifo_resolved&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A15&lt;/td&gt;
&lt;td&gt;roasting_orders.input_grams&lt;/td&gt;
&lt;td&gt;1500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A16&lt;/td&gt;
&lt;td&gt;weighted_avg_cost_per_g&lt;/td&gt;
&lt;td&gt;0.42&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;weighted_avg_cost_per_g&lt;/code&gt; formula: &lt;code&gt;(1200×0.4 + 300×0.5) / 1500 = 0.42&lt;/code&gt;. This one breaks most easily if someone changes the algorithm (for example, switching to simple average).&lt;/p&gt;

&lt;h3&gt;
  
  
  SEED Prefix Anti-Pollution
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;v_gb1_id&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'T021-GB1-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each run has a different prefix, so multiple runs do not collide. Also useful when cleanup fails: you can manually grep &lt;code&gt;WHERE id LIKE 'T021-%'&lt;/code&gt; to wipe leftovers.&lt;/p&gt;

&lt;h3&gt;
  
  
  EXCEPTION Cleanup, Real Value
&lt;/h3&gt;

&lt;p&gt;The first version of T-021 had no EXCEPTION block. When the RPC threw, seed data stayed in the database and polluted subsequent test runs. With EXCEPTION, even if the RPC throws, best-effort cleanup still runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
  &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="k"&gt;BEGIN&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_input_items&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;roasting_order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_orders&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_ro_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opened_beans&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;green_bean_id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_gb2_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_gb3_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;green_beans&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_gb1_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_gb2_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_gb3_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;coffee_bean_kinds&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v_kind_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The inner &lt;code&gt;EXCEPTION WHEN OTHERS THEN NULL&lt;/code&gt; swallows cleanup failures (such as FK conflicts). This ensures the outer &lt;code&gt;RAISE&lt;/code&gt; propagates the original error. Without this layer, cleanup failure would mask the original error and make debugging painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reverse Verification Inside SQL
&lt;/h2&gt;

&lt;p&gt;Piece 9 of the defense matrix ("reverse verification anchor-break flow") also applies to sql-only-trace, but the break point switches from helper logic to RPC logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  5-Step Reverse Verification (sql-only-trace variant)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write trace test, run green&lt;/strong&gt;: T-021 all 16 assertions PASS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deliberately break the RPC&lt;/strong&gt;: change &lt;code&gt;fn_roaster_consume_beans_fifo&lt;/code&gt;'s ORDER BY from &lt;code&gt;created_at ASC&lt;/code&gt; to &lt;code&gt;created_at DESC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run trace test, confirm red&lt;/strong&gt;: A10 (seq=1 should be GB1) will fail because under DESC, seq=1 becomes GB3&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revert to ORDER BY ASC&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run again, confirm green&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Going through all 5 steps is what makes "lock has actual catching power" true. If step 3 does not turn red, your trace test missed a core contract clause. Add the missing assertion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Choosing the Break Point
&lt;/h3&gt;

&lt;p&gt;T-021's break point is ORDER BY ASC vs DESC because that is the &lt;strong&gt;core contract&lt;/strong&gt; of FIFO. Changing something else (such as the &lt;code&gt;gen_random_uuid()&lt;/code&gt; prefix) would not break the trace (that is not part of FIFO behavior).&lt;/p&gt;

&lt;p&gt;Principle for choosing break points: choose a "core business contract logic" point, not an "implementation detail" point. FIFO's core is ORDER BY ordering + opened remainder priority + pack-breaking rule + weighted average formula. Any one of those four broken should turn the trace test red.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-Rot Value
&lt;/h3&gt;

&lt;p&gt;Three months from now, an AI pairing on &lt;code&gt;fn_roaster_consume_beans_fifo&lt;/code&gt; accidentally drops the ORDER BY (perhaps trying to optimize the query). The trace test goes red in CI and blocks the push.&lt;/p&gt;

&lt;p&gt;Without sql-only-trace, this bug only surfaces when a customer reports "why did you use the new batch first and leave the old batch to expire". By then, weeks of bad data may already exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Not to Use
&lt;/h2&gt;

&lt;p&gt;Five situations where porting sql-only-trace is &lt;strong&gt;not necessarily worth it&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pure Frontend / App-Layer Business Logic
&lt;/h3&gt;

&lt;p&gt;If your business logic lives entirely in Vue components / Pinia stores / composables / pure helpers, and the database is just a CRUD store, sql-only-trace has no anchor to attach to.&lt;/p&gt;

&lt;p&gt;My environment has both, so I use both: frontend traces for frontend logic (T-001 / T-002 / T-005), sql-only-trace for DB-pure logic (T-021 / T-022).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Multi-DB / Heterogeneous Data Stores
&lt;/h3&gt;

&lt;p&gt;When business spans PostgreSQL + Redis + Elasticsearch, sql-only-trace only covers the PostgreSQL portion. Redis has no PL/pgSQL, so you would write redis-cli scripts + Lua + redis-py yourself. Elasticsearch uses _search API + curl + jq.&lt;/p&gt;

&lt;p&gt;This is doable, but &lt;strong&gt;cross-store consistency tests&lt;/strong&gt; belong in the application layer (write integration tests from Node / Python that hit all three). sql-only-trace is not enough on its own.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No SQL Test Runner / No CI Hook
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npx supabase db query --linked -f&lt;/code&gt; is the standard runner for sql-only-trace. Other stacks have equivalents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DB&lt;/th&gt;
&lt;th&gt;CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL (native)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;psql -f file.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supabase&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npx supabase db query --linked -f file.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mysql -u user -p db &amp;lt; file.sql&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mongosh &amp;lt; file.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your stack lacks such a CLI, or your CI cannot run it, sql-only-trace has nowhere to attach. The pre-push hook also cannot wire it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross-Postgres-Version Compatibility
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;set_config('request.jwt.claims', ...)&lt;/code&gt; requires PostgreSQL 9.5+ for the &lt;code&gt;set_config&lt;/code&gt; function syntax. Older versions need &lt;code&gt;SET LOCAL "request.jwt.claims" = '...'&lt;/code&gt;. &lt;code&gt;gen_random_uuid()&lt;/code&gt; is built-in only in PostgreSQL 13+ (older versions need the &lt;code&gt;pgcrypto&lt;/code&gt; extension enabled).&lt;/p&gt;

&lt;p&gt;Deploying across multiple Postgres versions means maintaining sql-only-trace per version, which is more work than maintaining frontend traces.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. RLS Policies Themselves Are the Target
&lt;/h3&gt;

&lt;p&gt;sql-only-trace uses &lt;code&gt;set_config&lt;/code&gt; to inject a super_admin JWT and pass RLS. If the goal is to test the RLS policies themselves (such as "anon should not see X" / "customer should only see their own orders"), you need multiple JWT scenarios, each in its own run, and sql-only-trace becomes 2-3x the work.&lt;/p&gt;

&lt;p&gt;Mitigation: Split RLS tests into separate files (T-023-rls-customer-only.sql / T-024-rls-anon-readonly.sql), one JWT scenario per file. The engineering effort is 2-3x larger than for a typical sql-only-trace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-Database Downgrade Table
&lt;/h3&gt;

&lt;p&gt;If you are not on PostgreSQL, sql-only-trace downgrades to "application-layer integration test":&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;DO $$ ... $$&lt;/code&gt; block&lt;/td&gt;
&lt;td&gt;MySQL &lt;code&gt;CREATE PROCEDURE ... CALL&lt;/code&gt;, two steps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;set_config('request.jwt.claims', ...)&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MySQL has no equivalent. Skip RLS; test at app layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;RAISE EXCEPTION&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MySQL &lt;code&gt;SIGNAL SQLSTATE '45000'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;RAISE NOTICE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MySQL &lt;code&gt;SELECT 'msg' AS log&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;gen_random_uuid()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;MongoDB &lt;code&gt;ObjectId()&lt;/code&gt; or app-layer &lt;code&gt;uuid()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PG &lt;code&gt;npx supabase db query --linked -f&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Node + &lt;code&gt;pg-promise&lt;/code&gt; integration test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The skeleton (SEED / RUN / ASSERTIONS / CLEANUP / Conclusion) applies across all DBs. Only the location moves from SQL to the application layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Series Wrap-Up
&lt;/h2&gt;

&lt;p&gt;This is post 9 of 9 in "Trace Lock — Governance Notes from AI Pair-Programming" (series finale). The 9 posts span 3 series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Field Diary&lt;/strong&gt; (cross-field-diary) — E meta + index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Indie Dev Notes&lt;/strong&gt; (indie-dev-notes) — A1 / B1 / C1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer Diary&lt;/strong&gt; (engineer-diary) — A2 / B2 / C2 / D (this post)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Defense (A1 / A2 / parts of D), offense (B1 / B2), and combined (C1 / C2) form three interwoven axes. D fills in the DB-pure logic testing gap, completing the engineering details of the whole dual-blade approach.&lt;/p&gt;

&lt;p&gt;From the opening E meta post on "how I discovered methodology through conversations with AI" to this closing post on sql-only-trace details, the series records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The 11-piece matrix (C2)&lt;/li&gt;
&lt;li&gt;The 5-artifact defense framework (A2)&lt;/li&gt;
&lt;li&gt;The 6-piece fix pattern (B2)&lt;/li&gt;
&lt;li&gt;Cross-stack and cross-database mappings (C2 + D)&lt;/li&gt;
&lt;li&gt;8 not-applicable scenarios (C2) plus 5 sql-only-trace not-applicable scenarios (this post)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I gave these working names ("dual blade" / "Trace Lock" / "sql-only-trace") myself. None are industry-standard terminology. If your context happens to match the conditions (solo-maintained / many cross-layer dependencies / relatively stable business contracts / 6+ months maintenance period / AI pair-programming / pure PostgreSQL), parts of this may be useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related Posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="//./trace-lock-d-sql-only-trace-zh.md"&gt;D · sql-only-trace · 中文版&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 · Combined Engineering Edition (Cross-Project Reuse Matrix + When Not to Use)&lt;/a&gt; (previous post in series)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-b2-offense-engineering-en.md"&gt;B2 · Offense Engineering Edition (6-Piece Fix Pattern Implementation Details)&lt;/a&gt; (source of the sql-only-trace variant reference)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2 · Defense Engineering Edition (5-Artifact Implementation Details)&lt;/a&gt; (starting point of the defense axis)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 · Combined Offense + Defense (plain version)&lt;/a&gt; (plain-language counterpart)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (such as sql-only-trace / Trace Lock / dual blade / 11-piece matrix / 5 artifacts / 6-piece fix pattern / Anchor SSOT / fuse-style test / Decision Pinning / reverse verification anchor break / framework layer vs content layer / objective vs subjective projects / AI reminder skill) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer — just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any&lt;br&gt;
of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-d-sql-only-trace-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-d-sql-only-trace-en" rel="noopener noreferrer"&gt;sql-only-trace Engineering Edition — Testing DB-Side Pure Logic&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Combined Offense + Defense (Engineering Edition) — Cross-Project Reuse Matrix and When Not to Use</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Thu, 30 Jul 2026 01:08:56 +0000</pubDate>
      <link>https://dev.to/dexterlung/combined-offense-defense-engineering-edition-cross-project-reuse-matrix-and-when-not-to-use-1c6j</link>
      <guid>https://dev.to/dexterlung/combined-offense-defense-engineering-edition-cross-project-reuse-matrix-and-when-not-to-use-1c6j</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance Notes from AI Pair-Programming" · Post 8 of 9&lt;/p&gt;




&lt;p&gt;This is the engineering version of &lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 Combined Offense + Defense&lt;/a&gt;, and also the convergence point of &lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2 Defense Engineering Edition&lt;/a&gt; and &lt;a href="//./trace-lock-b2-offense-engineering-en.md"&gt;B2 Offense Engineering Edition&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A2 explains how to lock a single known trace using 5 artifacts. B2 explains how to fix N gaps surfaced by an audit using a 6-piece fix pattern. Each of those posts came with its own "cross-project reuse" table, but only covered its own side. This post merges the two tables into an 11-piece matrix, then adds &lt;strong&gt;cross-stack mapping&lt;/strong&gt;, &lt;strong&gt;objective-vs-subjective project differences&lt;/strong&gt;, &lt;strong&gt;AI pair-programming vs traditional development differences&lt;/strong&gt;, plus an expanded "when not to use" list.&lt;/p&gt;

&lt;p&gt;Written for engineers who already know A2's 5 artifacts and B2's 6-piece pattern. If you are a non-technical reader, &lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 Combined Offense + Defense (plain version)&lt;/a&gt; is the post you want.&lt;/p&gt;

&lt;p&gt;My environment: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js scripts. The "framework layer / content layer" split and porting cost estimates below are based on this stack. Moving to a different stack (Next.js + Prisma + Jest + tRPC, or Django + pytest + Celery) keeps the skeleton applicable, but code skeletons need to be replaced.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 11-Piece Matrix
&lt;/h2&gt;

&lt;p&gt;Merge A2's 5 artifacts with B2's 6-piece pattern. Deduplicate the overlap (Registry / Trace test / Governance rule are shared). Add each side's unique pieces. The result is an 11-piece total list:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;A2 uses?&lt;/th&gt;
&lt;th&gt;B2 uses?&lt;/th&gt;
&lt;th&gt;Main location&lt;/th&gt;
&lt;th&gt;Reusability&lt;/th&gt;
&lt;th&gt;Framework dependency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Registry markdown entry&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;文檔/data-source-registry.md&lt;/code&gt; Critical Traces section&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Trace test 5-section structure&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend-app/src/__tests__/traces/T{NN}-*.trace.test.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Any unit test runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Governance rule A (trace-registry-test-coverage)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Node.js script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Governance rule B (trace-test-must-import-anchor)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Node.js script&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;AI reminder skill&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;⚠️ partial&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.claude/skills/trace-lock-modify/SKILL.md&lt;/code&gt; + &lt;code&gt;.claude/skills/business-flow-audit-fix/SKILL.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;Claude Code / Cursor / other skill-enabled AI tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Pure-function helper convention&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend-app/src/lib/business-rules/&amp;lt;feature&amp;gt;Logic.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure JS module convention, framework-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Caller exemption comment convention&lt;/td&gt;
&lt;td&gt;⚠️ occasional&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;source code inline &lt;code&gt;@xxx-ok: &amp;lt;reason&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure text regex convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Iteration log 5-section structure&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;文檔/iteration-logs/&amp;lt;sprint&amp;gt;/NN_Gap-N_&amp;lt;topic&amp;gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure markdown record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Reverse verification anchor-break flow&lt;/td&gt;
&lt;td&gt;⚠️ occasional&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;5-step flow (write test → green → break anchor → confirm red → revert)&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Pure procedure, needs team / AI cooperation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;sql-only-trace variant&lt;/td&gt;
&lt;td&gt;⚠️ occasional&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;scripts/integration-test/T{NN}-*.sql&lt;/code&gt; + DO block&lt;/td&gt;
&lt;td&gt;★★★☆☆&lt;/td&gt;
&lt;td&gt;PostgreSQL-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Decision Pinning (business contract freezing)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;trace test's &lt;code&gt;pin current behavior&lt;/code&gt; section + registry's backlog finding&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure discipline, tool-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few observations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Framework layer vs content layer split
&lt;/h3&gt;

&lt;p&gt;Pull out pieces rated ≥ ★★★★☆:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Framework layer (high reusability):
   1 Registry markdown entry             ★★★★★
   2 Trace test 5-section structure       ★★★★☆
   3 Governance rule A                   ★★★★☆
   4 Governance rule B                   ★★★★☆
   6 Pure-function helper convention      ★★★★★
   7 Caller exemption comment             ★★★★★
   8 Iteration log 5-section              ★★★★★
   9 Reverse verification anchor-break    ★★★★☆
  11 Decision Pinning discipline          ★★★★★

Medium reusability (★★★☆☆):
   5 AI reminder skill         (tool-specific)
  10 sql-only-trace variant    (DB-specific)

Content layer (not reusable ★☆☆☆☆, reinvent per project):
  - Specific traces (business chain shape)
  - Specific helpers (business algorithm)
  - Specific business contracts (pinning content)
  - Specific incident pinning cases
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A2 line 321 and B2 line 644 each observed "framework layer 80-90% reusable, business contract 0% reusable". The 11-piece matrix matches: 9 framework-layer pieces, 2 medium-tier pieces, business contract 0% reusable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why piece 5 (AI reminder skill) and piece 10 (sql-only-trace) drop to ★★★☆☆
&lt;/h3&gt;

&lt;p&gt;Both have strong framework dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Piece 5 AI reminder skill&lt;/strong&gt; depends on Claude Code's skill auto-trigger mechanism (description-based fuzzy matching + frontmatter). Cursor has &lt;code&gt;.cursorrules&lt;/code&gt; with similar intent but different syntax. Other AI tools (GitHub Copilot / Continue.dev / Cody) currently lack this mechanism. &lt;strong&gt;Reusability depends on whether the target environment has the "skill auto-trigger" primitive&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code / Cursor: portable, syntax conversion needed&lt;/li&gt;
&lt;li&gt;VSCode + GitHub Copilot: not portable (no description-based trigger)&lt;/li&gt;
&lt;li&gt;Others: requires evaluation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without this primitive, fall back to "PR template + checklist". But losing the "auto-trigger" property means relying on humans to remember the checklist, which becomes another rot source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Piece 10 sql-only-trace variant&lt;/strong&gt; depends on PostgreSQL's &lt;code&gt;DO $$ ... $$ LANGUAGE plpgsql&lt;/code&gt; block + JWT config + &lt;code&gt;SET LOCAL&lt;/code&gt;. MySQL / SQLite / MongoDB each have their own stored-procedure / migration-test concepts, but with different syntax and portability (see cross-database mapping below).&lt;/p&gt;




&lt;h2&gt;
  
  
  Applicability Dimensions
&lt;/h2&gt;

&lt;p&gt;"My environment hits all the conditions" decomposes into 5 dimensions. Each dimension has its own marginal-value curve for the combined offense + defense pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension 1 (team size)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Offense applicable?&lt;/th&gt;
&lt;th&gt;Defense applicable?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Solo developer&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;Zero external correction force; engineering means is the only remedy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2-3 small team&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;td&gt;Before code review culture forms, similar to solo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-10 team&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;td&gt;Code review partly plays the attack role, but reviews do not actively audit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10+ organization&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;Multi-tier review + QA + senior dev pings, governance rule marginal returns diminish&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;10+ organizations are not "cannot use", but "ROI becomes hard to calculate". Yet piece 1 (Registry markdown) and piece 8 (Iteration log) remain applicable. These two are for "organizational memory", independent of team size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension 2 (project maintenance horizon)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Horizon&lt;/th&gt;
&lt;th&gt;Offense applicable?&lt;/th&gt;
&lt;th&gt;Defense applicable?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;lt; 1 month (hackathon / spike)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Forgetting "3 months later" does not exist; pinning via commit message is sufficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1-3 months (early MVP)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;Business contract not stable; frequent trace breaks drag iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-12 months (growth phase)&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;td&gt;Cross-layer bug experience accumulating; pick 2-3 painful traces to lock first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12+ months (long-term maintenance)&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;"Forgetting 3 months later" cost fully visible&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My environment (24+ months maintenance) hits the strongly recommended region.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension 3 (cross-layer complexity)
&lt;/h3&gt;

&lt;p&gt;"Cross-layer dependency" definition: a piece of data flowing from DB to UI through N transformation points (trigger / RPC / store / composable / component / helper).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Depth&lt;/th&gt;
&lt;th&gt;Offense applicable?&lt;/th&gt;
&lt;th&gt;Defense applicable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;≤ 2 layers (DB → UI direct)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-5 layers (with store + composable)&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6+ layers (with trigger + RPC + helper + ACL + multiple components)&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My environment (e-commerce + inventory + roasting + POS + multiple RLS policies) sits at roughly 6-9 layers per business chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension 4 (business contract stability)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change frequency&lt;/th&gt;
&lt;th&gt;Offense applicable?&lt;/th&gt;
&lt;th&gt;Defense applicable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weekly algorithm change&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly 1-2 changes&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;td&gt;⚠️ Evaluate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quarterly 0-1 change&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Yearly stable&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When business contracts churn often, pinning becomes a burden (every change requires updating trace test + registry + iteration log).&lt;/p&gt;

&lt;h3&gt;
  
  
  Dimension 5 (AI pair-programming intensity)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Intensity&lt;/th&gt;
&lt;th&gt;Offense applicable?&lt;/th&gt;
&lt;th&gt;Defense applicable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Never use AI&lt;/td&gt;
&lt;td&gt;✅ but piece 5 unused&lt;/td&gt;
&lt;td&gt;✅ but piece 5 unused&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Occasional (few times a week)&lt;/td&gt;
&lt;td&gt;✅ Recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy (daily main driver)&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;td&gt;✅ Strongly recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When AI pair-programming is heavy, piece 5 (AI reminder skill) provides the highest value because AI lacks the "I edited X last week" muscle memory; every new conversation starts from zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Objective vs Subjective Project Differences
&lt;/h2&gt;

&lt;p&gt;"Objective" vs "subjective" is my own coarse project classification (working name, my own placeholder term).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Objective&lt;/strong&gt;: Has clear "right / wrong" business logic. E-commerce (inventory / pricing / checkout), accounting, finance, ERP, CRM derived fields, ticketing, check-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subjective&lt;/strong&gt;: Output is "taste / style / subjective experience". Design mockups, copywriting, video editing, UX A/B test outcomes, recommendation systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two project classes have very different applicability profiles.&lt;/p&gt;

&lt;h3&gt;
  
  
  Objective projects
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Applicability for objective projects&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 Registry / 2 Trace test / 3-4 Governance rule&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;"Right / wrong" has explicit oracle (queryable from DB, expressible as spec)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 Pure-function helper&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;Business logic expressible as pure function (e.g. "annual threshold → tier")&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 Caller exemption&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;Thin wrapper scenarios common&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 Iteration log&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;Decisions like "why cash-first not bonus-first" need recording&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9 Reverse verification&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;Breaking the anchor produces a clear expected red&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 sql-only-trace&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;Useful when DB logic is dense&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 Business contract freezing&lt;/td&gt;
&lt;td&gt;✅ High&lt;/td&gt;
&lt;td&gt;"Current behavior" is definable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The combined pattern fits as a whole set. My environment is objective.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subjective projects
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Applicability for subjective projects&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 Registry / 2 Trace test&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;No clear "right / wrong" oracle; pinning does not land&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-4 Governance rule&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;Can pin "structure" (helper must exist) but not "content"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 Pure-function helper&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;But helper itself is a suggested value, not a SSOT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 Caller exemption&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Same applicability as objective&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 Iteration log&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;"Why this copy revision" decisions still valuable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9 Reverse verification&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Breaking the anchor does not necessarily produce red (no clear oracle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 sql-only-trace&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Subjective projects usually do not use DB logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11 Business contract freezing&lt;/td&gt;
&lt;td&gt;⚠️ Partial&lt;/td&gt;
&lt;td&gt;"Contract" concept on subjective side is closer to "style guide"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Subjective projects typically use 5-6 of the 11 pieces. A rubric-driven workflow + iteration log fits better than pinning test + governance rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hybrid projects
&lt;/h3&gt;

&lt;p&gt;Many projects are hybrid. Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My CS-SAAS: objective business logic (inventory / pricing / orders) + subjective blog copy&lt;/li&gt;
&lt;li&gt;Design tool: objective UI behavior + subjective aesthetics / typography&lt;/li&gt;
&lt;li&gt;Recommendation system: objective infra (instrumentation / statistics) + subjective recommendation result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hybrid projects use "zoned governance": objective zones get the full combined set, subjective zones get rubric + iteration log.&lt;/p&gt;




&lt;h2&gt;
  
  
  AI Pair-Programming vs Traditional Development Differences
&lt;/h2&gt;

&lt;p&gt;Why does AI pair-programming need trace lock more? Four structural differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference 1 (no muscle memory)
&lt;/h3&gt;

&lt;p&gt;A traditional developer who has spent 6 months in a codebase has "if I change X, also touch Y" baked into muscle memory. Grep is no longer needed every time; past pitfalls are avoided instinctively.&lt;/p&gt;

&lt;p&gt;An AI agent enters every new conversation with empty context. A bug fixed last week is rediscovered from symptom-grep this week. &lt;strong&gt;Institutional memory does not exist at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Trace lock externalizes muscle memory into AI-readable artifacts (registry markdown + skill auto-trigger), so each new conversation inherits the previous one's "muscle memory" directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference 2 (no colleague pings)
&lt;/h3&gt;

&lt;p&gt;Traditional teams have Slack threads, code review comments, standups. A senior dev seeing a PR will proactively ping "remember to also update Y after editing X".&lt;/p&gt;

&lt;p&gt;AI has no senior dev on Slack. Governance rule + skill auto-trigger is the engineering means of supplying the "senior dev on Slack" role.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference 3 (context window has a ceiling)
&lt;/h3&gt;

&lt;p&gt;A traditional developer does not "forget" after reading the codebase. An AI agent's context window has a hard cap (~200k tokens); long conversations drop early decisions.&lt;/p&gt;

&lt;p&gt;Iteration log + registry externalize decisions into markdown, so the next conversation reloads from markdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  Difference 4 (AI is more prone to "change without understanding why")
&lt;/h3&gt;

&lt;p&gt;When AI sees a red test, the instinct is to edit the test (turn it green) rather than ask "why was this test written this way".&lt;/p&gt;

&lt;p&gt;Business contract freezing (piece 11) + a &lt;code&gt;// Why: contract pinned after last week's bug; read incident pinning case before editing&lt;/code&gt; comment inside the trace test is what blocks the instinct. Without that comment, AI two turns later will flip &lt;code&gt;expect(x).toBe(true)&lt;/code&gt; to &lt;code&gt;expect(x).toBe(false)&lt;/code&gt; because "the test was red".&lt;/p&gt;

&lt;h3&gt;
  
  
  Concrete value-add to AI pair-programming
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI pair-programming pain        →  Combined-pattern piece
Unaware of N cross-layer rels   →  Offensive audit + piece 1 Registry
Unaware editing X breaks Y      →  Piece 2 Trace test + piece 5 skill auto-trigger
Internal helper edit unblocked  →  Piece 3-4 Governance rule
Unsure why this was written     →  Piece 8 Iteration log + piece 11 Decision Pinning
Red test → edit the test        →  Piece 11 + trace test's incident pinning comment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every pain point has a corresponding piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-Stack Mapping
&lt;/h2&gt;

&lt;p&gt;When porting pieces to a different stack, the skeleton stays but the implementation gets swapped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trace test framework mapping (piece 2)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source (Vitest)&lt;/th&gt;
&lt;th&gt;Target framework&lt;/th&gt;
&lt;th&gt;Main replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe / it / expect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Jest&lt;/td&gt;
&lt;td&gt;Same, near-zero edits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe / it / expect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mocha + Chai&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;expect(x).toBe(y)&lt;/code&gt; → &lt;code&gt;expect(x).to.equal(y)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe / it / expect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pytest&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;def test_xxx():&lt;/code&gt; + &lt;code&gt;assert x == y&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe / it / expect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RSpec&lt;/td&gt;
&lt;td&gt;&lt;code&gt;describe "x" do; it "..." do; expect(x).to eq(y); end; end&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;describe / it / expect&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go testing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;func TestXxx(t *testing.T)&lt;/code&gt; + &lt;code&gt;if x != y { t.Errorf(...) }&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 5-section structure (imports anchor / setup current state / assert behavior / future regression catch / why-comment) is framework-independent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Governance rule script mapping (pieces 3-4)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source (Node.js)&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Main replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fs.readFileSync&lt;/code&gt; + &lt;code&gt;/regex/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;open().read()&lt;/code&gt; + &lt;code&gt;re.compile(...)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node script&lt;/td&gt;
&lt;td&gt;Ruby rake task&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Rake::Task&lt;/code&gt; + &lt;code&gt;File.read&lt;/code&gt; + &lt;code&gt;Regexp&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node script&lt;/td&gt;
&lt;td&gt;bash + grep / awk&lt;/td&gt;
&lt;td&gt;Feasible but regex syntax inconsistent; recommend ripgrep + logic layer in bash&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node script&lt;/td&gt;
&lt;td&gt;Go binary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;os.ReadFile&lt;/code&gt; + &lt;code&gt;regexp.MustCompile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;CI integration: every stack supports pre-push hook (Husky / pre-commit / lefthook) + GitHub Actions / GitLab CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caller exemption comment convention (piece 7)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Replacement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;JS &lt;code&gt;// @xxx-ok: reason&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;# noqa: xxx-ok reason&lt;/code&gt; (or custom)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;//go:build xxx-ok&lt;/code&gt; or custom build tag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JS&lt;/td&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;# rubocop:disable Style/Xxx&lt;/code&gt; style&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key: the rule is "&lt;strong&gt;a comment containing the &lt;code&gt;xxx-ok&lt;/code&gt; token is recognized by the governance rule as a valid bypass&lt;/strong&gt;", language-independent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cross-database mapping (piece 10 sql-only-trace)
&lt;/h3&gt;

&lt;p&gt;This is the most stack-specific piece.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Corresponding mechanism&lt;/th&gt;
&lt;th&gt;Main difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DO $$ ... $$ LANGUAGE plpgsql&lt;/code&gt; block&lt;/td&gt;
&lt;td&gt;My environment, full support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DELIMITER // PROCEDURE ... //&lt;/code&gt; + manual CALL&lt;/td&gt;
&lt;td&gt;No DO block; needs CREATE PROCEDURE + CALL two-step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;No stored procedure; use BEGIN TRANSACTION + application-layer driver&lt;/td&gt;
&lt;td&gt;Most restricted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;No SQL; use aggregation pipeline or mongosh script&lt;/td&gt;
&lt;td&gt;Different paradigm entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Snowflake / BigQuery&lt;/td&gt;
&lt;td&gt;Stored procedures exist, syntax differs&lt;/td&gt;
&lt;td&gt;Cloud DW; CI integration harder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;SET LOCAL&lt;/code&gt; mechanism (RLS JWT simulation) is PostgreSQL-specific. MySQL / SQLite have no equivalent; you must test through the application layer or bypass RLS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your stack is not PostgreSQL, downgrade piece 10 to "application-layer integration test"&lt;/strong&gt; (run tests via Node / Python / Ruby code connecting to the DB). The skeleton stays "SEED / RUN / ASSERTIONS / CLEANUP" but sits in the application layer instead of SQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  When Not to Use (A2 + B2 + new additions = 8 cases)
&lt;/h2&gt;

&lt;p&gt;A2 line 327-337 lists 5 cases (against pieces 1-5); B2 line 650-660 lists 5 cases (against pieces 6-11). Merging, deduplicating, and adding fresh cases gives 8.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Hackathon / spike / project less than 1 month old
&lt;/h3&gt;

&lt;p&gt;Forgetting "3 months later" does not exist; pinning via commit message + PR description is sufficient. The setup cost of the combined pattern (A2 takes about 3-4h, B2's first gap takes 6-9h) exceeds the entire short-project development time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Only 1-2 cross-layer relationships
&lt;/h3&gt;

&lt;p&gt;Writing to commit message or PR description is enough; building a registry is over-engineering. The offensive audit also will not reach N BLOCKER (no N traces to find), so ROI does not justify the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. No markdown-friendly editing pipeline
&lt;/h3&gt;

&lt;p&gt;If the team lives in Notion / Confluence, syncing git and wiki in parallel becomes a drag. Piece 1 (Registry) and piece 8 (Iteration log) both assume markdown is first-class; moving them to Notion / Confluence is infeasible (no git diff, no CI integration).&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CI pipeline cannot accept custom scripts
&lt;/h3&gt;

&lt;p&gt;Pieces 3-4 (Governance rule) depend on pre-push hook or CI enforcement. If the CI pipeline cannot accept custom Node / Python scripts, governance rules have nowhere to attach and slowly drift to "ignored".&lt;/p&gt;

&lt;h3&gt;
  
  
  5. No AI pair-programming
&lt;/h3&gt;

&lt;p&gt;Piece 5 (AI reminder skill) targets the integration points of Claude Code / Cursor and similar tools. Without such a tool the skill has nowhere to attach. The pattern drops to "9 applicable pieces" and still works, but loses the "auto-trigger" key property.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Business contract churns extremely fast
&lt;/h3&gt;

&lt;p&gt;In scenarios where helper algorithms change weekly, trace test pinning becomes a drag. Stabilize business first, then lock. This is dimension 4's "weekly change" case.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Multi-person maintenance with strong code review culture
&lt;/h3&gt;

&lt;p&gt;Traditional code review already plays part of the governance role. The combined pattern still works but with diminishing marginal returns, so ROI needs evaluation. This is dimension 1's 10+ organization case.&lt;/p&gt;

&lt;p&gt;That said, &lt;strong&gt;piece 1 Registry + piece 8 Iteration log remain applicable&lt;/strong&gt; (for organizational memory), and can be adopted standalone.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Pure read-only reporting systems / pure ML data pipelines
&lt;/h3&gt;

&lt;p&gt;Two C1 cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pure read-only reporting systems have no write path, so there is no "data flow" and no trace to build&lt;/li&gt;
&lt;li&gt;Pure ML / data pipelines benefit more from data-contract tests (Great Expectations / Pandera). The "business contract" concept of the combined pattern does not fit ML pipelines well&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real Porting Cost Estimate (from my environment to a new project)
&lt;/h2&gt;

&lt;p&gt;Suppose I take on a contract and want to apply the combined pattern to a new Vue + Supabase project (same stack).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Estimated time&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Copy 9 framework-layer pieces&lt;/td&gt;
&lt;td&gt;1h&lt;/td&gt;
&lt;td&gt;Copy governance-guard.mjs / .claude/skills/ / registry markdown template / iteration log template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Update paths and env vars&lt;/td&gt;
&lt;td&gt;0.5h&lt;/td&gt;
&lt;td&gt;repoRoot logic / registry path / trace test directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Write first trace (client-specific)&lt;/td&gt;
&lt;td&gt;1-1.5h&lt;/td&gt;
&lt;td&gt;Run 5-step audit + write registry block + trace test&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Write first helper (business-specific)&lt;/td&gt;
&lt;td&gt;1-2h&lt;/td&gt;
&lt;td&gt;Extract pure-function logic from scattered callers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. CI / pre-push hook integration&lt;/td&gt;
&lt;td&gt;0.5h&lt;/td&gt;
&lt;td&gt;Add governance-guard.mjs to hook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Write the matching CLAUDE.md rule&lt;/td&gt;
&lt;td&gt;0.5h&lt;/td&gt;
&lt;td&gt;Document "this project's trace nodes are X; AI must run 5-step audit before editing"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.5-6h&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Includes first trace + first helper&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each subsequent trace is roughly 30-45 min (A2 estimate); each gap fix using the 6-piece pattern is roughly 1-1.5h (B2 estimate).&lt;/p&gt;

&lt;p&gt;Switching to a different stack (e.g. Next.js + Prisma + Jest) adds 2-3h of replacement work (trace test syntax / governance rule regex / sql-only-trace variant).&lt;/p&gt;

&lt;p&gt;ROI break-even: in my environment it is 2-3 months (past 6 months: 5 cross-layer bugs × 3-5h = 15-25h of fixing). &lt;strong&gt;The break-even on a new project depends on cross-layer bug frequency&lt;/strong&gt;: six months without a cross-layer bug is unprofitable; six months with 3+ such bugs is clearly profitable.&lt;/p&gt;




&lt;h2&gt;
  
  
  If You Want to Try This
&lt;/h2&gt;

&lt;p&gt;The order I would suggest (extending C1's suggestion with engineering details):&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1 (Defense, 3-4 hours)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Copy the 9 framework-layer templates (about 1h)&lt;/li&gt;
&lt;li&gt;Write the first trace (about 1.5h), choosing the one closest to "the bug I hit in the past 6 months"&lt;/li&gt;
&lt;li&gt;Integrate pre-push hook + CI (about 0.5h)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate&lt;/strong&gt;: intentionally break the trace test's expected value → governance should turn red → revert → should turn green&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2 (Offense, 5-10 hours)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Wait until business produces 2-3 cross-layer bugs, then run the first offensive audit&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="//../../.claude/skills/business-flow-audit-fix/SKILL.md"&gt;&lt;code&gt;business-flow-audit-fix&lt;/code&gt;&lt;/a&gt; skill to run 11 chain-nodes × 3 questions audit&lt;/li&gt;
&lt;li&gt;Fix each BLOCKER using the 6-piece pattern&lt;/li&gt;
&lt;li&gt;Write sprint summary&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 3 (Steady state, one offensive sprint per quarter)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Run one offensive audit per quarter to keep system entropy from rising&lt;/li&gt;
&lt;li&gt;Routine code change is auto-protected by the defensive side&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Not sure whether it is worth doing?
&lt;/h3&gt;

&lt;p&gt;C1's heuristic: did the past 6 months produce ≥ 3 "cross-layer contract drift" bugs? Yes → worth it. No → maybe not yet.&lt;/p&gt;

&lt;p&gt;The engineering version adds a quantified indicator: &lt;strong&gt;the cumulative hours spent fixing cross-layer bugs in the past 6 months&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cumulative &amp;lt; 5h: skip this pattern&lt;/li&gt;
&lt;li&gt;Cumulative 5-15h: consider Phase 1 (defense only)&lt;/li&gt;
&lt;li&gt;Cumulative 15-30h: consider Phase 1 + Phase 2&lt;/li&gt;
&lt;li&gt;Cumulative 30h+: strongly recommend the full set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My own past 6 months totals about 25-30h.&lt;/p&gt;

&lt;p&gt;I am also unsure whether this threshold transfers to your situation. But "cumulative bug-fix hours" is a more honest signal than "count" (5 small bugs differ from 1 big bug).&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 · Combined Offense + Defense (plain version)&lt;/a&gt; (series same-topic plain version)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2 · Defense Engineering Edition: 5 artifacts implementation details&lt;/a&gt; (series same-branch, two posts earlier)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-b2-offense-engineering-en.md"&gt;B2 · Offense Engineering Edition: 6-piece fix pattern&lt;/a&gt; (series same-branch, previous post)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-d-sql-only-trace-en.md"&gt;D · sql-only-trace Engineering Edition: testing DB-side pure logic&lt;/a&gt; (series same-branch, next post)&lt;/li&gt;
&lt;li&gt;&lt;a href="//./trace-lock-c2-combo-engineering-zh.md"&gt;Chinese version&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (11-piece matrix / framework layer vs content layer / objective vs subjective projects / combined pattern / Trace Lock / business contract freezing / Decision Pinning / 6-piece fix pattern / 5 artifacts / sql-only-trace / AI reminder skill / reverse verification anchor break) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer — just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-c2-combo-engineering-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-c2-combo-engineering-en" rel="noopener noreferrer"&gt;Combined Offense + Defense (Engineering Edition) — Cross-Project Reuse Matrix and When Not to Use&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Offense + Defense combined — How the two pair up (for self-taught developers)</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Wed, 29 Jul 2026 14:05:15 +0000</pubDate>
      <link>https://dev.to/dexterlung/offense-defense-combined-how-the-two-pair-up-for-self-taught-developers-49c0</link>
      <guid>https://dev.to/dexterlung/offense-defense-combined-how-the-two-pair-up-for-self-taught-developers-49c0</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 4 of 9&lt;/p&gt;




&lt;p&gt;The previous two posts covered the two trace-lock modes separately:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-a1-defense-en.md"&gt;A1 Defensive&lt;/a&gt;: after tripping on a cross-layer bug once, lock that relationship down so it doesn't rot&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-b1-offense-en.md"&gt;B1 Offensive&lt;/a&gt;: proactively audit a business flow, find unprotected chain nodes, fix them all at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post covers "how the two pair up." It's also what I saw when I looked back after the offensive sprint Claude and I finished over the past 2 weeks.&lt;/p&gt;

&lt;p&gt;This is still the plain-language version for self-taught developers. Engineering details (cross-project reuse matrix, what counts as transferable helper patterns, governance rule templates) go into C2 (engineer-facing).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why covering them separately isn't enough
&lt;/h2&gt;

&lt;p&gt;After writing the A1 post and re-reading it, I noticed a gap:&lt;/p&gt;

&lt;p&gt;The defensive mode sounds complete on its own. Five artifacts, registry, fuse test, two governance rules, AI reminder skill. Locking one cross-layer relationship takes 30-45 minutes. By the 7th lock, the total time invested breaks even.&lt;/p&gt;

&lt;p&gt;But there's an unanswered question: &lt;strong&gt;How do I know which relationships still need locking but haven't been locked yet?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The defensive mode answers "how to lock &lt;strong&gt;a known relationship&lt;/strong&gt;." It assumes you already know which one will break. But most of the time I have no idea how many cross-layer relationships my codebase actually contains, let alone which ones are most dangerous.&lt;/p&gt;

&lt;p&gt;It took the 5th cross-layer bug on May 25th for me to stop and ask Claude "should I go looking for other leaks of the same shape." That question opened the offensive sprint, expanding from 13 known traces to 17 plus fixing 4 BLOCKERs.&lt;/p&gt;

&lt;p&gt;In other words: &lt;strong&gt;without offensive, defensive only reacts to bugs already hit. Without defensive, the BLOCKERs offensive finds get re-fixed next month&lt;/strong&gt;. The two are one set; neither is complete alone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Division of labor between the two modes
&lt;/h2&gt;

&lt;p&gt;Claude and I summarized the two roles in this table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Offensive (B1)&lt;/th&gt;
&lt;th&gt;Defensive (A1)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trigger&lt;/td&gt;
&lt;td&gt;I say "audit the whole X chain"&lt;/td&gt;
&lt;td&gt;I (or AI) touch any node listed in trace nodes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frequency&lt;/td&gt;
&lt;td&gt;Once a month or quarter (planned, low frequency)&lt;/td&gt;
&lt;td&gt;Every code change auto-triggers (high frequency)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;N new traces locked (range expansion)&lt;/td&gt;
&lt;td&gt;Existing traces don't rot (range maintenance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;A whole business flow (containing N chain nodes)&lt;/td&gt;
&lt;td&gt;A single trace (one node in one chain)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost per use&lt;/td&gt;
&lt;td&gt;7-15 hours (audit + fix + summary)&lt;/td&gt;
&lt;td&gt;30-45 minutes (5-step checklist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost of not having it&lt;/td&gt;
&lt;td&gt;Trace count stays flat, always chasing known bugs&lt;/td&gt;
&lt;td&gt;Existing traces rot, fixing one breaks another&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The division is clean. Offensive handles "advancing into new territory." Defensive handles "holding territory already taken."&lt;/p&gt;




&lt;h2&gt;
  
  
  Four states of system entropy
&lt;/h2&gt;

&lt;p&gt;When Claude and I discussed ROI, I sketched another mental model. System entropy (total bugs + hidden debt) over time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No offense, no defense&lt;/strong&gt;: entropy increases monotonically. Bugs accumulate faster than I fix them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offense only&lt;/strong&gt;: entropy oscillates. Each sprint reduces it temporarily, climbs back up months later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defense only&lt;/strong&gt;: entropy stays flat at current level. Existing traces held, but no expansion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offense + defense&lt;/strong&gt;: entropy decreases monotonically. Quarterly offensive expands lock coverage, daily defensive maintains, overall trend down&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I only saw this picture after the sprint was done. While writing the code I was just thinking "fix these 4 BLOCKERs," not realizing system entropy had actually started trending down for the first time.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real sprint's numbers
&lt;/h2&gt;

&lt;p&gt;Pulling the past 2 weeks onto a timeline:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Day 1 (defensive emerges)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Customer screenshot: "half-pound drip bag only orderable as 1 pack"&lt;/li&gt;
&lt;li&gt;30-minute fix&lt;/li&gt;
&lt;li&gt;But realized this was the 6th similar bug in 6 months&lt;/li&gt;
&lt;li&gt;Discussed with Claude "why does this keep happening" → arrived at the 5-artifact Trace Lock pattern&lt;/li&gt;
&lt;li&gt;4 hours to build registry + first trace (T-018) + two governance checkers + skill&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Day 2-3 (defensive expands to 13 traces)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backfilled past cross-layer bugs into traces one by one (T-001 to T-017, partially historical events)&lt;/li&gt;
&lt;li&gt;30-45 minutes each&lt;/li&gt;
&lt;li&gt;13 locked. Self-estimated chain protection: 30% → 45%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Day 4 (offensive emerges)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5th cross-layer contract bug hit (roasting station "complete" button frozen)&lt;/li&gt;
&lt;li&gt;Asked Claude "can we find all same-shape leaks at once"&lt;/li&gt;
&lt;li&gt;1-hour audit → 11 chain nodes → 4 BLOCKERs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Day 4-5 (offensive sprint)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 hours to fix all 4 BLOCKERs + write sprint summary&lt;/li&gt;
&lt;li&gt;Each BLOCKER did Decision Pinning + 6-piece fix&lt;/li&gt;
&lt;li&gt;Also added these 4 to the defensive registry (T-019/020/021/022) → 13 expanded to 17&lt;/li&gt;
&lt;li&gt;Self-estimated chain protection: 45% → 81%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Day 6 (the dual-blade insight)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;While writing the E meta post, looking back I saw "offensive finds + defensive locks" as a closed loop&lt;/li&gt;
&lt;li&gt;Offensive finds N at a time, defensive maintains daily, next offensive round finds new candidates&lt;/li&gt;
&lt;li&gt;That's when I started writing this post&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total 17 days, including 4 blog posts of writing, around 50-60 hours of pure governance investment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why neither alone works (two counter-examples)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Counter-example 1: the defense-only developer
&lt;/h3&gt;

&lt;p&gt;After locking my first 13 traces, I had a brief "OK that's probably enough" illusion.&lt;/p&gt;

&lt;p&gt;The reality: those 13 were all "known bugs hit in the past 6 months." A bug I lock is one a customer already tripped on. &lt;strong&gt;Bugs not yet hit are completely outside the defensive perimeter&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Stopping there, 6 months later I'd hit bug #14, #15, each taking 3-5 hours to fix, then backfilled into the registry. &lt;strong&gt;Defense scope always trails the bugs. At best, you break even&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Counter-example 2: the offense-only developer
&lt;/h3&gt;

&lt;p&gt;Suppose I run an offensive audit every quarter, find 5 BLOCKERs, fix them all. But I have no defensive registry, no tests, no governance rules.&lt;/p&gt;

&lt;p&gt;3 months later, I (or an AI) change some internal logic of a helper. No fuse test fails. No governance rule warns. The change ships to prod, no one notices. &lt;strong&gt;The next audit re-discovers this as a new BLOCKER&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The offensive sprint becomes "perpetually re-fixing the same set of bugs." Marginal cost never goes down.&lt;/p&gt;

&lt;h3&gt;
  
  
  The closed loop when used together
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Offensive audit
    ↓
Find N BLOCKERs
    ↓
Decision Pinning
    ↓
6-piece fix + add to defensive registry
    ↓
Defensive maintains daily (fuse test + governance + skill)
    ↓
Next quarter's offensive doesn't re-find these N, only finds new candidates
    ↓
Coverage expands continuously, marginal cost drops
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this loop for 6-12 months, &lt;strong&gt;codebase shifts from "decays as I change it" to "stabilizes as I change it."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-project application: what transfers, what doesn't
&lt;/h2&gt;

&lt;p&gt;The next context I'd try this in is a different project (if I take on consulting work). Predicting in advance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directly copyable (framework layer)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registry markdown format&lt;/li&gt;
&lt;li&gt;The two governance checker script skeletons&lt;/li&gt;
&lt;li&gt;AI reminder skill's frontmatter structure&lt;/li&gt;
&lt;li&gt;6-piece fix pattern step order&lt;/li&gt;
&lt;li&gt;Iteration log's 5-section structure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not copyable (content layer)&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specific traces (every project's business flows differ)&lt;/li&gt;
&lt;li&gt;Specific helpers (depends on domain logic)&lt;/li&gt;
&lt;li&gt;Specific business contracts (every company / product makes different decisions)&lt;/li&gt;
&lt;li&gt;Migrations / RPC details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short: &lt;strong&gt;80% framework transfers, 20% content must be re-invented&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Time estimate for moving to a new project: ~4-6 hours to set up framework + first trace. Each subsequent trace takes about the same as in the original project (30-45 minutes).&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this doesn't apply (don't take the wrong lesson)
&lt;/h2&gt;

&lt;p&gt;There's a risk in writing this far. Readers might think "every project should use trace lock." That's not what I'm claiming.&lt;/p&gt;

&lt;p&gt;Scenarios I can think of where it doesn't fit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pure read-only reporting systems&lt;/strong&gt;: no write path means no "data flow," nothing to trace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Early-stage MVP under 2 months old&lt;/strong&gt;: business contracts aren't stable yet, traces break frequently and slow iteration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5+ person team with strong code review culture&lt;/strong&gt;: governance overlaps with human review, value drops (unless you use governance as a "save reviewer attention" tool)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure frontend SaaS with no DB&lt;/strong&gt;: few cross-layer dependencies, trace nodes mostly in frontend, regular hooks/composables extracted as helpers is enough&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure ML / data pipeline&lt;/strong&gt;: the "business contract" concept doesn't quite apply, data contract tests (Great Expectations / Pandera) fit better&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your project matches any of those, &lt;strong&gt;you don't need this&lt;/strong&gt;. Trace lock works best for "many cross-layer dependencies + ambiguous business contracts + AI pair programming." It's not a universal solution.&lt;/p&gt;




&lt;h2&gt;
  
  
  A transferable suggestion
&lt;/h2&gt;

&lt;p&gt;If you decide to try the dual-blade approach, the order I'd recommend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Defensive first&lt;/strong&gt; (3-4 hours to build framework + first trace). Simpler, immediate protection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock 1-2 traces to learn the pattern&lt;/strong&gt;, don't start with 10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After your business surfaces 2-3 cross-layer bugs&lt;/strong&gt;, run the first offensive audit (5-10 hours)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quarterly offensive&lt;/strong&gt; to keep system entropy from rising&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not sure if it's worth the investment? My own heuristic: &lt;strong&gt;have you had ≥ 3 "cross-layer contract drift" bugs in the past 6 months&lt;/strong&gt;? Yes → worth it. No → maybe not yet.&lt;/p&gt;

&lt;p&gt;I'm not sure if this threshold generalizes. But under 3, fixing one at a time might be cheaper than building governance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this works well for AI pair programming
&lt;/h2&gt;

&lt;p&gt;Both ends of the dual-blade are AI-friendly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offensive&lt;/strong&gt;: I ask Claude "audit the whole X chain," it has the &lt;code&gt;business-flow-audit-fix&lt;/code&gt; skill that auto-runs the 11-node × 3-question audit + 6-piece fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Defensive&lt;/strong&gt;: AI touches any node in trace nodes, &lt;code&gt;trace-lock-modify&lt;/code&gt; skill auto-triggers, reminds it to run the fuse test first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't have to re-explain governance rules each time. Skill written once, AI inherits it next session.&lt;/p&gt;

&lt;p&gt;Compared to "please AI be more careful" (which is not executable), "write the audit flow into a skill so AI auto-runs it" is an executable engineering intervention.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this series will expand on next
&lt;/h2&gt;

&lt;p&gt;The next few posts rewrite the plain-language content from an engineering angle (for engineers who've written governance rules / pinning tests):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A2 Defensive engineering version: 5-artifact implementation details (how the registry parser script is written, fuse test's 5-section structure, the two governance rules' code)&lt;/li&gt;
&lt;li&gt;B2 Offensive engineering version: 6-piece fix pattern templates (pure-function helper / trace test / governance rule / caller exemption list / iteration log)&lt;/li&gt;
&lt;li&gt;C2 Cross-project reuse matrix (each artifact's transferability rated 0-5 stars)&lt;/li&gt;
&lt;li&gt;D sql-only-trace: how to test pure-DB logic (why unit test frameworks aren't enough, gotchas of running SQL tests via Supabase CLI)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're an engineer who wants implementation details after reading this, those 4 posts are for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A1 · Defensive Trace Lock — How to lock down a cross-layer relationship (one of the previous two in series)&lt;/li&gt;
&lt;li&gt;B1 · Offensive audit — How to periodically audit a whole business flow (previous in series)&lt;/li&gt;
&lt;li&gt;中文版&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (dual-blade / offensive / defensive / Trace Lock / Decision Pinning / 6-piece fix pattern / sql-only-trace) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer, just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-c1-combo-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-c1-combo-en" rel="noopener noreferrer"&gt;Offense + Defense combined — How the two pair up (for self-taught developers)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Offensive audit, engineering edition — the 6-piece fix pattern in detail</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Wed, 29 Jul 2026 13:05:17 +0000</pubDate>
      <link>https://dev.to/dexterlung/offensive-audit-engineering-edition-the-6-piece-fix-pattern-in-detail-129m</link>
      <guid>https://dev.to/dexterlung/offensive-audit-engineering-edition-the-6-piece-fix-pattern-in-detail-129m</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 7 of 9&lt;/p&gt;




&lt;p&gt;This post is the engineering version of &lt;a href="//./trace-lock-b1-offense-en.md"&gt;B1 Offensive audit&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;B1 covered "why audit the whole chain" and "what the overall flow looks like." This post unpacks the 6-piece fix pattern (working name, my own tentative term) used to actually fix each gap that the audit surfaced: the code shape of each piece, why it is designed that way, what you can tune.&lt;/p&gt;

&lt;p&gt;Written for engineers already familiar with governance rules, pinning tests, or incident-driven tests. If you have a non-engineering background, B1 is more suitable.&lt;/p&gt;

&lt;p&gt;My stack: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js scripts. But the 6-piece pattern below is mostly independent of the frontend framework. It depends on &lt;strong&gt;filesystem conventions&lt;/strong&gt; + &lt;strong&gt;regex parsers&lt;/strong&gt; + &lt;strong&gt;CI-enforced execution&lt;/strong&gt; (pre-push hook + GitHub Actions).&lt;/p&gt;




&lt;h2&gt;
  
  
  The 6 pieces at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;File type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Pure-function helper&lt;/td&gt;
&lt;td&gt;Extracts business logic into a side-effect-free function&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend-app/src/lib/business-rules/&amp;lt;feature&amp;gt;Logic.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Trace test (fuse test)&lt;/td&gt;
&lt;td&gt;Pins the business contract + incident pinning cases&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend-app/src/__tests__/traces/T{NN}-*.trace.test.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Governance rule&lt;/td&gt;
&lt;td&gt;Pre-push enforces callers route through the helper (or carry an explicit exemption)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;checkXxxCoverage()&lt;/code&gt; in &lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Caller exemption comment&lt;/td&gt;
&lt;td&gt;Legitimate carve-out for thin wrapper / docstring scenarios&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@xxx-ok: &amp;lt;reason&amp;gt;&lt;/code&gt; comment in source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Registry entry&lt;/td&gt;
&lt;td&gt;The directory entry for this trace&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;## Critical Traces&lt;/code&gt; block in &lt;code&gt;文檔/data-source-registry.md&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Iteration log&lt;/td&gt;
&lt;td&gt;The record of how this gap was fixed (for the future you)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;文檔/iteration-logs/&amp;lt;sprint&amp;gt;/NN_Gap-N_&amp;lt;topic&amp;gt;.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The set corresponds to the pipeline: &lt;strong&gt;a. offense surfaces gap → b. freeze business contract → c. extract pure function → d. pin behavior → e. force callers → f. record decision&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mapping to A2's 5 artifacts
&lt;/h3&gt;

&lt;p&gt;&lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2's 5 artifacts&lt;/a&gt; (registry + trace test + 2 governance rules + AI reminder skill) solve "rot protection for a single, known trace."&lt;/p&gt;

&lt;p&gt;B2's 6 pieces solve "starting from zero, discover and fix N gaps in one sprint." Differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A2 does not need a helper (the trace usually already has an SSOT)&lt;/li&gt;
&lt;li&gt;B2 must add a helper (gaps are gaps precisely because no SSOT exists yet; logic is scattered)&lt;/li&gt;
&lt;li&gt;A2 does not need an iteration log (single trace, one-time setup)&lt;/li&gt;
&lt;li&gt;B2 must add an iteration log (multi-gap repair process needs to record escalations, reverse verification, self-checks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two sets compose. B2 surfaces and locks new gaps; A2 prevents subsequent rot.&lt;/p&gt;

&lt;h3&gt;
  
  
  What breaks if you skip any one piece
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Missing&lt;/th&gt;
&lt;th&gt;Consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pure-function helper&lt;/td&gt;
&lt;td&gt;Business logic stays scattered; the next caller re-implements it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace test&lt;/td&gt;
&lt;td&gt;The frozen contract lives only in your head; in 3 months you forget why&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance rule&lt;/td&gt;
&lt;td&gt;New callers bypass review and the helper is decorative&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caller exemption&lt;/td&gt;
&lt;td&gt;Some thin wrappers / cross-domain files get false-positive governance violations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry entry&lt;/td&gt;
&lt;td&gt;No central directory; AI does not know which files belong to which trace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iteration log&lt;/td&gt;
&lt;td&gt;The context for "why cash-first instead of bonus-first" evaporates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No single piece is sufficient on its own. The closed loop forms only when all 6 are present.&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 1 (Pure-function helper): extraction logic
&lt;/h2&gt;

&lt;p&gt;Once business logic is embedded in a Vue component, composable, or RPC handler, it carries side effects (DB reads/writes, store mutations, emits). The trace test cannot get a clean test target.&lt;/p&gt;

&lt;p&gt;The design conventions for pure-function helpers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend-app/src/lib/business-rules/cancelOrderLogic.js&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Calculate refund allocation (cash first, bonus second)
 *
 * @param {object} params
 * @param {number} params.refundAmount - Total to refund
 * @param {number} params.originalDeductFromCash - Original cash deduction
 * @param {number} params.originalDeductFromBonus - Original bonus deduction
 * @returns {{ refundToCash: number, refundToBonus: number }}
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;originalDeductFromCash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;originalDeductFromBonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refundToCash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalDeductFromCash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refundToBonus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refundAmount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;refundToCash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalDeductFromBonus&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;refundToCash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;refundToBonus&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few engineering details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero side effects + dependency injection
&lt;/h3&gt;

&lt;p&gt;The helper does not call &lt;code&gt;useUserStore()&lt;/code&gt;, &lt;code&gt;supabase.from(...)&lt;/code&gt;, or &lt;code&gt;window.localStorage&lt;/code&gt; directly. All dependencies are passed via parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Counter-example (coupled to store)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useUserStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;userStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deductFromCash&lt;/span&gt;  &lt;span class="c1"&gt;// side effect&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct (pure function)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;originalDeductFromCash&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reason: trace tests can call the helper with fixture data without mocking the store or Supabase. Test cost drops by an order of magnitude.&lt;/p&gt;

&lt;h3&gt;
  
  
  Place under &lt;code&gt;lib/business-rules/&lt;/code&gt;, not &lt;code&gt;composables/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;composables/&lt;/code&gt; is a Vue convention; functions there return refs / reactives to components. &lt;code&gt;lib/business-rules/&lt;/code&gt; is plain JS, completely decoupled from Vue.&lt;/p&gt;

&lt;p&gt;If in the future you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reuse the helper in an Edge Function (Deno runtime, cannot import Vue)&lt;/li&gt;
&lt;li&gt;Reuse in SSR (no &lt;code&gt;window&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Write unit tests (no need to spin up Vue test utils)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Placing it correctly makes reuse painless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Naming convention: &lt;code&gt;&amp;lt;feature&amp;gt;Logic.js&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;code&gt;cancelOrderLogic.js&lt;/code&gt; / &lt;code&gt;roastingLifecycle.js&lt;/code&gt; / &lt;code&gt;packagingTaskState.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;❌ &lt;code&gt;cancelOrderUtils.js&lt;/code&gt; / &lt;code&gt;refundHelper.js&lt;/code&gt; / &lt;code&gt;roastingService.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;Logic&lt;/code&gt; suffix lets a single grep find every SSOT helper. &lt;code&gt;Utils&lt;/code&gt; / &lt;code&gt;Helper&lt;/code&gt; / &lt;code&gt;Service&lt;/code&gt; are too generic and hard to audit.&lt;/p&gt;

&lt;h3&gt;
  
  
  When not to extract a helper
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Logic is called from only one place. Extracting scatters the reader's attention&lt;/li&gt;
&lt;li&gt;Logic is purely a read or has no branches. Inlining is fine&lt;/li&gt;
&lt;li&gt;Logic lives mostly in the DB (e.g., FIFO consumption). Go with the sql-only-trace variant (covered later)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Piece 2 (Trace test): the 5-section structure
&lt;/h2&gt;

&lt;p&gt;A trace test is a vitest spec, but its structure is stricter than a typical unit test. Each &lt;code&gt;describe&lt;/code&gt; block corresponds to a semantic facet. Here is the skeleton of T-019 order-cancel / refund:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend-app/src/__tests__/traces/T019-cancel-refund.trace.test.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;findRoastingOrdersToCancel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../lib/business-rules/cancelOrderLogic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// import anchor SSOT&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-019 trace: refund allocation frozen contract (incident pinning)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Original cash $30 + bonus $50 → refund $80 = cash 30 + bonus 50&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;originalDeductFromCash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;originalDeductFromBonus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;refundToCash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundToBonus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Partial refund $40 = cash 30 + bonus 10&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;refundAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;originalDeductFromCash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;originalDeductFromBonus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;refundToCash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;refundToBonus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-019 trace: legal paths&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-019 trace: illegal paths / edge cases&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-019 trace: cascading cancel rules for roasting_orders&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-019 trace: cross-layer structural contract&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What each section is for
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Section 1 (Business contract, incident pinning)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take the conversation that froze the business contract and write it as test cases. Each BLOCKER gap's decision was made by the user on the spot (e.g., "refund fills cash first"); this section directly pins that decision. Three months later, anyone changing the algorithm flips the test red and is forced to see the decision context.&lt;/p&gt;

&lt;p&gt;Each trace test gets at least 1 case here, often 5-10 (covering happy paths + boundaries).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 2 (Legal paths)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Variations of the full happy path. For example, T-022 packaging-task state machine has legal transitions &lt;code&gt;pending → in_progress → completed&lt;/code&gt; and &lt;code&gt;pending → cancelled&lt;/code&gt;. This section tests 4-6 legal paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 3 (Illegal paths / edge cases)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;null / undefined / empty array / missing fields / negatives / very large numbers. This section guards against future AI edits regressing edge cases.&lt;/p&gt;

&lt;p&gt;When writing this section, actively ask: "what does the helper do on dirty input? should it throw?" The answer is usually "return a safe fallback, do not throw" (because throws blow up the UI catch chain), but pin it in tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 4 (Role / permission / business rule limits)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the gap involves role limits (e.g., T-022 specifies "rollback completed → cancelled is admin/super_admin only"), this section tests the allow/deny matrix per role.&lt;/p&gt;

&lt;p&gt;Not every trace needs this section. Pure calculation logic can skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 5 (Cross-layer structural contract)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The contact surface between the helper and the DB schema, other helpers, or other anchors. For example, the T-022 state string must align with valid values inside &lt;code&gt;packaging_jobs.tasks&lt;/code&gt; JSONB on the DB. This section pins that the helper-known state set matches the DB expectation.&lt;/p&gt;

&lt;p&gt;This section catches "helper drifted from DB schema."&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the test must import the anchor
&lt;/h3&gt;

&lt;p&gt;The first line of a trace test imports the function from &lt;code&gt;lib/business-rules/&amp;lt;feature&amp;gt;Logic&lt;/code&gt;. Governance rule B (covered in A2) checks that every trace test really imports the anchor path declared in the registry.&lt;/p&gt;

&lt;p&gt;A trace test that does not import the anchor is testing a copy-pasted copy of the logic. The anchor moves, the test does not see it, the trace rots while the test still goes green.&lt;/p&gt;

&lt;h3&gt;
  
  
  Target 15+ cases per trace
&lt;/h3&gt;

&lt;p&gt;From the fulfillment-chain-fix sprint: T-019 / T-020 / T-022 averaged 30-38 cases. At least 1 pinning case in Section 1; 3-5 cases each in other sections.&lt;/p&gt;

&lt;p&gt;Less than 15 usually means the business contract is not crisp enough. Go back to the audit map and refine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 3 (Governance rule): code template
&lt;/h2&gt;

&lt;p&gt;Each gap gets a governance rule in &lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;. The pre-push hook and GitHub Actions both run it.&lt;/p&gt;

&lt;p&gt;A trimmed-down version of M5 (Cancel/Refund Helper Coverage):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scripts/governance-guard.mjs&lt;/span&gt;
&lt;span class="c1"&gt;// M5 Cancel/Refund Helper Coverage (2026-05-25, T-019)&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Motivation: cancel-order + complete-refund both involve wallet reverse-deduction&lt;/span&gt;
&lt;span class="c1"&gt;//             and cascading cancel of roasting_orders. Callers must route through&lt;/span&gt;
&lt;span class="c1"&gt;//             cancelOrderLogic. No scattered re-implementation.&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// Rule: any source file matching `fn_cancel_order_atomic` or `fn_admin_complete_refund_atomic`&lt;/span&gt;
&lt;span class="c1"&gt;//       must (a) import cancelOrderLogic OR&lt;/span&gt;
&lt;span class="c1"&gt;//            (b) carry `@cancel-refund-ok: &amp;lt;reason&amp;gt;` exemption&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkCancelRefundHelperCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;collectFiles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;frontend-app/src&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;js|vue&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RPC_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/fn_cancel_order_atomic|fn_admin_complete_refund_atomic/&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;relativePath&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;__tests__|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;test&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;js|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;spec&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;js&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;^|&lt;/span&gt;&lt;span class="se"&gt;\/)&lt;/span&gt;&lt;span class="sr"&gt;dist&lt;/span&gt;&lt;span class="se"&gt;\d?\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;abs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;RPC_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasHelperImport&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/cancelOrderLogic|calculateRefundAllocation/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasExemption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/@cancel-refund-ok&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;\S{5,}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasHelperImport&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;hasExemption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;relativePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`M5 Cancel/Refund Coverage: this file calls cancel/refund RPC but does not import cancelOrderLogic or carry @cancel-refund-ok exemption`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;excerpt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cancel/refund RPC call without helper / exemption&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;checkCancelRefundHelperCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// call from footer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Design points
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Header docstring must contain 3 things&lt;/strong&gt;: creation date + corresponding trace ID + motivation. The motivation must be a business motivation, not "to pass CI."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use a disjunction regex for RPC_PATTERN&lt;/strong&gt;: combine multiple RPC names with &lt;code&gt;|&lt;/code&gt; instead of &lt;code&gt;src.includes('fn_a') || src.includes('fn_b')&lt;/code&gt;. Cleaner and faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip &lt;code&gt;__tests__&lt;/code&gt; / &lt;code&gt;dist&lt;/code&gt;&lt;/strong&gt;: trace tests themselves import helpers and mention RPC names; without skipping they false-positive. &lt;code&gt;dist&lt;/code&gt; is build output, wasted scan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write &lt;code&gt;hasHelperImport&lt;/code&gt; loose&lt;/strong&gt;: callers might import the whole module (&lt;code&gt;import * as logic&lt;/code&gt;) or destructure (&lt;code&gt;import { calculateRefundAllocation }&lt;/code&gt;). Disjunction matches both.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;@cancel-refund-ok&lt;/code&gt; must satisfy &lt;code&gt;\S{5,}&lt;/code&gt;&lt;/strong&gt;: empty reasons are forbidden (covered as a trap below).&lt;/p&gt;

&lt;h3&gt;
  
  
  BLOCKER vs advisory
&lt;/h3&gt;

&lt;p&gt;Sprint experience: trace-lock-related rules are &lt;strong&gt;all BLOCKER&lt;/strong&gt; (pre-push exits 1, blocks push). Reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Traces are, by design, "supposedly stable" relationships&lt;/li&gt;
&lt;li&gt;Advisory equals not enforced. In 3 months it will be ignored&lt;/li&gt;
&lt;li&gt;BLOCKER false positives are handled by exemption comments (low cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exception: pure-text / docs / transition-period lint rules can be advisory. Trace lock is not in that category.&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 4 (Caller exemption comment): two traps
&lt;/h2&gt;

&lt;p&gt;The exemption comment is a legitimate escape hatch around a governance rule. The format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useOrders.js&lt;/span&gt;
&lt;span class="c1"&gt;// @cancel-refund-ok: thin-wrapper-only — this composable only relays RPC results to the UI&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cancelOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fn_cancel_order_atomic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;p_order_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exemptions should be a minority. Two main categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Thin wrapper&lt;/strong&gt;: caller only relays results, no business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docstring mention&lt;/strong&gt;: helper JSDoc references an RPC name but does not actually call it&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Trap A: the &lt;code&gt;\S{5,}&lt;/code&gt; regex and Chinese text
&lt;/h3&gt;

&lt;p&gt;The exemption regex &lt;code&gt;@xxx-ok\s*:\s*\S{5,}&lt;/code&gt; requires &lt;strong&gt;5 consecutive&lt;/strong&gt; non-whitespace characters after the colon. Chinese text like "此 helper 只透傳" has a space after the first character; regex sees one character then a break, fails to match.&lt;/p&gt;

&lt;p&gt;Fixes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fails — Chinese chars + space&lt;/span&gt;
&lt;span class="c1"&gt;// @cancel-refund-ok: 此 helper 只透傳&lt;/span&gt;

&lt;span class="c1"&gt;// passes — dash-connected English&lt;/span&gt;
&lt;span class="c1"&gt;// @cancel-refund-ok: thin-wrapper-only&lt;/span&gt;

&lt;span class="c1"&gt;// passes — 5+ Chinese chars with no whitespace&lt;/span&gt;
&lt;span class="c1"&gt;// @cancel-refund-ok: 此處僅透傳上游結果不涉及計算&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, the sprint adopted dash-connected English: short phrases parse easily, remain readable when &lt;code&gt;audit:all&lt;/code&gt; output is truncated, and are unambiguous in regex.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap B: docstring mentions of RPC names false-positive
&lt;/h3&gt;

&lt;p&gt;A helper's JSDoc may mention related RPC names (to help IDE jump-to-definition) without the helper actually calling them. The governance RPC_PATTERN regex cannot tell the semantic difference and false-positives.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * cancelOrderLogic
 *
 * Atomic RPCs: fn_cancel_order_atomic (migration 238)
 * Atomic RPCs: fn_admin_complete_refund_atomic (migration 239)
 */&lt;/span&gt;
&lt;span class="c1"&gt;// @cancel-refund-ok: docstring-mention-only — this helper only mentions RPC names in JSDoc for IDE jumps, does not call them&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateRefundAllocation&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: add the exemption comment at the top of the helper file, reason &lt;code&gt;docstring-mention-only&lt;/code&gt;. Governance sees the exemption and skips.&lt;/p&gt;

&lt;p&gt;Sprint statistics: 4 gaps used 5 exemptions total (thin wrapper × 3 + docstring-mention × 2), under 10% of total callers. Exemptions should not become the norm; most callers should import the helper.&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 5 (Registry entry): align with existing format
&lt;/h2&gt;

&lt;p&gt;Add a block for the new trace under &lt;code&gt;## Critical Traces&lt;/code&gt; in &lt;code&gt;文檔/data-source-registry.md&lt;/code&gt;. Format aligns with existing T-001 (covered in A2), plus a few sprint-discovered supplements:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### T-019: Order cancellation / refund SSOT&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Type**&lt;/span&gt;: data-flow trace
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Status**&lt;/span&gt;: locked (2026-05-25)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Anchor SSOT**&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;`frontend-app/src/lib/business-rules/cancelOrderLogic.js`&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace test**&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;`frontend-app/src/__tests__/traces/T019-cancel-refund.trace.test.js`&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Governance**&lt;/span&gt;: M5, Cancel/Refund Helper Coverage
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Related migration**&lt;/span&gt;: 238, 239
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Frozen business contract**&lt;/span&gt;:
&lt;span class="p"&gt;  -&lt;/span&gt; Refund allocation: cash first, bonus second (frozen by user 2026-05-25)
&lt;span class="p"&gt;  -&lt;/span&gt; Cascading cancel: source_orders contains only this order → cancel whole roasting; multiple orders share → remove share only
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace nodes**&lt;/span&gt;:
&lt;span class="p"&gt;  1.&lt;/span&gt; RPC &lt;span class="sb"&gt;`fn_cancel_order_atomic`&lt;/span&gt;
&lt;span class="p"&gt;  2.&lt;/span&gt; RPC &lt;span class="sb"&gt;`fn_admin_complete_refund_atomic`&lt;/span&gt;
&lt;span class="p"&gt;  3.&lt;/span&gt; helper &lt;span class="sb"&gt;`calculateRefundAllocation`&lt;/span&gt;
&lt;span class="p"&gt;  4.&lt;/span&gt; helper &lt;span class="sb"&gt;`findRoastingOrdersToCancel`&lt;/span&gt;
&lt;span class="p"&gt;  5.&lt;/span&gt; caller &lt;span class="sb"&gt;`useOrders.cancelOrder`&lt;/span&gt;
&lt;span class="p"&gt;  6.&lt;/span&gt; caller &lt;span class="sb"&gt;`useRefunds.completeRefund`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Related incident**&lt;/span&gt;: half-year-old partial-refund wallet imbalance event
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Last edited**&lt;/span&gt;: 2026-05-25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Two extra fields compared to A2's T-001
&lt;/h3&gt;

&lt;p&gt;Gap-fix-produced traces add two fields compared to the T-001 format in A2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Governance&lt;/strong&gt;: the governance rule ID for this trace. IDs M5 / M6 / M7 / M8 in the registry must match &lt;code&gt;governance-guard.mjs&lt;/code&gt; exactly. Future audits scan the registry and immediately see which rules pair with which trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Frozen business contract&lt;/strong&gt;: a bullet list of the decisions the user pinned on the spot. This is the biggest value-add over A2's 5 artifacts: every frozen decision is permanently traced.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing &lt;code&gt;Trace nodes&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Includes RPCs (DB write side) + helpers (pure-function SSOT) + callers (frontend composable / page). Order roughly follows "DB → UI," but in practice they interleave. This field mostly tells AI which files belong to this trace; ordering strictness is looser than the T-001 in A2.&lt;/p&gt;

&lt;h3&gt;
  
  
  sql-only-trace variant
&lt;/h3&gt;

&lt;p&gt;When a gap has no frontend helper (pure DB logic, e.g., T-021 FIFO consume):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Type**&lt;/span&gt;: sql-only-trace
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace test (SQL)**&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;`scripts/integration-test/T021-fifo-consume-test.sql`&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Type&lt;/code&gt; becomes &lt;code&gt;sql-only-trace&lt;/code&gt;. &lt;code&gt;Trace test&lt;/code&gt; becomes &lt;code&gt;Trace test (SQL)&lt;/code&gt;. &lt;code&gt;parseTraceRegistry&lt;/code&gt; sees both markers and skips the import check (covered later).&lt;/p&gt;




&lt;h2&gt;
  
  
  Piece 6 (Iteration log): 5-section structure
&lt;/h2&gt;

&lt;p&gt;The iteration log records "why this gap was fixed this way." Three months later, the future you sees context here that commit messages cannot show. Filename format &lt;code&gt;NN_Gap-N_&amp;lt;topic&amp;gt;.md&lt;/code&gt;, placed under &lt;code&gt;文檔/iteration-logs/&amp;lt;sprint&amp;gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Iteration 1, Gap-1: Order cancel / refund (T-019)&lt;/span&gt;

&lt;span class="gs"&gt;**Completion time**&lt;/span&gt;: 2026-05-25 23:38

&lt;span class="gu"&gt;## (a) Files touched&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; helper / trace test / registry / governance / caller exemption / migration with paths

&lt;span class="gu"&gt;## (b) Trace test result&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; First run: 30/30 green
&lt;span class="p"&gt;-&lt;/span&gt; Reverse verification A (helper anchor break): deliberately broke algorithm → N cases red → reverted → green
&lt;span class="p"&gt;-&lt;/span&gt; Reverse verification B (governance anchor break): removed exemption → governance red → reverted → green
&lt;span class="p"&gt;-&lt;/span&gt; Full trace suite: X files / Y cases all green
&lt;span class="p"&gt;-&lt;/span&gt; audit:all: full PASS

&lt;span class="gu"&gt;## (c) Escalate log&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Traps stepped on / name alignment issues / Rule 27 signature change judgement / business contract micro-adjustment

&lt;span class="gu"&gt;## (d) Self-check&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Context usage estimate: ~X%
&lt;span class="p"&gt;-&lt;/span&gt; Frozen Constraint drift check (per frozen point)
&lt;span class="p"&gt;-&lt;/span&gt; Cross-gap consistency confirmation

&lt;span class="gu"&gt;## (e) Next gap&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Next gap ID + estimated time + main actions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why none of the 5 sections can be skipped
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;(a) Files touched&lt;/strong&gt;: 3 months later, commit messages do not tell you which files were part of the same fix. This section is "the artifact list for this gap."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(b) Trace test result&lt;/strong&gt;: not just positive-green; must record the result of reverse verification anchor breaks (covered later).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(c) Escalate log&lt;/strong&gt;: traps, name misalignments, Rule 27 judgments, Edge Function vs DB trigger trade-offs. This section is "the decision log for why A and not B."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(d) Self-check&lt;/strong&gt;: context usage + frozen Constraints have not drifted + cross-gap consistency. If context usage exceeds 80%, time to stop &amp;amp; handoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;(e) Next gap&lt;/strong&gt;: a cue for the autonomous loop to continue.&lt;/p&gt;

&lt;p&gt;A full iteration log per gap runs 100-200 lines, which looks like a lot but is just raw facts, 5-10 minutes to write. Three months later you thank past-you for writing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reverse verification anchor break (mandatory at gap closure)
&lt;/h2&gt;

&lt;p&gt;Positive verification (trace test green + governance green) only proves "current state is not broken." It does not prove the lock has catching power. Reverse verification anchor break fills that gap:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Helper anchor break&lt;/strong&gt;: deliberately break the helper algorithm (e.g., swap &lt;code&gt;cash&lt;/code&gt; first to &lt;code&gt;bonus&lt;/code&gt; first) → the trace test should have at least 1 case red → revert → all green&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance anchor break&lt;/strong&gt;: deliberately remove the caller's exemption comment → governance should turn red → revert → all green&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both passing means "the lock really has catching power." If reverse verification does not turn red, the trace test / governance rule is too lax and needs strengthening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reverse verification surprises
&lt;/h3&gt;

&lt;p&gt;T-022 packaging-task's first reverse verification attempt was "add &lt;code&gt;skipped&lt;/code&gt; as a legal transition," expected to break, but &lt;code&gt;PACKAGING_TASK_STATES&lt;/code&gt; does not include &lt;code&gt;skipped&lt;/code&gt; → &lt;code&gt;INVALID_TO_STATE&lt;/code&gt; still blocks → the test does not turn red.&lt;/p&gt;

&lt;p&gt;The second attempt removed &lt;code&gt;in_progress → completed&lt;/code&gt; legal path, which is a core transition, and the test finally broke.&lt;/p&gt;

&lt;p&gt;Meaning: &lt;strong&gt;the reverse verification target must be a core contract the trace test actually protects, not an edge case&lt;/strong&gt;. If reverse verification does not break, either pick a more central break point or backfill missing test coverage.&lt;/p&gt;




&lt;h2&gt;
  
  
  sql-only-trace special category (when DB logic stands alone)
&lt;/h2&gt;

&lt;p&gt;When business logic lives purely inside the DB (complex triggers, FIFO RPCs, PL/pgSQL stored procedures), the 6 pieces become 5 pieces (no frontend helper) + 1 specialization (SQL test instead of vitest):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- scripts/integration-test/T021-fifo-consume-test.sql&lt;/span&gt;

&lt;span class="c1"&gt;-- Set JWT so RPC passes is_admin / has_operator_capability&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;set_config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'request.jwt.claims'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'{"sub":"test-user","system_role":"super_admin"}'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;jwt_set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;T021&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="n"&gt;v_errors&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[]::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="n"&gt;v_seed_prefix&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'T021-'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;-- ... seed IDs&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. SEED (build fixtures with unique prefix)&lt;/span&gt;
  &lt;span class="c1"&gt;-- 2. RUN (call the RPC under test, fn_consume_for_roasting)&lt;/span&gt;
  &lt;span class="c1"&gt;-- 3. ASSERTIONS (v_errors := array_append(v_errors, ...) on fail)&lt;/span&gt;
  &lt;span class="c1"&gt;-- 4. CLEANUP (explicit DELETE seed, do not rely on transaction)&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;array_length&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;NOTICE&lt;/span&gt; &lt;span class="s1"&gt;'T-021 ALL ASSERTIONS PASSED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;ELSE&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'T-021 FAILED: %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;array_to_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_errors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="c1"&gt;-- best-effort cleanup + re-raise&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="n"&gt;v_seed_prefix&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;RAISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;
&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;T021&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- CLI db query only returns the last SELECT rows; print PASS message here so it shows&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'T-021 PASSED'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;assertions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'FIFO consume invariants'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Engineering details
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;JWT via &lt;code&gt;set_config('request.jwt.claims', ...)&lt;/code&gt;&lt;/strong&gt;: before calling an RLS-gated RPC, set the JWT to simulate super_admin; otherwise permission denied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SEED with unique prefix&lt;/strong&gt;: &lt;code&gt;'T021-' || substr(md5(random()::text), 1, 8)&lt;/code&gt; ensures multiple runs do not interfere. CLI db query cannot wrap in a transaction, so cleanup must be explicit DELETE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best-effort cleanup&lt;/strong&gt;: cleanup inside EXCEPTION must swallow its own errors so the original error message is not masked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final SELECT prints PASS&lt;/strong&gt;: &lt;code&gt;npx supabase db query --linked -f&lt;/code&gt; only returns rows from the last SELECT. RAISE NOTICE is not shown by default. Put the PASS string in the final SELECT to see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RAISE EXCEPTION for full error output&lt;/strong&gt;: on failure, EXCEPTION carries the v_errors array string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registry side
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;parseTraceRegistry()&lt;/code&gt; sees &lt;code&gt;**Type**: sql-only-trace&lt;/code&gt; and &lt;code&gt;**Trace test (SQL)**&lt;/code&gt; and marks the trace &lt;code&gt;isSqlOnly = true&lt;/code&gt;. Governance rule &lt;code&gt;checkTraceTestImportsAnchor&lt;/code&gt; checks &lt;code&gt;isSqlOnly&lt;/code&gt; and skips the import check (SQL has no import semantics).&lt;/p&gt;

&lt;p&gt;The whole parser extension is about 10 lines of code, done as part of the sprint.&lt;/p&gt;

&lt;p&gt;Run command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx supabase db query &lt;span class="nt"&gt;--linked&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; scripts/integration-test/T021-fifo-consume-test.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On failure, EXCEPTION's v_errors print to CLI. On success, the final SELECT prints PASS. The pre-push hook runs this; non-zero exit blocks the push.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  CLI db query limitations
&lt;/h3&gt;

&lt;p&gt;Gotchas of &lt;code&gt;supabase db query --linked -f&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;By default does not display RAISE NOTICE, only returns the last SELECT rows&lt;/li&gt;
&lt;li&gt;Each statement auto-commits, cannot wrap in &lt;code&gt;BEGIN/ROLLBACK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cleanup must be explicit DELETE&lt;/li&gt;
&lt;li&gt;SEED must use a unique prefix to avoid pollution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fix: every sql-only-trace test follows the engineering template above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 27 RPC overload risk
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CREATE OR REPLACE&lt;/code&gt; does not remove other overloads. Changing the signature (including adding a default-NULL parameter) requires first &lt;code&gt;DROP FUNCTION IF EXISTS public.foo(old signature)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Judgement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Signature is fully equivalent (same parameter list, types, order) → no DROP needed&lt;/li&gt;
&lt;li&gt;Any signature difference → must DROP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the sprint, Gap-1 and Gap-2 RPC signatures stayed unchanged, no DROP. Gap-3 changed the FIFO RPC signature (added a new default-NULL parameter), so DROP before CREATE OR REPLACE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trigger: write warning, not RAISE
&lt;/h3&gt;

&lt;p&gt;When the frozen business contract says "do not block, write a warning" (e.g., T-020 roast-level mismatch), the trigger function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Old behavior (migration 177)&lt;/span&gt;
&lt;span class="c1"&gt;-- IF roast_level &amp;lt;&amp;gt; expected THEN&lt;/span&gt;
&lt;span class="c1"&gt;--   RAISE EXCEPTION '...'; -- workstation freezes&lt;/span&gt;
&lt;span class="c1"&gt;-- END IF;&lt;/span&gt;

&lt;span class="c1"&gt;-- New behavior (migration 240)&lt;/span&gt;
&lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;roast_level&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
  &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roasting_orders&lt;/span&gt;
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONCAT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COALESCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;E&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;[ROAST_LEVEL_MISMATCH] expected='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;', got='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roast_level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;related_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inventory_logs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
  &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ROAST_LEVEL_WARNING'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="k"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- continue write, workstation not frozen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trace test pins the warning prefix &lt;code&gt;[ROAST_LEVEL_MISMATCH]&lt;/code&gt;. DB and frontend share the same identifier string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Context window management
&lt;/h3&gt;

&lt;p&gt;Fixing 4 gaps used about 60-70% context (reading migrations + helpers + callers + trace tests + running reverse verification twice).&lt;/p&gt;

&lt;p&gt;Each gap's iteration log section &lt;code&gt;(d) Self-check&lt;/code&gt; estimates context %. At the 80% red line:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Immediately commit completed gaps&lt;/li&gt;
&lt;li&gt;Write a handoff doc to &lt;code&gt;補充資料/CLAUDE_HANDOFF_YYYY-MM-DD_&amp;lt;sprint&amp;gt;.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;/clear&lt;/code&gt; to start a new session, run &lt;code&gt;/goal --resume &amp;lt;sprint&amp;gt;&lt;/code&gt; to continue&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Red line at 80% rather than 90% because of buffer for: writing commit messages, running full audit:all after push, finishing the handoff doc. Pushing through to 90% risks context anxiety mid-closure and partial artifacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Time estimate
&lt;/h3&gt;

&lt;p&gt;Sprint statistics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit map: 1-2h&lt;/li&gt;
&lt;li&gt;Per gap 6-piece: 1-1.5h (including reverse verification)&lt;/li&gt;
&lt;li&gt;Writing iteration log: 5-10 min&lt;/li&gt;
&lt;li&gt;Sprint summary (only at ④): 1h&lt;/li&gt;
&lt;li&gt;Skill extraction (only at ⑤): 1h&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;4 gaps full set: 1h audit + 4-6h fix + 1h summary = &lt;strong&gt;7-9h&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compared to the "fix one at a time" mode: 3-5h per bug × N. ROI ~5-8x (each gap blocks N future bugs).&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-project reusability
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Reusability&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pure-function helper convention (&lt;code&gt;lib/business-rules/&amp;lt;feature&amp;gt;Logic.js&lt;/code&gt; + dependency injection)&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Plain JS module convention, framework-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace test 5-section structure&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Works with Vitest / Jest / Mocha, just swap import syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance rule code&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Node.js script, swap RPC pattern + helper name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Caller exemption comment convention&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure regex text convention, tool-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Registry entry format&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure markdown convention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Iteration log 5 sections&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure markdown record, tool-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reverse verification anchor break process&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Pure process, but requires team / AI cooperation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sql-only-trace variant&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;PostgreSQL-specific (DO block + JWT config), other DBs need adjustment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concrete business contract content&lt;/td&gt;
&lt;td&gt;★☆☆☆☆&lt;/td&gt;
&lt;td&gt;100% business-specific, every project reinvents&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Framework layer 80-90% portable, business contract 0% portable&lt;/strong&gt;. Same ratio as A2's 5 artifacts.&lt;/p&gt;

&lt;p&gt;For the full cross-project reusability matrix see &lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 cross-project reusability&lt;/a&gt; (same-branch next post).&lt;/p&gt;




&lt;h2&gt;
  
  
  When the 6-piece set does not fit
&lt;/h2&gt;

&lt;p&gt;Cases where copy-pasting the 6-piece set across is &lt;strong&gt;not necessarily worth it&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Single gap / single bug fix&lt;/strong&gt;: 1h audit + 6-piece is over-engineering. Just fix and commit. The 6-piece is for sprints with 3+ gaps found in audit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No governance script infrastructure&lt;/strong&gt;: the 6-piece set depends on pre-push hooks + CI enforcement. Without that infrastructure, build &lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2's 5 artifacts&lt;/a&gt; first&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business contracts churn very fast&lt;/strong&gt;: when helper algorithms must change weekly, trace test pinning becomes a drag. Stabilize the business first, then lock&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solo short-term project / first month after launch&lt;/strong&gt;: the future-self-3-months-later does not exist; commit messages + PR descriptions are enough for pinning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-person maintenance + QA / code review pipeline&lt;/strong&gt;: traditional code review already plays part of the governance role. The 6-piece set still works, but marginal value drops. Evaluate ROI&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;My environment (solo maintainer, many cross-layer dependencies, relatively stable business contracts, maintenance period of 6+ months, AI pair-programming) hits all the applicable conditions. Your context, you judge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-b1-offense-en.md"&gt;B1 · Offensive audit, how to periodically audit a whole business flow&lt;/a&gt; (same-topic plain version in this series)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-a2-defense-engineering-en.md"&gt;A2 · Defensive Trace Lock engineering version, 5 artifacts in detail&lt;/a&gt; (same-branch previous post in this series, the companion rot-prevention mechanism)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 · Cross-project reusability matrix + when it does not fit&lt;/a&gt; (same-branch next post in this series)&lt;/li&gt;
&lt;li&gt;&lt;a href="//./trace-lock-b2-offense-engineering-zh.md"&gt;中文版&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (6-piece fix pattern / frozen business contract / reverse verification anchor break / sql-only-trace / incident pinning case / Trace Lock) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer — just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any&lt;br&gt;
of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-b2-offense-engineering-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-b2-offense-engineering-en" rel="noopener noreferrer"&gt;Offensive audit, engineering edition — the 6-piece fix pattern in detail&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Offensive audit — How to periodically audit a whole business flow (for self-taught developers)</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:25:33 +0000</pubDate>
      <link>https://dev.to/dexterlung/offensive-audit-how-to-periodically-audit-a-whole-business-flow-for-self-taught-developers-45f1</link>
      <guid>https://dev.to/dexterlung/offensive-audit-how-to-periodically-audit-a-whole-business-flow-for-self-taught-developers-45f1</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 3 of 9&lt;/p&gt;




&lt;p&gt;The previous post (&lt;a href="//./trace-lock-a1-defense-en.md"&gt;A1 Defensive&lt;/a&gt;) covered "locking down a known cross-layer relationship" (defensive, reactive). This post covers the opposite direction: &lt;strong&gt;proactively running an audit pass to find unprotected chain nodes across a whole business flow, then fixing them all at once&lt;/strong&gt; (offensive, proactive).&lt;/p&gt;

&lt;p&gt;The two mental modes are complementary: A1 is "lock it down after you trip on it," this post is "patrol regularly to find unexploded bombs."&lt;/p&gt;

&lt;p&gt;This post is still the plain-language version for self-taught developers. Engineering details (6-piece fix pattern templates, how to write a governance rule, how to run SQL tests) go into B2 (engineer-facing).&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point: a customer's screenshot of a frozen button
&lt;/h2&gt;

&lt;p&gt;May 25th, midday. Customer screenshot: "The roasting station's 'complete' button is frozen, showing 'roast level mismatch' error."&lt;/p&gt;

&lt;p&gt;I stared at the screen for 30 seconds. Should I fix it the usual way? One line, comment out the RAISE statement, done.&lt;/p&gt;

&lt;p&gt;But this was the 5th "cross-layer contract drift" bug in 6 months. Each one took 3-5 hours, totaling 15-25 hours. Every fix I thought "this should be the last one," then the next month another one appeared.&lt;/p&gt;

&lt;p&gt;I asked Claude one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I've fixed 5 of these in 6 months. Should I stop and look for &lt;strong&gt;all leaks of the same shape&lt;/strong&gt;, fix them all at once?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude proposed: &lt;strong&gt;Spend 1 hour auditing the whole business flow first. List which chain nodes are unprotected. Then decide what to fix first.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the 1-hour audit actually did
&lt;/h2&gt;

&lt;p&gt;Claude and I sketched out the "order to shipment" business flow, listing 11 chain nodes (every step from customer order to shipped package that touches data).&lt;/p&gt;

&lt;p&gt;For each node, I asked 3 questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does this node have a corresponding test?&lt;/strong&gt; (would it get caught if it broke)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does this node have a corresponding governance rule?&lt;/strong&gt; (would it get caught if someone edited it wrong)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this node's "business contract" written down?&lt;/strong&gt; (why is this the logic and not something else)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After running through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🟢 Fully protected: 5 nodes (test + governance + contract all present)&lt;/li&gt;
&lt;li&gt;🟡 Partially protected: 2 nodes (missing one or two)&lt;/li&gt;
&lt;li&gt;🔴 &lt;strong&gt;Completely unprotected BLOCKERs: 4 nodes&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 4 BLOCKERs were:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Order cancel / refund flow (no spec for cash-first vs bonus-first refund)&lt;/li&gt;
&lt;li&gt;Roasting completion stage (no spec for "roast mismatch → block or warn")&lt;/li&gt;
&lt;li&gt;FIFO ingredient consumption (no spec for which bag of the same coffee goes first)&lt;/li&gt;
&lt;li&gt;Packaging station task state (no spec for whether "skipped" is a valid state)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each one was a "next customer to trip on this costs me 3-5 hours" landmine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision Pinning = the most critical ritual of the audit
&lt;/h2&gt;

&lt;p&gt;While listing the BLOCKERs, I noticed something easy to miss: &lt;strong&gt;these BLOCKERs are BLOCKERs not because the code is broken, but because "how the business should work" isn't written down&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Take "should refund go back to cash first or bonus first":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have an answer in my head (cash-first, because the original deduction prioritized bonus, so a cash-first reversal keeps things balanced)&lt;/li&gt;
&lt;li&gt;But this answer isn't written anywhere&lt;/li&gt;
&lt;li&gt;Three months later, I might look at the code and think "this is weird, let me clean it up"&lt;/li&gt;
&lt;li&gt;An AI pair tool sees it and acts on its own intuition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the first output of the audit &lt;strong&gt;isn't fixing code&lt;/strong&gt;. It's &lt;strong&gt;formally writing down these 4 business decisions&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Refund wallet order: original deduction $50 bonus + $30 cash → refund fills cash $30 first, remainder fills bonus $50&lt;/li&gt;
&lt;li&gt;Roasting completion roast mismatch: trigger RAISE → rewrite as warning into notes column, continue completion (avoid freezing the operator station)&lt;/li&gt;
&lt;li&gt;FIFO ingredient consumption: same coffee consumed in &lt;code&gt;created_at ASC&lt;/code&gt; order; inventory invariant: total before minus total after equals consumed&lt;/li&gt;
&lt;li&gt;Packaging task state machine: purely linear pending → in_progress → completed/cancelled, &lt;strong&gt;no&lt;/strong&gt; skipped / partial&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once written down, these decisions transform from "my personal preference" to "system contract." Anyone (future me, AI included) who wants to change them will see them.&lt;/p&gt;

&lt;p&gt;I gave this step a working name: "&lt;strong&gt;Decision Pinning&lt;/strong&gt;" (working name, my own).&lt;/p&gt;




&lt;h2&gt;
  
  
  6-piece fix pattern: every BLOCKER follows the same recipe
&lt;/h2&gt;

&lt;p&gt;When fixing the 4 BLOCKERs, each one applied the same 6-step template:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pure-function helper&lt;/strong&gt;: extract scattered logic into one file, all callers go through it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuse test (trace test)&lt;/strong&gt;: pin the helper's current correct behavior (including the incident-pinning case for Decision Pinning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Registry entry&lt;/strong&gt;: add this trace to the registry mentioned in A1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Governance rule&lt;/strong&gt;: auto-scan for callers not going through the helper, block commits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caller exemption list&lt;/strong&gt;: explicitly list files that have legitimate reasons not to use the helper (avoids permanent red)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iteration log&lt;/strong&gt;: record what was fixed, what the contract is, what not to miss next audit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first BLOCKER took 2 hours to get familiar with the template. Second: 1.5 hours. Third: 1 hour. Fourth: 45 minutes (excluding one special detail for a new category). &lt;strong&gt;Marginal cost decreases each iteration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The whole sprint took 7 hours for 4 BLOCKERs, including writing the sprint summary. Compared to the "fix one at a time" mode (estimated 12-20 hours), &lt;strong&gt;direct-time ROI is about 2-3x&lt;/strong&gt;. But the audit path also prevents N future bugs, so equivalent ROI I estimate at &lt;strong&gt;5-8x&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reverse verification ritual: prove the lock actually catches
&lt;/h2&gt;

&lt;p&gt;After fixing each BLOCKER, I did one thing: &lt;strong&gt;deliberately break the helper to confirm the test actually fails red, and remove the caller exemption to confirm the governance rule actually blocks the commit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sounds redundant? It caught me twice writing "locks that are too loose":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once breaking the test only failed 2 cases because many tests used a single input that didn't exercise the distribution algorithm&lt;/li&gt;
&lt;li&gt;Another time removing governance exemption didn't block, because the exemption regex was too loose and every file passed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reverse verification is the lock for the lock.&lt;/strong&gt; Protection without reverse verification might be "tested but doesn't actually test," worse than no test (gives false confidence).&lt;/p&gt;




&lt;h2&gt;
  
  
  7 things learned across BLOCKERs
&lt;/h2&gt;

&lt;p&gt;The process of fixing all 4 also surfaced "things not to step on next audit":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The 6-piece pattern takes 2 hours the first time, 1.5 hours the second, 45 minutes the fourth&lt;/li&gt;
&lt;li&gt;Pure-DB logic has its own testing method (use Supabase CLI to run SQL, not frontend unit test frameworks). I gave it a category name: &lt;code&gt;sql-only-trace&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Reverse verification is a mandatory ritual, not optional&lt;/li&gt;
&lt;li&gt;Chinese regex has traps (&lt;code&gt;\S{5,}&lt;/code&gt; doesn't necessarily match 5 consecutive Chinese characters)&lt;/li&gt;
&lt;li&gt;Governance docstring scans easily false-positive (a helper mentioning an RPC name gets misclassified as a caller)&lt;/li&gt;
&lt;li&gt;Context window needs active estimation (after each BLOCKER, run a self-estimate of "how much context I burned")&lt;/li&gt;
&lt;li&gt;Postgres function signature changes need a DROP of the old version before adding a default-NULL parameter, otherwise you get an overload conflict&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wrote these 7 into the sprint summary so the next business-flow audit can reuse them directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  A transferable suggestion
&lt;/h2&gt;

&lt;p&gt;If you're a solo dev who pairs with AI a lot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;List your 5 most important business flows&lt;/strong&gt; (checkout / order / inventory / member / payment...)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick one a month&lt;/strong&gt;, spend 1 hour running an audit (11 chain nodes × 3 questions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find BLOCKERs → fix with the 6-piece pattern&lt;/strong&gt; (each one 1-2 hours, gets faster)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every BLOCKER does Decision Pinning&lt;/strong&gt; (this is the real core of the audit, not the code fix)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Every year, do one full audit pass on your core business flows&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Don't wait for the next bug to surface. By the time a bug surfaces, a customer has already tripped on it. The audit catches things before customers do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why A1 (Defensive) + B1 (Offensive) must be used together
&lt;/h2&gt;

&lt;p&gt;With only A1 (reactive lock-down of known traces), protection always trails the bugs. At best you break even.&lt;/p&gt;

&lt;p&gt;With only B1 (proactive audit), the BLOCKERs you find rot if not turned into trace locks. Next month they reappear.&lt;/p&gt;

&lt;p&gt;Used together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;B1 finds unprotected chain nodes&lt;/li&gt;
&lt;li&gt;A1 locks them down&lt;/li&gt;
&lt;li&gt;Next round of B1 finds new candidates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the "dual-blade" approach covered in C1 Offense + Defense combined.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A1 · Defensive Trace Lock — How to lock down a cross-layer relationship (previous in series)&lt;/li&gt;
&lt;li&gt;C1 · Offense + Defense combined — How the two pair up (next in series)&lt;/li&gt;
&lt;li&gt;中文版&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (offensive audit / Decision Pinning / 6-piece fix pattern / sql-only-trace) are working names I gave them myself, not industry-standard terminology&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer, just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-b1-offense-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-b1-offense-en" rel="noopener noreferrer"&gt;Offensive audit — How to periodically audit a whole business flow (for self-taught developers)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Defensive Trace Lock, engineering edition — 5 artifacts in detail</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:24:33 +0000</pubDate>
      <link>https://dev.to/dexterlung/defensive-trace-lock-engineering-edition-5-artifacts-in-detail-4bie</link>
      <guid>https://dev.to/dexterlung/defensive-trace-lock-engineering-edition-5-artifacts-in-detail-4bie</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 6 of 9&lt;/p&gt;




&lt;p&gt;This post is the engineering version of &lt;a href="//./trace-lock-a1-defense-en.md"&gt;A1 Defensive Trace Lock&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 artifacts at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;File type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;The trace directory, hand-written markdown&lt;/td&gt;
&lt;td&gt;&lt;code&gt;文檔/data-source-registry.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Trace test (fuse test)&lt;/td&gt;
&lt;td&gt;A pure-function test that pins the trace's current correct behavior&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend-app/src/__tests__/traces/T{id}-*.trace.test.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Governance rule A&lt;/td&gt;
&lt;td&gt;Checks every registry trace has a corresponding test file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Governance rule B&lt;/td&gt;
&lt;td&gt;Checks the trace test actually imports the declared anchor&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;AI reminder skill&lt;/td&gt;
&lt;td&gt;When AI touches trace nodes, auto-triggers the 5-step checklist&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/skills/trace-lock-modify/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artifact 1: Registry markdown format
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Each trace is a &lt;code&gt;### T-{id}: title&lt;/code&gt; block. Fields use the &lt;code&gt;**Field Name**: value&lt;/code&gt; convention, which makes them easy to grab with regex. Here is a trimmed version of the real T-001:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;### T-001: Roasted bean stock grams → POS variant orderable count&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Type**&lt;/span&gt;: data-flow trace
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Status**&lt;/span&gt;: locked (2026-05-25)
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Anchor SSOT**&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;`frontend-app/src/lib/business-rules/variantInventory.js`&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace test**&lt;/span&gt;: &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;`frontend-app/src/__tests__/traces/T001-variant-inventory.trace.test.js`&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="sx"&gt;../path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Trace nodes**&lt;/span&gt; (write side to display side):
&lt;span class="p"&gt;  1.&lt;/span&gt; &lt;span class="gs"&gt;**DB column**&lt;/span&gt;: &lt;span class="sb"&gt;`roasted_batches.remaining_weight`&lt;/span&gt;
&lt;span class="p"&gt;  2.&lt;/span&gt; &lt;span class="gs"&gt;**DB trigger**&lt;/span&gt;: &lt;span class="sb"&gt;`trg_sync_products_stock_virtual_from_batch_iu`&lt;/span&gt;
&lt;span class="p"&gt;  3.&lt;/span&gt; &lt;span class="gs"&gt;**DB column**&lt;/span&gt;: &lt;span class="sb"&gt;`products.stock_virtual_grams`&lt;/span&gt;
&lt;span class="p"&gt;  4.&lt;/span&gt; &lt;span class="gs"&gt;**Frontend fetch**&lt;/span&gt;: useProducts.js fetchProducts
&lt;span class="p"&gt;  5.&lt;/span&gt; &lt;span class="gs"&gt;**SSOT helper**&lt;/span&gt;: variantInventory.getVariantAvailableQuantity
&lt;span class="p"&gt;  6.&lt;/span&gt; &lt;span class="gs"&gt;**Entry A (modal)**&lt;/span&gt;: ProductVariantModal.variantInventoryLimit
&lt;span class="p"&gt;  7.&lt;/span&gt; &lt;span class="gs"&gt;**Entry B (checkout)**&lt;/span&gt;: AdminPOS.cartStockSafety
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Related incident**&lt;/span&gt;: San Agustin drip-bag-only-1-pack issue
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Last edited**&lt;/span&gt;: 2026-05-25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why fields are sliced this way
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Anchor SSOT&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trace nodes&lt;/strong&gt; 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."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entry points&lt;/strong&gt; are the multiple entry pathways to the same anchor SSOT. For example, T-001's anchor &lt;code&gt;variantInventory.js&lt;/code&gt; 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?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last edited&lt;/strong&gt; 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."&lt;/p&gt;

&lt;h3&gt;
  
  
  Design tradeoff: why not YAML or JSON
&lt;/h3&gt;

&lt;p&gt;I considered YAML but rejected it. Reasons:&lt;/p&gt;

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

&lt;p&gt;The cost is that parsing is ~30% trickier than YAML (regex is more error-prone than &lt;code&gt;yaml.parse&lt;/code&gt;). I accept the tradeoff because the registry has a practical upper bound of ~30 traces per project, so parsing complexity is also bounded.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artifact 2: Trace test 5-section structure
&lt;/h2&gt;

&lt;p&gt;The T-001 trace test contains 5 &lt;code&gt;describe&lt;/code&gt; blocks. Each section pins one semantic facet of the trace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vitest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;getVariantWeightPerPack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;getVariantAvailableQuantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;resolveCartItemVariantFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../lib/business-rules/variantInventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// ← import the anchor SSOT&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-001 trace: variant weight contract&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Section 1 */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-001 trace: variant available quantity (incident pinning)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Section 2 */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-001 trace: edge cases&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Section 3 */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-001 trace: cart item variant format resolution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Section 4 */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;T-001 trace: SSOT cross-entry consistency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* Section 5 */&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What each section pins
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Section 1, Contract.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 2, Incident pinning.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 3, Edge cases.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 4, Reverse resolution.&lt;/strong&gt; If the trace requires "reversing from an entry point back to a format the anchor understands," this section tests reverse resolution. For T-001, &lt;code&gt;resolveCartItemVariantFormat&lt;/code&gt; derives variant format from a cart item object.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section 5, Cross-entry consistency.&lt;/strong&gt; 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."&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why importing the anchor is mandatory
&lt;/h3&gt;

&lt;p&gt;Artifact 4 (governance rule B) checks that the test file's first &lt;code&gt;import&lt;/code&gt; actually points at the anchor path declared in the registry.&lt;/p&gt;

&lt;p&gt;Reason: if the trace test doesn't import the anchor, what it's testing is "&lt;strong&gt;a copy-pasted logic snippet inside the test file&lt;/strong&gt;," not the SSOT. When the anchor later changes, the test can't see it. The trace rots while the test stays green.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artifacts 3 &amp;amp; 4: governance rule code walkthrough
&lt;/h2&gt;

&lt;p&gt;The two governance rules live in &lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt; and run on every pre-push or CI cycle. The core is &lt;code&gt;parseTraceRegistry()&lt;/code&gt;, which extracts markdown into an object array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseTraceRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registryPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;文檔/data-source-registry.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registryPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;registryPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Only parse the content between ## Critical Traces and the next ## heading&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sectionMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/##&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*Critical Traces&lt;/span&gt;&lt;span class="se"&gt;[\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?(?=\n&lt;/span&gt;&lt;span class="sr"&gt;##&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;|$&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sectionMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;section&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sectionMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;traceBlocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;section&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sr"&gt;###&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(?=&lt;/span&gt;&lt;span class="sr"&gt;T-&lt;/span&gt;&lt;span class="se"&gt;\d&lt;/span&gt;&lt;span class="sr"&gt;+:&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;traceBlocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^T-&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;$/m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;idMatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="c1"&gt;// ... extract Anchor SSOT / Trace test / Type fields&lt;/span&gt;
    &lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isSqlOnly&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few engineering details:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;[\s\S]*?&lt;/code&gt;, not &lt;code&gt;.*?&lt;/code&gt;.&lt;/strong&gt; JS regex doesn't match newlines with &lt;code&gt;.&lt;/code&gt; by default. Markdown blocks span multiple lines, so &lt;code&gt;[\s\S]&lt;/code&gt; is required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;(?=\n##\s|$)&lt;/code&gt; as the terminator.&lt;/strong&gt; The lookahead ensures parsing stops at the next &lt;code&gt;##&lt;/code&gt; heading or end-of-file. Without it, the parser would scoop up unrelated sections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.slice(1)&lt;/code&gt; skips the prelude.&lt;/strong&gt; The first chunk from &lt;code&gt;split&lt;/code&gt; is the content between the &lt;code&gt;## Critical Traces&lt;/code&gt; heading and the first &lt;code&gt;###&lt;/code&gt;. That's the section preamble, not a trace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;isSqlOnly carveout.&lt;/strong&gt; T-021 (FIFO consumption trace) is sql-only. Its test is a &lt;code&gt;.sql&lt;/code&gt; file, not &lt;code&gt;.js&lt;/code&gt;. The Type field is marked &lt;code&gt;sql-only-trace&lt;/code&gt; and rule B skips the import check (because SQL tests have no import concept).&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule A: every trace has a corresponding test file
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkTraceRegistryTestCoverage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseTraceRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`T-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; registry has no Trace test field`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;absTestPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;existsSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absTestPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`T-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; declared Trace test file does not exist`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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).&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule B: trace test imports its declared anchor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkTraceTestImportsAnchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trace&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;traces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;testPath&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isSqlOnly&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;testSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;absTestPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchorBasename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;anchorPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;importPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="s2"&gt;`(?:from|require\s*\()\s*['"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;][^'"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;]*&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;anchorBasename&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(?:\.[jt]s)?['"&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;g&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;importPattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testSource&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`T-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;trace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; trace test must import its declared Anchor SSOT`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Loose match on the anchor basename (path-relative-flexible) so any of &lt;code&gt;import { ... } from '...variantInventory(.js)'&lt;/code&gt; or &lt;code&gt;require(...)&lt;/code&gt; passes. &lt;code&gt;String.raw&lt;/code&gt; avoids backslash escape hell.&lt;/p&gt;

&lt;p&gt;Both rules combined: roughly 60 lines of code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Artifact 5: the AI reminder skill
&lt;/h2&gt;

&lt;p&gt;The skill is a markdown file. The frontmatter &lt;code&gt;description&lt;/code&gt; declares the trigger condition. The body holds the 5-step audit checklist. Claude Code auto-loads it when a task description matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trace-lock-modify&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Use&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;modifying&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;any&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;listed&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;in&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;文檔/data-source-registry.md&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Critical&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Traces&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;的&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Anchor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;SSOT&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Trace&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Entry&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;points.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Cross-layer&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trace&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;guardrail.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;AI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trace's&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chain&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;trace&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pin&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;baseline&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;re-run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;after&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;edit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;+&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;remind&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;bump&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Last&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;edited."&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few engineering details:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The description must start with "Use when ...".&lt;/strong&gt; Claude Code uses this phrasing to judge trigger conditions. Other phrasings have measurably lower trigger rates (observed across 4 skill rewrites).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;List specific file paths.&lt;/strong&gt; The description directly lists the three categories (&lt;code&gt;Anchor SSOT / Trace nodes / Entry points&lt;/code&gt;). When AI sees a task description that mentions these paths, the match rate is high.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 5-step checklist lives in the body.&lt;/strong&gt; 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.).&lt;/p&gt;

&lt;h3&gt;
  
  
  What each of the 5 steps enforces
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Confirm which trace the file belongs to (grep the registry)&lt;/li&gt;
&lt;li&gt;List the trace's full chain to the user (even if AI already knows, the user might not remember)&lt;/li&gt;
&lt;li&gt;Run the trace test before editing (record the baseline)&lt;/li&gt;
&lt;li&gt;Re-run the trace test after editing (confirm nothing broke)&lt;/li&gt;
&lt;li&gt;Update the &lt;code&gt;Last edited&lt;/code&gt; field in the registry&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Engineering considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Parser tolerance
&lt;/h3&gt;

&lt;p&gt;Regex parsing has 3 common failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Markdown links and plain text coexist.&lt;/strong&gt; Both &lt;code&gt;**Anchor SSOT**: [\&lt;/code&gt;path&lt;code&gt;](url)&lt;/code&gt; and &lt;code&gt;**Anchor SSOT**: path&lt;/code&gt; need to be recognized. My regex uses &lt;code&gt;\[?&lt;/code&gt; / &lt;code&gt;\]?&lt;/code&gt; to accept either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Annotations after field values.&lt;/strong&gt; Field names can be followed by emoji or markers. &lt;code&gt;/\*\*Type\*\*:\s*&lt;/code&gt; plus a &lt;code&gt;?&lt;/code&gt; accepts cases like &lt;code&gt;**Type**: data-flow ⚠️&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty fields.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Cross-platform paths
&lt;/h3&gt;

&lt;p&gt;The Node.js script runs on Windows, Mac, and Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repoRoot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dirname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fileURLToPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/scripts$/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registryPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;repoRoot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;文檔/data-source-registry.md&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never hardcode &lt;code&gt;'/'&lt;/code&gt; or &lt;code&gt;'\\'&lt;/code&gt;. Always use &lt;code&gt;path.join&lt;/code&gt;. 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 (&lt;code&gt;chcp 65001&lt;/code&gt; + &lt;code&gt;LC_ALL=C.UTF-8&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  CI integration
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scripts/governance-guard.mjs&lt;/code&gt; is written as a standalone entry. Pre-push hooks and GitHub Actions both run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# pre-push hook&lt;/span&gt;
node scripts/governance-guard.mjs &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;violations&lt;/code&gt; array collects all violation messages. At the end, it prints them and exits with code 1. Key point: &lt;strong&gt;all trace rules are BLOCKER tier&lt;/strong&gt; (not advisory). They block the push. Reason: traces are by design "relationships that should already be stable." Advisory equals no enforcement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Incremental cost
&lt;/h3&gt;

&lt;p&gt;Each new trace (not the first one, but a new one added with the framework already in place):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write the registry block: ~10 minutes&lt;/li&gt;
&lt;li&gt;Write the trace test (5 sections): 20-30 minutes&lt;/li&gt;
&lt;li&gt;Run the two governance rules to verify: 30 seconds&lt;/li&gt;
&lt;li&gt;Update &lt;code&gt;Last edited&lt;/code&gt;: 30 seconds&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total 30-45 minutes per trace. Consistent with the estimate from A1.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-project portability
&lt;/h2&gt;

&lt;p&gt;How portable each artifact is when copied to a different project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Portability&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registry markdown format&lt;/td&gt;
&lt;td&gt;★★★★★&lt;/td&gt;
&lt;td&gt;Pure convention, project-independent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trace test 5-section structure&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Vitest / Jest / Mocha all work; only the import syntax changes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Governance rule code&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Node.js script, replace the registry path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI reminder skill format&lt;/td&gt;
&lt;td&gt;★★★★☆&lt;/td&gt;
&lt;td&gt;Claude Code skill format; other AI tools use different syntax&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Specific trace contents&lt;/td&gt;
&lt;td&gt;★☆☆☆☆&lt;/td&gt;
&lt;td&gt;100% business-specific; reinvented per project&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In other words: &lt;strong&gt;the framework layer is ~80% portable; the content layer is 0% portable.&lt;/strong&gt; This ratio is consistent with the "80% framework portable, 20% content reinvented" estimate from &lt;a href="//./trace-lock-c1-combo-en.md"&gt;C1 Offense + Defense combined&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Detailed cross-project reuse analysis is in &lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 Cross-project reuse matrix&lt;/a&gt; (next post in the same series branch).&lt;/p&gt;




&lt;h2&gt;
  
  
  When this doesn't pay off
&lt;/h2&gt;

&lt;p&gt;Lifting these 5 artifacts wholesale &lt;strong&gt;isn't worth it&lt;/strong&gt; when:&lt;/p&gt;

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

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-a1-defense-en.md"&gt;A1 · Defensive Trace Lock, how to lock one cross-layer relationship&lt;/a&gt; (same topic, plain-language version)&lt;/li&gt;
&lt;li&gt;
&lt;a href="//./trace-lock-c2-combo-engineering-en.md"&gt;C2 · Cross-project reuse matrix + when not to use it&lt;/a&gt; (same series branch, next post)&lt;/li&gt;
&lt;li&gt;&lt;a href="//./trace-lock-a2-defense-engineering-zh.md"&gt;Chinese version&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer — just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-a2-defense-engineering-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-a2-defense-engineering-en" rel="noopener noreferrer"&gt;Defensive Trace Lock, engineering edition — 5 artifacts in detail&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>Defensive Trace Lock — How to lock down a single cross-layer relationship (for self-taught developers)</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:24:03 +0000</pubDate>
      <link>https://dev.to/dexterlung/defensive-trace-lock-how-to-lock-down-a-single-cross-layer-relationship-for-self-taught-3l45</link>
      <guid>https://dev.to/dexterlung/defensive-trace-lock-how-to-lock-down-a-single-cross-layer-relationship-for-self-taught-3l45</guid>
      <description>&lt;p&gt;&lt;strong&gt;May 2026&lt;/strong&gt; · Series "Trace Lock — Governance notes from pairing with AI to write code" · Post 2 of 9&lt;/p&gt;




&lt;p&gt;The previous post (&lt;a href="//./trace-lock-e-meta-en.md"&gt;E meta&lt;/a&gt;) mentioned that Claude and I gradually identified 4 patterns over 6 months. The third one we call "Trace Lock" (working name, my own). This post unpacks what Trace Lock actually looks like and how you lock down a single trace.&lt;/p&gt;

&lt;p&gt;This post is the plain-language version for self-taught developers. If you're an engineer and want code-level detail (which file goes where, how the SQL looks, how the governance rule is written), see the A2 engineering post (same topic, different audience).&lt;/p&gt;

&lt;p&gt;Goal of this post: someone who has never written a governance rule or a pinning test should finish reading and know "Trace Lock is these 5 artifacts, why each one is needed, and roughly how long locking one trace takes."&lt;/p&gt;




&lt;h2&gt;
  
  
  First, the bug that triggered this whole thing
&lt;/h2&gt;

&lt;p&gt;May 25th, early morning. A customer messaged: "I'm trying to order 2 drip-bags of the San Agustin coffee, why does the system say max 1?"&lt;/p&gt;

&lt;p&gt;Each drip-bag is 103 grams. The backend roasted-bean barrel had 244 grams left, so 2 bags should fit. I went to check why the system calculated 1.&lt;/p&gt;

&lt;p&gt;Tracing through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend correctly wrote 244 grams to &lt;code&gt;products.stock_virtual_grams&lt;/code&gt; ✅&lt;/li&gt;
&lt;li&gt;Frontend correctly read 244 grams ✅&lt;/li&gt;
&lt;li&gt;But the frontend divided 244 by &lt;strong&gt;the half-pound product's gram weight&lt;/strong&gt; (227g) = 1 bag ❌&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The half-pound product had 4 variants: half-pound whole bag, quarter pound, one pound, drip-bag. The bug: the system didn't differentiate by variant. It always used "the half-pound product's gram weight" to compute orderable count. Drip-bags should use their own gram weight (103g).&lt;/p&gt;

&lt;p&gt;Fixing this bug: 30 minutes. But it raised a deeper question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why have I hit 5 of these "cross-layer contract drift" bugs in 6 months?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Order payment_status didn't sync with status&lt;/li&gt;
&lt;li&gt;Top-up plans got pushed into the packaging worksheet&lt;/li&gt;
&lt;li&gt;13 coffee products showed "out of stock" to consumers (admin view was fine)&lt;/li&gt;
&lt;li&gt;A VVIP customer topped up $5000 and got the wrong bonus amount&lt;/li&gt;
&lt;li&gt;After adding a medium_dark roast tier, the UI showed the English enum code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common factor: &lt;strong&gt;data flows from a write-side to a render-side, and somewhere a layer forgot to sync&lt;/strong&gt;. Teams have code review, SRE, QA, PM to catch these. Solo devs don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cross-layer relationships live in your head = forgotten in 3 months
&lt;/h2&gt;

&lt;p&gt;I noticed something: every time I fix one of these bugs, I spend 30-60 minutes &lt;strong&gt;from scratch&lt;/strong&gt; grepping the code to re-figure-out "which write-side, which intermediate layers, who renders."&lt;/p&gt;

&lt;p&gt;I fixed a similar one 3 months ago, but the details are gone. Back then, the "cross-layer relationship" only lived in my head's working memory. Session ends, it's cleared.&lt;/p&gt;

&lt;p&gt;AI pair-programming tools work the same way. Every new conversation, Claude starts from zero. It doesn't automatically know "last time you changed A and forgot B and it cost you 3 days."&lt;/p&gt;

&lt;p&gt;So this time Claude and I thought: &lt;strong&gt;can we pull the cross-layer relationship out of my head and store it as a codebase-visible artifact, so 3-months-later me (and the next new-session AI) sees it the moment they open the project?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's Trace Lock.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 5 artifacts
&lt;/h2&gt;

&lt;p&gt;Each trace (one cross-layer relationship) is composed of 5 artifacts:&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifact 1: Registry
&lt;/h3&gt;

&lt;p&gt;A markdown table. Each row is one trace, columns include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ID (like T-001)&lt;/li&gt;
&lt;li&gt;Anchor SSOT (which function/field is this trace's "source of truth")&lt;/li&gt;
&lt;li&gt;Trace nodes (the 8-12 files/functions the data passes through on its way from source to render)&lt;/li&gt;
&lt;li&gt;Why (why this trace exists, historical context)&lt;/li&gt;
&lt;li&gt;Last edited (when was it last touched)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The registry isn't documentation. It's an &lt;strong&gt;index&lt;/strong&gt;. Three months from now, when I want to find "how the half-pound drip-bag orderable count is calculated," I look in the registry and know which function is the anchor.&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifact 2: Fuse test (pinning test)
&lt;/h3&gt;

&lt;p&gt;A pure-function test that &lt;strong&gt;pins down the anchor's currently-correct behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Note: "currently-correct" not "what it should be." This distinction matters. More on it below.&lt;/p&gt;

&lt;p&gt;When someone changes the anchor in the future, the test fails red, giving them a chance to ask "was this change intentional?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifacts 3 + 4: Two auto-inspectors (governance rules)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Inspector A&lt;/strong&gt;: every trace in the registry must have a corresponding test file (prevents "registered but no test written")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspector B&lt;/strong&gt;: every test file must actually import the anchor function it claims to test (prevents the test from &lt;strong&gt;rotting&lt;/strong&gt; into "testing something else but claiming to test the anchor")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both inspectors run automatically before commit / push. If they fail, the push doesn't go through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Artifact 5: AI reminder (skill)
&lt;/h3&gt;

&lt;p&gt;A markdown file that says: "When you (the AI) are about to edit any node listed in the trace nodes, you must first do these 5 steps: list the chain → run the current test → make the change → run the test again → update the registry's Last edited."&lt;/p&gt;

&lt;p&gt;The AI tool I use (Claude Code) supports skill auto-triggering. It sees the file I'm about to edit is in a trace nodes list and proactively triggers this skill. If you use a different AI tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cursor: write into &lt;code&gt;.cursorrules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Aider: write into &lt;code&gt;CONVENTIONS.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;ChatGPT: put it in system prompt or custom instructions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same principle. Make the AI pair tool automatically inherit your governance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why all 5 are needed
&lt;/h2&gt;

&lt;p&gt;I thought "registry + test" would be enough at first. Doing it, I realized any missing piece breaks the chain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No registry → tests have no index, 3 months later nobody knows which traces have been locked&lt;/li&gt;
&lt;li&gt;No test → registry is just text, nobody reads it&lt;/li&gt;
&lt;li&gt;No Inspector A → registered without a test, nothing blocks it&lt;/li&gt;
&lt;li&gt;No Inspector B → tests "rot" into testing something else, giving a false sense of safety&lt;/li&gt;
&lt;li&gt;No AI reminder → new AI conversations don't trigger the governance flow automatically&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How long does locking one trace take?
&lt;/h2&gt;

&lt;p&gt;First-time setup: 4 hours (build the registry format, write the first trace, write two inspectors, write the skill).&lt;/p&gt;

&lt;p&gt;Each additional trace: 30-45 minutes (add a row to the registry, write the corresponding test, do reverse verification).&lt;/p&gt;

&lt;p&gt;Claude and I calculated the break-even point: &lt;strong&gt;after locking the 7th trace, total time spent is less than "grepping from scratch each time."&lt;/strong&gt; I've now locked 13.&lt;/p&gt;




&lt;h2&gt;
  
  
  A common trap: fuse tests are NOT unit tests
&lt;/h2&gt;

&lt;p&gt;The most common mistake writing fuse tests: &lt;strong&gt;treating them like unit tests&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests pin down "what it should be"&lt;/strong&gt;: find a bug, change both the test and the code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuse tests pin down "what is currently correct"&lt;/strong&gt;: find anchor behavior that contradicts intuition, &lt;strong&gt;pin the current behavior first, don't fix it in passing&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: while writing one trace, I found that &lt;code&gt;getRoastLevelLabel(null)&lt;/code&gt; returned "medium roast" instead of "no roast label." Gut reaction: "this is a bug, fix it."&lt;/p&gt;

&lt;p&gt;But the fuse test's job is not to correct "currently-designed choices." Its job is to &lt;strong&gt;prevent the current behavior from changing unintentionally in the future&lt;/strong&gt;. Whether to change the &lt;code&gt;null → medium roast&lt;/code&gt; design is a separate discussion (should the UI distinguish "unfilled" from "medium roast"), not something the AI casually decides.&lt;/p&gt;

&lt;p&gt;So the test I wrote was: &lt;code&gt;expect(getRoastLevelLabel(null)).toBe('medium')&lt;/code&gt;. And I added a comment in the registry: "Currently designed choice: no roast data = default to medium. To distinguish 'unfilled' vs 'medium', that's a product discussion, not an anchor change."&lt;/p&gt;

&lt;p&gt;If anyone (or AI) wants to change the anchor in the future, the test fails red, forcing them to see the comment and realize "this isn't a bug, it's a design choice."&lt;/p&gt;




&lt;h2&gt;
  
  
  Always do reverse verification
&lt;/h2&gt;

&lt;p&gt;After locking each trace, I deliberately break the anchor function (return wrong direction, return null) to confirm the test &lt;strong&gt;actually fails red&lt;/strong&gt;. Then revert, confirm the test is green again.&lt;/p&gt;

&lt;p&gt;Sounds redundant? It caught me twice writing "tests that are too loose":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Once only 2 cases failed because many tests used a single input that didn't exercise the distribution algorithm&lt;/li&gt;
&lt;li&gt;Another time 5 cases failed (normal water level)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reverse verification is the fuse for the fuse.&lt;/strong&gt; A test without reverse verification might be "tested but doesn't actually test," worse than no test at all (gives false confidence).&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is especially effective for AI pair-programming
&lt;/h2&gt;

&lt;p&gt;Claude and I have noticed two AI failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;AI doesn't know some invariant&lt;/strong&gt;: e.g., "VIP tier can only be customer/vip/vvip/svip"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI knows but is misled by the user&lt;/strong&gt;: e.g., I say "add a 'gold' tier," the AI doesn't check governance and just adds it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Trace Lock catches both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mode 1 → registry + skill surface the rules to the AI; the AI reads CLAUDE.md and triggers the skill at session start&lt;/li&gt;
&lt;li&gt;Mode 2 → the two inspectors block commits even if the AI was misled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Compared to "please make the AI more careful" (unactionable), "write the rule in .mjs and run it automatically" is an actionable engineering intervention.&lt;/p&gt;




&lt;h2&gt;
  
  
  A transferable suggestion
&lt;/h2&gt;

&lt;p&gt;If you're a solo dev who pairs with AI a lot, ask yourself 3 questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Which 5-10 "if I change A I must also change B" chains exist in your codebase?&lt;/strong&gt; (those are your trace candidates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How are these chains currently protected?&lt;/strong&gt; (tests? docs? personal memory? None are enough)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tomorrow when you /clear and start a new conversation, will your AI remember these?&lt;/strong&gt; (probably not)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If no → you need Trace Lock (or something like it).&lt;/p&gt;

&lt;p&gt;Minimum viable version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a "Critical Traces" table to your docs (paper and pen works)&lt;/li&gt;
&lt;li&gt;Write 1 fuse test pinning the first trace&lt;/li&gt;
&lt;li&gt;Write 1 script that scans the registry to confirm the test exists&lt;/li&gt;
&lt;li&gt;Add the script to a pre-commit hook&lt;/li&gt;
&lt;li&gt;Write a skill (or &lt;code&gt;.cursorrules&lt;/code&gt; / &lt;code&gt;CONVENTIONS.md&lt;/code&gt;) telling the AI "before changing a trace, run the test first"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 matters most. Without a reminder, the AI doesn't proactively trigger the governance flow, and the first 4 steps just sit there decoratively.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;E · meta — How we found the methodology through AI conversations (previous in series)&lt;/li&gt;
&lt;li&gt;A2 · Defensive Trace Lock Engineering Edition — 5 artifacts implementation detail (same topic, engineer-facing depth)&lt;/li&gt;
&lt;li&gt;中文版&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  About this post
&lt;/h2&gt;

&lt;p&gt;This post is an organized record of conversations I had with Claude (an AI pair-programming tool)&lt;br&gt;
during May 2026. I noticed some patterns worth keeping for my own future reference,&lt;br&gt;
so I asked Claude to help structure them into writing.&lt;/p&gt;

&lt;p&gt;A few things I'm &lt;strong&gt;not&lt;/strong&gt; claiming:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terms used in this post (Trace Lock / fuse test / auto-inspector / AI reminder) are working names I gave them myself, not industry-standard terminology. Pinning test is a concept Michael Feathers wrote about in "Working Effectively with Legacy Code," but my usage may not be rigorous&lt;/li&gt;
&lt;li&gt;My system has a specific shape (solo-maintained, many cross-layer dependencies, ambiguous business contracts). These patterns may not apply to your context&lt;/li&gt;
&lt;li&gt;I'm not a software engineer, just a barista who pairs with AI to write code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a professional engineer spots misuse, or there's already a more standard name for any of these concepts, &lt;strong&gt;I genuinely welcome corrections&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;本文原載於我的部落格：&lt;a href="https://coffeeshooters.com/content/trace-lock-a1-defense-en?utm_source=devto&amp;amp;utm_medium=social&amp;amp;utm_campaign=blog-trace-lock-a1-defense-en" rel="noopener noreferrer"&gt;Defensive Trace Lock — How to lock down a single cross-layer relationship (for self-taught developers)&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>solodev</category>
    </item>
    <item>
      <title>CREATE OR REPLACE didn't replace: one optional parameter, and my API 400'd in production</title>
      <dc:creator>Dexterlung</dc:creator>
      <pubDate>Sun, 26 Jul 2026 09:33:11 +0000</pubDate>
      <link>https://dev.to/dexterlung/create-or-replace-didnt-replace-one-optional-parameter-and-my-api-400d-in-production-39jk</link>
      <guid>https://dev.to/dexterlung/create-or-replace-didnt-replace-one-optional-parameter-and-my-api-400d-in-production-39jk</guid>
      <description>&lt;p&gt;A button in my admin panel stopped working. It had worked the day before. I hadn't touched it.&lt;/p&gt;

&lt;p&gt;I run a small coffee e-commerce platform, alone. One of the things it does is issue signed links — the customer gets a link, opens it, confirms they received their order. There are three entry points: a button in the admin order list, a button that pushes the link to the customer over LINE, and a button inside the customer's own order page.&lt;/p&gt;

&lt;p&gt;All three broke at once. All three showed the same toast: &lt;em&gt;"Couldn't generate the confirmation link, please try again later."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Which told me nothing at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error was there. My frontend was eating it.
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;catch&lt;/code&gt; block behind that button looked like every &lt;code&gt;catch&lt;/code&gt; block I've ever written:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Could not create confirmation link&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="nf"&gt;setLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caller only sees &lt;code&gt;null&lt;/code&gt;, so it throws the generic toast. The actual message never reaches a human.&lt;/p&gt;

&lt;p&gt;I found the real error the way you always find it — DevTools → Network → click the failing &lt;code&gt;rpc/...&lt;/code&gt; request → read the response body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"42725"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function fn_issue_order_action_token(...) is not unique"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;42725&lt;/code&gt;. Function is not unique. Postgres was telling me there were &lt;strong&gt;two&lt;/strong&gt; functions with that name, and it refused to guess which one I meant.&lt;/p&gt;

&lt;p&gt;I only ever wrote one.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;CREATE OR REPLACE FUNCTION&lt;/code&gt; does not replace your function
&lt;/h2&gt;

&lt;p&gt;Here is the thing I understood wrong for about six months.&lt;/p&gt;

&lt;p&gt;The original function had three parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;fn_issue_order_action_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_order_id&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_action&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_expires_minutes&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later I added support for partial pickups, which needed to carry some extra data along with the token. So I added one parameter — optional, defaulted, entirely backward compatible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;fn_issue_order_action_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;p_order_id&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_action&lt;/span&gt;         &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_expires_minutes&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;p_metadata&lt;/span&gt;       &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;   &lt;span class="c1"&gt;-- &amp;lt;- the whole bug&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;RETURNS&lt;/span&gt; &lt;span class="n"&gt;jsonb&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every existing caller passes three arguments. The fourth defaults to NULL. Nothing breaks. That's the reasoning, and it's wrong.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CREATE OR REPLACE FUNCTION&lt;/code&gt; replaces a function &lt;strong&gt;only if the identity arguments match exactly&lt;/strong&gt;. &lt;code&gt;(text, text, integer, jsonb)&lt;/code&gt; is not &lt;code&gt;(text, text, integer)&lt;/code&gt;. So it didn't replace anything. It created a second function, and now the database held both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oid 109091 | (text, text, integer)          &amp;lt;- from an old migration
oid 108290 | (text, text, integer, jsonb)   &amp;lt;- the "replacement"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then PostgREST called it with three arguments. The 3-arg version matches. The 4-arg version &lt;em&gt;also&lt;/em&gt; matches, because its fourth parameter has a default. Two candidates, no tiebreaker, &lt;code&gt;42725&lt;/code&gt;, HTTP 400.&lt;/p&gt;

&lt;p&gt;The symptom — "worked in the last PR, started 400ing after I added a parameter" — is genuinely hard to connect to the cause, because the parameter you added is the one nobody is passing.&lt;/p&gt;

&lt;p&gt;The fix is thirty seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_issue_order_action_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;NOTIFY&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reload schema'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing here. Get the exact signature from &lt;code&gt;pg_get_function_identity_arguments&lt;/code&gt; rather than typing it from memory — the &lt;code&gt;DROP&lt;/code&gt; silently no-ops if you get one type wrong, and &lt;code&gt;IF EXISTS&lt;/code&gt; means you won't even be told:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_get_function_identity_arguments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_proc&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;proname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'fn_issue_order_action_token'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the &lt;code&gt;NOTIFY pgrst&lt;/code&gt; matters if you're on PostgREST/Supabase — it caches the schema, so without it the API keeps serving the ambiguity you just resolved.&lt;/p&gt;

&lt;p&gt;That's the bug. If the post ended here it would be a fine TIL. It doesn't end here, because I'd already fixed this bug. Twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rule, then guard, then it came back anyway
&lt;/h2&gt;

&lt;p&gt;Pulling &lt;code&gt;git log&lt;/code&gt; afterward, this class of bug had visited me on a schedule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;What I did about it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Feb 27&lt;/td&gt;
&lt;td&gt;Return-type conflict (&lt;code&gt;42P13&lt;/code&gt;), needed a &lt;code&gt;DROP&lt;/code&gt; first&lt;/td&gt;
&lt;td&gt;Fixed it inline. Forgot about it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 17&lt;/td&gt;
&lt;td&gt;Changed an &lt;code&gt;OUT&lt;/code&gt; parameter on the topup RPC — same trap&lt;/td&gt;
&lt;td&gt;Fixed it inline again, still relying on remembering&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 20&lt;/td&gt;
&lt;td&gt;The signed-link RPC, ambiguous overload&lt;/td&gt;
&lt;td&gt;Fixed it, &lt;strong&gt;and finally wrote an automated check&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May 26&lt;/td&gt;
&lt;td&gt;The signed-link RPC. Again.&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three months between the first and the second, so I never connected them. Then two in one week, which is what it took for me to stop patching and write a static check: scan every migration, if it changes a function signature without a preceding &lt;code&gt;DROP FUNCTION&lt;/code&gt;, block the commit.&lt;/p&gt;

&lt;p&gt;Six days later the same RPC broke the same way.&lt;/p&gt;

&lt;p&gt;I want to be precise about why, because it's the part I got wrong and it's the part that generalizes.&lt;/p&gt;

&lt;p&gt;My check asked: &lt;em&gt;does this new migration introduce an overload without dropping the old one?&lt;/em&gt; Every migration in the repo passed. It kept passing while production was broken — because nothing new had been written. What happened was that an &lt;strong&gt;old&lt;/strong&gt; migration — the one that created the 3-arg version, written months earlier and long since superseded — got applied again. Somewhere. Somehow. It re-created a function I had already dropped.&lt;/p&gt;

&lt;p&gt;My repo said the 3-arg function was gone. My repo was right about its own contents and wrong about the world.&lt;/p&gt;

&lt;p&gt;That's the shape of the mistake: &lt;strong&gt;I had written a guard against the failure I had just experienced, and assumed it was a guard against the failure mode.&lt;/strong&gt; Introducing a bad overload and resurrecting a dead one produce the identical broken state, and my check could only see the first one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things fixed it, and neither of them was a rule
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The migration now checks itself.&lt;/strong&gt; Instead of trusting the check that runs on my machine, the migration asserts against the database it's actually modifying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fn_issue_order_action_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;DO&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;
&lt;span class="k"&gt;DECLARE&lt;/span&gt; &lt;span class="n"&gt;v_count&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;v_count&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_proc&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;proname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'fn_issue_order_action_token'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="n"&gt;v_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE&lt;/span&gt; &lt;span class="n"&gt;EXCEPTION&lt;/span&gt; &lt;span class="s1"&gt;'expected 1 overload, got %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="err"&gt;$$&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;NOTIFY&lt;/span&gt; &lt;span class="n"&gt;pgrst&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'reload schema'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't care how the extra function got there. It doesn't care whether the migration was new, old, or replayed by something I don't know about. It looks at &lt;code&gt;pg_proc&lt;/code&gt;, counts, and refuses to commit if reality disagrees. Same principle now runs as a smoke test against the live database over a whitelist of critical RPCs — the repo can't answer "how many overloads exist in production," so I stopped asking the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And I removed the race that likely caused it.&lt;/strong&gt; My migrations were numbered sequentially — &lt;code&gt;217_&lt;/code&gt;, &lt;code&gt;218_&lt;/code&gt;. Going back through the folder I found five numbers that each had &lt;em&gt;two different files&lt;/em&gt;: &lt;code&gt;217&lt;/code&gt;, &lt;code&gt;218&lt;/code&gt;, &lt;code&gt;219&lt;/code&gt;, &lt;code&gt;230&lt;/code&gt;, &lt;code&gt;231&lt;/code&gt;. Sequential numbering means every branch and every parallel session guesses the same next number, and whoever writes second wins the filename. I never proved that's how the old migration got replayed, but it's an obvious way to end up applying a file you didn't mean to, so I moved everything to UTC timestamps. New filenames can't collide.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I gave up on
&lt;/h2&gt;

&lt;p&gt;I never found out how that old migration got re-applied.&lt;/p&gt;

&lt;p&gt;I tried. The migrations table had exactly one row for it and zero rows for everything in the 217–231 range, because these were applied by hand and hand-applied migrations don't register. Git had a single commit and nothing suspicious. The production logs from the relevant window had already rotated out.&lt;/p&gt;

&lt;p&gt;So I wrote down that the forensics failed, and stopped. The self-asserting migration plus the live overload smoke test mean that if it happens a third time I'll know within one command instead of one customer complaint — and I decided that's the property I actually needed. Knowing the exact mechanism would have been satisfying. It wouldn't have made the next occurrence any less likely.&lt;/p&gt;

&lt;p&gt;If there's one thing to take from this, it's not the &lt;code&gt;DROP FUNCTION&lt;/code&gt; — you'll hit that once and remember it forever. It's the shape of the second mistake: a guard written from a single incident tends to encode &lt;em&gt;that incident's route to the failure&lt;/em&gt;, not the failure. Mine watched the front door for six days while the same bug walked back in through a door I'd already locked and never checked again.&lt;/p&gt;

&lt;p&gt;The cheapest way I've found to avoid that is to make the check ask the system what state it's in, rather than asking my repo what state it should be in.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>backend</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
