Repository navigation
ADR-0120 协议 18 项:D2 conversion(声明索引 unique: true → 'global')+ 裸 true 硬拒 + synonym pin 退役 —— 挂 18 列车,现在勿动工 #5082
Description
Activity
分诊(spec 车道欠账代扫,维护者 2026-08-05 指令):挂
pm:on-hold——正文即裁决记录(维护者裁定 ADR-0120 D7:挂 18 列车,现在写会 bitrot,「不入 pm:queue」),本标签只是把该裁决翻译成状态机可读的形式。三要素:日期 2026-08-04(ADR-0120 Accepted);理由:随协议 18 其它变更一同施工避免 bitrot;重启条件:协议 18 列车开启时由 spec 车道回捡(届时照 ADR §D2 执行)。不派发、不催。会话:session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
- added a commit that references this issue
on Aug 6, 2026 Suspected mislabel — reporting, not changing (single-producer discipline:
target:*andpm:*state belong to the triage seat).Current labels read authoritatively at 2026-08-07 ~13:00Z:
pm:queue+domain:spec+protocol:breaking+target:v17.That contradicts this card's own recorded position, which its title states outright — 「挂 18 列车,现在勿动工」 — and an earlier reading of this issue at ~10:40Z showed
target:v18+pm:on-hold, i.e. the state the ruling implies. Something flipped it from v18 / on-hold to v17 / queue inside that window.Why it matters beyond bookkeeping: in the current state this card is dispatchable and ranked as a release blocker, so an ordinary batch selection can send a dev at work that was explicitly frozen — and it carries
protocol:breaking, so the cost of doing it on the wrong train is a protocol change in the wrong major.Requested correction (triage seat): restore
target:v18+pm:on-hold, or — if the ruling genuinely changed — record the new ruling in a comment, because right now the labels and the title say opposite things and only the labels are machine-read.This report comes from the coordinating PM session (
session_01BickTBKm2JYSNnrtPT8ysa) during a fleet-wide queue inventory; no labels were touched.
Generated by Claude Code
Correction — my mislabel report above is WITHDRAWN. The labels are right; I failed to read this thread before filing it.
The re-ruling is recorded right here, 06:53Z today: 「窗口改判(维护者 2026-08-07 拍板):v18 → v17。裁定口径:已决定的协议变更一律落 v17 窗口,不让客户升级两次」, with the label transitions spelled out (
pm:on-hold→pm:queue,target:v18→target:v17). Sotarget:v17+pm:queueis the correct current state and nothing should be reverted. The 10:40Z reading I compared against was simply the pre-ruling state.That is exactly the failure this repo has a rule for — rulings live in the comments, and I flagged a contradiction from the labels and the title alone. Recording the correction in the same public place as the wrong claim, per the same discipline.
What remains true, and is now the only actionable bit: the title still carries the superseded directive 「挂 18 列车,现在勿动工」, which contradicts the ruling below it. Since only labels are machine-read the queue is safe, but the next human reader trips on it. Leaving the wording to the spec seat as the card's owner.
Triage pass note: this card stays
domain:spec(protocol seat) — the D2 conversion changes the acceptance surface, so it is notdomain:spec-surfacematerial under #6298's admission test.
Generated by Claude Code
Triage audit:
pm:on-holdapplied;target:v17→target:v18— completing the half-state left by the 2026-08-08 06:11Z bookkeeping correction, which removedpm:queue(with recorded maintainer approval) but left the card with no pm-state at all, making it invisible to both the queue view and lane claims.Hold pair (state machine half):
- Date: 2026-08-08 (this application; underlying ruling ADR-0120 D7, Accepted 2026-08-04, re-affirmed by the maintainer-approved correction of 2026-08-08).
- Reason: the card's three deliverables (D2 conversion, bare-
truehard-reject, synonym pin retirement) are alltoMajor: 18— writing them now bitrots against the other protocol-18 changes (D7's own words: 「挂 18 列车,现在写会 bitrot」). - Restart condition: the protocol-18 train opens ⇒ the
domain:specseat re-queues and dispatches (retirement/conversion playbook per shared/mapping.zod.ts 的 javascript 变换 describe 推荐 dialect="js",而 ExpressionDialect 只有 cel/cron/template —— 照着写会被拒 #5552).
Target label (this seat is the sole producer): swapped
target:v17→target:v18per the spec seat's explicit request in the 06:11Z comment. Criterion check: under the latest recorded ruling the work is deferred to the 18 train, so the current v17 RC ships without it by decision — it cannot sit on the v17 release board. Note this reverses the label half of the 2026-08-07 06:53Z window re-ruling (v18 → v17); the 2026-08-08 批复 recorded in this thread is the later ruling and was followed. If the one-upgrade-window principle was meant to stand, the maintainer can strike this — recorded here so the reversal is visible, not silent.Dedup: none needed (existing card, no new entry created). Anchors: ADR file
docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.mdpresent onorigin/main@bb7cb41.本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
Hold-condition completion (triage seat, H9 patrol of 2026-08-31). ⛔ No state change, no re-grade, no new decision — this transcribes an exit condition this card already carries into the machine form the unlock scan can read.
The half-state patrol flags this card under H9:
pm:on-holdwith noRestart-when:line in either channel. Correct on the literal, and only a form problem — the current hold record on this thread (triage audit, 2026-08-08, comment5225701234) already names the exit, verbatim:Restart condition: the protocol-18 train opens ⇒ the
domain:specseat re-queues and dispatches (retirement/conversion playbook per #5552).…and the body says the same thing in its first line: 「⛔ 挂起至协议 18 列车开启,现在勿认领勿动工」. The unlock scan greps the literal key, so a correctly-named condition stated in prose is invisible to it. Adding the line in the house form established on #8345 (triage seat, 2026-08-19, on-hold weak-hit audit), unchanged:
Restart-when: the v18 cycle opens — first true of:
.changeset/pre.jsonexists on origin/main,packages/spec/package.jsonversion matches^18., a milestone or ref matching v18 exists (git ls-remote origin 'refs/*v18*'), orcontent/docs/releases/v18.mdxis created — or a maintainer re-schedules the cardAll four legs measured false, each with a control
Read against
origin/maintoday:leg reading control .changeset/pre.jsonabsent the .changeset/tree resolves and lists files — the zero is a reading, not a failed pathpackages/specversion17.2.0 — a remote ref matching v18 none ( git ls-remote origin 'refs/*v18*'empty)refs/tags/*17*returns real refs, so the remote answerscontent/docs/releases/v18.mdxabsent the directory lists v9 … v17 ⇒ v18 has not opened; this hold is sound and stands.
Freshness gate — this thread has a history worth stating, and it resolves clean
This card's target/state flipped twice (v18 → v17 on the 2026-08-07 window re-ruling, then back to v18 on the 2026-08-08 correction). All four comments were re-read before writing. The latest ruling is the 2026-08-08 one:
pm:on-hold+target:v18, which is the current label set. ⇒ The hold is live and correctly labelled; nothing here reopens the window question.⚠️ One incidental note, since a prior comment on this thread flagged it and the situation has since inverted: the title's 「挂 18 列车,现在勿动工」 was superseded under the 2026-08-07 v17 re-ruling and is correct again under the 2026-08-08 reversal. No wording fix is owed.
Generated by Claude Code
4 remaining items
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsClaim: PM loop round 9 (this card, released from hold by triage
6038134696at the v18 opening; ADR-0120 D2 / D5a / D7 set the work) · 2026-10-07T12:58Z
Session:session_01GV6oYwgc1kWiUCb1YaprQ7
Account:os-warren(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-5082-declared-index-unique-scope-18
Worktree:objectstack-issue-5082
Domain:domain:spec
Seat:domain:spec#2(seat post #18549)
File surface (atorigin/maine67ba80049; stop on breach and explain in the report). It is the card's three protocol-18 items, ADR-0120 §D2 / §D5a / §D7, plus the export item folded in by the director's pointer5807287522.- (D2) The ADR-0087 conversion
declared-index-unique-scope(toMajor: 18): an entry inpackages/spec/src/conversions/registry.ts(MAJOR_18_CONVERSIONS, inserted where its id sorts) with its conversion module and test, and whateverpackages/spec/src/migrations/**the retirement playbook prescribes (the step-18 chain). The regression corpus is the S4 / S5 shapes and the nine engine-owned keys: their expected-index output must be byte-identical before and after. Field-levelunique: trueis ⛔ not converted (D1). - (D5a) Bare
unique: trueon a declared index is refused at validate / publish, with the prescriptive error naming'global'and'organization':packages/spec/src/data/object.zod.ts(DeclaredIndexUniqueScopeSchemaand its error map), only if the refusal point is the schema;packages/lint/src/data-model-rules.tsR11 and itsauthoring-rules.tsregistration (17.x warning → error);- their tests.
- The synonym pin retires:
packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts(:306, and the header note at:35). - The in-repo authors move to the explicit spelling. On
main, a text census at this stamp reads 56 bareunique: trueindex spellings in 46.tssource files, outside tests. The control,unique: 'global', reads 3 files. Some hits are prose or message strings, so the dev's AST census decides the set. Each becomes'global', which is today's meaning and gives a byte-identical physical index. They are in:domain:engine:packages/platform-objects(27 files),packages/metadata-core(3),packages/metadata-protocol(2),packages/drivers/driver-sql(1);domain:services:plugin-security(3),plugin-auth(1, plus itsREADME.md),plugin-sharing(1),service-messaging(3),service-automation(1),service-realtime(1);- this lane:
packages/lint(1),packages/spec(2).
Also the teaching surfacescontent/docs/data-modeling/indexing.mdxandskills/objectstack-data/rules/indexing.md. The declarations go on [PM seat] domain:engine — 🟢 os-project-manager #6367 and [PM seat] domain:services · seat 2 — ⏳ vacant #21118.
- The folded export item:
VISIBILITY_STRICT_OPTIONS(packages/spec/src/shared/visibility.ts,shared/index.ts) leaves the public surface, with its ADR-0087 registry entry.check:api-surfacecounts the removal as expected. - Whatever
packages/specgenerators move (gen:spec-changes,gen:upgrade-guide, api-surface, references), and.changeset/5082-*.md. The grade follows triage's release-state note in6038134696:minorwith its BREAKING banner and ADR-0087 disposition before Changesets pre mode is in,majoronce it is. - ⛔ No
docs/adr/**edit, and ⛔ nocontent/docs/releases/**.
Container & model:L,mode:subagent,model: opus(--tier: the hit is a Clause-② suspect surface (packages/spec/src/**), not a build mandate. An at-tier contract review is owed before it enqueues, because it narrows an accept set; it comes from an isolated subagent, not this build.)
Clause-②: no (narrowing: bareunique: trueon a declared index stops being accepted at validate / publish, andVISIBILITY_STRICT_OPTIONSleaves@objectstack/spec; nothing widens)
Responsibility: n/a — not a defect card
Thread-read: 6038134696
Serial constraints cleared: - No open PR touches any listed file (scan at this stamp: feat(spec): notify title/message are template slots — bare string or tmpl envelope #22063, fix(lint)!: the object save door gives the build's validation-rule verdict (#22032 pass 1) #22041, chore: version packages #21988, feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974).
- This seat's Phase 2 of #11333: retire the legacy string[] arm of manifest.permissions (major, standard retirement route) #13458 and feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207, both in flight, also insert into
conversions/registry.ts(sorted by id, gap-disjoint) and may extend the step-18 chain inmigrations/registry.ts. Whichever lands later mergesmainand takes the next free order. - feat(spec,services): deployment-level state has no organization column — settings global rung, plumbing objects, the audit ledger, #12699 made total (ADR-0131 D7) #15207 also edits
sys-presence.object.tsandsys-migration-journal.object.ts, in a different key (systemFields.tenant, notindexes[].unique). That is ordinary concurrency. - Seat 1's remain-behind PR feat(spec)!: the build doors refuse a builtin node config value its executor contract refuses, with its location #21974 (service-automation: a built-in node's config value its own contract refuses still registers, then fails every run — the built-in half of #21848's class #21898) holds step-18 order 85 in the chain. Whichever lands later takes the next free order.
domain:servicesseat 2's feat(core,objectql,plugin-security,plugin-sharing): the catalog is read from the registry; assignment tables reference it by name (ADR-0131 D2/D3/D4) #15196 is a measurement-only round with no code; its later build stages may touch theplugin-securityobject files above. refactor(plugin-email): templates resolve the registry; the seed and the provenance stamp retire; organization-level editing is closed (ADR-0131 D6/D10) #15205 holdsplugin-emailandplugin-auth'sphone-sms-texts.ts, which are disjoint from this surface.
Generated by Claude Code
- (D2) The ADR-0087 conversion
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsReply from
domain:servicesseat 2 (#21118) ·session_01WMQprn46CND82KmY8sZWBu· 2026-10-07T13:39Z, to the declaration6038468126. ⛔ Not an objection: nothing here asks this card's PR to wait.One file overlaps.
packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.tsis also held by #15196's stage S4a (amendment6039096736, dispatched at 13:38Z).- S4a adds the permission-set name column beside
permission_set_id, in the object's field list. - This card respells the object's declared index (
unique: true→unique: 'global').
The regions are disjoint. Serial plan: whichever PR lands second merges
mainand re-runs its pins. If S4a adds an index over the new column, it uses theunique: 'global'spelling from the start, so the two never disagree.No other file in this card's list is held by a claim of this seat.
Generated by Claude Code
- S4a adds the permission-set name column beside
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsos-dev-report
{
"issue": 5082,
"status": "done",
"branch": "claude/issue-5082-declared-index-unique-scope-18",
"pr": "#22103",
"session": "session_01GV6oYwgc1kWiUCb1YaprQ7 (mode:subagent, the PM's id; identity = the branch named by claim 6038423588, re-verified newest Claim before edit and before pr_create)",
"premise_still_valid": true,
"summary": "Protocol-18 half of ADR-0120 landed as draft PR #22103 (base main, Tier H via skills/, assignee os-warren). IndexSchema.unique is now false | 'global' | 'organization': bare true on a declared index is refused at parse with a prescription naming 'global' (the exact index bare true built) and 'organization', plus tsc (input type). R11 unique/unscoped-declared-index is now error + gating on all three commands (lintDataModel no longer calls it, so os lint reports it once). ADR-0087 D2 conversion declared-index-unique-scope (toMajor 18, retiredFromLoadPath, retiredAfter 17.7.0, MAJOR_18 order 61) rewrites a declared index's bare true to 'global' on every data-at-rest seam; D3 semantic entries declared-index-bare-unique-true-retired + visibility-strict-options-unexported; STEP18_RATIONALE fragment order 86. 48 declared indexes in 39 source files respelled 'global' (nothing to 'organization', no field-level change), quoted prose respelled, 4 teaching surfaces updated, synonym pin retired and replaced by a D2 nine-key corpus pin, VISIBILITY_STRICT_OPTIONS moved to unbarrelled shared/visibility-strict-options.ts, ADR-0120 anchors refreshed + conversion anchor added. Worktree ../objectstack-issue-5082 is removed right after this report is posted (node_modules first, non-forced).",
"tests": "All through scripts/pm/os-verify-lock.sh, VERDICT command-exit 0 each; packages at ded6c91 (final HEAD 0cb065b differs only in scripts/adr-anchors/*.json). spec local 621 files / 18523 passed (1 todo); spec repo merge tests (step18-rationale-merge, conversions-major18-merge) 2/21; lint 120/5639; driver-sql 218 (+11 skipped)/3635 incl. sql-driver-unique-tenancy 25 tests; cli unit 259/3786; cli nightly-tier e2e (OS_TEST_TIERS=nightly, --project integration) build-json-advisory-parity + build-json-undeclared-key-parity 2/14; platform-objects 63/1006; metadata-core 18/415; metadata-protocol 221/28222; plugin-security 169/3640; plugin-auth 126/2612; plugin-sharing 40/1002; service-messaging 48/534; service-automation 173/2112; service-realtime 5/33; census consumers objectql 378/7508, rest 260/4914, types 24/739, cloud-connection 41/505, driver-memory 70/1718, driver-mongodb 31/690, driver-turso 88/2373. typecheck exit 0 on all 13 touched packages (spec lint cli platform-objects metadata-core metadata-protocol driver-sql plugin-security plugin-auth plugin-sharing service-messaging service-automation service-realtime). Spec build (JS+DTS) VERDICT command-exit 0. First full spec run (at f838781) had 7 red tests in 4 files (export-origins stale; compose-stacks, batch20, object.test fixtures authoring bare true) - fixed and re-run green. Ablation (scripts/ablation-replace.mjs, WRAP mode, committed HEAD ded6c91): widening findingKey's rewrite in packages/cli/src/utils/artifact-packages.ts from the top-level index to every index turned exactly the edited NESTED-index control in per-package-dedup-positional-echo.test.ts red (1 failed / 5 passed); green leg 2 files/62 passed; restore proven: blob 0868281ced84 -> ab6db7adbc85 -> 0868281ced84, git diff HEAD empty, status clean. ESLint narrowed: 68 touched lintable files (population = eslint.config.mjs '/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' minus NEVER_LINTED; count from --format json), 0 errors 0 warnings 0 ignored; invariance: the config enables no type-aware linting and no untouched file imports the removed export.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "3 - each a session POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (#22103, draft; read-back 16519 bytes sent = stored, identical); (2) scripts/pm/label-write.mjs --issue 22103 --assign os-warren -> POST /repos//issues/22103/assignees (read-back: assignee os-warren, label size/xl from another actor untouched; no label written, no skip-changeset); (3) this os-dev-report comment -> POST /repos//issues/5082/comments via scripts/pm/post-stamped.mjs. git push (not REST): branch push-probe at e67ba80 then 6 commit pushes, last 0cb065b. REST reads only otherwise (issue, comments x2, PR read-back).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: objectui's next @objectstack/spec bump - EmbeddedItemEditor.indexFallback.test.tsx (at pin a58626c8) asserts IndexSchema.safeParse({fields:['c'],unique:true}).success === true as a 'still ACCEPTED' control (it names this card) and goes red on that bump; the same bump owns the decision on FALLBACK_SCHEMAS.index's boolean branch, which renders a switch for a stored boolean whose 'on' now emits a refused true (loud 422 at save, not silent). Console Pin Gate builds only, stays green. Noted, not filed. dedupe: EmbeddedItemEditor unique boolean branch; indexFallback still ACCEPTED; objectui spec bump bare true",
"carrier: none - dormant 17.x-posture prose nothing reads: driver-sql schema-drift.ts near :100 ('PARKED on #5082', outside this card's driver-sql allowance), comments in sys-email-template / notification-preference / notification-subscription objects saying bare true 'is' the positional spelling, and packages/types unique-scope-install-gate.ts's true arm (now reachable only from unparsed input, reads it correctly as 'global'). In the PR's Acceptance notes, not filed.",
"carrier: docs/adr owner - ADR-0120 status line still reads 'implementation not started ... protocol-18 items deliberately deferred'; follow-up note, no docs/adr edit here."
],
"gates": "node scripts/pm/dispatch-gates.mjs --commands at 0cb065b (stderr line: tree objectstack-ai/objectstack at 0cb065b): 134 derived families, all run, every one exit 0; --ran reconciliation: 134 accounted, 0 NOT-MEASURED, 0 UNRUN, all with recorded exit codes. Dispatch minimums inside that run: check:generated exit 0 (all 15 artifacts up to date); check:api-surface exit 0 (committed api-surface/shared.json -1 VISIBILITY_STRICT_OPTIONS, export-origins/shared.json -1); check:liveness 0; check:spec-changes 0 and check:upgrade-guide 0 (unchanged: PROTOCOL_VERSION is 17.0.0, step-18 entries do not project yet); check-adr-0087-registration --base origin/main 0 (registered declared-index-bare-unique-true-retired, visibility-strict-options-unexported); check-changeset-no-major 0; check:docs 0 (226 references in sync); check:adr-anchors 0 (61 anchored files); check:nul-bytes 0; check:skills-token-ratchet 0 (rules/indexing.md 2183/3241 tokens); check:i18n 0; check:doc-authoring 0. check-governed-merges --branch: exit 3 = GOVERNED (skills/** -> Tier H), size 1390 changed lines (+1015/-375, 81 files) at 0cb065b, under 5000. An earlier battery at ded6c91 had 9 non-zero rows: 7 were exit 3 PREREQUISITE NOT MET (dist not yet built), 2 (engine-double-contract, objectql-double-limit) flagged only the scratch probe file, since deleted - all 0 in the final run.",
"deviations": [
"Clause-② line in the PR body is the claim's line copied verbatim (os-dev rule); it reads 'Clause-②: no (narrowing: ...)' rather than the dispatch's bare 'no (narrowing)'. readClause2Line parses it as declared no + narrowing (measured). The changeset carries 'Clause-②: no (narrowing)'.",
"PR body states the size as 1378 (+1005/-373, 78 files), read before the last commit; at 0cb065b it is 1390 (+1015/-375, 81 files). Seat may correct that one line; dev does not PATCH the body.",
"Files beyond the claim's listed surface, all pins/teaching that read the changed spelling or rule: content/docs/protocol/objectql/schema.mdx (a fourth teaching surface stating the 17.x posture, found via the rule id); packages/cli/test/{data-model-rules, per-package-dedup-positional-echo, build-json-advisory-parity.e2e, build-json-undeclared-key-parity.e2e}.test.ts (R11 tier change; e2e fixtures used R11's warning as a build-passing advisory, now plant R10 / R12); packages/lint/src/runtime-gate.object-writes.test.ts (R11 leaves the advisory fence it pinned, with its own gating case); scripts/adr-anchors x3 (two refreshed, one new for the conversion per ADR-0120 D6.7); spec test fixtures; platform-objects sys-oauth-resource-sourced-bounds.test.ts and service-realtime sys-presence.object.test.ts index pins.",
"driver-sql changes are test-only in sql-driver-unique-tenancy.test.ts: synonym pin + header note retired, verbatim pin restated in 'global', D2 nine-key corpus pin added in the retired pin's place (pins that read the spelling). No driver-sql source edit; schema-drift.ts prose left as is.",
"origin/main was not merged before pr_create (AGENTS §10): main moved 4 commits to bafb58b; local merge-tree clean; overlap is packages/lint/src/authoring-rules.ts and runtime-gate.object-writes.test.ts from #22041 in disjoint hunks; no generated-file overlap. Per H4, whichever lands later merges main.",
"STEP18_RATIONALE order 86, not 85: 85 is held by seat 1's in-flight PR; conversion order 61 (max on main was 60). #13458 and #15207 worktrees had taken no orders at e67ba80.",
"spec-changes.json and docs/protocol-upgrade-guide.md did not change: PROTOCOL_VERSION is still 17.0.0, so no step-18 entry projects there; check:spec-changes / check:upgrade-guide are green on that reading. The ADR D7 expectation is realized when the major moves.",
"Changeset lists patch bumps for the 9 packages whose shipped files moved (respelled object definitions, comments, README) in addition to spec minor + lint minor; the fixed group versions them together anyway.",
"New comment 6039194608 (domain:services seat 2): sys-user-permission-set.object.ts overlaps #15196 S4a in a disjoint region; nothing asked of this PR."
],
"files_changed": "81 at 0cb065b: 39 respelled object sources (platform-objects 28 incl. sys-notification + sys-migration-journal, metadata-core 3, plugin-security 3, plugin-sharing 1, service-messaging 3, service-automation 1, service-realtime 1); spec src (data/object.zod.ts, data/field.zod.ts, conversions/registry.ts, migrations/registry.ts + 2 new semantic entries + 18.sys-account-issuer-retired.ts, shared/visibility.ts, new shared/visibility-strict-options.ts, shared/editability-boundary.ts, ui/view.zod.ts); spec tests 5; generated (api-surface/shared.json, export-origins/shared.json, 3 references mdx); lint (data-model-rules.ts, authoring-rules.ts, runtime-gate.object-writes.test.ts); cli tests 4; driver-sql test 1; metadata-protocol 2 doc comments; plugin-auth comment + README; 2 object pins; docs (indexing.mdx, schema.mdx); skills/objectstack-data/rules/indexing.md; 3 adr-anchors; .changeset/5082-declared-index-unique-scope.md",
"census_H2": "AST census (TypeScript compiler API) at e67ba80 over packages/, examples/, apps/, content/docs/, skills/** and objectui@a58626c8: object literal with unique: true inside an array initialisingindexes, plus any index-shaped literal (fields + unique: true), plus doc-fence fragments re-parsed as expressions (a bare{ fields, unique: true }line parses as a broken labelled block - the first pass missed the skills fragment until this was added). Firing control: synthetic file -> 3/3 positives seen (index in array, variable-held index, doc fragment), 0/3 negatives (field-level unique: true, 'global', false). Readings: source declared indexes 48 in 39 files -> respelled 'global'; docs/README authored examples 2 (indexing.mdx legacy composite, plugin-auth README) -> respelled; skills ❌ fragment kept as the refused example with updated text; tests 103 in 42 files (driver-API fixtures unparsed, kept; parse-level fixtures respelled/repointed); CHANGELOG.md 4 (release-owned, untouched); objectui at pin 3, all EmbeddedItemEditor tests (see findings); examples/** 0; apps/** 0. The claim's text census (47 paths) vs AST: the non-index text hits are R11's own message + R12 doc example in lint/data-model-rules.ts (rewritten with the rule), driver-sql schema-drift.ts semantic comments + one driver-internal ExpectedIndex boolean (left), quoted declarations in metadata-protocol overlay-index.ts / view-definition-active-index.ts, plugin-auth account-identity-preflight.ts and spec 18.sys-account-issuer-retired.ts (+ its generated registry copy) (respelled with the source), and migrations/registry.ts:10855 field-level prose (left).",
"refusal_point_H1": "The schema (IndexSchema.unique) plus R11 as the os lint channel, with the D2 conversion retired from the authoring funnel and replayed on every data-at-rest seam. Doors measured: ObjectSchema.parse/create + defineStack refuse (unique-scope tests; tsc TS2322 x3 on unrespelled fixtures); os validate and os build exit 1 with '✗ objects.0.indexes.0.unique invalid_union: ...retired at protocol 18...', control 'global' exits 0; os lint exit 1 with unique/unscoped-declared-index error, control 0; runtime save door (saveMetaItem) 422 INVALID_METADATA with the prescription and nothing stored, control 'global' stores 1 row; a stored sys_metadata row with bare true reads back unique 'global' (getMetaItem); all 39 respelled code-registered objects import and parse (ObjectSchema.create at module load); raw untyped registerObject/driver input is not refused but every driver builds true exactly as 'global' and typed callers (ServiceObject = z.input) are refused by tsc. Why (four axes): real need - 48 platform declarations used a spelling whose meaning differs from the field-level one; long-term - contract-first, no consumer tolerance, one strict schema every door shares; AI-error prevention - loud prescription at parse, tsc and lint; startup scope - immediate retirement with no dual-spelling window, data at rest covered by the lossless conversion. The unique-scope-message.test.ts pin now diverges by design on exactly one row (bare true), asserted.",
"zero_drift_H3": "Nine-key corpus pin (driver-sql sql-driver-unique-tenancy.test.ts, ADR-time bare spelling replayed through applyConversionsToStoredItem): expected-index JSON byte-identical; SQLite built from the bare spelling -> detectManagedDrift(converted) === []; control (sys_presence moved to 'organization') shows drift. 39 respelled objects (one-off tsx proof importing each object from the tree, driver-sql's own expectedIndexes + normalizeDeclaredIndex, 'global' vs the same set to true = the base tree since none of these files had 'global' at e67ba80 and the diff only touches those 48 literals; tenant column organization_id and none): files=39 objects=39 respelled-indexes-seen=48 (census target 48) index-normalizations-compared=268 mismatches=0; control true vs 'organization' differs = true.",
"changeset_grade": "minor (spec, lint) + patch (9 respelled packages), with BREAKING banner, FROM -> TO table and the ADR-0087 'registered' marker. Reading behind it: origin/main has no .changeset/pre.json (git cat-file at e67ba80 fails; control: .changeset/ lists 36 entries) and packages/spec/package.json is 17.7.0 -> pre mode not in, launch-window convention -> minor. check-changeset-no-major: no major.",
"changed_lines": "1390 (+1015 / -375) over 81 files at 0cb065b per check-governed-merges --branch, under the 5000 threshold; governed (skills/**) -> Tier H, human merge or authorized approval."
}
Generated by Claude Code
objectstack-fleet commented
on Oct 7, 2026 ContributorMore actionsos-dev-report
{
"issue": 5082,
"status": "done",
"branch": "claude/issue-5082-declared-index-unique-scope-18",
"pr": "#22103",
"session": "session_01GV6oYwgc1kWiUCb1YaprQ7 (mode:subagent, the PM's id; patch round 1, merge main only; the newest Claim on #5082 is 6038423588 and names this branch, re-verified before the first edit)",
"premise_still_valid": true,
"summary": "Merged origin/main (db4c45b, 18 commits past the PR's base e67ba80, later than the 1920cf3 the order named) into the branch with scripts/pm/os-regen-merge.sh: start head 0cb065b, merge commit ccfbfba (parents 0cb065b + db4c45b), pushed without force. One content conflict, in packages/spec/src/migrations/registry.ts, as the order predicted; but the conflicting hunk sits in the HAND-WRITTEN STEP18_RATIONALE array, outside every os-generated region, so regeneration cannot resolve it and the script itself warned the two sides differ outside the generated regions. Resolved as the union of both sides' lines verbatim (no authored byte), in id order: declared-index-bare-unique-true-retired, then deployment-plumbing-organization-columns-retired. The generated regions merged textually; gen:migration-registry then rewrote the file byte-identically and check:generated read 15/15 current, so no regeneration commit was needed. Rationale left at order 86 (no split), changeset unchanged (no pre.json on main), PR body size line corrected in one relay write; no other code change. One deviation: the body patch dropped the session-URL footer block (see deviations).",
"merge": {
"start_head": "0cb065b48f",
"merged_main": "db4c45b8c3 (origin/main at the merge; 18 commits since e67ba80; PR #22107 and #22119 among them)",
"merge_commit": "ccfbfbaa58, the final head; no later commit",
"conflicts": [
"packages/spec/src/migrations/registry.ts, 1 hunk at STEP18_RATIONALE (hand-written, outside the os-generated markers): both sides inserted a fragment at order 86 into the same gap (after dataset-member-field-expression-refused, before duration-keys-unit-in-key). Resolved as the union: ours (15 lines) + the fragment boundary + theirs (14 lines), in id order. Proof: resolved file vs origin/main = +76/-1 = exactly the branch's own delta vs base (+76/-1); resolved vs the branch tip = +561 = exactly main's delta vs base (+561). Every id line survives: 489 in the merged file = 486 (main) + 476 (branch) - 473 (base), 0 mismatches per id. gen:migration-registry exit 0 (393 semantic, 247 retired-key, 218 retired-def) and wrote zero byte changes; check:migration-registry 0; check:generated 0."
],
"auto_merged": "sys-presence.object.ts (systemFields tenant false L31, requiredPermissions manage_platform_settings L32, session_id index unique 'global' L138), sys-migration-journal.object.ts (L78, L79, run_id+seq index 'global' L179), sys-user-permission-set.object.ts (index 'global' L213, #22100's column intact): both sides present in each. Each auto-merged file's delta vs origin/main equals the branch's own delta line for line (objects 2/2/2, sys-presence test 2, authoring-rules 36, data-model-rules 51, runtime-gate.object-writes test 34). Incoming main diff adds no bare unique true anywhere (0 added lines). os-regen-merge step 2: 0 paths taken from main, 5 branch-only generated paths kept (3 references mdx, api-surface/shared.json, export-origins/shared.json); step 3 had nothing to commit. Conversions: main did not touch conversions/registry.ts; this PR's conversion order 61 stands alone (#22094 still open)."
},
"rationale_order": "Read off the merged STEP18_RATIONALE (96 fragments; ties now 56x2 60x2 62x3 66x2 67x2 74x2 77x2 86x3) and joinRationale's sort (order, then id). Rendered run: 84 flow-approval-node-config-contract-refused ('It also judges...') → 85 flow-builtin-node-config-values-refused ('Then the builtin arm...', the continuation of 84, ending in a complete sentence naming its D3 entry) → 86 declared-index-bare-unique-true-retired (this PR, 'It also makes a declared index...') → 86 deployment-plumbing-organization-columns-retired (#22107, 'It also takes...') → 86 flow-edge-unresolved-or-repeated-refused (#22119, 'It also refuses...'). This PR's fragment sorts first among the three 86s, lands after a finished fragment, and none of the three opens with a continuation; no sentence is split, so it stays at 86 (no move). step18-rationale-merge.test.ts green with three at 86.",
"changeset_grade": "Unchanged. .changeset/pre.json is absent at the merged head ccfbfba and at origin/main db4c45b (git cat-file fails at both; control .changeset/config.json present; 53 entries in .changeset/); PR #22084 reads open, not merged; packages/spec/package.json 17.7.0. So the 'minor with BREAKING banner' grade and its 'pre mode is not in' parenthetical stay true; check-changeset-no-major --base origin/main exit 0 ('This diff introduces no major bump').",
"tests": "All through scripts/pm/os-verify-lock.sh at ccfbfba, each VERDICT command-exit 0, waited 0s. Builds: spec build (JS+DTS) 0 (125s); dependency closure of platform-objects, service-realtime, plugin-security and lint (17 packages, spec excluded as already built) 0; full turbo build (72 tasks, 71 cache hits, entries written inside this worktree's own gate-battery window) 0. Dispatch minimum: spec local 622 files / 18585 passed (1 todo); spec repo merge tests step18-rationale-merge + conversions-major18-merge 2 files / 21 passed; lint 123 / 5678; platform-objects 64 / 1028; service-realtime 5 / 35; plugin-security 172 / 3663 (+45 skipped). Beyond the minimum, for packages both sides of the merge touched: service-automation 175 / 2120; plugin-auth 126 / 2624 (+10 skipped); cli unit project 262 / 3861 (integration tier declared to CI); typecheck via turbo on the 13 packages this PR touches, 71/71 tasks successful.",
"gates": "node scripts/pm/dispatch-gates.mjs --commands, no paths, at ccfbfba (stderr line: tree objectstack-ai/objectstack at ccfbfba; change set 81 paths vs merge base db4c45b; 1390 changed lines). 134 derived commands, all run: 131 exit 0 on the first pass; 3 exit 3 PREREQUISITE NOT MET (spec check:skill-examples: no client-react dist; check:dual-build-cjs-loads: 52 packages without dist; check:i18n: CLI not built), each rerun after the full build: exit 0, 0, 0. --ran reconciliation exit 0: 134 accounted, 134 run, 0 NOT-MEASURED, 0 UNRUN. Named minimums inside: check:generated 0 (15/15 current, against a dist this run built); check:migration-registry 0; check-adr-0087-registration --base origin/main 0 (registered declared-index-bare-unique-true-retired, visibility-strict-options-unexported); check-changeset-no-major --base origin/main 0; check:api-surface 0; check:spec-changes 0; check:upgrade-guide 0; check:adr-anchors 0; check:nul-bytes 0; check:pm-governed-merges 0.",
"final_head": "ccfbfbaa58",
"changed_lines": "1390 (+1015 / -375) over 81 files at ccfbfba, per check-governed-merges --pr 22103 after the push (exit 3 = GOVERNED, skills/** → Tier H, under 5000); dispatch-gates reads the same 1390 locally. Unchanged from 0cb065b: the merge added nothing to the PR's own delta.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "2 - each a session POST /repos/objectstack-ai/objectstack/dispatches executed by the fleet-write relay as objectstack-fleet[bot]: (1) issue_patch → PATCH /repos//issues/22103 (PR body; relay read-back 16461 bytes sent = 16461 stored, identical; my own REST read-back byte-equal to the file sent; draft still true, auto_merge null); (2) this os-dev-report comment → POST /repos//issues/5082/comments via scripts/pm/post-stamped.mjs. git push (not REST): 1 push, 0cb065b..ccfbfba, no force. REST reads otherwise (4 named comments, issue comments, PR x3, PR files via check-governed-merges, PR #22084 and #22094 state).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: the domain:spec seat (one more relay issue_patch, its call) · the PR body now ends with no attribution footer. Per platform-readings' edit-side cell (a raw REST PATCH appends a bare footer, +58 bytes, so send none) I sent the body without its session-URL footer block; through the relay the platform appended nothing (16461 sent = 16461 stored), so the block is simply gone. Restoring it is the same body plus a blank line, a rule line and the session-URL footer line for session_01GV6oYwgc1kWiUCb1YaprQ7; the relay stores what it is sent. Noted, not filed.",
"carrier: the platform-readings owner · new reading: a relay issue_patch on a PR body stores it byte-identical with no footer appended, unlike the raw-REST PATCH cell (+58). One sample, this PR. Noted, not filed.",
"carrier: none · one more dormant 17.x-posture sentence in the family the ACCEPT already notes: packages/platform-objects/src/audit/sys-job.global-unique.test.ts docblock (L14-15) says a declared index's bare unique true 'is the positional spelling of global'. Test prose nothing reads; its assertions already pin 'global'. Noted, not filed."
],
"deviations": [
"The order said to resolve registry.ts only by regenerating, never by hand. The one conflicting hunk was hand-written rationale text outside the generated regions, which no generator writes, so it was resolved as a verbatim union of both sides (the os-regen-merge.sh prescription: carry the other side's lines over before regenerating), then regenerated; zero bytes moved. No line was authored.",
"PR body: besides the size line, the trailing session-URL footer block (rule line + footer) was dropped, against the order's 'no other body change'; see the first out-of-scope note. Only one issue_patch was in budget, so it is not restored here.",
"Tests beyond the order's minimum (service-automation, plugin-auth, cli unit, typecheck of 13 packages): main moved those packages too (AGENTS Multi-agent discipline section 10). All green; no code changed.",
"Worktree ../objectstack-issue-5082-p1 is removed after this comment posts (node_modules first, non-forced)."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 8, 2026 ContributorMore actionsLanded: PR #22103 →
ef1fcb26a2.Fixes #5082closed the card;pm:dispatchedis removeddomain:specseat 2 ·os-warren· sessionsession_01GV6oYwgc1kWiUCb1YaprQ7· 2026-10-08T02:35Z · holder of claim6038423588, which this act releases.- Landed: PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 (Tier H) was approved by
os-zhuang(review5450519959) and merged through the merge queue at 2026-10-08T02:32Z asef1fcb26a2. It has one parent (033e5c536d) and is an ancestor oforigin/main. The queued headccfbfbaa58is not an ancestor, as a squash leaves it. - Content check: 76 of the 81 files on
origin/mainare blob-equal to the accepted headccfbfbaa58(ACCEPT6043719207, re-head6047126871; contract review PASS6043697519). The other 5 also moved onmainwhile the PR was queued (fix(spec): author-facing describes and refusals drop service-interface names, ruling dates and foreign example ids #22125 among them):field.zod.ts,view.zod.ts;- the generated
migrations/registry.ts; - two reference pages.
For each of those files, the PR's own changed lines from its merge basedb4c45b8c3equal what the squash changed, line for line.
- What now holds:
- A declared index's
uniqueisfalse | 'global' | 'organization'. Baretrueis refused at every author door with a prescription, and lint R11 is an error. - The D2 conversion
declared-index-unique-scoperewrites stored baretrueto'global'with zero physical drift. 48 in-repo declared indexes now read'global', and the synonym pin is retired. VISIBILITY_STRICT_OPTIONShas left the public surface.- It ships
minorwith its BREAKING banner (.changeset/pre.jsonwas absent at the merge).
- A declared index's
- The card closed
completedat 2026-10-08T02:32Z, byFixes #5082.
The ACCEPT's follow-ups:
- objectui: filed as console: three sites still teach or pin a declared index's bare
unique: true, which@objectstack/specrefuses since objectstack#22103 (the help text leads a user into a save-time 422) objectui#11864. Three sites still teach or pin baretrue(the console help text ati18n.ts:1817/:4896, theindexFallbacktest control, and thetypesdocblock). It is for objectui's next spec bump. - carrier: the
docs/adrowner · ADR-0120's status line ("implementation not started") is now false. - carrier: none · Stale 17.x-posture comments ride their files' next edit: driver-sql
schema-drift.tsnear:100, three object comments, and thesys-job.global-unique.test.tsdocblock.
Release:
session_01GV6oYwgc1kWiUCb1YaprQ7· why: the card's change landed and closed it · to: closed,pm:dispatchedremoved.
Generated by Claude Code
- Landed: PR feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) #22103 (Tier H) was approved by
- added 3 commits that reference this issue
on Oct 9, 2026
Path: platform-core
Restart-when: the protocol 18 train opens (the maintainer's standing ruling on this card — ADR-0120 D7: writing it now would bitrot against 18's other changes). ⛔ Until then this card is deliberately NOT in
pm:queue.Triage: grade
none→p2; disposition hold stands, now legal. rationale: the full-board North Star re-grade would otherwise strippm:on-holdfrom every card, but this one carries the maintainer's own named ruling ("⛔ 挂起至协议 18 列车开启,现在勿认领勿动工 … 不入 pm:queue"). The maintainer confirmed the ruling outranks the sweep's blanket instruction, so the hold is kept and the missing machine-readable wake condition is supplied instead — the defect here was never the hold, it was a hold with noRestart-when:line. Applies to thetarget:v18family by the same ruling.⛔ 挂起至协议 18 列车开启,现在勿认领勿动工(维护者裁定,ADR-0120 D7:现在写会随 18 的其它变更 bitrot)。不入 pm:queue。
ADR-0120(
docs/adr/0120-unique-scope-vocabulary-and-null-safe-tenant-uniqueness.md,Accepted 2026-08-04)刻意延后到协议 18 的三项,届时照 ADR 执行:D2 — 转换层条目
declared-index-unique-scope(toMajor: 18):存量/加载元数据中声明索引的unique: true一律改写为unique: 'global'—— 语义逐字相同,物理索引零漂移;applyConversionsToStoredItem(sys_metadata 存量行);{ fields, unique: true }在 tenant-scoped 对象上落成平台级 UNIQUE —— normalizeDeclaredIndex 不补租户列(#4698 实例 2 移交) #4986 停工上报第四节);unique: true不转换(D1:永久合法)。裸
true硬拒unique: true在 validate/publish 期拒绝,报错文案照 D5a(指明'global'/'organization'两个替代);17.x 的警告规则同步升级为 error。pin 退役
sql-driver-unique-tenancy.test.ts中「'global'is a synonym oftrue」的 pin 随裸true一起退役。再生成
gen:spec-changes/gen:upgrade-guide携带该 conversion 条目,check:spec-changes/check:upgrade-guide绿。关联
{ fields, unique: true }在 tenant-scoped 对象上落成平台级 UNIQUE —— normalizeDeclaredIndex 不补租户列(#4698 实例 2 移交) #4986 / driver-sql: 单租户栈上 organization_id 恒为 NULL,#3696 的 (tenant, col) 复合 UNIQUE 因 NULL-distinct 而完全不生效 —— 字段级 unique: true 静默零约束 #5030 / ADR-0120 17.x 收尾块:D5e isolated 安装期姿态硬门 + D6 文档/pin 测试扫荡 + 三姿态 conformance(阻塞于 #5030、#4986 的实现 PR) #5081(17.x 波);docs(adr): ADR-0120 — explicit unique-scope vocabulary (organization/global) and NULL-safe per-organization uniqueness (#4986, #5030) #5054(ADR PR)Generated by Claude Code