Skip to content

fix(spec): the liveness gate stops trusting the direction it never checked - #4094

Merged
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr
Jul 31, 2026
Merged

fix(spec): the liveness gate stops trusting the direction it never checked#4094
os-zhuang merged 1 commit into
mainfrom
claude/sharing-rules-schema-bypass-h4c7xr

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #4080, which documented this hole. Closing it turned out to be ~130 lines.

The hole

The liveness gate walks the schema and looks each property up in the ledger. That direction is well defended — a property with no row is UNCLASSIFIED and fails CI (the ratchet). The reverse had no check at all, and the two removal routes make that gap load-bearing:

RouteKey in the walked shape?Its rowWhat happened if you got it wrong
retiredKey() tombstoneyes (z.never() is a property)must staydeleting it → UNCLASSIFIED, fails CI (14 at once in the #3896 sweep)
strict removalnomust goleaving it → nothing asked, ever

The forward pass stops enquiring about a key it can no longer see, so a stale dead/live claim about a property that does not exist rots in place — still read by anyone treating the ledger as the capability catalogue it doubles as.

Not hypothetical: the report aria/performance rows outlived their schema keys by a full release and were deleted by hand in the #3896 sweep, noticed only because someone happened to be reading the file. One direction failed loudly; the other never failed at all — the same silent-no-op shape this whole ledger exists to catch, one layer up.

The fix, and why it fails rather than warns

scripts/liveness/orphans.mts — a pure scan run off the same walk the forward pass classifies against, so the two directions can never disagree about what the schema contains.

It's a hard failure. The tree is orphan-free across all sixteen governed types, so there's no debt to amortise and nothing to soften; a warning would have re-created the original defect one layer up — the ledger README's own verdict is that a permanently-noisy check is a check nobody reads, the same way a stale row is a claim nobody re-tests.

The error text names both causes, because which one applies is the author's judgement:

  1. delete the row (the strict-removal case), or
  2. if the property really is still authorable, fix the walk — a property the walk cannot see is a property the ratchet cannot govern (e.g. one living on a union member shapeOf skips).

It also restates the asymmetry inline, so the tempting wrong fix — deleting a tombstoned key's row — is warned against at the point of failure rather than in a doc.

Deliberately silent in one case: a row declaring children on a non-container. The forward pass already reports that as UNCLASSIFIED with a more precise message, and two headings for one fix reads as two problems. Confirmed against tool.parameters (a z.record, undrillable): the forward pass claimed it, the orphan scan stayed quiet.

How it's proven, given the tree is clean

A green gate proves nothing about whether the scan can fire, so:

  • orphans.test.ts (9 cases) — both orphan levels, the tombstone that must NOT flag, the deferred non-container, entry fields (status/evidence/verifiedAt/authorWarn) that are not child props, and absent/empty props. The logic module + colocated test matches the evidence.mts / verification.mts / empty-state.mts pattern already in that directory.
  • End-to-end injection — a real orphan added to a real ledger, twice: tool/category (top level) and view/list.ghostListKey (drilled child). Each failed the gate with exit 1 and printed the full prescription. Ledgers restored and verified byte-identical (git diff on liveness/*.json empty).

Docs corrected

Both the skill (#4080) and the ledger README carried the now-false claim that this direction never fails. Both updated to describe the orphan leg as the fix, with the asymmetry table intact.

Verification

  • All ten spec gates pass. check:api-surface failed first on a dist predating 2af1988 — the stale-dist false alarm the skill documents; rebuilt, then green. Reporting it because the trap is the point.
  • Liveness script suite 98/98; spec tsc clean.
  • Gate-only change under packages/spec/scripts/, which is not in the package's published files — empty changeset.

Generated by Claude Code

@vercel

vercelBot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredJul 31, 2026 3:52am

Request Review

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx(via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx(via @objectstack/spec)
  • content/docs/ai/skills.mdx(via @objectstack/spec)
  • content/docs/api/client-sdk.mdx(via @objectstack/spec)
  • content/docs/api/environment-routing.mdx(via @objectstack/spec)
  • content/docs/api/error-catalog.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx(via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx(via @objectstack/spec)
  • content/docs/api/index.mdx(via @objectstack/spec)
  • content/docs/automation/approvals.mdx(via packages/spec)
  • content/docs/automation/flows.mdx(via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx(via packages/spec)
  • content/docs/automation/hooks.mdx(via @objectstack/spec)
  • content/docs/automation/index.mdx(via @objectstack/spec)
  • content/docs/automation/webhooks.mdx(via @objectstack/spec)
  • content/docs/automation/workflows.mdx(via @objectstack/spec)
  • content/docs/concepts/architecture.mdx(via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx(via packages/spec)
  • content/docs/concepts/index.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx(via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx(via packages/spec)
  • content/docs/concepts/north-star.mdx(via packages/spec)
  • content/docs/data-modeling/analytics.mdx(via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx(via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx(via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx(via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx(via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx(via @objectstack/spec)
  • content/docs/data-modeling/index.mdx(via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx(via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx(via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx(via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx(via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx(via @objectstack/spec)
  • content/docs/deployment/cli.mdx(via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx(via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx(via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx(via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx(via @objectstack/spec)
  • content/docs/getting-started/examples.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx(via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx(via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx(via @objectstack/spec)
  • content/docs/kernel/cluster.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx(via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx(via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx(via packages/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via packages/spec)
  • content/docs/kernel/services-checklist.mdx(via @objectstack/spec)
  • content/docs/kernel/services.mdx(via @objectstack/spec)
  • content/docs/permissions/authorization.mdx(via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx(via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx(via @objectstack/spec)
  • content/docs/permissions/positions.mdx(via @objectstack/spec)
  • content/docs/permissions/rls.mdx(via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx(via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx(via @objectstack/spec)
  • content/docs/plugins/development.mdx(via @objectstack/spec)
  • content/docs/plugins/index.mdx(via @objectstack/spec)
  • content/docs/plugins/packages.mdx(via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx(via @objectstack/spec)
  • content/docs/protocol/diagram.mdx(via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx(via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx(via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx(via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx(via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx(via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx(via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx(via @objectstack/spec)
  • content/docs/releases/index.mdx(via @objectstack/spec)
  • content/docs/releases/v12.mdx(via @objectstack/spec)
  • content/docs/releases/v13.mdx(via @objectstack/spec)
  • content/docs/releases/v16.mdx(via @objectstack/spec)
  • content/docs/releases/v17.mdx(via @objectstack/spec)
  • content/docs/releases/v9.mdx(via @objectstack/spec)
  • content/docs/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx(via @objectstack/spec)
  • content/docs/ui/dashboards.mdx(via @objectstack/spec)
  • content/docs/ui/forms.mdx(via @objectstack/spec)
  • content/docs/ui/index.mdx(via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx(via @objectstack/spec)
  • content/docs/ui/setup-app.mdx(via @objectstack/spec)
  • content/docs/ui/translations.mdx(via @objectstack/spec)
  • content/docs/ui/views.mdx(via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 03:47
…ecked
The gate walked the schema and looked each property up in the ledger. That
direction was well defended — a property with no row is UNCLASSIFIED and fails
CI. The reverse had no check at all, and the two removal routes make that gap
load-bearing:
- a `retiredKey()` tombstone leaves `z.never()` in the walked shape, so the key
is still a property and its row must STAY (deleting it fails as UNCLASSIFIED —
14 at once in the #3896 close-out sweep);
- a strict removal takes the key OUT of the shape, so its row must GO. Nothing
asked. The forward pass stops enquiring about a key it can no longer see, and
a stale `dead`/`live` claim about a nonexistent property rots in place — still
read by anyone treating the ledger as the capability catalogue it doubles as.
Not hypothetical: the report `aria`/`performance` rows outlived their keys by a
full release and were deleted by hand in the #3896 sweep, noticed only because
someone happened to be reading the file. One direction failed loudly; the other
never failed at all — the same silent-no-op shape this whole ledger exists to
catch, one layer up.
`scripts/liveness/orphans.mts` closes it, as a FAILURE rather than a warning.
The tree is orphan-free across all sixteen governed types, so there is no debt
to amortise and nothing to soften: a warning here would have re-created the
original defect (the README's own verdict is that a permanently-noisy check is a
check nobody reads, the same way a stale row is a claim nobody re-tests). The
error text names BOTH causes, because which one applies is the author's
judgement: delete the row, or — if the property really is still authorable — fix
the WALK, since a property the walk cannot see is one the ratchet cannot govern.
Deliberately silent in one case: a row declaring `children` on a non-container.
The forward pass already reports that as UNCLASSIFIED with a more precise
message, and two headings for one fix reads as two problems. Confirmed against
`tool.parameters` (a `z.record`, undrillable) — the forward pass claimed it and
the orphan scan stayed quiet.
Because the tree is clean, a green gate proves nothing about whether the scan
can fire, so the proof lives in `orphans.test.ts` (9 cases: both orphan levels,
the tombstone that must NOT flag, the deferred non-container, entry fields that
are not child props, absent/empty props). Both legs were also driven end-to-end
by injecting a real orphan into a real ledger: `tool/category` (top level) and
`view/list.ghostListKey` (drilled child) each failed the gate with exit 1 and
the full prescription; the ledgers were restored and verified byte-identical.
The skill and the ledger README carried the now-false claim that this direction
never fails; both corrected, with the orphan leg documented as the fix. Gate-only
change under `packages/spec/scripts/`, which is not published — empty changeset.
Verification: all ten spec gates pass (`check:api-surface` needed a rebuild
first — the documented stale-dist false alarm, on a dist predating 2af1988);
liveness script suite 98/98; spec `tsc` clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX
@xuyushun441-sys
xuyushun441-sysforce-pushed the claude/sharing-rules-schema-bypass-h4c7xr branch from caed06d to ff08dfdCompareJuly 31, 2026 03:52
@os-zhuang
os-zhuang merged commit c9c98c1 into mainJul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/sharing-rules-schema-bypass-h4c7xr branch July 31, 2026 04:06
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-zhuang@claude