Skip to content

feat(spec): gate the liveness README's state table against GOVERNED (#7257) - #7374

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-7257-liveness-readme-gate
Aug 10, 2026
Merged

feat(spec): gate the liveness README's state table against GOVERNED (#7257)#7374
os-zhuang merged 1 commit into
mainfrom
claude/issue-7257-liveness-readme-gate

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Closes#7257

Option 3 as the spine, option 2 riding, option 1 by measurement only.

Premise check (on merged main, f188ed6)

PR #7255 landed earlier today: it moved the heading to 30 and added a note admitting the table carried 28 of them, deliberately not back-filling. That premise held exactly — before this change the section read ## Current state — 30 governed types (complete registry coverage) over 28 rows, with GOVERNED at 30. The new gate's first run against that tree reproduced it without being told:

✗ 2 governed type(s) with NO row in the README's "Current state" table:
api
capability
✗ 1 README state-table heading error(s):
heading says 30 governed types, the table has 28 row(s)

#7255's admission note is replaced (not merely edited) — the paragraph that said "filed rather than back-filled from a guess" now records that the heading is checked and that the rows were back-filled from measurement, keeping its reasoning about why the Notes cell is the part a gate must not author.

(3) The gate — a fourth direction, wired the existing way

scripts/liveness/readme-table.mts: pure, unit-tested, imported by check-liveness.mts, exactly the shape of orphans.mts / drill.mts / producer.mts. No new check:/gen: script — it rides inside check:liveness, which spec-liveness-check.yml already runs on every PR touching packages/spec/**. check:generated --reconcile-only (the #4203 meta-gate) stays green with no ledger change: 20 check: + 14 gen: scripts, all classified.

It fails, not warns — the population is exact, there is no debt to amortise once the rows are back-filled, and this README's own verdict is that a permanently-noisy check is a check nobody reads. Four findings:

FindingWhy
a GOVERNED type with no row#7257 itself
a row GOVERNED does not backthe mirror — same rot as an orphan ledger row, one level up
the same type in two rowswould let the row count reach GOVERNED.length while a real type has none
N ≠ row count, or NGOVERNED.lengthall three legs, because two of them agreeing is exactly the state this issue found

Unreadable table lines are reported, never skipped — a row the parser cannot see is a row it cannot govern, which is #4956 again. Fenced code blocks are skipped so the regeneration snippet's print(f"| {t} | …") template is not read as a row, and only the "Current state" section is parsed so the Status-vocabulary and proof tables above it don't inflate the count.

(2) The regeneration snippet prints a skeleton

The documented snippet now reads the table back as well as the report, so a governed type with no row prints a marked skeleton instead of silently not being printed next to 29 siblings that are. Both legs run:

$ … --json | python3 -c "…" # on this branch: no skeletons, and
| api | 25 | 0 | 0 | 2 | # the counts match the rows written below
| capability | 12 | 0 | 0 | 0 |
$ … # against a README with the qa row removed:
| qa | 4 | 0 | 5 | 0 | **NO ROW YET (#7257) — write this Notes cell from the seeding PR measurement, never from a guess** |

Both were needed: the count columns get regenerated far more often than the row set gets audited. The skeleton stops at the count columns on purpose — it prints a marker where the Notes cell goes, never a guess at what belongs there.

(1) The two rows — counts from --json, Notes from the landing evidence

Counts are the gate's own report, run fresh (the #4488 method). Every clause of both Notes cells is quoted or paraphrased from a source below; nothing is inferred from the counts or the type name.

api — 25 / 0 / 0 / 2. Sources: liveness/api.json's own _note; issue #5206 (the defect); issue #5271 and its PM acceptance comment for PR #5312; the two transform entries' notes.

#5271 acceptance: "liveness 走路线 1 —— GOVERNED + 播种 liveness/api.json,27 键分类(live 25 / planned 2 / dead 0),每键 file:line 按消费层引证 … transformplanned 的理由成立(已被响亮拒绝的键不是 enforce-or-remove 候选)"

25 + 2 = 27 — the seeding measurement and today's --json agree exactly. #5206 measured the pre-fix state: api in neither DEFAULT_METADATA_TYPE_REGISTRY nor BUILTIN_METADATA_TYPE_SCHEMAS, so resolveOverlaySchema('api', …)getMetadataTypeSchema('api') returned undefined and saveMetaItem took its documented "unregistered type → stored unvalidated" branch, while getMetaTypes() could not enumerate it. The planned verdict is quoted from the ledger, not restated:

inputMapping.transform: "PLANNED, deliberately not dead, and the difference is the point. dead in this ledger means 'parsed, no consumer' — a silent no-op. transform is the opposite: it is parsed and then LOUDLY REFUSED, at publish (endpoint-publish-gate.ts mappingGate) and again at runtime (:259), because there is no transformation-function registry anywhere in the platform."

capability — 12 / 0 / 0 / 0. Sources: liveness/capability.json's _note; issue #5961; PR #6540 (merged 2026-08-08, Fixes #5961).

PR #6540: "新建 liveness/capability.json(5 个可授权属性逐条 call-graph 闭合到真实读者,全 live,未走 PENDING_GOVERNANCE 记债)"

The gate's 12 and the PR's 5 are the same measurement at two granularities — 5 authorable properties plus the 7 ADR-0010 envelope keys the gate auto-classifies live (they carry null verdicts in the file, as on permission/position). The row says so, because the README's own caveat is that counts are at the gate's one-level walk granularity. PR #6540's reverse verification is recorded because it measured something sharper than it predicted:

"逆向两肢各自重建 dist 后跑:肢一删注册条目 7 红 3 绿(且发现比预判更锐的事实:垃圾载荷转 422 而非 resolve——schema 绑定是条目之后真实的第二道防线);肢二删 schema 绑定恰 3 红。"

Both types turn out to be the same worked example, and PR #6540 says so in those words ("enforced but undeclared,#5271 关掉那个洞的镜像") — the mirror of this ledger's usual declared ≠ enforced. Nothing in either cell is prose I supplied about somebody else's measurement.

Also fixed: a stray blank line between the view and report rows that had been splitting the table into two rendered tables.

Reverse verification — predicted first, and pinned in CI

Predicted before running: on the pre-backfill tree the gate exits 1 naming exactly api and capability plus one heading error (30 vs 28 rows), with no heading-vs-GOVERNED error (30 === 30), no orphan rows and no malformed lines. That is verbatim what it printed (above) — the two-of-three-legs-agreeing signature the issue describes.

Rather than perform it once, three cases in check-liveness.test.ts drive the real gate (--ledger-root= at a copy of the real ledgers, which cpSync carries README.md into) and read its exit code:

Mutation in the copyResult
delete the qa rowexit 1, names qa, and asserts the "never from a guess" prescription survives with the check
heading → 99 governed typesexit 1, both heading legs reported
add a notatype rowexit 1, orphan direction
verbatim copy (the control)exit 0 — without it every "exit 1" above is also satisfied by an unreadable copy

Gates

GateResult
check:livenessexit 0 — success line now ends "…and the README state table carries a row for each of the 30 governed type(s) it claims to index"
packages/spec vitest363 files / 9491 tests passed
readme-table.test.ts + check-liveness.test.ts31 passed (16 new)
check:scripts-typecheckexit 0
check:generated --reconcile-only (#4203)exit 0, ledger unchanged
check:empty-state, check:variant-docs, check:strictness-ledgerexit 0
scripts/check-nul-bytes.mjsOK
CI on c3a3aceall 26 check-runs green (2 path-filtered skips: Console Pin Gate, Build Docs)

Heavy runs under flock /tmp/os-heavy-verify.lock.

Re-checked against main at 3c03725 (3 commits ahead of the branch point): merges clean, and check:liveness is green on the merged tree — verified in a throwaway worktree so this branch's green CI stays untouched. One incoming commit edits liveness/action.json, which changes no row of the table's row set and no GOVERNED entry.

Scope notes

…7257)
The ledger README's "Current state" table is the liveness ledger's own index —
one row per governed type, counts regenerated from the gate's --json report, a
hand-written Notes cell per row. Its heading, `## Current state — N governed
types (complete registry coverage)`, is a completeness CLAIM that nothing could
falsify: N was the count of ROWS, not of governed types, and the two agreed only
by coincidence. They stopped. `api` and `capability` were both in GOVERNED, both
had ledgers, both were counted by the gate, and neither had a row.
Same shape the file spends 500 lines warning about, one level up: `dashboard.
widgets` asserted in prose that its 22 child keys were classified in a subtree
that never existed, and survived a release because prose cannot fail a build
(#4956). Every other claim in the file became data the gate resolves; the index
was the last one riding on a human reading it.
- readme-table.mts — the fourth direction, pure and unit-tested for the same
reason as orphans.mts: on a green tree the table is complete, so a passing gate
proves nothing about whether the check can fire. Fails (not warns) on a missing
row, an orphan row, a duplicated row, or a heading N that disagrees with either
the row count or GOVERNED.length. All three heading legs, because two of them
agreeing is exactly the state this issue found.
- No new check:/gen: script — it rides inside check:liveness, which the Spec
property liveness workflow already runs on packages/spec/**. check:generated
--reconcile-only stays green with no ledger change.
- The documented regeneration snippet now reads the table back as well as the
report, so a governed type with no row prints a SKELETON row: the omission is
visible at regeneration time as well as at CI time.
- The two missing rows are back-filled from their seeding PRs' own measurements —
counts from --json, prose from what #5271/PR #5312 and #5961/PR #6540 actually
measured. The Notes cell is the one thing the gate deliberately does not check,
which is why they were filed rather than fixed on the spot.
- Also removes a stray blank line that had been splitting the table in two.
Reverse verification is pinned in CI, not just performed once: three cases in
check-liveness.test.ts drive the REAL gate against a copy of the real README with
a row deleted, the heading skewed, and an ungoverned row added, and read exit 1.
Closes#7257
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 10, 2026 8:10am

Request Review

@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 @objectstack/spec)
  • content/docs/automation/connectors.mdx(via @objectstack/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 @objectstack/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/tenancy-modes.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 @objectstack/spec)
  • content/docs/kernel/index.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx(via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx(via @objectstack/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 @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx(via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx(via @objectstack/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/permissions/system-context.mdx(via packages/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/http-protocol.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/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/ui/actions.mdx(via @objectstack/spec)
  • content/docs/ui/apps.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/field-grouping-and-order.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)

7 release-owned page(s) also reference the affected code. These are read-only:

  • 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/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 10, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 10, 2026 08:58
@os-zhuang
os-zhuang added this pull request to the merge queueAug 10, 2026
Merged via the queue into main with commit fe2e15aAug 10, 2026
28 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-7257-liveness-readme-gate branch August 10, 2026 09:14
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec(liveness): the README state table has no rows for api / capability — the table silently fell behind its own registry

2 participants

@os-zhuang@claude