Skip to content

Commit 9e254cb

Browse files
committed
docs(adr): index self-test judges the generator only; check:adr-index alias; versions cite-as; 0071 status on one line
Contract-review respin of the ADR index (seat 1, record 6434963378): - scripts/adr-index.mjs --self-test no longer asserts on what the live records say (the one-spelling and ui-parses assertions moved out; they were already --report's warnings). The live-tree battery now proves only that the generator reads and renders the real registry. A record with another status spelling is a warning, never a red — ruling A, 门禁不加. - The lint step is its own, report-only step running the root alias `pnpm check:adr-index` (= --self-test then --report), per the GATE INVOCATION IDIOM that landed in c91544a; the anchors step is back to `pnpm check:adr-anchors` alone. `adr:index` stays the regeneration alias. - Shared numbers: a versions group states that the bare number names the decision and its current text is the highest version; the superseded unversioned file is cited by its stem, never offered as `ADR-0006`. - A `**Decided**:` header line is a date line (ADR-0079 now dates to its decision, 2026-06-28, not its 2026-08-08 reconstruction). - ADR-0071's status paragraph folded onto its one `**Status**:` line. Claude-Session: https://claude.ai/code/session_01WGufzgAZv5vcb4H6qvqBAM Co-authored-by: Claude <noreply@anthropic.com>
1 parent ae90b7c commit 9e254cb

4 files changed

Lines changed: 55 additions & 35 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -596,24 +596,25 @@ jobs:
596596
# The `--self-test` in the pnpm script provokes that red path on synthetic
597597
# directories every run, because a collision is rare enough that nobody
598598
# would otherwise see the gate fail before it mattered.
599-
#
600-
# #26076: the same step also REPORTS on the generated ADR index
601-
# (docs/adr/README.md, `scripts/adr-index.mjs`). Report-only by the
602-
# maintainer's ruling (门禁不加): `--report` prints a `::warning::` for a
603-
# stale index or a record without the one `**Status**:` spelling and
604-
# exits 0 whatever it finds, so the INDEX never reds this step. The
605-
# generator's own `--self-test` runs with the flag like every other
606-
# script's (check-self-test-wired: an unrun self-test is a phantom check)
607-
# — that verifies the script, not the records. `pnpm adr:index`
608-
# regenerates; `--check` is the gate's spelling for the day a maintainer
609-
# names one.
610599
- name: ADR anchors + number uniqueness (governed code names its decision)
611600
id: adr-anchors-number-uniqueness-governed-code
612601
continue-on-error: true
613-
run: |
614-
pnpm check:adr-anchors
615-
node scripts/adr-index.mjs --self-test
616-
pnpm adr:index:report
602+
run: pnpm check:adr-anchors
603+
604+
# #26076 ADR index, REPORT-ONLY by the maintainer's ruling (门禁不加).
605+
# docs/adr/README.md is generated by scripts/adr-index.mjs from the
606+
# records' one `**Status**:` line; the alias runs the generator's
607+
# `--self-test` (fixtures only — it never asserts on what the records
608+
# say) and then `--report`, which prints a `::warning::` for a stale
609+
# index or a record whose status line is missing or unreadable and exits
610+
# 0 whatever it finds. So the only red this step can show is a broken
611+
# generator, never a record. `pnpm adr:index` regenerates; `--check` is
612+
# the gate's spelling for the day a maintainer names one. Alias form per
613+
# the GATE INVOCATION IDIOM note at the top of this file.
614+
- name: ADR index is current (report-only — warnings, never a red on a record)
615+
id: adr-index-current-report-only
616+
continue-on-error: true
617+
run: pnpm check:adr-index
617618

618619
# @objectstack/verify stand-in erasure guard (#6399). The third and
619620
# narrowest member of the slot-lookup and query-options erasure ratchets

‎docs/adr/0071-dataset-semantic-layer-depth.md‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# ADR-0071: Dataset semantic-layer depth — multi-hop joins
22

3-
**Status**: Proposed. (Refocused — see **Correction** below. The first merged draft also
4-
proposed matrix/across pivoting; a code check found that already implemented, so
5-
this ADR now covers only the remaining gap: multi-hop joins.) **Multi-hop to-one joins (≤3 hops / 4 objects) have since been implemented & are live — header pending formal update.**
3+
**Status**: Proposed. (Refocused — see **Correction** below. The first merged draft also proposed matrix/across pivoting; a code check found that already implemented, so this ADR now covers only the remaining gap: multi-hop joins.) **Multi-hop to-one joins (≤3 hops / 4 objects) have since been implemented & are live — header pending formal update.**
64

75
## Correction (2026-06-24)
86

‎docs/adr/README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ Listed side by side, never renumbered (Prime Directive 13). A **collision** is t
3434

3535
### ADR-0006 — one decision, 3 versions
3636

37+
A bare `ADR-0006` names the decision; its current text is [0006-project-environment-split.v4.md](./0006-project-environment-split.v4.md). Cite one revision by its version or its stem.
38+
3739
| Cite as | File | Title | Status | Date |
3840
|---|---|---|---|---|
39-
| `ADR-0006` | [0006-project-environment-split.md](./0006-project-environment-split.md) | Environment & Project as Independent Siblings — v3 | Superseded — by v4 (`0006-project-environment-split.v4.md`) — 2026-05-20 | 2026-05-20 |
41+
| `ADR-0006-project-environment-split` (unversioned file, superseded) | [0006-project-environment-split.md](./0006-project-environment-split.md) | Environment & Project as Independent Siblings — v3 | Superseded — by v4 (`0006-project-environment-split.v4.md`) — 2026-05-20 | 2026-05-20 |
4042
| `ADR-0006 v2` / `ADR-0006-project-environment-split.v2` | [0006-project-environment-split.v2.md](./0006-project-environment-split.v2.md) | Three-Layer Tenancy — Organization, Project, Environment | Superseded — by v4 (`0006-project-environment-split.v4.md`) — 2026-05-20 | 2026-05-20 |
41-
| `ADR-0006 v4` / `ADR-0006-project-environment-split.v4` | [0006-project-environment-split.v4.md](./0006-project-environment-split.v4.md) | Environment & Project — v4 (drop dev-workspace Project, unify on Package) | Accepted — v4 — supersedes v3 — API-surface vocabulary boundary recorded 2… | 2026-05-20 |
43+
| `ADR-0006` (current) / `ADR-0006 v4` / `ADR-0006-project-environment-split.v4` | [0006-project-environment-split.v4.md](./0006-project-environment-split.v4.md) | Environment & Project — v4 (drop dev-workspace Project, unify on Package) | Accepted — v4 — supersedes v3 — API-surface vocabulary boundary recorded 2… | 2026-05-20 |
4244

4345
### ADR-0010 — 2 unrelated decisions — a collision
4446

@@ -140,14 +142,14 @@ Listed side by side, never renumbered (Prime Directive 13). A **collision** is t
140142
| [ADR-0068](./0068-unified-user-context-and-built-in-identity-roles.md) | Unified user-context contract & built-in identity roles — one `currentuser` shape across formula/RLS/client,… | Accepted — 2026-06-24 — implemented under the position model: `EvalUser` f… | 2026-06-24 | — | — |
141143
| [ADR-0069](./0069-enterprise-authentication-hardening.md) | Enterprise authentication hardening — password policy, enforced MFA, SSO, session controls, network gating, a… | Accepted — P1 + P2 implemented (2026-07-04); P3 partially landed (see Adde… | 2026-07-04 | — | — |
142144
| [ADR-0070](./0070-package-first-authoring.md) | Package-first authoring — every runtime-authored item lives in a writable package (base); no orphans | Accepted — 2026-06-24 — P1–P3 implemented, merged & live-verified (see Imp… | 2026-06-24 | — | — |
143-
| [ADR-0071](./0071-dataset-semantic-layer-depth.md) | Dataset semantic-layer depth — multi-hop joins | Proposed — (Refocused — see Correction below. The first merged draft also | — | — | — |
145+
| [ADR-0071](./0071-dataset-semantic-layer-depth.md) | Dataset semantic-layer depth — multi-hop joins | Proposed — Refocused — see Correction below. The first merged draft also p… | — | — | — |
144146
| [ADR-0072](./0072-reference-scope-and-resolvability.md) | Reference Scope & Resolvability — the authored-expression data-picker model (#1934) | Proposed — 2026-06-25 | 2026-06-25 | — | — |
145147
| [ADR-0073](./0073-automation-execution-identity.md) | Automation execution identity — a built-in non-human principal for user-less runs, `runAs` as authorization p… | Proposed — 2026-06-25 | 2026-06-25 | — | — |
146148
| [ADR-0074](./0074-canonical-audit-timestamp-storage-on-sqlite.md) | Audit timestamps are stored in one canonical, timezone-explicit format on SQLite | Accepted — 2026-06-26. Read side extended (2026-09-07): the read repair th… | 2026-06-26 | — | — |
147149
| [ADR-0076](./0076-objectql-core-tiering.md) | objectql is the data engine — relocate metadata management (protocol) out of it; enforce the boundary; defer… | Accepted — D1/D2/D4/D5/D6/D8/D9 implemented; D10/D12 partially landed; D11… | 2026-08-15 | — | — |
148150
| [ADR-0077](./0077-authoring-surface-boundary-hook-flow-validation.md) | The authoring-surface boundary — route by intent, not mechanism; verifiability tier = friction tier; loud-not… | Proposed — 2026-06-28 | 2026-06-28 | — | — |
149151
| [ADR-0078](./0078-no-silently-inert-metadata.md) | A Zod-valid metadata instance that is functionally incomplete must fail loudly at author time, uniformly acro… | Accepted — framework implemented; the cloud half of §2/§3 pending (cloud)… | 2026-06-28 | — | — |
150-
| [ADR-0079](./0079-record-display-name.md) | The record display-name contract — `nameField` is the one canonical primary-title pointer | Accepted — implemented. This file is a RETROACTIVE RECONSTRUCTION written… | 2026-08-08 | — | — |
152+
| [ADR-0079](./0079-record-display-name.md) | The record display-name contract — `nameField` is the one canonical primary-title pointer | Accepted — implemented. This file is a RETROACTIVE RECONSTRUCTION written… | 2026-06-28 | — | — |
151153
| [ADR-0080](./0080-ai-authored-ui-jsx-source.md) | AI-authored UI pages — JSX source compiled to the SDUI tree (parse ≠ execute); the component registry is the… | Proposed — 2026-06-29 — partially implemented (2026-07-16 audit): `sdui-pa… | 2026-06-29 | — | ADR-0065 |
152154
| [ADR-0081](./0081-trusted-react-page-tier.md) | A trusted `kind:'react'` page tier — real React executed in the main tree, gated by a host capability; and re… | Accepted — 2026-06-30 — framework scope implemented (`html`/`react` page k… | 2026-06-30 | — | ADR-0065 |
153155
| [ADR-0082](./0082-react-component-contract-governance.md) | Governing the react-tier component contract — spec is the protocol source of truth, the registry is a designe… | Accepted — 2026-06-30 | 2026-06-30 | — | — |
@@ -202,7 +204,7 @@ Listed side by side, never renumbered (Prime Directive 13). A **collision** is t
202204
| [ADR-0132](./0132-multi-organization-runtime-is-open-core.md) | The multi-organization runtime is open core — single-database organization isolation ships open; only the ent… | Accepted — 2026-09-07 — accepted by the merge that landed it on `main` | 2026-09-07 | — | — |
203205
| [ADR-0133](./0133-org-management-open-basics.md) | Organization management — the open basics; a mirror of cloud ADR-0081 D1/D3/D4 in the repository whose code e… | Accepted — 2026-09-07 — accepted by the merge that landed it on `main` (#1… | 2026-09-07 | — | — |
204206
| [ADR-0134](./0134-env-side-scim-provisioning.md) | Env-side SCIM provisioning — `@better-auth/scim` is the mechanism, and this platform writes the deactivation | Proposed — 2026-09-07. This file is a MIRROR of the open, mechanism half of | 2026-09-07 | — | — |
205-
| [ADR-0135](./0135-identity-and-access-architecture.md) | Identity and access architecture — the open half, mirrored from cloud ADR-0024 | Accepted — The DECISION is — it is `objectstack-ai/cloud` ADR-0024, accept… | — | — | — |
207+
| [ADR-0135](./0135-identity-and-access-architecture.md) | Identity and access architecture — the open half, mirrored from cloud ADR-0024 | Accepted — The DECISION is — it is `objectstack-ai/cloud` ADR-0024, accept… | 2026-06-25 | — | — |
206208
| [ADR-0137](./0137-predicate-fault-semantics-are-contract.md) | A predicate's FAULT semantics are part of the protocol — loud at submit, fail-open at render, and a blank pre… | Accepted — 2026-09-18 — this record declares; it implements nothing. D1's… | 2026-09-18 | — | — |
207209
| [ADR-0138](./0138-guest-model-anonymous-principal-doors-grants-and-organization.md) | The guest model — one anonymous principal, a closed list of doors, one grants channel, one organization rule,… | Accepted — 2026-10-13, accepted by the maintainer's approval of the pull r… | 2026-10-13 | — | — |
208210
| [ADR-0139](./0139-engine-internal-semi-join-leaf-readable-parent-ids.md) | One engine-internal semi-join leaf — "the ids of object X this caller may read" — pushed down by ObjectQL, ne… | Accepted — 2026-10-11, accepted by the merge that landed it on `main` | 2026-10-11 | — | — |

‎scripts/adr-index.mjs‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ const TOMBSTONE_SLUG_PREFIX = 'withdrawn-';
108108

109109
// ── The self-test's own battery roster and floor ───────────────────────────
110110
const SELF_TEST_BATTERIES = Object.freeze({
111-
'record parsing: filename, title, status, date (#26076)': 11,
111+
'record parsing: filename, title, status, date (#26076)': 12,
112112
'the one status-line spelling: every other spelling is Unparsed, never guessed': 5,
113113
'supersession references are extracted, not invented': 8,
114-
'shared numbers and versions group side by side, registries never merge': 5,
114+
'shared numbers and versions group side by side, registries never merge': 6,
115115
'rendering is deterministic and lists both registries': 7,
116116
'drift verdicts: --check reds, --report never does': 4,
117-
'Live tree: every record parses and the registry is not empty': 6,
117+
'Live tree: the generator reads the real registry (the records themselves are --report\'s business)': 4,
118118
});
119119
const SELF_TEST_BATTERY_FLOOR = 7;
120120
const UNATTRIBUTED_BATTERY = '(no battery open)';
@@ -233,7 +233,7 @@ export function parseRecord(name, text) {
233233
const keyword = statusValue === null ? UNPARSED : statusKeywordOf(statusValue);
234234
const header = lines.slice(0, HEADER_WINDOW);
235235
const fields = headerFields(header);
236-
const dateField = fields.find((f) => /^(date|日期)$/i.test(f.key));
236+
const dateField = fields.find((f) => /^(date|decided|日期)$/i.test(f.key));
237237
const date = (dateField && ISO_DATE.exec(dateField.value)?.[0]) ?? (statusValue && ISO_DATE.exec(statusValue)?.[0]) ?? null;
238238

239239
const supersedesFields = fields.filter((f) => /supersedes/i.test(f.key) && !/superseded by/i.test(f.key));
@@ -414,9 +414,21 @@ export function renderIndex({ root, ui }) {
414414
for (const s of shared) {
415415
const kind = s.kind === 'collision' ? `${s.records.length} unrelated decisions — a collision` : `one decision, ${s.records.length} versions`;
416416
out += `### ADR-${s.number} — ${kind}\n\n`;
417+
const current = s.kind === 'versions' ? s.records[s.records.length - 1] : null;
418+
if (current) {
419+
out += `A bare \`ADR-${s.number}\` names the decision; its current text is [${current.file}](./${current.file}). Cite one revision by its version or its stem.\n\n`;
420+
}
417421
out += '| Cite as | File | Title | Status | Date |\n|---|---|---|---|---|\n';
418422
for (const r of s.records) {
419-
const citeAs = s.kind === 'collision' ? `\`ADR-${r.stem}\`` : r.version === null ? `\`ADR-${r.number}\`` : `\`ADR-${r.number} v${r.version}\` / \`ADR-${r.stem}.v${r.version}\``;
423+
const stemId = r.version === null ? `ADR-${r.stem}` : `ADR-${r.stem}.v${r.version}`;
424+
const citeAs =
425+
s.kind === 'collision'
426+
? `\`ADR-${r.stem}\``
427+
: r === current
428+
? `\`ADR-${r.number}\` (current) / \`ADR-${r.number} v${r.version}\` / \`${stemId}\``
429+
: r.version === null
430+
? `\`${stemId}\` (unversioned file, superseded)`
431+
: `\`ADR-${r.number} v${r.version}\` / \`${stemId}\``;
420432
out += `| ${citeAs} | [${r.file}](./${r.file}) | ${cell(r.title, 90)} | ${statusCell(r)} | ${r.date ?? '—'} |\n`;
421433
}
422434
out += '\n';
@@ -607,6 +619,7 @@ export function runSelfTest() {
607619
assert('status-value-is-read-off-the-line', r.statusValue === 'Accepted — implemented (2026-07-15)', JSON.stringify(r.statusValue));
608620
assert('keyword-is-the-earliest-vocabulary-entry', r.keyword === 'Accepted' && statusKeywordOf('The DECISION is Accepted — it is cloud ADR-0024') === 'Accepted' && statusKeywordOf('Partially implemented (revised)') === 'Partially implemented' && statusKeywordOf('Proposed — partially implemented') === 'Proposed', 'earliest entry wins, multi-word entries are whole');
609621
assert('date-prefers-the-date-line', r.date === '2026-06-01', JSON.stringify(r.date));
622+
assert('a-decided-line-is-a-date-line', rec('0042-answer.md', canonical('Accepted — reconstructed 2026-08-08', '- **Decided**: 2026-06-28 (the decision), reconstructed 2026-08-08 (this file)\n')).date === '2026-06-28', 'the decision date, not the reconstruction date');
610623
assert('date-falls-back-to-the-status-line', rec('0042-answer.md', canonical('Proposed (2026-06-22)')).date === '2026-06-22', 'the first ISO date in the status value');
611624
assert('date-is-null-when-nothing-states-one', rec('0042-answer.md', canonical('Proposed')).date === null, 'no date is invented');
612625
}
@@ -648,6 +661,8 @@ export function runSelfTest() {
648661
assert('two-stems-under-one-number-group-as-collision', s.find((x) => x.number === '0010')?.kind === 'collision', JSON.stringify(s));
649662
assert('a-number-with-one-file-is-not-shared', !s.some((x) => x.number === '0011') && s.length === 2, JSON.stringify(s));
650663
assert('shared-groups-are-sorted-by-number-then-version', s[0].number === '0006' && s[0].records.map((r) => r.version).join(',') === ',2,4', JSON.stringify(s[0].records.map((r) => r.file)));
664+
const vtext = renderIndex({ root: R, ui: [] });
665+
assert('a-versions-group-cites-the-bare-number-as-the-highest-version', vtext.includes('A bare `ADR-0006` names the decision; its current text is [0006-split.v4.md](./0006-split.v4.md)') && vtext.includes('| `ADR-0006` (current) / `ADR-0006 v4` / `ADR-0006-split' + '.v4` |') && vtext.includes('| `ADR-0006-split` (unversioned file, superseded) |'), 'the superseded unversioned file is never offered as the bare citation');
651666
const text = renderIndex({ root: [rec('0001-root.md', canonical('Accepted'))], ui: [rec('0001-ui.md', canonical('Accepted'))] });
652667
assert('ui-records-never-join-a-root-number-group', !text.includes('## Shared numbers\n\nListed') && text.includes('[ui-0001](../../ui/docs/adr/0001-ui.md)'), 'the two registries are indexed apart');
653668
}
@@ -684,16 +699,20 @@ export function runSelfTest() {
684699
assert('report-mode-is-exit-zero-by-construction', /return 0;\s*}\s*function modeJson/.test(readFileSync(join(process.cwd(), SELF_PATH), 'utf8')) && !/process\.exit\(1\)[^\n]*report/.test(readFileSync(join(process.cwd(), SELF_PATH), 'utf8')), 'modeReport must end in `return 0` and never exit 1');
685700
}
686701

687-
battery('Live tree: every record parses and the registry is not empty');
702+
battery('Live tree: the generator reads the real registry (the records themselves are --report\'s business)');
688703
{
704+
// ⛔ Nothing here asserts on what the records SAY. A record with another
705+
// status spelling, a stray note in the directory or a stale index is
706+
// --report's warning (#26076, ruling A: 门禁不加), never this self-test's
707+
// red — this battery only proves the generator finds and renders the
708+
// real tree, so a wrong ADR_DIR or a renderer that throws on live input
709+
// cannot pass on fixtures alone.
689710
const live = readRegistries(process.cwd());
690-
assert('live-root-registry-is-populated', live.root.records.length >= 100, `got ${live.root.records.length} records under ${ADR_DIR}/`);
691-
assert('live-root-registry-has-no-foreign-files', live.root.foreign.length === 0, `non-record markdown under ${ADR_DIR}/: ${live.root.foreign.join(', ')}`);
692-
const unparsed = live.root.records.filter((r) => r.keyword === UNPARSED);
693-
assert('live-every-root-record-carries-the-one-status-spelling', unparsed.length === 0, `Unparsed: ${unparsed.map((r) => r.file).join(', ')}`);
694-
assert('live-shared-numbers-are-listed', sharedNumbers(live.root.records).length >= 1, 'the registry carries shared numbers today');
695-
assert('live-ui-registry-parses', live.ui.records.length >= 1 && live.ui.records.every((r) => r.keyword !== UNPARSED), `ui: ${live.ui.records.length} records, unparsed ${live.ui.records.filter((r) => r.keyword === UNPARSED).map((r) => r.file).join(', ')}`);
711+
assert('live-root-registry-is-read', live.root.records.length >= 1, `got ${live.root.records.length} records under ${ADR_DIR}/ — run from the repository root`);
712+
assert('live-ui-registry-is-read', live.ui.records.length >= 1, `got ${live.ui.records.length} records under ${UI_ADR_DIR}/`);
696713
assert('live-index-renders', renderIndex({ root: live.root.records, ui: live.ui.records }).length > 10_000, 'the live rendering is not trivially small');
714+
const v = verdict(live, readCommitted(process.cwd()));
715+
assert('live-verdict-is-a-reading-not-a-throw', typeof v.stale === 'boolean' && Array.isArray(v.problems) && typeof v.expected === 'string', 'verdict() over the live tree returns its three fields');
697716
}
698717
} catch (e) {
699718
failures.push(`self-test threw before finishing — ${e && e.stack ? e.stack : e}`);

0 commit comments

Comments
 (0)