Skip to content

fix(spec): ratchet the authorable key surface — the one contract no witness watched (#3855) - #3895

Merged
os-zhuang merged 1 commit into
mainfrom
claude/action-target-alias-discard-ahpgl5
Jul 28, 2026
Merged

fix(spec): ratchet the authorable key surface — the one contract no witness watched (#3855)#3895
os-zhuang merged 1 commit into
mainfrom
claude/action-target-alias-discard-ahpgl5

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Follow-up to #3883. Non-breaking — a new gate plus a corrected docblock.

The gap

For a metadata-driven platform the third-party API is what an author may write: the keys inside each schema. Nothing guarded them.

Witness Records Sees a removed key?
api-surface.json exported name (kind) ❌ a key inside a schema is not an export
api-surface-signatures.json hash of each defineX factory's type ❌ hashes checker.typeToString(), which prints a type reference (z.input<typeof ActionSchema>) and never expands it structurally
spec-changes.json removed[] diff of api-surface.json ❌ inherits the same blind spot
downstream-contract fixture frozen third-party consumer ❌ only what it happens to exercise
json-schema.manifest.json whole schemas AIKnowledge still exists after topics goes

Verified concretely: #3883 narrowed defineAction's input by three keys and api-surface-signatures.json did not change — its last commit is still the unrelated #3735.

And this is not only a "deliberate change slipped a gate" story. #3733 was the accidental version: dataQuality / cached outlived their keys, stayed discoverable on the API surface and in the reference docs, and an author writing them got a clean parse with the key silently stripped.

ADR-0059 §5 named the trigger:

A signature-level snapshot is deferred until a narrowing actually slips both (evidence-gated, like ADR-0054's Phase 3).

It has now, twice.

The gate

authorable-surface.json — a committed ratchet of all 8588 authorable keys, derived from the walk that already emits the JSON Schemas. One level deeper, no new introspection, one pass.

It distinguishes three states for free, because a tombstoned key (retiredKey(), from #3883) is z.never(), which Zod renders as { "not": {} }:

ai/AIKnowledge:topics [RETIRED]
data/Field:conditionalRequired [RETIRED]
ui/Action:execute [RETIRED]
ui/Action:body
ui/Action:target
…
State Meaning
live a normal property — the author may write it
[RETIRED] present but unwritable, carrying its own upgrade prescription
absent gone from the contract with nothing left to say

Three failure modes, each proven non-vacuous

I simulated all three rather than assuming they fire — the whole point of this PR is that a gate can look present and be dead.

1. A key vanishes with no tombstone — the silent-strip class.

❌ 1 authorable key(s) disappeared from the contract:
     - ui/Action:phantomLegacyKey

   These schemas are NOT .strict(), so Zod silently STRIPS an unknown key: an author
   who keeps writing one gets a clean parse and a setting that never takes effect —
   no error, nothing to grep, nothing pointing at the changelog (#3733, ADR-0104).

…followed by the full retirement protocol (tombstone → D2/D3 entry → major changeset).

2. A key is tombstoned with no registered migration — audible to whoever hits it, but the change documentation is still empty.

❌ 1 key(s) were tombstoned with no registered migration:
     - ai/AIKnowledge:indexes

   The tombstone makes the removal audible to whoever hits it, but the change
   documentation is the primary channel and it is still empty: spec-changes.json
   (ADR-0087 D4) is a projection of the conversion + migration registries…

This is the rule that mechanizes what I did by hand in #3883 — where nothing would have stopped me skipping it.

3. An addition is left uncommitted (--check mode).

❌ authorable-surface.json is out of date (1 key(s) not recorded).
     + ui/Action:aria

An unrecorded key is invisible to the ratchet forever after, since it can only detect the disappearance of something it once saw.

Failures process.exit(1) before the snapshot write, so a red run never clobbers the baseline.

Where it runs

  • check:docs (gen:schema && build-docs --check) already runs in lint.yml's typecheck job, which has no paths filter and is a required check — the job whose own comment says "the gate cannot go dormant". Removal detection lands there for free.
  • check:authorable-surface as an explicit step in Check Generated Artifacts, with the generated paths filter widened to packages/spec/src/** + the script + the snapshot — per that filter's own rule that "a path missing here makes that gate dormant on exactly the PRs that can break it, so keep the two in lockstep".

Scope, deliberately

Value-level narrowing (an enum losing a member, a union narrowing) stays ungated, per ADR-0059 §5's evidence gate — no such narrowing has slipped yet, and snapshotting it would churn on every internal tweak, which is exactly why the signature hash was scoped to factories in the first place. Noise kills a ratchet faster than a gap does.

What I did fix is the over-promise: build-api-surface.ts advertised its signature snapshot as answering "did the accepted authoring shape narrow?". It does not, and believing it did is what let this gap sit. The docblock now states the scope and points at the new ratchet.

Testing

  • @objectstack/spec 6818 passed · spec tsc --noEmit clean
  • Every ESLint-job and typecheck-job gate green locally, including the two the new snapshot could disturb: check:docs, check:api-surface, plus check:spec-changes / check:upgrade-guide / check:liveness / check:skill-refs / check:react-blocks
  • The three simulations above, each reverted afterwards; the committed snapshot is byte-identical to a clean regeneration

Generated by Claude Code

…itness watched (#3855)

For a metadata-driven platform the third-party API is WHAT AN AUTHOR MAY WRITE:
the keys inside each schema. Nothing guarded them.

Both existing witnesses look at the TypeScript surface instead. `api-surface.json`
records exported `name (kind)`, and a key inside a schema is not an export.
`api-surface-signatures.json` hashes each `defineX` factory's type — but via
`checker.typeToString()`, which prints a type REFERENCE (`z.input<typeof
ActionSchema>`) and never expands it, so member-level narrowing cannot reach the
hash. `spec-changes.json` inherits the blind spot: its added/removed arrays are a
diff of `api-surface.json`.

So #3883 removed three authorable keys with every witness green, and #3733 did
the same BY ACCIDENT — `dataQuality` / `cached` outlived their keys and were
silently stripped. ADR-0059 §5 deferred a deeper gate "until a narrowing actually
slips both". It has, twice.

`authorable-surface.json` ratchets all 8588 authorable keys, derived from the
walk that already emits the JSON Schemas — one level deeper, no new
introspection. Three states, distinguishable because a tombstoned key
(`retiredKey()`) is `z.never()`, which Zod renders as `{ "not": {} }`: live,
[RETIRED], absent.

Three failure modes now fail the build, each verified non-vacuous by simulation
rather than assumed:

  1. A key vanishes with no tombstone. These schemas are not `.strict()`, so Zod
     silently strips an unknown key — clean parse, setting never takes effect.
     The error spells out the retirement protocol.
  2. A key is tombstoned with no registered migration. The tombstone is audible
     to whoever hits it, but the change documentation is still empty. Requires a
     D2 conversion / D3 chain entry naming the surface.
  3. An addition is left uncommitted (`--check`). An unrecorded key is invisible
     to the ratchet forever after — it can only detect the disappearance of
     something it once saw.

Runs in `check:docs` (unconditional, required — cannot go dormant) and as an
explicit step in Check Generated Artifacts, with the paths filter widened in
lockstep per that filter's own stated rule.

Also corrects the `build-api-surface.ts` docblock, which advertised the signature
snapshot as answering "did the accepted authoring shape narrow?" — it does not,
and believing it did is what let this gap sit. Value-level narrowing (an enum
losing a member) stays ungated per ADR-0059 §5's evidence gate, and the docblock
now says so.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
@vercel

vercel Bot commented Jul 28, 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 28, 2026 3:16pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling size/xl labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

104 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/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/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.

Copy link
Copy Markdown
Contributor Author

One more piece of evidence, surfaced by the docs-drift check on this PR.

content/docs/protocol/backward-compatibility.mdx has been making this exact promise to third parties all along:

Zod schemas are part of the public API surface. Adding optional properties is a MINOR change; removing or renaming properties is a MAJOR change.

That is precisely the rule this ratchet enforces — and until now nothing backed it. The guarantee was published, the enforcement was absent, and the two gates that looked like they covered it were watching the TypeScript export surface instead.

So this is the same declared ≠ enforced pattern the whole #3743#3855 line has been closing, except this instance was written into a consumer-facing compatibility guarantee rather than into metadata. The doc needs no edit — it is more true after this PR than before it.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 15:32
@os-zhuang
os-zhuang merged commit 8f9689f into main Jul 28, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/action-target-alias-discard-ahpgl5 branch July 28, 2026 15:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants