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:

Route Key in the walked shape? Its row What happened if you got it wrong
retiredKey() tombstone yes (z.never() is a property) must stay deleting it → UNCLASSIFIED, fails CI (14 at once in the #3896 sweep)
strict removal no must go leaving 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

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 3:52am

Request Review

@github-actions github-actions Bot 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-sys force-pushed the claude/sharing-rules-schema-bypass-h4c7xr branch from caed06d to ff08dfd Compare July 31, 2026 03:52
@os-zhuang
os-zhuang merged commit c9c98c1 into main Jul 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 free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants