Skip to content

docs(adr): ADR-0005 states the five-part overlay key — bundle_member since 5677add93f (#23006) - #23037

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-23006-adr-0005-five-part-overlay-key
Oct 12, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-23006-adr-0005-five-part-overlay-key

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23006
Clause-②: no

ADR-0005's present-tense "What delivers overlay uniqueness today" block still spelled the overlay uniqueness key with four parts and quoted the declared index with four fields and the retired bare unique: true. Since 5677add93f (PR #22954, #22877 ruling A) the runtime key and the declared index both carry a fifth part, bundle_member. This PR restates the current key from the tree and records the change as a dated amendment. Docs and ADR-anchor text only; nothing publishes.

What changed

docs/adr/0005-metadata-customization-overlay.md (898 → 908 lines, +19 / −9, net +10):

  • Status line: one entry appended in the record's existing format: · **Amended** (2026-10-12, #22877 ruling A — …). It names the fifth part and what it is (a bundled type's member, such as an email_template's locale). It also states that NULL is the canonical member, so name-only types are unchanged, and names the producer 5677add93f (PR fix(metadata-protocol): two locales of one email template survive a restart — sys_metadata overlay rows keyed by bundle member (#22877, stage 1) #22954). The entry goes on the existing line, so no line above the body moves.
  • The [metadata] addSysMetadataOverlayIndex 用 ADR-0048 之前的键抢占 idx_sys_metadata_overlay_active——同名索引有两个生产者,先跑的赢 #6771 measurement ("a plain DatabaseLoader boot already stores CREATE UNIQUE INDEX … (type, name, organization_id, package_id) — the declared index, with the current key"): kept verbatim, because it records a measurement. A dated 2026-10-12 note follows it, saying that key was the four-part key of its day and pointing to the status line and the two owners.
  • Owner 1 (runtime, overlay-index.ts): (type, name, organization_id, COALESCE(package_id, ''), COALESCE(bundle_member, '')) WHERE state = 'active'. One sentence gives the reason, taken from the module header: two members of one item are two rows. NULL is the canonical member and every row of a name-only type, and folding it to '' constrains those rows exactly as the four-part key did.
  • Owner 2 (declared, sys-metadata.object.ts): the quotation now matches the tree: fields: ['type', 'name', 'organization_id', 'package_id', 'bundle_member'], unique: 'global'. A parenthetical notes that 'global' is protocol 18's spelling of the bare true it retired (ADR-0120 D2). The trailing sentence about the coarse fallback is restated from measurement (see Acceptance notes ②).

scripts/adr-anchors/…overlay-index.ts.json and …sys-metadata.object.ts.json change one key span each, from the four-part to the five-part spelling. This is a bounded in-place fix (Acceptance notes ③).

The quoted code is taken from the tree at edecd4485: OVERLAY_INDEX_COLUMNS (packages/metadata-protocol/src/migrations/overlay-index.ts:169), buildOverlayIndexSql (same file), and the declared index (packages/metadata-core/src/objects/sys-metadata.object.ts:250-254).

Gates (all on HEAD a75f9f225)

I derived the gate list with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, passing no paths. It found 3 paths against merge base edecd4485 and listed 27 commands. All 27 exit 0:

check-adr-links (+ --self-test) · check-adr-symbol-anchors (+ --self-test) · check-ci-filter-parity · check-closing-keyword-parity (+ --self-test) · check-comment-mask-corpus · check-scripts-symbol-anchors (+ --self-test) · @objectstack/lint check:doc-formula-expressions · check:adr-anchors · check:agent-test-spelling · check:bash32-floor · check:cli-command-ids · check:cross-package-test-inputs · check:doc-authoring · check:driver-memory-census · check:entry-guard · check:gitlink-declared · check:nul-bytes · check:parse-guard · check:pm-governed-merges · check:pm-prior-rulings · check:pnpm-filter-targets · check:refd-timer-probe · check:watch-hint-literal.

Key verdict lines:

  • check-adr-anchors: OK (68 anchored file(s), every governing ADR still referenced; … 44138 citation(s) across 5846 file(s) resolve …)
  • check-adr-links: 730 relative link destination(s) under docs/adr/ resolve
  • check-nul-bytes: OK (scanned 11039 text file(s) …; no raw ASCII control bytes)

The first run of check:doc-formula-expressions exited 3 (PREREQUISITE NOT MET: @objectstack/formula and @objectstack/lint were unbuilt). I built them under the verify lock (VERDICT command-exit 0) and re-ran it: exit 0, 22 record-scoped formula example(s) … judged clean.

dispatch-gates --ran reconciliation: 27 derived famil(ies) accounted for — 27 run, 0 NOT-MEASURED (a DERIVED zero — all 27 recorded an exit code and none of them is 3).

I also ran some roster gates outside the 27:

  • Exit 0: check:overlay-whitelist-table, check-published-list-mirrors, check:dts-closure, check:engine-double-contract, check:i18n-stale-fill.
  • NOT MEASURED: check:published-readme-exports exited 3 because it needs a full build; this diff touches no package and no README.
  • NOT MEASURED: check:console-injection skipped itself because there is no console dist.

No changeset: docs/adr/** and repo-root scripts/adr-anchors/** publish nothing, so this PR takes skip-changeset.

Acceptance notes

  1. Every present-tense four-part site, found by a full re-grep. At edecd4485 there were three, not the two the card names: the [metadata] addSysMetadataOverlayIndex 用 ADR-0048 之前的键抢占 idx_sys_metadata_overlay_active——同名索引有两个生产者,先跑的赢 #6771 measurement (:835), Owner 1 (:867) and the Owner 2 quotation (:878, which the card does not name). After the edit, the only four-part spelling left in present-tense context is the preserved :835 measurement, and the dated note now follows it. Every other four-part or older spelling is in dated historical blocks that this record keeps standing on purpose:

    • the Addendum (d) YAML: project_id, scope, unique: true, partial;
    • the 2026-05-22 amendment;
    • the 2026-08-09 amendment's statement that the decision "at most one active customization of a given (type, name) per organization" is unchanged.
  2. Owner 2's trailing sentence was false after 5677add93f and is restated. The old text read "it still prevents duplicate active overlays … and it stays NULL-distinct on package_id". The declared index is a plain UNIQUE over five columns. It is NULL-distinct on bundle_member, and that column is NULL for every canonical member, so the fallback no longer holds any canonical row unique. I measured this on an in-memory SQLite (better-sqlite3 12.11.1) with that exact five-column plain CREATE UNIQUE INDEX:

    • two identical packaged canonical rows (bundle_member NULL): both INSERTED;
    • two identical zh-CN member rows: the second REFUSED with SQLITE_CONSTRAINT_UNIQUE.

    The ADR now says "NULL-distinct on package_id and on bundle_member — NULL for every canonical member, so it holds no canonical row unique".

    The code comment beside the declaration (sys-metadata.object.ts:244-249, "it still prevents duplicate ACTIVE overlays") carries the same stale claim. It is outside this card's file surface and is not edited. I have not measured whether any real composition gets only the declared tier, so this stays a note, not a card.

  3. Bounded in-place fix: two ADR anchors. Both invariant strings stated the current key in the present tense with four parts:

    • overlay-index.ts.json: "(type, name, organization_id, COALESCE(package_id, '')) WHERE state = 'active'";
    • sys-metadata.object.ts.json: "the declared idx_sys_metadata_overlay_active on (type, name, organization_id, package_id)".

    Each now spells the five-part key, and nothing else in either string changed. All four conditions hold:

    • same defect class (a present-tense four-part key after 5677add93f);
    • a mechanical one-span edit each;
    • both files declared in the claim's file surface;
    • check:adr-anchors is in the derived family, and it is green.
  4. The ADR line range another anchor cites did not move. scripts/adr-anchors/packages__spec__src__kernel__metadata-plugin.zod.ts.json cites docs/adr/0005-metadata-customization-overlay.md:53-64. The status entry was appended to the existing line, so lines 53-64 are byte-identical between edecd4485 and HEAD (diff exit 0). That anchor file is not touched.

  5. The same four-part statement in other places, not edited here (other files or other governed surfaces; listed for whoever closes the family):

    • docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md:669
    • docs/adr/0126-packaged-metadata-customization-model.md:313
    • skills/objectstack-data/rules/indexing.md:165 (the published skill's partial-index example quotes the platform's runtime SQL with four parts)
    • docs/qa/platform-checklist/areas/cli.json:1125 (a QA step recreates "the declared NULL-distinct form" with four columns; the step still seeds the duplicates it needs, so this is drift only)
  6. Pre-existing, not introduced here. Both indexes keep organization_id bare and NULL-distinct. The overlay-index module header already names this as "a separate question with its own ADR-0120 D4 ceremony". Environment rows are written with organization_id NULL (sys-metadata-repository.ts, the ADR-0131 D6 environment-rows read), so the DB-level key does not constrain those rows on either tier. This is a read-only observation, not filed.

维护者速读(草稿)

改了什么

为什么改

风险与代价(含回滚)

  • 只改文档和锚点文字:不发布任何包,不改运行时行为。
  • ADR-0005 净增 10 行(898 → 908)。
  • 回滚:revert 本 PR 的单个提交即可。

席位意见

你要做的

  • 审阅措辞,重点是声明索引那句新结论:「不再约束任何规范成员行」。
  • 同意就给一个 APPROVED 评审,之后由席位落地;不同意就在 PR 上留言指出要改的句子。

Generated by Claude Code

…since 5677add

ADR-0005 still spelled the overlay uniqueness key with four parts in its
present-tense "What delivers overlay uniqueness today" block, and quoted the
declared index with four fields and the retired bare `unique: true`. Since
5677add (#22877 ruling A) both the runtime partial indexes and the
declared index carry `bundle_member`.

- Status line: one dated Amended entry for #22877 ruling A.
- The #6771 measurement sentence is kept verbatim and gains a dated note.
- Owner 1 (runtime): the five-part key, with the member's reason.
- Owner 2 (declared): five fields, `unique: 'global'`, and its
  NULL-distinctness on `bundle_member` stated.
- The two ADR-0005 anchors' invariant text spells the five-part key.

Claude-Session: https://claude.ai/code/session_012dGTuJen61ECNHXYXKChoC
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a75f9f22585f8c1274e9f5acf317b77a01017c65
Local-runs: none

Inputs read: card #23006 (body and all three comments: triage 6123056260, the dispatch Claim: 6123468051, the os-dev-report 6123800633); PR #23037 (body, file list, the three-dot diff against main; the head's parent is edecd4485); the check-runs on the head, read three times (the last at 2026-10-12T12:07:57Z). Every factual claim the diff adds about code was read against origin/main at 0ede0f8ab3 with git show / git grep. Nothing was built, run or re-run.

① Derived judgments

Accept-set and public surface: none changed — right. The diff touches docs/adr/0005-metadata-customization-overlay.md (+19/−9, 898 → 908 lines) and the invariant string of two scripts/adr-anchors/*.json files (one span each). No packages/** path, no schema, no runtime code, no SQL, no generated artifact. The anchor files are read only by scripts/check-adr-anchors.mjs, which audits the ADR-NNNN Dk citations inside an invariant; neither edited string changes a citation. Both anchor base blobs (de4e27e13a4, 797dfd110fb) are the blobs at origin/main, so the one-span edits are against the current text.

Factual claims the diff adds, each read against source — all right:

Claim added by the diff Source on origin/main Verdict
The declared index is { name: 'idx_sys_metadata_overlay_active', fields: ['type', 'name', 'organization_id', 'package_id', 'bundle_member'], unique: 'global' } at sys-metadata.object.ts:250-254 packages/metadata-core/src/objects/sys-metadata.object.ts:250-254 reads exactly that right
Runtime key (type, name, organization_id, COALESCE(package_id, ''), COALESCE(bundle_member, '')) WHERE state = 'active', with the _draft sibling packages/metadata-protocol/src/migrations/overlay-index.ts: OVERLAY_INDEX_COLUMNS (:169) names the five columns; OVERLAY_NULL_SENTINELS (:190) folds package_id and bundle_member to ''; overlayIndexKeyParts() plus buildOverlayIndexSql() (:210) emit CREATE UNIQUE INDEX … WHERE state = '…' per state; the names idx_sys_metadata_overlay_active / _draft are at :151-152 right
bundle_member is a bundled type's member (an email_template's locale); two members of one item are two rows; NULL is the canonical member and every row of a name-only type; folded to '', those rows are constrained exactly as under the four-part key overlay-index.ts module header :95-105 (ITEM_KEY_DISCRIMINATORS, today email_template's (name, locale); "NULL is the canonical member and every row of an undiscriminated type, folded to the '' bucket so those rows are constrained exactly as before"); sys-metadata.object.ts:227-229 says the same right
In force since 5677add93f (PR #22954, #22877 ruling A) 5677add93f is an ancestor of origin/main, subject "fix(metadata-protocol): two locales of one email template survive a restart — … (#22877, stage 1) (#22954)", 2026-10-12 09:01Z; its diff adds bundle_member to both files above. The card body itself names ruling 6116165299, letter A right
'global' is protocol 18's spelling of the bare true it retired (ADR-0120 D2) ADR-0120 D1 (bare true on a declared index: 17.x warn → protocol 18 reject) and D2 (the ADR-0087 entry rewrites unique: true → 'global'); ef1fcb26a (#22103, 2026-10-08) made exactly that one-token edit in sys-metadata.object.ts right
The declared (coarse) index is NULL-distinct on package_id and on bundle_member, so it holds no canonical row unique Derived, and it follows: a plain UNIQUE over five bare columns, bundle_member NULL on every canonical and undiscriminated row, SQL UNIQUE treating NULLs as distinct — the premise the ADR already carried, and overlay-index.ts:184-186 restates it for this very column ("a bare part would leave exactly the rows this index has always constrained unconstrained"). The old sentence "it still prevents duplicate active overlays" was false after 5677add93f; the restatement is a correction, not a widening right
The #6771 measurement sentence (:835) is kept verbatim and gains a dated note It records a measurement of its day; the note points at the status line and the two owners, and the section it names ("What delivers overlay uniqueness today — exactly two owners") exists at :861 right

Site coverage: at the head's parent edecd4485, ADR-0005 carries three present-tense four-part sites — :835 (measurement, kept), :867 (Owner 1), :878 (Owner 2 quotation, with unique: true). The card named two (:833, :865 at 5677add93f; the +2 is PR #23011's top note), and its re-check grep could not match :878's quoted form. The dev's full re-grep is the right reading. :570 (unique: true in the Addendum (d) YAML) is a dated historical block and is rightly left standing.

Line citations into ADR-0005: all eight on origin/main (metadata-plugin.zod.ts:896 and :938, its anchor JSON, three tests, check-spec-docblock-symbol-anchors.mjs:611 and :619) point at :53-64 or :57. The status entry replaces line 8 one-for-one and the body hunks start at :838, so every cited range is byte-identical; the dev's H4 check on one anchor generalises to all eight.

Check-runs on the head (their conclusions are the gate verdicts): Lint & Repo Gates success (the job that carries check:adr-anchors and check-adr-links); Governed Surface Queue Guard success; Check Changeset success; Check PR Size success; Check Documentation Links — the first run (114799118871) failed in the lychee binary download before any link was checked, the re-run (114801046985) is success and is the latest conclusion; Test Core shards 1/6 through 6/6 all success; Type Check · source gates, · consumer gates, · debt ledger success; every Lint · shard success; Type Check · workspace (114799118670) was still in_progress at my 12:05:12Z read and is success at my last read (2026-10-12T12:07:57Z), when the TypeScript Type Check (114802989657) and Test Core (114802466655) aggregate contexts had also been emitted, both success; Build Core, Console Pin Gate, Temporal Conformance, Build Docs, the dogfood matrix and the tarball smoke are skipped by the path filter (docs-only diff), and the Dogfood Regression Gate aggregate is success. At the last read every one of the 43 check-runs is completed with conclusion success or skipped; nothing is red and nothing is in progress.

② Semver level

skip-changeset is right: no released package is in the diff (docs/adr/** and repo-root scripts/adr-anchors/** publish nothing), so there is nothing a changeset could describe, and Check Changeset passed on the label. The PR body carries Clause-②: no as its second line — right: no accept-set widens or narrows, no public surface moves, no author-writable key changes. No ADR-0087 disposition is owed, since nothing is breaking.

③ Boundary flags

open_questions: empty — nothing to answer.

Dev deviations (four), each judged:

  1. Owner 2's trailing sentence restated beyond "five fields plus the corrected unique value" — accepted: it is a present-tense statement of the current key's effect, inside the claim's file surface, and the old sentence was false after 5677add93f (table row 6 above). The PR body flags it as the one sentence for the maintainer to read closely; that is the right handling for Tier H text.
  2. The 'global' parenthetical — accepted, verified against ADR-0120 D1/D2 and ef1fcb26a.
  3. Commit trailers in the model-free pair (Claude-Session: plus Co-authored-by: Claude), not the harness's model-named form — accepted: read on the head commit; it is AGENTS.md's form, and no model identifier appears in the title, body, commit or label set.
  4. PR body footer in the session-URL form — accepted: AGENTS.md's form for PR bodies; the body read back intact, the Chinese 维护者速读 block included.

The two anchor edits: the dispatch's Claim: (6123468051) named both anchor files conditionally — the key spelling in their invariant only, if measured stale. Both were stale (the base blobs spell four parts), each edit is one span, and the citations inside the strings are unchanged. I could not find the phrase "bounded in-place rule" in .claude/agents/os-dev.md or the pm-dispatch skill at origin/main; the dispatch's own conditional authorisation and the measured staleness decide it, so this is a note, not a flag. One residual: the sys-metadata.object.ts.json invariant still says knex "can express neither WHERE state = 'active' nor COALESCE(package_id, '')" without naming COALESCE(bundle_member, '') — true as written, incomplete rather than false, and outside a one-span edit; not a defect.

Out-of-scope findings (five), escalated to the seat, none a FAIL reason — every one is outside the claim's file surface and the dev reported it rather than expanding scope:

  • (a) docs/adr/0029-kernel-object-ownership-and-platform-objects-decomposition.md:669 and docs/adr/0126-packaged-metadata-customization-model.md:313 still spell the four-part key — verified on origin/main. Same class (b), same producer; the dev's "one family closure card" is the right shape, and the seat files it (docs/adr/**, Tier H).
  • (b) skills/objectstack-data/rules/indexing.md:165 (published skill, Tier H) quotes the four-part runtime SQL — verified; rides the family card.
  • (c) docs/qa/platform-checklist/areas/cli.json:1125 recreates the declared form with four columns — verified; drift only, rides the family card.
  • (d) packages/metadata-core/src/objects/sys-metadata.object.ts:246-249 still says the declared index "still prevents duplicate ACTIVE overlays" — verified; once this PR lands, that code comment contradicts ADR-0005's Owner 2 sentence. A one-line comment fix in a non-governed file; the seat should carry it on or beside the family card so the record and the code comment do not disagree.
  • (e) organization_id bare and NULL-distinct on both tiers — pre-existing, and overlay-index.ts:117-118 already names it as its own ADR-0120 D4 ceremony. Noted, no action.

Tier: docs/adr/** is the adr row of GOVERNED_SURFACES, Tier H. This record is the contract-tier review of record for this head; it does not lift Tier H — landing waits for an authorized APPROVED review from a GOVERNED_APPROVERS account, after which the owning seat lands it. The PR is draft, auto-merge is not armed, and the head repo equals the base repo (not a fork). Nothing in this record is a reason to ready, queue or arm it.

Implemented-by: claude/issue-23006-adr-0005-five-part-overlay-key
Reviewed-by: session_012dGTuJen61ECNHXYXKChoC

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读(终稿)

PR #23037 · card #23006 · head a75f9f2258 · seat domain:skills#2 (session_012dGTuJen61ECNHXYXKChoC), written 2026-10-12T12:10Z. The seat checked the dev's draft against the diff it read; this comment is the version to read.

改了什么

为什么改

风险与代价(含回滚)

席位意见

  • 建议批准。
  • 三处现在时陈述逐一对过 origin/main 代码原文:OVERLAY_INDEX_COLUMNS / overlayIndexKeyParts,以及 sys-metadata.object.ts 的声明索引。
  • 另一份锚点按行号区间(53–64)引用 ADR-0005,这段没有挪动。
  • 契约复核:达档 PASS,记录为本 PR 评论 6123969188(同 head a75f9f2258);CI 43 项全部完成(success 或按路径 skipped)。

你要做的


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants