Skip to content

Commit 486454d

Browse files
xuyushun441-sysos-zhuangclaude
authored
docs(adr): ADR-0060 — Conformance Ledger as a Platform Pattern (#2103)
The platform has hand-written the same conformance ledger twice (ADR-0056 D10 authz matrix + ADR-0058 D7 expression surface) to defend against the declared-but-unenforced failure class (#1887). Promote it to a reusable pattern: one ledger model + assertLedger CI helper (in @objectstack/verify), the two existing instances refactored onto it, and a third for the object validation-rule surface (pinning the ADR-0020 state_machine enforcement). A declaration surface is "landed" iff it has a conformance ledger whose ratchet is green. Decision-only; implementation follows P1–P3. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c189745 commit 486454d

1 file changed

Lines changed: 182 additions & 0 deletions

File tree

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# ADR-0060: Conformance Ledger as a Platform Pattern
2+
3+
**Status**: Proposed (2026-06-21)
4+
**Deciders**: ObjectStack Protocol Architects
5+
**Builds on**: ADR-0049 (enforce-or-remove / no unenforced declaration), ADR-0054
6+
(runtime proof per enforced high-risk primitive), ADR-0056 D10 (Authorization
7+
Conformance Matrix), ADR-0058 D7 (Expression Surface Conformance ledger), ADR-0020
8+
(state-machine converge-and-enforce)
9+
**Consumers**: verify, dogfood, spec, plugin-security, plugin-sharing, objectql, CI
10+
**References**: #1887 (declared-but-unwired sharing condition — the canonical failure)
11+
12+
## TL;DR
13+
14+
The platform has now hand-written the **same conformance ledger twice** — the
15+
ADR-0056 D10 Authorization Conformance Matrix and the ADR-0058 D7 Expression
16+
Surface ledger — to defend against the **declared-but-unenforced** failure class:
17+
a property that *looks* authorable but the runtime never wires, so it silently
18+
does nothing (#1887: a sharing `condition` the interpreter understands but no
19+
compiler lowered). Two instances with near-identical shape (`id` / `state` /
20+
`enforcement-site` / `proof` + a CI test that asserts completeness and that every
21+
proof exists) is the signal to **promote the ledger from a habit to a platform
22+
pattern**: one reusable ledger model + CI helper, the two existing instances
23+
refactored onto it, and a third instance for the **object validation-rule surface**
24+
(which now includes the ADR-0020-enforced `state_machine`). A declaration surface
25+
is **"landed" iff it has a conformance ledger whose CI ratchet is green** — no
26+
ledger, not landed.
27+
28+
## Context
29+
30+
### The recurring failure: declared-but-unenforced
31+
32+
ObjectStack is metadata-driven and increasingly **AI-authored**. Its defining
33+
risk is not a crash — it is a primitive that is *declarable* but *inert*:
34+
35+
- **#1887**`SharingRuleSchema.condition` (CEL) was authorable and type-checked,
36+
but no compiler lowered it; authoring a rule granted nothing. Silent.
37+
- **ADR-0020 (pre)**`state_machine` existed as three declaration shapes and
38+
**zero** runtime enforcement; a Flow could drive `status` straight to `closed`.
39+
- **ADR-0049** catalogues a long tail of `[EXPERIMENTAL — not enforced]` bits
40+
(transfer/restore/purge, masking, policy, …) that read as features but enforce
41+
nothing.
42+
43+
AI authorship **amplifies** this class: an LLM is excellent at producing
44+
configuration that *looks* complete and terrible at noticing that nothing reads
45+
it. The defence that works is not "review harder" — it is a **machine-checked
46+
ledger** that, for every declarable property, forces the answer to "where is this
47+
enforced, and what proves it?", and **breaks the build** when a new property
48+
appears with no answer.
49+
50+
### Two hand-written ledgers, one shape
51+
52+
- **ADR-0056 D10**`authz-conformance.matrix.ts`: `{ id, summary, state, enforcement?, proof?, note? }` + `authz-conformance.test.ts` asserting valid state, enforced-has-site, proof-file-exists.
53+
- **ADR-0058 D7**`expression-conformance.ledger.ts`: the same core plus `{ dialect, mode, failPolicy, covers[] }` and a **ratchet** that re-discovers every `ExpressionInputSchema` field in `packages/spec/src` and fails if any is unclassified.
54+
55+
They share a model and a test discipline; they were written twice. The second
56+
one's **ratchet** (discover the real surface from source, assert the ledger
57+
covers it) is the strictly stronger pattern and the one worth standardizing.
58+
59+
## Decision
60+
61+
Governing rules: ADR-0049 (a declaration is enforced / experimental / removed —
62+
never a silent fourth state), ADR-0054 (each enforced high-risk primitive carries
63+
a runtime proof).
64+
65+
### D1 — One reusable ledger model + CI helper
66+
67+
Define a single conformance model and an `assertLedger` helper:
68+
69+
interface ConformanceRow {
70+
id: string;
71+
summary: string;
72+
surface?: string; // the declaration site this row classifies
73+
state: 'enforced' | 'experimental' | 'removed';
74+
enforcement?: string; // runtime site — REQUIRED when enforced
75+
proof?: string; // repo-root-relative; file must exist
76+
covers?: string[]; // ratchet keys this row accounts for
77+
note?: string; // REQUIRED when experimental/removed
78+
meta?: Record<string, unknown>; // per-surface extras (dialect, mode, failPolicy, …)
79+
}
80+
81+
assertLedger(rows, {
82+
proofRoot, // resolve `proof` against this
83+
discover?: () => Set<string>, // optional: the real surface, from source
84+
highRisk?: string[], // ids that MUST carry a proof
85+
})
86+
87+
`assertLedger` encodes the shared invariants once: unique ids, valid state,
88+
enforced-has-enforcement, experimental/removed-has-note, every `proof` exists,
89+
each surface covered by exactly one row, and — when `discover` is supplied — the
90+
**ratchet**: every discovered surface is `covers`-ed (else "classify it") and no
91+
`covers` is stale. Per-surface extras (dialect/mode/fail-policy) live in `meta`,
92+
so the model is universal without losing the richer expression-surface fields.
93+
94+
### D2 — Refactor the two existing ledgers onto the model
95+
96+
`authz-conformance` and `expression-conformance` keep their data and their
97+
domain-specific `discover`/`highRisk`, but delegate all structural assertions to
98+
`assertLedger`. This deletes the duplicated test logic and proves the model is
99+
genuinely shared (not a third bespoke shape).
100+
101+
### D3 — Third instance: the object validation-rule surface
102+
103+
Add a `validation-conformance` ledger over the `validations` union
104+
(`state_machine`, `cross_field`, `script`, `format`, `json_schema`, `conditional`,
105+
`unique`, `required`, …) with a `discover` that enumerates the rule `type`
106+
literals from `packages/spec/src/data/validation.zod.ts`. Each rule type is
107+
classified with its enforcement site and (for enforced) a proof. This **pins the
108+
ADR-0020 `state_machine` enforcement** (and the now-unblocked `cross_field` /
109+
`script`) into CI, and makes "a new validation rule type with no enforcement" —
110+
the exact pre-ADR-0020 `state_machine` disease — a build break.
111+
112+
### D4 — Home the helper in `@objectstack/verify`
113+
114+
The reusable model + `assertLedger` live in `@objectstack/verify` (a small
115+
`conformance` submodule). `verify` already owns "prove the app actually behaves"
116+
via its runtime harness; the static conformance ledger is its **compile-time
117+
complement** — both answer "is this primitive real?", one by booting, one by
118+
ledger. The per-surface ledgers + their `discover` functions stay where their
119+
proofs live (dogfood), importing the helper.
120+
121+
### D5 — "Landed" is defined by the ledger
122+
123+
A declaration surface is **landed iff it has a conformance ledger whose ratchet
124+
is green**. This becomes the platform's definition of done for any new authorable
125+
surface: you do not get to add an `ExpressionInputSchema`-style declaration family
126+
without a ledger row + (if enforced) a proof. ADR-0049's "enforce or remove" gains
127+
a third, mechanical leg: **ledger or it isn't landed.**
128+
129+
### D6 — A single CI aggregate (optional, P3)
130+
131+
A meta-test can import every registered ledger and assert each passes
132+
`assertLedger`, so "the conformance surface is whole" is one green check. Until
133+
then, each ledger's own test is the gate (as today).
134+
135+
## Consequences
136+
137+
Positive: the declared-but-unenforced defence becomes a **reusable platform
138+
capability** rather than copy-paste; adding a new declaration surface is "fill in
139+
a ledger", not "re-derive the discipline"; the ADR-0020 state-machine enforcement
140+
stops being implicit and becomes a CI-pinned, proof-backed row; AI authorship gets
141+
a uniform, machine-checked guardrail across surfaces.
142+
143+
Negative / cost: refactoring the two existing ledgers carries migration risk
144+
(mitigated — same data, only the assertion layer moves, tests stay green);
145+
`@objectstack/verify` gains a static-analysis responsibility alongside its runtime
146+
one (kept in a separate submodule so the boot harness is untouched).
147+
148+
Neutral / open: which additional surfaces get a ledger next (flows, UI actions,
149+
connectors) is evidence-gated, not mandated here — D5 sets the rule, P3 applies it
150+
where the declared-but-unenforced risk is highest.
151+
152+
## Non-goals
153+
154+
Not a change to any **runtime** semantics — purely the conformance/verification
155+
layer. Not a replacement for the `@objectstack/verify` runtime harness (it is the
156+
static complement). Not a mandate that every surface acquire a ledger immediately
157+
— D5 is the standard going forward; existing surfaces migrate as P3 reaches them.
158+
159+
## Alternatives considered
160+
161+
(a) **Keep hand-writing each ledger** — rejected: every new surface re-pays the
162+
discipline and drifts (the two existing ones already diverged in richness).
163+
(b) **One giant global ledger** for the whole platform — rejected: couples
164+
unrelated surfaces, loses the per-surface `discover` that makes the ratchet sharp.
165+
(c, chosen) **One reusable model + helper, one ledger instance per surface**
166+
shared invariants, independent ratchets, additive.
167+
168+
## Phasing
169+
170+
- **P1** — D1 model + `assertLedger` in `@objectstack/verify`; D2 refactor the
171+
authz + expression ledgers onto it (tests stay green; proves reuse).
172+
- **P2** — D3 validation-rule-surface ledger + ratchet; classify every rule type,
173+
pin the ADR-0020 `state_machine` enforcement with a proof.
174+
- **P3** — D5 as the documented "landed" bar; D6 CI aggregate; extend to the next
175+
highest-risk surfaces (flow conditions, UI actions, connectors) as evidence warrants.
176+
177+
## References
178+
179+
ADRs 0020, 0049, 0054, 0056 (D10), 0058 (D7). Issue #1887. Existing instances:
180+
`packages/dogfood/test/authz-conformance.{matrix,test}.ts`,
181+
`packages/dogfood/test/expression-conformance.{ledger,test}.ts`. Helper home:
182+
`packages/verify/`. Target surface: `packages/spec/src/data/validation.zod.ts`.

0 commit comments

Comments
 (0)