diff --git a/.changeset/liveness-symbol-anchors-action-object.md b/.changeset/liveness-symbol-anchors-action-object.md new file mode 100644 index 0000000000..3f9bbc757f --- /dev/null +++ b/.changeset/liveness-symbol-anchors-action-object.md @@ -0,0 +1,29 @@ +--- +"@objectstack/spec": patch +--- + +chore(spec): re-anchor the `action` and `object` liveness ledgers to consuming symbols (#13003) + +Adoption batch 1 of the symbol-anchor citation grammar landed by #12516. The +`liveness/` ledgers ship inside this package's npm tarball (they are named in +`files`), so this is a published-data change even though no runtime behaviour +moves and no schema key changes. + +Twenty `path:NNN` evidence citations across `liveness/action.json` and +`liveness/object.json` are now written `path#symbol`, each re-closed by reading +the code on the current tree rather than by shifting a line number. A symbol +moves with its consumer, so the pointer survives the in-file drift that rots a +line, and goes red when the consumer is renamed or deleted — a direction a +stale line can never produce. + +What the re-closure found, which is the reason the migration is not mechanical: +fourteen of the fifteen `object.json` citations were pointing at the wrong place +already, every one of them IN RANGE and so invisible to all three existing +checks. They had drifted onto a docblock about aggregate-function lowering, a +job-scheduling block, a neighbouring `const`, and — for `object.enable.clone` — +a sort-node normalizer roughly 6,700 lines from its actual reader. Three had +additionally moved package: the `ownership`, `managedBy` and `tenancy.enabled` +readers now live in `@objectstack/spec` itself, and one of those citations +carried a parenthetical (`applySystemFields reads schema.ownership`) that the +re-read falsified outright — that function's nine `ownership` occurrences are +all comments about a decision it delegates. diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index c4667678d9..d9938f92e0 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -37,9 +37,9 @@ }, "type": { "status": "live", - "verifiedAt": "2026-08-26", - "evidence": "packages/runtime/src/sandbox/body-runner.ts:385 (the #4352 gate — `const type = action.type ?? 'script'` decides whether a `body` binds an executable handler at all); packages/runtime/src/action-execution.ts:1309 (`action.type === 'flow'` routes to dispatchFlowAction), :524 (isHeadlessInvokableAction), :552 (headlessActionTypeError names the type it refuses), :906 (MCP action summary projects it); packages/runtime/src/domains/actions.ts:591 (the REST route's own `actionType` resolution); objectui @732b1bf core ActionRunner.ts:513-541 switches every variant to its own executor (executeScript/Url/Modal/Flow/API/Form/Navigation)", - "note": "api/script/flow wired; url thinner; modal PARTIAL (maps to serverActionHandler, not a real modal); form LIVE via objectui ActionRunner.executeForm (routes a type:'form' action to the FormView at /forms/:target, forwarding the current record id) — the 2026-06 audit mis-classified as dead (objectui renderer not re-verified; fixed the 'Log Time does nothing' report). Build-time lint-view-refs.ts validates the form target resolves to a form view. RE-VERIFIED 2026-07 (#3714 follow-up): `api` -> executeAPI (:974, string-or-ApiConfig endpoint, method/headers/queryParams/responseType) and `form` -> executeForm (:920) both resolve; content/docs/ui/actions.mdx had been telling authors the opposite (\"no runtime executor / renderer today\") and was corrected in the same pass. 2026-08-26: REPOINTED (framework half only) — the evidence led with packages/runtime/src/http-dispatcher.ts, which reads this key nowhere. That file's 9 word-`type` occurrences are ALL something else: four are the TypeScript `import type` keyword (:9, :13, :14, :21) and the rest are other domains' prose or data (`error.type` :862, a field-type→JSON-Schema mapper :926, `details.type` :1156, a metadata type list :1634, an inbox query param :1765). The action consumer was extracted into domains/actions.ts + action-execution.ts (+ the sandbox body-runner, which is where the type gate has always lived) and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). WHY THE GATE COULD NOT SEE THE ROT — and this is the part that differs from the `target`/`requiredPermissions` siblings repointed the day before: those were caught because their cited file had 0 occurrences of the key, whereas `type` is a common English word AND a TypeScript keyword, so the word-bounded key-mention check anchors on the coincidence and PASSES. This entry was repaired by a hand call-graph read, not by tooling; the check's designed, honest limit is what left it standing. The objectui half is unchanged and stays as measured @732b1bf (2026-07-28) — it was not re-graded here." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/sandbox/body-runner.ts#actionBodyRunnerFactory (the #4352 gate — `const type = action.type ?? 'script'` decides whether a `body` binds an executable handler at all); packages/runtime/src/action-execution.ts#invokeBusinessAction (`action.type === 'flow'` routes to dispatchFlowAction); packages/runtime/src/action-execution.ts#isHeadlessInvokableAction (gates headless dispatch on the type); packages/runtime/src/action-execution.ts#headlessActionTypeError (names the type it refuses); packages/runtime/src/action-execution.ts#summarizeAction (MCP action summary projects it); packages/runtime/src/domains/actions.ts#handleActionsRequest (the REST route's own `actionType` resolution, read again at the flow branch); objectui @732b1bf core ActionRunner.ts:513-541 switches every variant to its own executor (executeScript/Url/Modal/Flow/API/Form/Navigation)", + "note": "api/script/flow wired; url thinner; modal PARTIAL (maps to serverActionHandler, not a real modal); form LIVE via objectui ActionRunner.executeForm (routes a type:'form' action to the FormView at /forms/:target, forwarding the current record id) — the 2026-06 audit mis-classified as dead (objectui renderer not re-verified; fixed the 'Log Time does nothing' report). Build-time lint-view-refs.ts validates the form target resolves to a form view. RE-VERIFIED 2026-07 (#3714 follow-up): `api` -> executeAPI (:974, string-or-ApiConfig endpoint, method/headers/queryParams/responseType) and `form` -> executeForm (:920) both resolve; content/docs/ui/actions.mdx had been telling authors the opposite (\"no runtime executor / renderer today\") and was corrected in the same pass. 2026-08-26: REPOINTED (framework half only) — the evidence led with packages/runtime/src/http-dispatcher.ts, which reads this key nowhere. That file's 9 word-`type` occurrences are ALL something else: four are the TypeScript `import type` keyword (:9, :13, :14, :21) and the rest are other domains' prose or data (`error.type` :862, a field-type→JSON-Schema mapper :926, `details.type` :1156, a metadata type list :1634, an inbox query param :1765). The action consumer was extracted into domains/actions.ts + action-execution.ts (+ the sandbox body-runner, which is where the type gate has always lived) and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). WHY THE GATE COULD NOT SEE THE ROT — and this is the part that differs from the `target`/`requiredPermissions` siblings repointed the day before: those were caught because their cited file had 0 occurrences of the key, whereas `type` is a common English word AND a TypeScript keyword, so the word-bounded key-mention check anchors on the coincidence and PASSES. This entry was repaired by a hand call-graph read, not by tooling; the check's designed, honest limit is what left it standing. The objectui half is unchanged and stays as measured @732b1bf (2026-07-28) — it was not re-graded here. 2026-08-28: RE-ANCHORED (#13003, adoption of the #12516 grammar) — re-closed by hand against c459da6bc. All six framework consumers stand and none had left its file, so this is a grammar migration rather than a repair; what it DOES repair is a second, quieter gap: `:524`/`:552`/`:906` were written as bare line suffixes with no path in front of them, which the evidence scanner cannot parse as citations at all (`PATH_RE` needs a repo-rooted token), so three of this entry's six consumers were unfalsifiable prose that no check has ever resolved. They are now anchors of their own." }, "target": { "status": "live", @@ -49,9 +49,9 @@ }, "body": { "status": "live", - "verifiedAt": "2026-08-26", - "evidence": "packages/runtime/src/sandbox/body-runner.ts:356 (`const raw = action.body` in actionBodyRunnerFactory, whose own header at :342 calls it \"the ONE choke point where an `action.body` becomes an executable handler\" — both bind paths reach it: AppPlugin's collectBundleActions walk and engine.setDefaultActionRunner); packages/runtime/src/action-execution.ts:525 (isHeadlessInvokableAction gates headless dispatch on `action?.target || action?.body`)", - "note": "server script (L1/L2) via engine.executeAction→body-runner. 2026-08-26: REPOINTED — the evidence cited packages/runtime/src/http-dispatcher.ts, which reads this key nowhere. Its 68 word-`body` occurrences are the inbound HTTP request body — the `body: any` parameter threaded through the handleX delegates (:890, :961, :1732, :1969 …) and the result envelope at :728 — plus the file's own \"Thin delegate — body extracted to ./domains/…\" extraction comments, where `body` means a function body. The consumer this note already NAMED in prose (body-runner) is where the read has always been; the surrounding action path was extracted into domains/actions.ts + action-execution.ts, and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). WHY THE GATE COULD NOT SEE THE ROT: `body` is both a common English word and the name of the HTTP request member this dispatcher is built around, so the word-bounded key-mention check added in #11457 anchors on 68 unrelated hits and passes — the designed, honest limit of that signal, which is why this needed a hand call-graph read. Note the shape the repoint exposes: the NOTE's pointer (\"via engine.executeAction→body-runner\") stayed true the whole time while the EVIDENCE pointer rotted — the same split recorded on `requiredPermissions`, one layer over." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/sandbox/body-runner.ts#actionBodyRunnerFactory (`const raw = action.body`, in the factory whose own header calls it \"the ONE choke point where an `action.body` becomes an executable handler\" — both bind paths reach it: AppPlugin's collectBundleActions walk and engine.setDefaultActionRunner); packages/runtime/src/action-execution.ts#isHeadlessInvokableAction (gates headless dispatch on `action?.target || action?.body`)", + "note": "server script (L1/L2) via engine.executeAction→body-runner. 2026-08-26: REPOINTED — the evidence cited packages/runtime/src/http-dispatcher.ts, which reads this key nowhere. Its 68 word-`body` occurrences are the inbound HTTP request body — the `body: any` parameter threaded through the handleX delegates (:890, :961, :1732, :1969 …) and the result envelope at :728 — plus the file's own \"Thin delegate — body extracted to ./domains/…\" extraction comments, where `body` means a function body. The consumer this note already NAMED in prose (body-runner) is where the read has always been; the surrounding action path was extracted into domains/actions.ts + action-execution.ts, and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). WHY THE GATE COULD NOT SEE THE ROT: `body` is both a common English word and the name of the HTTP request member this dispatcher is built around, so the word-bounded key-mention check added in #11457 anchors on 68 unrelated hits and passes — the designed, honest limit of that signal, which is why this needed a hand call-graph read. Note the shape the repoint exposes: the NOTE's pointer (\"via engine.executeAction→body-runner\") stayed true the whole time while the EVIDENCE pointer rotted — the same split recorded on `requiredPermissions`, one layer over. 2026-08-28: RE-ANCHORED (#13003) — re-closed by hand against c459da6bc; both consumers stand where the 08-26 repoint put them, so the lines had not yet rotted and this is the grammar migration, not a second repair. The anchor is what makes that claim re-testable next time: `actionBodyRunnerFactory` is the symbol the header already calls the ONE choke point, so a consumer that moves inside this 798-line file keeps the pointer true and one that is deleted turns it red." }, "execute": { "status": "live", diff --git a/packages/spec/liveness/object.json b/packages/spec/liveness/object.json index a071cfdbcf..6dab1226a0 100644 --- a/packages/spec/liveness/object.json +++ b/packages/spec/liveness/object.json @@ -65,8 +65,9 @@ }, "datasource": { "status": "live", - "evidence": "packages/objectql/src/engine.ts:1147", - "note": "driver routing." + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/engine.ts#resolveDatasourceBinding (limb 1 of the routing precedence — `object?.datasource && object.datasource !== 'default'` returns `{ datasource, via: 'explicit' }`, which getDriver then resolves to a driver); packages/objectql/src/engine.ts#assertWriteAllowed (reads the same key to find the datasource DEFINITION whose `external.allowWrites` gates the write)", + "note": "driver routing. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED in the same pass — the citation was `engine.ts:1147`, which is IN RANGE of this 13593-line file and lands in a docblock about `assertWriteAllowed`'s placement, not on any read of this key. That is the #12516 class exactly: file present, line inside it, and the file names `datasource` on dozens of lines that all mean the engine's own DatasourceDef/driver registry rather than the object key. Re-closed by hand against c459da6bc." }, "lifecycle": { "status": "live", @@ -81,14 +82,15 @@ }, "indexes": { "status": "live", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1181", - "verifiedAt": "2026-08-06", - "note": "DDL — but only through `name`/`fields`/`unique`. `syncDeclaredIndexes` creates every declared index via knex's `table.index()` / `table.unique()`, and the differ's `DeclaredIndexInput` carries `name`/`fields`/`unique`/`nullSafeColumns`. The other two child keys were RETIRED 2026-08-06 (#5248, #4943, ADR-0049): `indexes[].type` selected no access method (and its `.default('btree')` materialized an inert knob into every parse output) and `indexes[].partial` produced a FULL index with the predicate discarded. Both are `retiredKey()` tombstones at `IndexSchema` and are stripped from sources by the protocol-17 conversion `object-index-type-partial-removed`. The container stays `live` because the surviving keys drive real DDL. ⚠ Not to be confused with driver-sql's own `partial: boolean`, which is parsed back out of the database's own CREATE INDEX DDL for drift detection and is unrelated to the declaration surface." + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#syncTableIndexes (takes the object's declared indexes, unions them with the field-derived uniques and hands the result to the DDL writer); packages/drivers/driver-sql/src/sql-driver.ts#syncDeclaredIndexes (the DDL writer named in this note — knex `table.index()` / `table.unique()` per declared entry); packages/drivers/driver-sql/src/sql-driver.ts#initObjects (registration-time read of `(obj as any).indexes`)", + "verifiedAt": "2026-08-28", + "note": "DDL — but only through `name`/`fields`/`unique`. `syncDeclaredIndexes` creates every declared index via knex's `table.index()` / `table.unique()`, and the differ's `DeclaredIndexInput` carries `name`/`fields`/`unique`/`nullSafeColumns`. The other two child keys were RETIRED 2026-08-06 (#5248, #4943, ADR-0049): `indexes[].type` selected no access method (and its `.default('btree')` materialized an inert knob into every parse output) and `indexes[].partial` produced a FULL index with the predicate discarded. Both are `retiredKey()` tombstones at `IndexSchema` and are stripped from sources by the protocol-17 conversion `object-index-type-partial-removed`. The container stays `live` because the surviving keys drive real DDL. ⚠ Not to be confused with driver-sql's own `partial: boolean`, which is parsed back out of the database's own CREATE INDEX DDL for drift detection and is unrelated to the declaration surface. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the citation was `sql-driver.ts:1181`, in range of this 16289-line file but landing in a docblock about aggregate-function lowering (`count_distinct`), nowhere near an index read. The note already named `syncDeclaredIndexes` in prose and that pointer stayed true the whole time while the evidence line rotted — the same note-survives/evidence-rots split recorded on action.requiredPermissions. Re-closed by hand against c459da6bc." }, "validations": { "status": "live", - "evidence": "packages/objectql/src/validation/rule-validator.ts:154", - "note": "incl. state_machine." + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/validation/rule-validator.ts#evaluateValidationRules (`const rules = objectSchema?.validations` — the write-path evaluator); packages/objectql/src/validation/rule-validator.ts#legalNextStates (the same read, for the `state_machine` member this note names); packages/objectql/src/validation/rule-validator.ts#needsPriorRecord (reads the rule list to decide whether the prior record must be loaded)", + "note": "incl. state_machine. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the citation was `rule-validator.ts:154`, in range of this 2228-line file and landing inside a docblock about `requiredWhen` root binding; the three real reads of `objectSchema.validations` are at the symbols above. Re-closed by hand against c459da6bc." }, "activityMilestones": { "status": "live", @@ -97,19 +99,21 @@ }, "actions": { "status": "live", - "evidence": "packages/runtime/src/app-plugin.ts:929", - "note": "served on /meta/objects/:name." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/app-plugin.ts#collectBundleActions (walks `bundle.objects[*].actions` and `bundle.manifest.objects[*].actions`, stamping each entry with its parent object name — the list the action body-runner binding and the /meta surface are both built from)", + "note": "served on /meta/objects/:name. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the citation was `app-plugin.ts:929`, in range of this 1840-line file but landing in the JOB scheduling block (`svc.schedule(...)`), which reads nothing of this key. The file mentions `actions` on plenty of lines, so the #11457 key-mention check anchors and passes; only a hand read separates the per-object action walk from the unrelated `bundle.actions` and job paths. Re-closed by hand against c459da6bc." }, "managedBy": { "status": "live", - "evidence": "packages/objectql/src/registry.ts:208", - "note": "default perms; ui crudAffordances." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/data/object.zod.ts#resolveCrudAffordances (`obj?.managedBy ?? 'platform'` selects the bucket's default CRUD affordance row — the `ui crudAffordances` half of this note); packages/plugins/plugin-security/src/system-write-guard.ts#assertEngineOwnedWriteAllowed (`const bucket = schema?.managedBy` — engine-owned buckets refuse user-context writes the bucket's affordances forbid); packages/spec/src/data/injected-system-columns.ts#resolveInjectedSystemColumns (a set `managedBy` withholds the ownership anchors, and `'better-auth'` withholds every injected column); packages/objectql/src/secret-fields.ts#collectMaskedReadFields (a `'better-auth'` table's own columns are exempted from password/secret masking, sibling `collectMaskedPasswordFields` the same)", + "note": "default perms; ui crudAffordances. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the citation was `registry.ts:208`, in range of that 3921-line file and landing on `merged.indexes = [...]`, the INDEX merge of a sibling key. registry.ts names `managedBy` on ten lines so the key-mention check passes, but nine of them are prose in comments and the tenth (:986) interpolates the value into a WARNING string — a diagnostic, not a decision. The decisions moved into @objectstack/spec (`resolveCrudAffordances`, `resolveInjectedSystemColumns`) and plugin-security: the consumer-promoted-into-another-package shape the gate's own guidance names. Re-closed by hand against c459da6bc." }, "ownership": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/objectql/src/registry.ts:292 (applySystemFields reads schema.ownership)", - "note": "#3175 record-ownership model. applySystemFields injects the reassignable owner_id lookup by default (ownership:'user'); 'org'|'none' opt out of BOTH ownership anchors (Dataverse-style catalog/junction tables); ADR-0117 D1's 'business_unit' tier (engine #5677, enum member #5678) takes owning_business_unit_id and deliberately no owner_id. Per-tier authority is resolveInjectedSystemColumns (spec/data). Proven in objectql/src/registry.test.ts. Evidence line refreshed 2026-07-30 (was :272, drifted)." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/data/injected-system-columns.ts#resolveInjectedSystemColumns (`typeof obj.ownership === 'string'` then the POSITIVE per-tier list — `'user'`/omitted takes both anchors, `'business_unit'` takes only `owning_business_unit_id`, `'org'`/`'none'` take neither); packages/objectql/src/registry.ts#applySystemFields (materializes that plan's columns onto the schema — it consults the plan and does NOT read the key itself)", + "note": "#3175 record-ownership model. applySystemFields injects the reassignable owner_id lookup by default (ownership:'user'); 'org'|'none' opt out of BOTH ownership anchors (Dataverse-style catalog/junction tables); ADR-0117 D1's 'business_unit' tier (engine #5677, enum member #5678) takes owning_business_unit_id and deliberately no owner_id. Per-tier authority is resolveInjectedSystemColumns (spec/data). Proven in objectql/src/registry.test.ts. Evidence line refreshed 2026-07-30 (was :272, drifted). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `registry.ts:292` had drifted again (it now lands in a docblock about the `multiTenant` env var), and the re-closure falsified the parenthetical as well: `applySystemFields` reads `ownership` NOWHERE in code. Its nine occurrences in that function are all comments explaining a decision it delegates; the read is `resolveInjectedSystemColumns` in @objectstack/spec, which this note already named as \"per-tier authority\". So the entry stays `live` on stronger evidence, and the drift it demonstrates is the one the line bound cannot see twice over — a line refreshed 2026-07-30 rotting again inside the same file, with the prose beside it pointing at a reader that had already moved packages. Re-closed by hand against c459da6bc." }, "access": { "status": "live", @@ -118,9 +122,9 @@ }, "requiredPermissions": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-security/src/security-plugin.ts:132 (NormalizedRequiredPermissions — per-CRUD buckets) + packages/plugins/plugin-security/src/permission-evaluator.ts (crudBucketForOperation keeps the buckets in lockstep with OPERATION_TO_PERMISSION)", - "note": "ADR-0066 D3 object capability contract — the security middleware denies unless the caller's systemPermissions union covers it (AND-gate, before the CRUD grant). Mirrors App.requiredPermissions. Unit + full-middleware proven in plugin-security/security-plugin.test.ts." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-security/src/security-plugin.ts#getObjectSecurityMeta (the read — `normalizeRequiredPermissions((obj as any)?.requiredPermissions)` onto the cached per-object posture); packages/plugins/plugin-security/src/security-plugin.ts#normalizeRequiredPermissions (folds the `string[] | per-op map` authoring shape into per-CRUD buckets); packages/plugins/plugin-security/src/security-plugin.ts#requiredCapsForOperation (the AND-gate lookup the middleware denies on); packages/plugins/plugin-security/src/permission-evaluator.ts#crudBucketForOperation (keeps the buckets in lockstep with OPERATION_TO_PERMISSION)", + "note": "ADR-0066 D3 object capability contract — the security middleware denies unless the caller's systemPermissions union covers it (AND-gate, before the CRUD grant). Mirrors App.requiredPermissions. Unit + full-middleware proven in plugin-security/security-plugin.test.ts. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `security-plugin.ts:132` is in range of this 6862-line file but lands in a docblock about platform-admin-EXCLUSIVE capabilities; the type it claimed to name (`NormalizedRequiredPermissions`) is declared at :190 and the actual key read is ~6400 lines further down, in `getObjectSecurityMeta`. Re-closed by hand against c459da6bc." }, "userActions": { "status": "live", @@ -128,27 +132,30 @@ }, "systemFields": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/objectql/src/registry.ts (column injection) + packages/plugins/plugin-security/src/security-plugin.ts:3281 (systemFields.tenant === false read as the tenancy opt-out)", - "note": "organization_id auto-inject gate." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/data/injected-system-columns.ts#resolveInjectedSystemColumns (`sf?.tenant !== false` decides the organization_id column and `sf?.audit !== false` the audit family — the injection authority); packages/objectql/src/registry.ts#applySystemFields (materializes that plan); packages/plugins/plugin-security/src/security-plugin.ts#getObjectSecurityMeta (`systemFields?.tenant === false` read as the tenancy opt-out on the per-object posture); packages/plugins/plugin-security/src/security-plugin.ts#loadObjectFieldNames (the same opt-out, cached for the RLS wildcard-policy applicability decision)", + "note": "organization_id auto-inject gate. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `security-plugin.ts:3281` had rotted in range onto a bootstrap `catalogSeedPasses()` call; both real reads live ~3200 lines later. The `registry.ts (column injection)` half was a citation with no line at all, which no line bound could ever falsify: the injection DECISION is `resolveInjectedSystemColumns` in @objectstack/spec and registry only applies it, so the plan is now cited beside the applier. Re-closed by hand against c459da6bc." }, "sharingModel": { "status": "live", - "verifiedAt": "2026-07-30", + "verifiedAt": "2026-08-28", "proof": "packages/qa/dogfood/test/controlled-by-parent.dogfood.test.ts#cbp-controlled-by-parent", - "evidence": "packages/plugins/plugin-sharing/src/sharing-service.ts:54", - "note": "ADR-0055 high-risk class (sharing): the `controlled_by_parent` value derives a detail object's access from its master — the security layer injects `masterFK IN (accessible master ids)` on reads and requires master edit-access on by-id writes. The proof asserts a member who cannot read the master can neither read nor by-id-write the detail, and is not over-blocked on a master they own." + "evidence": "packages/plugins/plugin-sharing/src/sharing-service.ts#effectiveSharingModel (`schema?.sharingModel ?? schema?.security?.sharingModel`, collapsed onto the three behaviours the service enforces; `controlled_by_parent` folds to the master-derived branch and an unrecognised stored value fails CLOSED to `private`)", + "note": "ADR-0055 high-risk class (sharing): the `controlled_by_parent` value derives a detail object's access from its master — the security layer injects `masterFK IN (accessible master ids)` on reads and requires master edit-access on by-id writes. The proof asserts a member who cannot read the master can neither read nor by-id-write the detail, and is not over-blocked on a master they own. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sharing-service.ts:54` had rotted onto a blank line between an interface and a `makeShareId` docblock. Re-closed by hand against c459da6bc." }, "publicSharing": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-sharing/src/share-link-service.ts:48 (policy extraction) — share-link creation on an object without publicSharing.enabled=true is rejected 422 (share-link-service.ts:170)" + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-sharing/src/share-link-service.ts#getPolicy (`const raw = schema?.publicSharing` — the policy extraction: enabled, allowedAudiences, allowedPermissions, maxExpiryDays, redactFields, eligibility); packages/plugins/plugin-sharing/src/share-link-service.ts#createLink (mint authority — an object that has not enabled publicSharing is rejected 422)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — BOTH line citations had rotted in range. `:48` landed in a docblock about `sys_share_link` being `managedBy: 'engine-owned'` (the extraction is at ~:97), and the `:170` the prose gave for the 422 is now inside `createLink` ~250 lines further down. Re-closed by hand against c459da6bc." }, "tenancy": { "children": { "enabled": { "status": "live", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1081" + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/data/object.zod.ts#isTenancyDisabled (the predicate itself — `tenancy?.enabled === false`); packages/drivers/driver-sql/src/sql-driver.ts#computeTenantField (calls it as the explicit opt-out that wins over any column-presence heuristic, returning `null` so no driver-level org scope is applied); packages/plugins/plugin-security/src/security-plugin.ts#getObjectSecurityMeta (reads `tenancy?.enabled === false` directly onto the posture's `tenancyDisabled`); packages/spec/src/data/injected-system-columns.ts#resolveInjectedSystemColumns (the same predicate withholds the organization_id column)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1081` is in range of this 16289-line file and lands in a docblock about withheld cross-field filter diagnostics, unrelated to tenancy. The predicate itself has since been promoted into @objectstack/spec (`isTenancyDisabled`), which the driver imports; citing only the driver left the actual read uncited. Re-closed by hand against c459da6bc." }, "tenantField": { "status": "live", @@ -157,9 +164,9 @@ }, "organizationField": { "status": "live", - "evidence": "packages/metadata-core/src/record-organization.ts:177-180 (resolveRecordOrganizationField reads `tenancy.organizationField` as limb 0 of the precedence, and falls through when the object has no such column)", - "note": "STAMP-ONLY by the #8778 maintainer ruling (option A): consulted exclusively by resolveRecordOrganizationField when audit rows are stamped, so a credential table can stay unwalled while its trail follows the record's own organization (sys_api_key.active_organization_id, #8707/#8287). Deliberately read by NO tenant-scoping path; read-neutrality is pinned by tests beside applyTenantScope/injectTenantOnInsert (driver-sql), computeTenantLayer0Filter (plugin-security) and resolveInjectedSystemColumns (spec). 2026-08-25: REPOINTED — the evidence cited plugin-audit/src/audit-writers.ts, which has read the key through `createRecordOrganizationResolver` ever since #10101 promoted the resolver into @objectstack/metadata-core; audit-writers.ts:220 says so in its own re-export comment. The cited file still existed and the citation carried no line, so neither the existence check nor the #11210 line bound could see it. The stamping CALL SITE is audit-writers.ts:863 — kept here in prose rather than as a citation, because a call site that never names the key belongs in `producer`, not in `evidence`.", - "verifiedAt": "2026-08-15" + "evidence": "packages/metadata-core/src/record-organization.ts#resolveRecordOrganizationField (reads `tenancy.organizationField` as limb 0 of the precedence, and falls through when the object has no such column)", + "note": "STAMP-ONLY by the #8778 maintainer ruling (option A): consulted exclusively by resolveRecordOrganizationField when audit rows are stamped, so a credential table can stay unwalled while its trail follows the record's own organization (sys_api_key.active_organization_id, #8707/#8287). Deliberately read by NO tenant-scoping path; read-neutrality is pinned by tests beside applyTenantScope/injectTenantOnInsert (driver-sql), computeTenantLayer0Filter (plugin-security) and resolveInjectedSystemColumns (spec). 2026-08-25: REPOINTED — the evidence cited plugin-audit/src/audit-writers.ts, which has read the key through `createRecordOrganizationResolver` ever since #10101 promoted the resolver into @objectstack/metadata-core; audit-writers.ts:220 says so in its own re-export comment. The cited file still existed and the citation carried no line, so neither the existence check nor the #11210 line bound could see it. The stamping CALL SITE is audit-writers.ts:863 — kept here in prose rather than as a citation, because a call site that never names the key belongs in `producer`, not in `evidence`. 2026-08-28: RE-ANCHORED (#13003) — the ONLY entry in this batch whose citation was still accurate: `:177-180` still bracket the limb-0 read, so the range was correct and the migration is pure grammar. Worth recording as the control case — it is a 248-line file, and the batch's fourteen rotted siblings were all in files of 700 to 20255 lines.", + "verifiedAt": "2026-08-28" } } }, @@ -181,8 +188,9 @@ }, "searchable": { "status": "live", - "evidence": "packages/metadata-protocol/src/protocol.ts:2967", - "note": "Global Search (searchAll, M10.5): `enable.searchable === false` opts the object out of cross-object search. Behavior-changing reader — the 2026-06 audit mis-classified as dead (search executor not re-verified). default(true), so still not authorWarn'd." + "verifiedAt": "2026-08-28", + "evidence": "packages/metadata-protocol/src/protocol.ts#searchAll (`const enable = obj.enable ?? {}` then `if (enable.searchable === false) continue` — the object is skipped for cross-object search)", + "note": "Global Search (searchAll, M10.5): `enable.searchable === false` opts the object out of cross-object search. Behavior-changing reader — the 2026-06 audit mis-classified as dead (search executor not re-verified). default(true), so still not authorWarn'd. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `protocol.ts:2967` is in range of this 20255-line file and lands inside `normalizeSortNodes`, ~7100 lines from the read; the note already named `searchAll` in prose. Note what the key-mention check sees here: the file's `searchable` hits are almost all `searchableFields`, a DIFFERENT object key, which `\\bsearchable\\b` does not match — the check passes on standalone `searchable` in comment prose instead. Re-closed by hand against c459da6bc." }, "files": { "status": "live", @@ -199,17 +207,17 @@ }, "clone": { "status": "live", - "verifiedAt": "2026-07-28", - "evidence": "packages/metadata-protocol/src/protocol.ts:2938", - "note": "cloneData() gates on schema.enable.clone (explicit false ⇒ 403 CLONE_DISABLED); exposed at POST /data/:object/:id/clone (rest-server.ts registerDataActionEndpoints). POINTER REPAIRED 2026-07 — cited packages/objectql/src/protocol.ts:2259 until the protocol moved to @objectstack/metadata-protocol. The claim never stopped being true, but the stale-evidence check could not surface the rot: 47 parse-artefact false positives were burying it (fixed in the same pass). Behaviour guarded by packages/objectql/src/protocol-clone-real-engine.test.ts." + "verifiedAt": "2026-08-28", + "evidence": "packages/metadata-protocol/src/protocol.ts#cloneData (`if (schema?.enable?.clone === false)` ⇒ 403 CLONE_DISABLED; an absent block or absent flag stays enabled, matching the spec default)", + "note": "cloneData() gates on schema.enable.clone (explicit false ⇒ 403 CLONE_DISABLED); exposed at POST /data/:object/:id/clone (rest-server.ts registerDataActionEndpoints). POINTER REPAIRED 2026-07 — cited packages/objectql/src/protocol.ts:2259 until the protocol moved to @objectstack/metadata-protocol. The claim never stopped being true, but the stale-evidence check could not surface the rot: 47 parse-artefact false positives were burying it (fixed in the same pass). Behaviour guarded by packages/objectql/src/protocol-clone-real-engine.test.ts. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED AGAIN — the 2026-07 repair moved the citation to the right FILE and it has been wrong ever since inside it: `:2938` lands in `normalizeSortNodes` while `cloneData` is at ~:9648, a 6700-line in-range drift. This entry is therefore the worked example of why the anchor exists — its pointer has now been repaired twice, once for a cross-package move that the existence check could see, and once for an in-file move that nothing in the gate could. Re-closed by hand against c459da6bc." } } }, "isSystem": { "status": "live", - "verifiedAt": "2026-07-30", - "evidence": "packages/plugins/plugin-sharing/src/sharing-service.ts:75", - "note": "effectiveSharingModel: an object with no sharingModel and isSystem===true defaults to 'public' (else 'private') — fail-closed org-wide-default posture; ORed with the sys_ name-prefix fallback. Also read by lint/src/validate-security-posture.ts:98 (isSystemObject exempts system objects from the master-detail CRUD-grant lint) and mirrored in metadata-protocol/src/protocol.ts:106. The 2026-06 audit mis-classified as dead (sharing/lint readers not re-verified)." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-sharing/src/sharing-service.ts#effectiveSharingModel (`schema?.isSystem === true || String(schema?.name ?? '').startsWith('sys_')` on the no-declared-model branch)", + "note": "effectiveSharingModel: an object with no sharingModel and isSystem===true defaults to 'public' (else 'private') — fail-closed org-wide-default posture; ORed with the sys_ name-prefix fallback. Also read by lint/src/validate-security-posture.ts:98 (isSystemObject exempts system objects from the master-detail CRUD-grant lint) and mirrored in metadata-protocol/src/protocol.ts:106. The 2026-06 audit mis-classified as dead (sharing/lint readers not re-verified). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sharing-service.ts:75` had rotted onto `const OWNER_FIELD = 'owner_id'`, a neighbouring constant. Both this entry and `sharingModel` cite the same symbol because one function reads both keys, which the two rotted lines (:54 and :75) obscured by pointing at different unrelated places. Re-closed by hand against c459da6bc." }, "searchableFields": { "status": "live",