Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .changeset/liveness-citation-key-mention.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
'@objectstack/spec': patch
---

liveness gate: a citation must name the property it is evidence for

Two checks already bounded a `live` entry's citation, and both bounded it from the
outside — the cited file must exist (#5623), and a cited line must be inside it
(#11210). Between them sat a gap neither could see: a consumer that moves *within*
the file it is cited to, or a citation written with no line at all, leaves the file
present and every named line in range. The pointer is wrong and the gate is green.

Measured over the whole ledger before anything was switched on: 403 (entry, cited
local file) pairs, **11** where the cited file never mentions the property's own key,
and **7 of those 11 were real rot** — repaired here:

- `permission.objects.allowExport` — `annotateEffectiveApiOperations` moved to
`current-user-endpoints.ts`; the same repos-internal movement that had already
rotted `permission.systemPermissions` and `permission.tabPermissions`.
- `object.tenancy.organizationField` — the resolver was promoted into
`@objectstack/metadata-core`; the cited `audit-writers.ts` says so itself, in the
re-export comment left behind.
- `action.target` / `action.requiredPermissions` — the actions domain was extracted
out of `http-dispatcher.ts`, which retains 0 occurrences of either key.
- `action.bodyShape` / `action.bodyExtra` — client-dispatched keys whose only
consumer has always been the renderer; the in-repo citation could not have been
right at any point. Now attributed to `objectui` with the commit pinned.
- `field.requiredWhen` — cited its *sibling* `record-validator.ts`, which enforces the
static `required` contract; the CEL predicate is evaluated one file over in
`rule-validator.ts`. Both files exist, so nothing could see it.

The remaining 4 are the `camelCase` → `snake_case` convention this platform mandates
(Prime Directive #3): a property persisted as a column is read as `body_html`,
`managed_by`, never as the authoring key. Three are handled **structurally** — the
matcher folds the key across the naming convention rather than exempting them — and
the match is word-bounded so a prefix cannot satisfy the key (`required` is not
`requiredWhen`, which is precisely how that rot stayed hidden). The one residual is a
compound *child*-key remap (`fromOverride.address` → `from_address`) that no fold of
the parent key reaches, and it is a single explicit row in the shrink-only
`scripts/liveness/key-mention.baseline.json`, which fails in **both** directions: a
row whose pair later anchors must be deleted.

So the check ships red-capable at zero unexplained hits, which is the whole reason the
census came first — `evidence.mts`'s header records what the alternative costs, when
48 of 227 entries were flagged, every one was a false positive, and the single genuine
rot inside that list sat unread.

The check asks `evidence` only, never `producer`: a producer cites *who supplies a
second input* (#4837), which is by definition a call site and need not name the key at
all.
21 changes: 14 additions & 7 deletions packages/spec/liveness/action.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -43,8 +43,9 @@
},
"target": {
"status": "live",
"evidence": "packages/runtime/src/http-dispatcher.ts",
"note": "URL/script/flow/endpoint + ${param}/${ctx} interpolation."
"verifiedAt": "2026-08-25",
"evidence": "packages/runtime/src/action-execution.ts:725 (type:'flow' server dispatch — automation.execute(action.target, …), with :718 rejecting an unknown flow name by that same value); packages/runtime/src/action-execution.ts:472 (headlessActionTypeError names the target the client-dispatched types go to instead)",
"note": "URL/script/flow/endpoint + ${param}/${ctx} interpolation. 2026-08-25: REPOINTED — the evidence cited packages/runtime/src/http-dispatcher.ts, which has 0 occurrences of `target`; the action consumer was extracted into domains/actions.ts + action-execution.ts and http-dispatcher.ts now only delegates (handleActions → handleActionsRequest at :1969-1970). The file still existed and the citation carried no line, so nothing in the gate could see it."
},
"body": {
"status": "live",
Expand DownExpand Up@@ -107,8 +108,9 @@
},
"requiredPermissions": {
"status": "live",
"evidence": "packages/runtime/src/http-dispatcher.ts",
"note": "ADR-0066 D4 dual-surface action gate. Server is source of truth: handleActions rejects (403) when the caller's systemPermissions don't cover action.requiredPermissions; objectui ActionRunner derives the same UI hide/disable. Unit-proven in packages/runtime/src/http-dispatcher.test.ts + packages/spec/src/ui/action.test.ts."
"verifiedAt": "2026-08-25",
"evidence": "packages/runtime/src/domains/actions.ts:172-180 (ADR-0066 D4 dual-surface gate — resolves the action's declared requiredPermissions and rejects 403 when the caller's systemPermissions don't cover them); packages/runtime/src/action-execution.ts (resolveRouteActionDeclaration supplies the declaration the gate reads)",
"note": "ADR-0066 D4 dual-surface action gate. Server is source of truth: handleActionsRequest rejects (403) when the caller's systemPermissions don't cover action.requiredPermissions; objectui ActionRunner derives the same UI hide/disable. Unit-proven in packages/runtime/src/http-dispatcher.test.ts + packages/spec/src/ui/action.test.ts (both re-measured 2026-08-25 and still naming the key). 2026-08-25: REPOINTED — the evidence cited packages/runtime/src/http-dispatcher.ts, which has 0 occurrences of `requiredPermissions` since the actions domain was extracted into domains/actions.ts. Note the split the repoint exposes: the TEST kept the name while the source lost it, so the note's pointer stayed true while the evidence pointer rotted."
},
"disabled": {
"status": "live",
Expand All@@ -130,16 +132,21 @@
},
"bodyShape": {
"status": "live",
"evidence": "packages/runtime/src/http-dispatcher.ts",
"note": "{wrap} request shaping."
"verifiedAt": "2026-08-25",
"evidenceScope": "cross-repo",
"evidence": "objectui @a76b18cf2: packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:359-360 reads action.bodyShape.wrap and nests the payload under that key, and objectui packages/components/src/renderers/action/action-button.tsx:173 forwards it off the schema onto that runtime call (siblings action-icon.tsx:114, action-group.tsx:264, action-menu.tsx:244)",
"note": "{wrap} request shaping. 2026-08-25: REPOINTED, and the repoint CHANGED THE REALM — the evidence cited packages/runtime/src/http-dispatcher.ts, which has 0 occurrences of `bodyShape`, and so does every other file in this repo outside the schema/conversions/CLI declaration sites. `type:'api'` actions are client-dispatched by design: action-execution.ts:468-477 refuses them server-side with the message \"it dispatches on `target`, not through the action registry\". So the only consumer this key has ever had is the renderer, and the in-repo citation could not have been right at any point."
},
"method": {
"status": "live",
"evidence": "packages/runtime/src/http-dispatcher.ts"
},
"bodyExtra": {
"status": "live",
"evidence": "packages/runtime/src/http-dispatcher.ts"
"verifiedAt": "2026-08-25",
"evidenceScope": "cross-repo",
"evidence": "objectui @a76b18cf2: packages/app-shell/src/hooks/useConsoleActionRuntime.tsx:397-398 merges action.bodyExtra into the request body after resolving page-var tokens, and :474-475 is the form-submit branch merging it into `fields`, with objectui packages/components/src/renderers/action/action-button.tsx:166 forwarding it off the schema (siblings action-icon.tsx:112, action-group.tsx:262, action-menu.tsx:242)",
"note": "The payload key for an inline `type:'api'` action — `params` stays the ActionParam[] definition array (#5777; the object form of `params` is converted to `bodyExtra` by the ADR-0087 entry in packages/spec/src/conversions/registry.ts:5578). 2026-08-25: REPOINTED to the renderer for the same reason as its sibling `bodyShape` — `type:'api'` actions are client-dispatched, so packages/runtime/src/http-dispatcher.ts never read this key."
},
"mode": {
"status": "live",
Expand Down
5 changes: 3 additions & 2 deletions packages/spec/liveness/field.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,8 +77,9 @@
},
"requiredWhen": {
"status": "live",
"evidence": "packages/objectql/src/validation/record-validator.ts",
"note": "CEL."
"verifiedAt": "2026-08-25",
"evidence": "packages/objectql/src/validation/rule-validator.ts:1792-1810 (evaluates the field's requiredWhen predicate on the write path and reports the unevaluable/unbound cases by name); packages/objectql/src/validation/rule-validator.ts:762-763 (fieldRequiresParentRoot — a parent-scoped predicate widens the binding, #4977)",
"note": "CEL. 2026-08-25: REPOINTED — the evidence cited its SIBLING packages/objectql/src/validation/record-validator.ts, which has 0 occurrences of `requiredWhen`: that module enforces the static ADR-0113 `required` contract, while the CEL predicate is evaluated one file over in rule-validator.ts. Both files exist and neither citation carried a line, so this was invisible to every check the gate had — the plausible-neighbour shape of citation rot rather than a code movement."
},
"readonlyWhen": {
"status": "live",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/liveness/object.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -153,8 +153,8 @@
},
"organizationField": {
"status": "live",
"evidence": "packages/plugins/plugin-audit/src/audit-writers.ts",
"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).",
"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"
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/liveness/permission.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -77,8 +77,8 @@
"allowExport": {
"status": "live",
"verifiedAt": "2026-07-30",
"evidence": "packages/rest/src/rest-server.ts (enforceExportPermission — caller-level 403 gate on the bulk-egress route, fail-closed when the security service cannot answer) + packages/plugins/plugin-security/src/security-plugin.ts (canExport → checkObjectPermission('export'), posture-unresolvable → deny) + packages/plugins/plugin-hono-server/src/hono-plugin.ts (annotateEffectiveApiOperations, the /me/permissions projection the frontend renders)",
"note": "#3544 — user-level export axis over read. Re-verified 2026-07-30: enforcement is SERVER-side, not only the projection — the export route calls enforceExportPermission (403), separate from the object-level 405; the annotate path is the display half. Optional/no-default = backward-compatible opt-out (unset inherits read); `false` denies export while keeping read."
"evidence": "packages/rest/src/rest-server.ts (enforceExportPermission — caller-level 403 gate on the bulk-egress route, fail-closed when the security service cannot answer) + packages/plugins/plugin-security/src/security-plugin.ts (canExport → checkObjectPermission('export'), posture-unresolvable → deny) + packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:493-502 (annotateEffectiveApiOperations reads the per-object allowExport bit and the `'*'` wildcard for the /me/permissions projection the frontend renders)",
"note": "#3544 — user-level export axis over read. Re-verified 2026-07-30: enforcement is SERVER-side, not only the projection — the export route calls enforceExportPermission (403), separate from the object-level 405; the annotate path is the display half. Optional/no-default = backward-compatible opt-out (unset inherits read); `false` denies export while keeping read. 2026-08-25: the annotate pointer was REPOINTED — `annotateEffectiveApiOperations` moved out of hono-plugin.ts into current-user-endpoints.ts, the same repos-internal code movement that rotted systemPermissions and tabPermissions. The old citation carried no line, so the #11210 line bound could not see it; the key-mention signal is what found it."
},
"allowTransfer": {
"status": "live",
Expand Down
Loading
Loading