Repository navigation
feat(spec)!: refuse bare unique: true on a declared index at protocol 18 — stated scope, zero-drift conversion (ADR-0120 D2/D5a/D7) - #22103
Conversation
… 18 (ADR-0120 D2/D5a/D7) WIP: schema refusal, declared-index-unique-scope conversion (toMajor 18), D3 semantic entries, R11 to error, in-repo respelling to 'global', synonym pin retired with a D2 nine-key corpus pin, VISIBILITY_STRICT_OPTIONS moved out of the shared barrel. Generated artifacts follow. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…pec fixtures state the index scope Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
… scope Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…minor, BREAKING, ADR-0087 registered) Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…global' scope Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…or the declared-index-unique-scope conversion (D6.7) Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 11 package(s): 39 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 149 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3cec7c6a55563a08d0a58fdb01a5f2bc081014fe && git checkout 3cec7c6a55563a08d0a58fdb01a5f2bc081014fe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin db4c45b8c3c5d35eb4c6774e1ce832258c264806 ccfbfbaa58733f7552f7a64e80f4df01dfe815f2 && git checkout -B drift-repro db4c45b8c3c5d35eb4c6774e1ce832258c264806 && git merge --no-ff ccfbfbaa58733f7552f7a64e80f4df01dfe815f2
node scripts/docs-audit/affected-docs.mjs --json db4c45b8c3c5d35eb4c6774e1ce832258c264806
|
Contract reviewServed-tier: Inputs read: card #5082 (body and all 10 comments; triage release ① Derived judgmentsAccept set. Lint R11. Conversion Ledger. D3 entries Respelling. Counted from the diff: 48 Synonym pin. The " Public surface. Written surfaces. Governed skill edit (Tier H). ② Semver level
③ Boundary flagsDev report
Out-of-scope findings:
Implemented-by: VERDICT: PASS Generated by Claude Code |
✅ ACCEPT: PR #22103 at
|
…lared-index-unique-scope-18 Conflict: packages/spec/src/migrations/registry.ts, one hunk, in the hand-written STEP18_RATIONALE array (outside every os-generated region). Both sides inserted a fragment at order 86 at the same anchor. Resolved as the union of both sides' lines verbatim, in id order: declared-index-bare-unique-true-retired, then deployment-plumbing-organization-columns-retired. The generated regions merged textually and are re-derived by gen:migration-registry next. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
维护者速读(终稿):PR #22103 · #5082 协议 18 的唯一范围收口
改了什么:声明索引( 为什么改:ADR-0120 已裁定(D1 / D7):裸 风险与代价(含回滚):
席位意见:建议批准。复审 PASS;CI 在 你要做的:在本 PR 上给出 APPROVED 审核( Generated by Claude Code |
ACCEPT re-head: PR #22103 at
|
Landing after the authorized approval: PR #22103 at
|
Fixes #5082
Clause-②: no (narrowing: bare
unique: trueon a declared index stops being accepted at validate / publish, andVISIBILITY_STRICT_OPTIONSleaves@objectstack/spec; nothing widens)The protocol-18 half of ADR-0120 (D2, D5a, D7), plus the export item folded into this card. On a declared index, bare
unique: trueis refused with a prescription. Stored and built metadata converts it to'global', which is the same physical index. Every in-repo author moves to the explicit spelling.What changes
The refusal (D5a), on every door an author's declared index reaches.
IndexSchema.uniqueis nowfalse | 'global' | 'organization'. Baretrueis refused (invalid_union, pathunique) with its own prescription. The prescription names'global'(installation-wide, the exact index baretruebuilt) and'organization'(one holder per organization), says field-levelunique: trueis unaffected, and ends with the houseos migrate meta --from 17sentence.tscrefuses it too, because the input type no longer admitstrue(ServiceObjectandObjectSchema.createinputs included).unique/unscoped-declared-index(R11) moves fromwarningtoerror, and from advisory to gating on all three commands.lintDataModelstops calling it, soos lintreports it once, through the registry. It stays off the runtime door. The surface reason is new and measured: the save door's ownObjectSchemaparse refuses the spelling before the authoring gate runs, so a runtime crossing could never fire.The conversion (D2).
declared-index-unique-scope(toMajor: 18,retiredFromLoadPath: true,retiredAfter: '17.7.0') rewrites a declared index's baretrueto'global'onobjects[]andobjectExtensions[]. Field-levelunique: trueis never touched. It is inserted where its identifier sorts inMAJOR_18_CONVERSIONS, atorder: 61, with an S4/S5 fixture. It is retired from the authoring funnel, so authors are refused. The data-at-rest seams replay it:applyConversionsToStoredItem, the artifact door inside its declared-floor window, andos migrate meta --from 17.The ledger.
declared-index-bare-unique-true-retired(judging the conversion's applied edits: keep'global', or move to'organization') andvisibility-strict-options-unexported.STEP18_RATIONALEfragment atorder: 86. Order 85 is held by an in-flight PR, so this takes the next free number.spec-changes.jsonand the upgrade guide do not move.PROTOCOL_VERSIONis still17.0.0, so no step-18 entry projects there yet.check:spec-changesandcheck:upgrade-guideare green on that reading.The synonym pin retires. In
sql-driver-unique-tenancy.test.ts:unique: 'global'on a declared index as a synonym of true" pin and its header note are gone.'global'(ADR-0120 D6.6).applyConversionsToStoredItem. Their expected-index output is byte-identical before and after. On a SQLite database built from the bare spelling,detectManagedDriftfor the converted metadata is[]. A lit control (one key moved to'organization') shows drift.The in-repo respelling. Every declared index with a literal
unique: truebecomes'global'. That is 48 indexes in 39 source files acrossplatform-objects,metadata-core,plugin-security,plugin-sharing,service-messaging,service-automationandservice-realtime. Nothing becomes'organization', and no field-leveluniquemoves. The prose that quotes those declarations is respelled with them:metadata-protocoloverlay-index.tsandview-definition-active-index.ts;plugin-authaccount-identity-preflight.tsandREADME.md;18.sys-account-issuer-retiredentry.The teaching surfaces now say "refused" instead of "deprecated":
content/docs/data-modeling/indexing.mdx;skills/objectstack-data/rules/indexing.md;content/docs/protocol/objectql/schema.mdx. This is a fourth teaching surface, found by grepping the rule id. It stated the 17.x posture.The export item.
VISIBILITY_STRICT_OPTIONSmoves, unchanged, to the unbarrelledshared/visibility-strict-options.ts, beside its typeStrictObjectOptions.check:api-surfacereadsshared.json−1, the expected reading. The type is not published instead.Anchors. The two ADR-0120 anchors now read the protocol-18 state, and the conversion entry gains its own anchor (ADR-0120 D6.7).
The refusal point (H1), door by door, measured
indexes: [{ fields: ['code'], unique: true }]ObjectSchema.parse/.create,defineStackinvalid_unionatindexes.0.unique, with the prescriptionunique-scope-message.test.tsandunique-scope.test.ts. A respelled object reverted totruefails to compile (3 TS2322 inobject.test.ts, seen before its fixtures were respelled)os validate/os build✗ objects.0.indexes.0.unique invalid_union: …retired at protocol 18…. Control:'global'exits 0os lintunique/unscoped-declared-indexerror atobjects[0].indexes[0]. Control:'global'exits 0saveMetaItem)INVALID_METADATA/ 422 with the prescription, nothing stored. Control:'global'stores one rowObjectStackProtocolImplementation(deleted, not committed)sys_metadatarow carrying itunique: 'global'getMetaItemObjectSchema.createcalls, so they are refused at module load and bytsc. All 39 respelled objects import and parseregistry.registerObject/ driver inputtrueexactly as'global'tscWhy the schema. It is the one contract every parsing door shares, and it is the only place the refusal reaches
ObjectSchema.createand the save door. Against the four-axis framework:tscmakes the spelling hard to write.Lint R11 is kept as the second channel because
os lintnever parses.Zero drift (H3)
driver-sql's ownexpectedIndexesandnormalizeDeclaredIndexover the'global'declarations and over the same declarations with'global'set back totrue, which is exactly the base tree: none of these files carried'global'ate67ba80049, and the diff touches only those 48 literals. Both tenancy shapes were checked (tenant column and none). Result:files=39 objects=39 respelled-indexes-seen=48 (census target 48) index-normalizations-compared=268 mismatches=0. Control:truevs'organization'differs.Census (H2)
Run against
e67ba80049, with an AST walk (TypeScript compiler API). It finds an object literal withunique: trueinside an array that initialisesindexes, and any index-shaped literal (fields+unique: true). Doc fences are read too, including bare fragments, which parse as broken labelled blocks rather than objects. Firing control: a synthetic file with an index hit, a held variable index and a doc fragment was seen 3/3, while its field-levelunique: true,'global'andfalsewere seen 0/3.'global'indexing.mdx(legacy composite example), plugin-authREADME.md, the skill's refused exampleunique/double-declaration), or R12 for the nested-index controlCHANGELOG.mda58626c8EmbeddedItemEditortestsexamples/**,apps/**The claim's text census reached 47 paths. The difference is prose and code that is not an authored index:
data-model-rules.ts: R11's own message and R12's doc example, updated.schema-drift.ts: driver comments about semantics, and one driver-internalExpectedIndexboolean. Unchanged (driver-sql takes the respelling only).overlay-index.ts,view-definition-active-index.ts,account-identity-preflight.tsand18.sys-account-issuer-retired.ts: quotes of respelled declarations, respelled with them.migrations/registry.ts:10855: field-level prose, unchanged.Verification at
0cb065b48fTests, each run through the shared verify lock. They ran at
ded6c918f6. The only commit after it touchesscripts/adr-anchors/*.jsonand no package source.@objectstack/spec(localproject)@objectstack/spec(repoproject: the step-18 rationale and major-18 conversion merge tests)@objectstack/lint@objectstack/driver-sql@objectstack/cli,unitproject@objectstack/cli, the two edited*.e2efiles (OS_TEST_TIERS=nightly,integrationproject)platform-objects·metadata-core·metadata-protocolplugin-security·plugin-auth·plugin-sharingservice-messaging·service-automation·service-realtimeobjectql·rest·types·cloud-connectiondriver-memory·driver-mongodb·driver-tursotypecheckexits 0 on all 13 packages this diff touches: spec, lint, cli, platform-objects, metadata-core, metadata-protocol, driver-sql, plugin-security, plugin-auth, plugin-sharing, service-messaging, service-automation and service-realtime.Gates.
node scripts/pm/dispatch-gates.mjs --commandsat0cb065b48fderives 134 families. All 134 ran and exited 0.--ranreconciliation: 0 NOT-MEASURED, 0 UNRUN, and every entry carries its exit code. Readings from that run:check:generated: all 15 artifacts up to date.check:api-surface✓. The removal is the committedapi-surface/shared.json−1 (VISIBILITY_STRICT_OPTIONS) andexport-origins/shared.json−1.check:spec-changesandcheck:upgrade-guide✓, with no change: step 18 does not project until the protocol major moves.check:liveness✓. No ledger row moves: the key lives, and a value is not a property.check-adr-0087-registration✓, registeringdeclared-index-bare-unique-true-retiredandvisibility-strict-options-unexported.check-changeset-no-major✓.check:docs✓ (226 generated reference files in sync).check:adr-anchors✓ (61 anchored files).check:nul-bytes✓.check:skills-token-ratchet✓ (rules/indexing.md2183 of 3241 tokens).check:i18n✓.ESLint, narrowed and proven. The population comes from
eslint.config.mjs:**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED. I ran all 68 lintable files this diff touches (--format json: 68 files, 0 errors, 0 warnings, none ignored). Untouched files cannot change verdict: the config enables no type-aware linting (its own note says so), and no untouched file imports the one removed export.Size and tier.
check-governed-merges --pr 22103reads 1390 changed lines (+1015 / −375 over 81 files, generated files included) atccfbfbaa58, under 5000. One path is on the governed register (skills/**), so this PR is Tier H.skills/**readings.rules/indexing.md: 229 → 229 lines (1249 → 1259 words). The edit rewrites three lines in place and buys no line.SKILL.md: 4411 → 4411 lines. NoSKILL.mdis touched.skills/tree: 13366 → 13366 lines.Ablation, on the edited dedup control.
per-package-dedup-positional-echo.test.tsnow builds its nested-index control on R12. Its header asks for its ablation to be re-run on edit. WideningfindingKey's rewrite from the top-level index to every index turns exactly that control red (1 failed, 5 passed). The restore was proven by blob hash (0868281before and after) and an emptygit diff HEAD.Acceptance notes (not filed, nothing changed for them)
EmbeddedItemEditor.indexFallback.test.tsxassertsIndexSchema.safeParse({ fields: ['c'], unique: true }).success === trueas a "still ACCEPTED" control, and names this card. It turns red on objectui's next@objectstack/specbump. That bump is also where its fallback schema's boolean branch has to be decided: that branch renders a switch for a stored boolean, and switched on it would now be refused at save, loudly. The Console Pin Gate builds and does not run tests, so it stays green. Carrier: objectui's next spec bump.driver-sqlschema-drift.tsnear:100("PARKED on ADR-0120 协议 18 项:D2 conversion(声明索引 unique: true → 'global')+ 裸 true 硬拒 + synonym pin 退役 —— 挂 18 列车,现在勿动工 #5082"), outside this card's driver-sql allowance;true"is" the positional spelling (sys-email-template,notification-preference,notification-subscription).isolatedinstall gate.packages/typesunique-scope-install-gate.tsstill treats baretrueas'global'. That is now reachable only from unparsed input, and it reads the spelling correctly. Carrier: none.docs/adrowner. This PR does not touchdocs/adr/**.维护者速读(草稿)
改了什么:声明索引(
indexes[])上的裸unique: true从协议 18 起被拒绝,报错直接告诉作者写'global'(全安装唯一,和原来建出的索引完全一样)或'organization'(每个组织内唯一)。已经存进数据库或已构建产物里的旧写法,加载时自动改写成'global',物理索引一字节不变。仓库里 48 处平台对象的声明全部改成'global';字段级unique: true不变,继续有效。另外把一个外部用不了的内部常量VISIBILITY_STRICT_OPTIONS从公开导出里撤掉。为什么改:ADR-0120 已裁定(D7):裸
true在声明索引上读起来像"每个组织唯一",实际却是"全安装唯一",AI 和人都会照字面误用。17.x 只警告,协议 18 起改为直接拒绝,让作者必须把范围写明。风险与代价(含回滚):对仓库外仍写裸
true的应用是破坏性变更:os validate/os build/ 保存元数据会报错,按提示改成'global'即可(os migrate meta --from 17列出改点),已存储的数据不受影响。已实测零漂移:39 个平台对象、9 个引擎去重键前后索引输出逐字节相同。objectui 有一个测试断言"裸 true 仍可解析",下次升级 spec 时会变红,需要在 objectui 那边跟进。回滚即还原本 PR(无数据迁移)。席位意见:
你要做的:本 PR 改到
skills/**(Tier H),需要你本人审核合并或给出授权批准。Generated by Claude Code