Priya's PR
An agent-authored PR adds a nightly job that writes to the positions table directly — bypassing the adapter that emits the audit events. The code is clean, the tests pass, a reviewer new to the team approves it. A two-year-old ADR forbade exactly this; its author had left; nobody current remembered. The ADR was in the repository. It was never surfaced. This example records that decision as data — scope, enforcement, posture, reason — and lets the guardrail, not anyone's memory, catch the PR: once at design, once at merge, with no model on the verdict path.1
1That is the point of spec-driven development, and of spectastic in particular: a decision is an artifact — written once, in the design that made it, with a stable id — and everything downstream references it rather than re-deriving it. Remembering is a human skill; referencing is a tool's. ADR-0007 isn't a memo in a wiki; it is 002-downstream-consumers/D-007, addressable by coordinate from the spec, the plan, the verdict, and the reviewer.
The scenario is Theo Valmis's, from Architectural Guardrails for AI-Generated Code. He names the failure architectural drift — code that individually looks fine but collectively pulls the codebase away from where the team agreed it should go — and makes two claims that bite. First, an instruction file like CLAUDE.md is the right instinct at the wrong resolution: free text with no precedence, no lifecycle, no enforcement. Second, a second model reviewing the PR cannot fix this, because it shares the first model's blind spot — “two probabilistic passes over the same blind spot are not one deterministic pass with sight.” What's needed is a decision that is retrievable by path, injected before writing, and enforced by a script — not an agent, not retrieval-as-verdict, not AI-reviews-AI. spectastic's own principle P-8 says the same thing: guarantees live in the kernel or CI.
The distance
A securities position keeper — seven features, scaffolded by spectastic init --profile standard and driven through the whole lifecycle — spec → design → tasks → implement → verify — onto a real two-service Gradle build. The decision that matters was born in feature 2 as the answer to a requirement, not as governance ceremony: what guarantees every position change emits an event? Then five features and two and a half years go by without anyone mentioning it.
The 2024–25 dates are narrated (docs/timeline.md); the build-out is real git history — every bundle, the Gradle build, the CI gate and every command output are committed.
The decision — recorded as data
A Nygard card with prose Context / Decision / Consequences is a decision log. What makes it a governance record is the structured half: a scope a tool can retrieve by, a posture that says block or warn, enforcement rule-ids that join a detector's finding back to the decision, and a reason that lives once — here — and is never re-typed into the enforcer's own message channel, where it would drift. And, since the amendment below, a resource: the store the decision governs, by coordinate, and the one project that owns it.
D-007 · ADR-0007 — only the persistence adapter touches the data store
- Grounding
- ·
KB-0001@2026-09-13 - Status
- accepted · review by 2027-03-11
- Scope
src/**/persistence/**xyz.briancorbin.pk.hex.persistence..- Resource
spectastic://briancorbin/position-keeper-guardrails/datastore/positions
owner briancorbin/position-keeper-guardrails · direct writes allowed insrc/**/persistence/**- Enforcement
- archunit only_the_persistence_adapter_touches_data_access_apissemgrep no_sql_write_to_positions_outside_adapter
- Reason
- Every position change must emit PositionChanged so Risk, P&L, and the audit trail stay consistent; a direct write to the position store bypasses the emission and the audit hooks that live in the adapter.
- Decision
- Only
xyz.briancorbin.pk.hex.persistencemay reach the data-access APIs or write the positions store.
D-008 · DB-grant backstop for the positions table
- Grounding
- Status
- accepted
- Scope
db/**- Enforcement
- none, with reason — a database GRANT restricts writes on
positionsto the adapter's role. Operational; no static check exists, so it is recorded as unenforceable rather than pretended. - Reason
- Even if a static rule is evaded, the database itself refuses a write to positions from any role but the adapter's.
Two accepted decisions, one honest gap. D-008 carries no executable check and says so — which is what lets the coverage report count it as excused rather than silently uncovered.
→ A decision-well-formed rule in spectastic validate rejects an illegal status or posture, an accepted blocking decision with no scope, and an enforcement that is neither ≥1 rule nor exactly one none with a reason. Absent metadata leaves an older card exactly as it was. A sibling rule, resource-scope-well-formed, rejects a malformed coordinate, an ill-formed owner, or an owner that disagrees with the coordinate's own authority. The decision is addressable: spectastic://briancorbin/position-keeper-guardrails/decision/002-downstream-consumers/D-007.
Show me
The same reconciliation request, routed four ways. Pick a route and watch which stage the change reaches — and where it dies. The outputs are the real commands' real output against the committed repo.
Two gates, one aid. Retrieval and injection help the agent get it right; the design constraint and the merge verdict are the guarantee — scripts that run and stop, reconstructable from disk, with no model anywhere on the path. The gate's answer is identical whether or not the agent read what was injected.
Killed twice
The layer is six stages — author → register → retrieve → inject → gate → teach — and the PR dies at two independent points. Both are deterministic. The second one also tells the author exactly why, and where to go instead.
Retrieval surfaces the decision — by path, to a log
Before writing, the agent (or the human) asks which decisions govern the paths about to change. The answer is a glob match over every active decision's scope — no embeddings, no ranking, the same paths always return the same decisions — and it writes a retrieval log so the plan's view can be diffed against the merge's.
The same block — id, coordinate, reason — is injected into the design and tasks prompts, fenced as data to account for, never an instruction the tool obeys. It's an aid: an advisory prompt cannot be a guarantee, so nothing downstream assumes it was read.
Death at design — the plan constraint
A draft design's project-structure tree is its declared surface. If that surface touches a path governed by an accepted decision from another spec, the design must acknowledge it — by coordinate — or spectastic validate fails at the standard profile and up. spectastic has no diff at design time, only prose; this is a nudge where the PR should die in the happy path, and it binds forward-only (a new decision never retroactively fails the ninety-seven designs that shipped before it).
Death at merge — the verdict
Skip spec and design entirely; commit PositionReconciliationJob straight to a branch. The merge gate judges the actual diff against the decisions governing it and composes a verdict — {decision, rule, reason, file:line, source → target, cause} — persisted as plain JSON that reconstructs from the changed file and the decision record. Exit 1. The verdict kernel's transitive imports carry zero child-process modules: running a foreign command is not something it can do.
A failure teaches — the explanation, then one question
A terse VIOLATION line tells a reviewer what fired; it still sends them off to read the ADR. --explain renders each violation to the clarity floor of a triage card — the offending code in context, the decision made readable, the sanctioned path — composed entirely from the decision record and the changed file. --teach adds one Socratic question templated from the decision's reason, and never proposes a fix. Both are output-only: the exit code and the artifact are byte-identical with or without them.
The reason appears in the ADR, in the verdict, in the explanation and in the question — and it is stored once. Delete it from the enforcer's .because() and nothing here changes.
The residual — detector and explainer, split
spectastic is a Node CLI. It does not reimplement ArchUnit or Semgrep, and it never executes them. For language-agnostic rules — a path boundary, a forbidden pattern — it runs its own deterministic content detector. For a type-graph rule it ingests the ecosystem enforcer's SARIF and joins the rule-id back to the decision. Either way spectastic owns the one thing no architecture-test tool provides: the composition rule-id ↔ decision-id ↔ reason ↔ location.
JdbcPositionRepositoryAdapter runs the identical UPDATE positions — and emits PositionChanged in the same transaction. It sits inside src/**/persistence/**, the rule's allowed-in, so the content detector says nothing.
The build runs the fitness function on the bypass branch and writes SARIF — ArchUnit ships no emitter, so the test does it. spectastic reads it, maps only_the_persistence_adapter_touches_data_access_apis to D-007, and composes the same reason — a second violation, from a detector spectastic didn't write.
This is the number that would have made “the ADR governing positions has no check” visible before the incident. It counts a wired rule-id, not a green run — present is not the same as live, and the report says so.
Ownership, not layering
Split the estate into services and the sharper version of Priya's PR appears: the reconciliation team builds the nightly job in their service, and writes the position store from a persistence adapter of their own — cleanly layered, ports-and-adapters by the book. A path glob has no grammar for this. src/**/persistence/** is satisfied; the write is still wrong, because of whose store it is.
So D-007 was amended to name the store it governs by coordinate and the one project that owns it. The verdict becomes owner-aware: the current project identity is resolved once at the CLI edge from spectastic.json and injected into the pure kernel as data. If it equals the owner, the path rule applies exactly as before — demos 1–7 reproduce byte-identically. If it is anything else — another project, no identity at all, a bare unqualified default — the allowed zone is empty, and every detected touch flags.
A second project — services/reconciliation-service/, a real Gradle subproject with its own spectastic.json — carrying a copy of D-007 and a textbook adapter that writes positions. Well layered; flagged.
Flip spectastic.json to briancorbin/position-keeper-guardrails and nothing else. The identical file is inside the owner's allowed-in; the path rule applies; the adapter is sanctioned.
The verdict also says why. The owner comparison happens once, in the kernel, and is recorded on the violation as cause: "ownership" with the store's coordinate and owner — so the explanation and the teaching question read it rather than re-deriving it. A non-owner is no longer pointed at a "sanctioned path" (which is where the file already was); it is pointed at the authority.
The reason text is the same in both directions — it was written for the single-repo case and still holds. Every violation on the owner side carries cause: "path" and renders exactly as before; a pre-existing artifact with no cause is read as path. That gap was found by dogfooding this page, triaged, and closed the same day.
The rubric, scored twice
Before building the layer, spectastic scored itself against Valmis's rubric under a harsh bar — a row is full only against a committed passing test — and came out 2 full · 5 partial · 3 does-not. The Priya PR would have passed. After seven specs and this example: 9 full · 1 partial · 0, with the one partial kept on purpose.
| # | Rubric item | Was | Now | Evidence |
|---|---|---|---|---|
| 1 | Structured decision corpus — precedence, lifecycle, supersedes | partial | full | The extended <spec-decision>; overlapping scopes without supersedes are reported, never silently resolved. |
| 2 | Reliable retrieval — path-scoped, deterministic, logged | does not | full | adrs --for + the retrieval log. |
| 3 | Context injection before writing | does not | partial | Real, tested injection into design and tasks; at the code-writing moment it is an advisory nudge, because an advisory prompt cannot be a guarantee. Kept partial on purpose. |
| 4 | Deterministic CI enforcement, decision-traceable | partial | full | The plan constraint and the merge verdict. |
| 5 | Not an agent | partial | full | A test asserts the verdict kernel imports zero child-process modules. |
| 6 | Not RAG-as-verdict | partial | full | A gate-independence test: injection never changes the verdict. |
| 7 | Not AI-reviews-AI | full | full | Unchanged — the critic recommends, the deterministic gate decides. |
| 8 | Not vendor-locked | full | full | A Node CLI and plain JSON; AGENTS.md points at the commands and restates no ADR. |
| 9 | Enforcement coverage is visible | does not | full | adrs --coverage. |
| 10 | A failure teaches | partial | full | Reason composition, --explain, --teach. |
Items 9 and 10 are spectastic's additions to the rubric. Building the layer also taught two things the rubric doesn't name: a governance layer needs forward-only binding — a new decision binds work in flight, not history — and “a failure teaches” has two halves, the data and the rendering; full data behind a one-line render reads as done and is not.
What it can't see
A guardrail that overstates its reach is a guardrail nobody should trust. Three boundaries, recorded in the tool rather than in the marketing.
A decision can now name a store in another service and its owner — but the verdict still reads only this checkout's specs/*/design.html. The consumer in demo 8 works because D-007 is copied into it; the federated read that would deliver the owner's decision into a consumer's CI is deferred to spectastic-mcp. And a "touch" is still whatever the content pattern matches: an API call, a topic subscription, a migration is not a SQL string. So a green verdict carries "scope": "repo-local" and the count of decisions it evaluated — a repo-local green, reconstructable as exactly that, never misread as an estate-wide guarantee.
Database-per-service saves you from the wrong thing. A resource-scoped decision (and a per-service grant) stops Priya reaching into the position store; nothing stops her rebuilding its responsibility inside the repo her team knows — a parallel store that never touches the named one. That's a boundary duplication, not a bypass — Conway's Law operating as designed — and it is invisible to every mechanical check for a correct reason. spectastic keeps it where the industry does: a design-time, human-confirmed suggestion, never a merge gate.
The governing decisions are injected before writing — but a prompt is a channel the model may ignore, and spectastic has watched a weaker model ignore one. Injection improves the odds the gate stays quiet. The gate fires regardless, and a test proves its answer is the same whether or not the block was injected.
Also honest: the Java is now a real two-service Gradle build — 38 tests, green on main, red on the bypass branch — but the point is still not that a project compiles. It is that a decision recorded as data, two and a half years before a request that tests it, is surfaced and enforced deterministically; what remains narrated is only the calendar.
The artifacts
The position-keeper-guardrails repo, bundled here: seven spec bundles (spec · design · tasks · verify), one applied change proposal, the decision record, the sanctioned writer and the bypass, the enforcement rules, a second non-owner project built as a subproject, and the eight demonstrations with their committed output. Every artifact passes spectastic validate.
These are the real, rendered artifacts — the same files spectastic validate checks and the same repo the commands above ran against; the excerpts are drawn from them.