Chapter 2 — The Constitution, Enforced
# Chapter 2 — The Constitution, Enforced
Every sufficiently old codebase has a constitution: the set of rules everyone claims to follow and no tool enforces. It lives in a CONTRIBUTING file, it is enforced by code review and good intentions, and it loses one clause per quarter to “just this once.” Chapter 1 showed you that Relurpify’s map cannot drift silently. This chapter shows the same is true of its law: seven architecture decision records, each with a mechanism that makes it non-optional — and, because the maintainers know fences can rot, a gate that watches the fences themselves.
The chapter’s lab is simple and slightly transgressive: you break one of the seven rules with a single line of Go, watch two independent layers catch you, and revert. Read the outputs carefully — they are the closest thing this tree has to a voice.
A note on citations. As in Chapter 1: tracked files are cited
path:lineand verifiable in the clone; the authoring corpus underdevdocs/(handoff docs, whitepaper, plans) is cited by document and line but is not part of the public clone (.gitignore:41), so every claim it supports is anchored to a tracked file too. Captured at commit1bc57c62; see the pin note in the front matter.
# Where you are
From Chapter 1 you have the map: fifteen domains in an enforced DAG
(tooling/arch/domaindag.go:37-117), the two-directory config/state
split, and the knowledge that gates — not goodwill — hold the shape. This
chapter reads the law those gates enforce. You need nothing new: same
toolchain, no model backend, and one clean checkout you are about to
dirtied on purpose.
By the end you will be able to:
- State each of the seven ADRs and name the mechanism that enforces it.
- Explain why the fences come in two layers (AST and grep) and what each catches that the other cannot.
- Read an exemption table — and explain why this tree treats every exemption as debt with a scoreboard.
- Break a rule on purpose and read the two failure outputs you get.
# Technical requirements
| Requirement | Version / notes |
|---|---|
| OS / toolchain | As Chapter 1 (Linux primary; Go toolchain pin go1.27.2, go.mod:5) |
| Model backend | Not needed |
| Extra | Nothing. The lab dirties one tracked file and reverts it |
Captured on the Chapter 1 machine, commit 1bc57c62.
# 2.1 The written law and the enforced law
The constitution is written in three places, and the division of labor between them matters.
The ADRs — docs/ARCHITECTURE.md:235-296 holds ADR-001 through
ADR-007, each in the same three-part shape: context (what went wrong
before the rule), decision (the rule), consequences (what the rule
costs you). Two further decisions are normative but live in code, not in
the ADR file: vocabulary-with-owner and consumer-defined ports
(ADR-008 and ADR-009 “in the domain DAG”,
devdocs/HANDOFF-docs/DECISIONS.md:121-147) — you met both in Chapter 1
as the no-bucket rule and the context/ports seam.
The invariants — devdocs/HANDOFF-docs/INVARIANTS.md is the derived
law: what must always hold if the ADRs are true. Its section 1 restates
the eight architectural rules with an enforcement column, and marks every
row high for one reason only: each has a named gate in
tooling/arch/ or the Makefile (devdocs/HANDOFF-docs/INVARIANTS.md:17-36).
The document is explicit about its own epistemology — “high = enforced by
a named gate, test, or explicit code path that I read”
(devdocs/HANDOFF-docs/INVARIANTS.md:9).
The gates — tooling/arch/ and the Makefile targets that drive it.
This is the law’s executable form, and it is the form the project trusts:
Why it won: the docs in this repo are unusually honest about what is unfinished, and the failure they could not defend against was documentation drifting from reality. A gate cannot drift silently. —
devdocs/relurpify-technical-whitepaper.md:91
The daily driver is make lint-arch (Makefile:15-24): class-string
normalization, no permissive HITL defaults, the three AST gates
(envcheck, shimcheck, symcheck), then archcheck and
domaincheck in enforce mode across three checks — direction,
governance-orchestration, context-ports. In CI it runs as its own job
(.github/workflows/ci.yml:55), alongside check-gates-honest
(:57) and check-gates-slice10 (:140).
# 2.2 The seven ADRs, each with its teeth
A rule you cannot break by accident is a preference. The table below is the constitution’s map of itself: each ADR’s decision, and the mechanism that would fail if you violated it. “Teeth” varies by rule — some are AST gates, some are grep fences, some are structural facts of the DAG, one is a deleted runner guarded by a test that keeps it deleted.
| ADR | The decision (cite) | The teeth |
|---|---|---|
| 001 — one env boundary | Only userconfig/config reads the process environment; it snapshots once at boot (docs/ARCHITECTURE.md:235-243) |
envcheck AST gate (Makefile:122, tooling/arch/envcheck.go:23-59) plus a grep fence with the same pattern (Makefile:265-270) |
| 002 — generated config | The embedded template bundle is the single source of truth; relurpify_cfg/ is its materialization (docs/ARCHITECTURE.md:244-252) |
check-config-tree-drift (Makefile:26; CI .github/workflows/ci.yml:137) |
| 003 — fail closed | Unset scopes deny; nil policies deny; the default posture is ask; private/loopback egress denied unconditionally (docs/ARCHITECTURE.md:253-262) |
A shape, not one gate: defaults in the decision paths (e.g. governance/permissions/decision.go, governance/netpolicy/resolve.go), pinned by the fail-closed inventory (devdocs/HANDOFF-docs/INVARIANTS.md:40-60) and by grep fences such as INV-1 — no network I/O in governance/authorization (Makefile:243-250) |
| 004 — all execution sandboxed | Every command goes through the sandbox chain; the local host-exec runner was removed and a guard test keeps it removed (docs/ARCHITECTURE.md:263-271) |
The guard test; the sandbox backend contract (Chapter 6) |
| 005 — no shims, compat layers, aliases, stubs | The charter forbids all four; bridged types hid real behavior and bred stale docs (docs/ARCHITECTURE.md:273-278) |
shimcheck (forbidden language in string literals, tooling/arch/shimcheck.go:21-33) + symcheck (deleted symbols cannot resurrect, tooling/arch/symcheck.go:14-49) + a grep fence (Makefile:272) + no-dead |
| 006 — deterministic routing | Default selection posture deterministic; ambiguity asks, never coin-flips; model_assisted is opt-in (docs/ARCHITECTURE.md:280-287) |
Behavioral: the posture default and route provenance (decided_by) in telemetry; the DSL is kept statically analyzable by the euclo-* gates in lint-all (Makefile:146) |
| 007 — durable jobs run in a sidecar | relurpify-runner owns the jobs store, receives work through an atomic file spool, and nothing needing governance runs there (docs/ARCHITECTURE.md:289-296) |
The DAG itself: ayenitd’s allowed imports exclude capability, governance, and model — the sidecar rule is a DAG row (tooling/arch/domaindag.go:57-59) |
Three of these deserve a second look.
Rendered as the constitution’s own wiring — each written rule, the mechanism that fails when you break it, and the meta-gate watching the mechanisms (§2.5):
flowchart LR
subgraph WRITTEN["Written law — docs/ARCHITECTURE.md:235-296"]
A1["ADR-001<br/>one env boundary"]
A2["ADR-002<br/>generated config"]
A3["ADR-003<br/>fail closed"]
A4["ADR-004<br/>all execution sandboxed"]
A5["ADR-005<br/>no shims · compat · aliases · stubs"]
A6["ADR-006<br/>deterministic routing"]
A7["ADR-007<br/>sidecar jobs"]
end
subgraph ENFORCED["Enforced law — tooling/arch + Makefile"]
ENV["envcheck AST gate<br/>+ env grep fence"]
DRIFT["check-config-tree-drift"]
SHAPE["decision-path defaults<br/>+ INV-1 grep fence"]
GUARD["guard test —<br/>the deleted runner stays deleted"]
SHIM["shimcheck + symcheck<br/>+ grep fence"]
EUCLO["posture default<br/>+ euclo-* DSL gates"]
DAGROW["a domain-DAG row:<br/>ayenitd's allowed imports"]
end
A1 --> ENV
A2 --> DRIFT
A3 --> SHAPE
A4 --> GUARD
A5 --> SHIM
A6 --> EUCLO
A7 --> DAGROW
HONEST["check-gates-honest<br/>patterns cannot be weakened"] -.->|watches| ENV
HONEST -.-> SHIM
HONEST -.-> DRIFTADR-005’s vocabulary law has an escape hatch, and the escape hatch has
a rule. shimcheck forbids “shim”, “compatibility”, “stub”, and
“backward compatibility” as string literals — but the short spelling
“compat” is deliberately not on the list, because as a substring it is
indistinguishable from the project’s own canonical vocabulary:
platform/llm/openaicompat, the config kind openai_compatible, the
capability ID euclo:cap.api_compat. It is matched on word boundaries
instead, so “compat layer” is a violation while “api_compat” is not
(tooling/arch/shimcheck.go:15-33). Hyphens are boundaries, so
“back-compat” violates too. The gate’s comment says why in one line:
a word boundary would let “Shim-” and “shimmy” through.
ADR-004 is enforced by absence. The host-exec runner was not
deprecated; it was deleted, and a guard test exists to keep it deleted
(docs/ARCHITECTURE.md:263-271). symcheck extends the same idea into
the everyday: its RemovedSymbols table lists thirty-one identifiers
deleted in earlier remediation slices, and any occurrence in production
code fails the gate — “their return would resurrect the design the
slices removed” (tooling/arch/symcheck.go:10-49). In this tree, the
past is fenced too.
ADR-007’s teeth are a row you have already read. Chapter 1 quoted
the DAG comment: the runner supervises services and drains the spool,
and “it must never link app composition” — its allowed imports exclude
capability, governance, and model (tooling/arch/domaindag.go:57-59).
“Nothing needing governance runs in the runner” is not a policy someone
must remember; it is an import direction the domain checker refuses to
reverse. And Chapter 1’s other finding stands: the binary has never
shipped (D10 in the book plan). The boundary is real and enforced; its
tenant is absent — the framework degrades gracefully and doctor
reports the missing runner as an informational warning.
# 2.3 Two fence layers, one violation, two verdicts
The gates come in two layers, and CONTRIBUTING states the division
cleanly (CONTRIBUTING.md:60-65):
- AST gates (
envcheck,shimcheck,symcheck) parse the syntax tree, “so they cannot be bypassed by aliasing an import, concatenating a forbidden string, or renaming around a pattern” (Makefile:114-119). - Grep gates remain “for what a syntax tree cannot express, such as
a forbidden string assembled by concatenation” (
CONTRIBUTING.md:64-65).
The layers are not redundant; each covers the other’s blind spot.
envcheck resolves imports on the tree, so import o "os"; o.Getenv(…)
is caught — the gate’s own doc comment says this is exactly what the
grep version could not do (tooling/arch/envcheck.go:16-22). But a
forbidden string assembled at runtime ("sh" + "im") is a binary
expression, not a literal, so the AST gate cannot see it — and the shim
gate says so in its comment, deferring to the grep layer
(tooling/arch/shimcheck.go:43-45).
Do not take the doc’s word for it. Here is the chapter’s experiment:
one line of Go, breaking ADR-001 in the leaf domain model/. The diff
(the lab ships it as violation.diff):
--- a/model/runtime.go
+++ b/model/runtime.go
@@ -1,6 +1,9 @@
package model
import "context"
+import "os"
+
+// lab violation: one deliberate environment read
+var labViolation = os.Getenv("HOME")
Layer one, the AST gate:
$ make envcheck
[FAIL] env: model/runtime.go:8 reads the process environment via os.Getenv
[FAIL] envcheck: 1 env access violation(s) outside userconfig
exit status 1
make: *** [Makefile:123: envcheck] Error 1Layer two, the grep fence, against the same violation:
$ make grep-architecture-gates
[PASS] check-contract-dissolution: manifest spine removed
[FAIL] grep-architecture-gates: direct env access remains outside userconfig
./model/runtime.go:8:var labViolation = os.Getenv("HOME")
make: *** [Makefile:266: grep-architecture-gates] Error 1Read the two failures as fingerprints. The AST gate names the semantic
fact — a selector that resolves to the os package, at a position —
and would have caught the same read behind an import alias. The grep
fence quotes the raw line — and would catch the alias evasion only if
the literal text still matched, which is precisely the case it exists
for when the AST layer cannot parse the shape. Two independent
verdicts, one violation, and removing either layer leaves the other
still watching.
One wrinkle, stated by the tree itself: tooling/arch is exempt from
the env grep fence — but from that fence only. The AST gate’s own
source must name os.Getenv to forbid it, and grep cannot tell a
comment from a call, so the Makefile’s comment reads: “make envcheck
polices that tree on the tree” (Makefile:263-268). An exemption with
its reason attached is the theme of the next section.
# 2.4 Exemption tables are load-bearing
Older fence systems hide their exceptions in grep -v filters, and the
project’s exemption doc explains exactly why that fails:
“basename filters that silently exempted every file so named anywhere in
the tree, with no record of why the exception existed” — so a newly
added file that happens to share a name is silently un-policed
(tooling/arch/exemption.go:7-11). The Exemption type replaces that
with explicit path prefixes matched at component boundaries and exact
files, “never a basename: the point is to exempt one known site, not a
name” (tooling/arch/exemption.go:15-23). And above the type, the
philosophy, in the shortest sentence in this book:
Prefer no exemption. A gate that accepts nothing cannot be argued with. —
tooling/arch/exemption.go:13
The tree currently practices what that sentence preaches, and the practice is gated:
- The direction-violation exceptions file is empty by policy, its header
saying so outright: “All direction violations are resolved
structurally — no suppressions” (
tooling/arch/exceptions.yaml:1-3). - The
exception-countgate holds the file to a baseline of zero: a net-new exception fails CI unless the spec is extended (Makefile:105-119). - The gates’ own tests police the exemptions from the other side:
TestExemptionCoversNothingByDefault(tooling/arch/exemption_test.go:32) andTestNoStaleDirectionEdges(tooling/arch/stale_edges_test.go:13) — CONTRIBUTING’s summary: “Every entry is load-bearing: the gates’ own tests fail if an exemption stops being needed, so a hole cannot quietly grow a comment” (CONTRIBUTING.md:67-70). - The archcheck allowlist lives at
devdocs/ref/arch-allowlist.yaml— outside the public clone — and this is survivable by design: a missing allowlist loads as empty (tooling/arch/allowlist.go:18-21), the local file currently exempts nothing, and its header records the rule that the list “can only shrink” because the runner fails a stale entry that no longer matches a live violation. Delete the file and the gate judges the tree identically.
That last point is worth pausing on, because it is the constitution’s design in one mechanism: the tree is so close to violation-free that the exception machinery can be absent without changing any verdict. The fences are not holding back a flood of suppressions; there is no flood.
# 2.5 The gate that watches the gates
A fence pattern is itself text in a Makefile, and text can be edited.
check-gates-honest exists because the maintainers know the weakest
link in any gate system is the gate. Its mechanism (Makefile:401-435):
Each check expands the gate’s own recipe with
make -nand greps the command that would actually run, so a pattern must live in the gate itself: the patterns named below cannot satisfy their own test, and deleting a pattern, an AST gate, or its wiring all fail here. —Makefile:402-404
Concretely, it fails if: the env fence loses its
Getenv|LookupEnv|Environ pattern; the compatibility fence loses its
pattern; no-dead loses its removed-symbol patterns; lint-arch loses
the domaincheck direction-enforce run; or any of the three AST gate
binaries goes missing from the tree or from lint-arch’s recipe. It
runs in CI (.github/workflows/ci.yml:57), and on the pristine tree:
$ make check-gates-honest
[PASS] check-gates-honest: all gate patterns and AST gates presentThe same cross-checking instinct appears inside the gates: symcheck’s
removed-symbol table and the no-dead grep gate “keep the two in sync”
(tooling/arch/symcheck.go:14-16) — yet another place where two layers
watch each other. This is the chapter’s thesis in miniature: in this
tree, the enforcement is itself enforced, and the regress stops at
the meta-gate.
# 2.6 Hands-on: break the law, get caught, revert
The lab (docs/relurpic-internals/labs/ch02-constitution/) ships the
command card, the violation diff, and the captured transcript. Run it
in a clean checkout; every step is reversible.
1. Baseline: the constitution holds.
make lint-arch
make check-gates-honestCaptured: lint-arch ends with [PASS] All architecture gates passed
and three [PASS] … All domain checks passed lines (direction,
governance-no-orchestration, context-ports); check-gates-honest ends
[PASS] check-gates-honest: all gate patterns and AST gates present.
2. Apply the one-line violation. The diff in §2.3 — add import "os" and one os.Getenv variable to model/runtime.go
(violation.diff in the lab has the exact hunks).
3. Watch layer one catch you.
make envcheck; echo "exit=$?"Captured: [FAIL] env: model/runtime.go:8 reads the process environment via os.Getenv, exit 1. Note the verdict names the file, line, and the
semantic reason — not a text match.
4. Watch layer two catch you too.
make grep-architecture-gatesCaptured: [FAIL] grep-architecture-gates: direct env access remains outside userconfig, quoting the raw line ./model/runtime.go:8:var labViolation = os.Getenv("HOME").
5. Revert, verify nothing happened.
git checkout -- model/runtime.go
git status --porcelain model/ # empty
make envcheck # PASS again# Verification block
| Chapter claim | Where captured | Lab file |
|---|---|---|
Pristine tree passes lint-arch (all four AST results + three domain checks) |
§2.6 step 1 | transcript.txt §1 |
Pristine tree passes check-gates-honest |
§2.6 step 1 | transcript.txt §1 |
One env line fails envcheck with a semantic verdict |
§2.6 step 3 | transcript.txt §3 |
| The same line fails the grep fence with the raw line | §2.6 step 4 | transcript.txt §4 |
| Revert restores the PASS baseline | §2.6 step 5 | transcript.txt §5 |
| Exemption machinery is at zero and can only shrink | §2.4 | manual: tooling/arch/exceptions.yaml:1-3, Makefile:105-119, devdocs/ref/arch-allowlist.yaml |
# Summary
- Seven ADRs, each with teeth — but “teeth” is a spectrum: AST gates (ADR-001, ADR-005), a drift gate (ADR-002), a guarded deletion (ADR-004), behavioral defaults plus DSL gates (ADR-006), and a DAG row (ADR-007). ADR-003 is a shape across decision paths, inventoried in INVARIANTS.md, not a single gate.
- Two fence layers by design: the AST layer cannot be evaded by aliasing or renaming; the grep layer covers strings the tree cannot express. One violation produced two structurally different verdicts.
- Exemptions are explicit, path-precise, held to a zero baseline by
exception-count, and failed by the gates’ own tests when they go stale. “Prefer no exemption. A gate that accepts nothing cannot be argued with.” check-gates-honestverifies the fences themselves have not been weakened — the enforcement is enforced.- The sidecar rule of ADR-007 is a row in the domain DAG; the runner it names has never shipped, and the framework degrades by design.
# Check yourself
envcheckand the env grep fence enforce the same ADR. Name one evasion the AST gate catches that the grep fence would miss, and one the grep fence catches that the AST gate cannot.- You need a genuine, permanent exception to the env fence in a new package. Walk the path: what must you change, and which gate fails first if you just add an entry?
- ADR-003 has no dedicated AST gate. What enforces fail-closed, and how does INVARIANTS.md justify marking its rows high anyway?
- A teammate proposes deleting
tooling/arch/cmd/symcheck/main.gobecause “the grep gates cover it.” Which gate fails, and what does its failure output look like? - Why can the archcheck allowlist file be absent from a fresh clone without changing any gate verdict?
Answers. 1. Aliased import (import o "os"; o.Getenv) — semantic
resolution the grep fence misses if the text is aliased; a string
assembled by concatenation ("sh" + "im") — a binary expression, not a
literal, invisible to the AST layer (tooling/arch/shimcheck.go:43-45 states the
delegation for the shim pair). 2. Extend the spec first — exception-count
holds exceptions.yaml to a baseline of zero and fails a net-new entry
(Makefile:105-119); the entry must also name an exact path, never a
basename (tooling/arch/exemption.go:15-23), and the gates’ own tests fail it when it
goes stale. 3. The decision paths’ defaults (deny on unset, ask on
ambiguity, deny on unresolvable egress), pinned by the fail-closed
inventory and grep fences such as INV-1 (Makefile:243-250);
INVARIANTS marks rows high only where a named gate, test, or read code
path backs them (devdocs/HANDOFF-docs/INVARIANTS.md:9). 4. check-gates-honest: “AST gate
symcheck/main.go is missing from the tree” (Makefile:426-429) — and
separately, lint-arch would fail because symcheck is missing from its
recipe (Makefile:430-433). The grep gates do not cover it: the AST
layer exists precisely for what grep cannot express. 5. A missing
allowlist loads as empty (tooling/arch/allowlist.go:18-21), the local file exempts
nothing, and a stale entry — not a missing file — is what the gate
fails on (tooling/arch/stale_edges_test.go:13).
# Try it yourself
- Break ADR-005 instead. Add a Go string literal containing
“compatibility layer” to any file under
execution/session, runmake shimcheck, read the verdict, revert. Then try the word-boundary nuance: a literal containingapi_compatshould pass. If it does not, you have found a gate bug — capture it. - Read the third domain check.
lint-archenforces-check=context-ports(Makefile:19). Find its implementation (tooling/arch/context_ports_check.go), state the rule it enforces in one sentence, and check it against the consumer-defined-ports decision (devdocs/HANDOFF-docs/DECISIONS.md:135).
# Where next
The constitution tells you what may happen in this tree; it says nothing about what the tree offers. Chapter 3 inventories the surfaces — CLIs, config schemas, contracts, the telemetry event vocabulary — and grades them by confidence and stability, including the one surface the project explicitly refuses to promise: the Go package API. You will verify five “high”-confidence claims against the tree yourself, which by now you know is the only kind of claim this book lets stand.
The lab for this chapter lives in
docs/relurpic-internals/labs/ch02-constitution/ — the command card,
the violation diff, the captured transcript from the pin, and
verify.sh ch02 to check your machine against the deterministic
outputs.