feat(spec)!: retire the last three deprecated authorable aliases (#3855) - #3883
Merged
Conversation
Protocol 17 removes the three keys a schema transform used to fold into a
canonical slot and drop from the parsed output. Every slot now has exactly one
spelling.
action.execute -> action.target
field.conditionalRequired -> field.requiredWhen
agent.knowledge.topics -> agent.knowledge.sources
All three are pure key renames with unchanged values, and no runtime behaviour
changes: each was already lowered at parse time and erased before any consumer
saw it. What shrinks is the authorable surface.
CHANGE DOCUMENTATION IS THE PRIMARY CHANNEL, not the error. Three registry
entries carry the removal:
- Three D2 conversions (`toMajor: 17`, `retiredFromLoadPath: true` from the
day they land — 17 gives the aliases no acceptance window, deliberately).
They put the rename into `spec-changes.json` (ADR-0087 D4), which composes
across majors, so a consumer jumping several at once gets ONE answer rather
than N changelogs. The generated upgrade guide and the `spec_changes` MCP
tool are projections of that record.
- A step-17 migration chain entry referencing them, so
`os migrate meta --from <N>` rewrites the consumer's source mechanically.
Verified end to end: all three keys rename in one pass.
Same shape as `object-compactLayout-to-highlightFields`, which is already a
retired-from-load-path entry paired with a schema tombstone.
WHY THE KEYS REJECT RATHER THAN VANISH. None of the three schemas is
`.strict()`, so deleting a key makes Zod silently STRIP it — the metadata parses
clean and the setting never takes effect: a script action bound to nothing, a
field that is never required, an agent recruiting no RAG context. `FieldSchema`
already carries a comment about the last time that happened (`dataQuality` /
`cached`, #3726 / #3733). Each key is therefore tombstoned via a new
`retiredKey()` helper: declared as `never`, so writing it is a tsc error at the
authoring site AND a parse error carrying the rename and the `migrate meta`
pointer. It also renders as a `[REMOVED]` row in the generated reference docs.
`lintDeprecatedAliases` retires with its subject. It warned when an author
declared both an alias and its canonical key, because the parse resolved that
silently; the parse now rejects, which is strictly louder. Its `defineStack` and
CLI wiring goes with it — the conversion-notice wiring from #3872 stays.
`lowerCallables` stops binding a function on `execute`. It runs before the
parse, so binding it there would have kept the removed alias quietly working for
inline-function authoring while every other style rejected it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The docs-drift check surfaced seven places still presenting the aliases as
usable — four of them in `skills/`, which is what an AI reads to author
metadata AND what ships to third parties via `npx skills add`. Leaving them
would publish a corpus that generates metadata protocol 17 rejects: the
"declared ≠ enforced" trap, pointed the other way.
skills/objectstack-ui — "conditionalRequired is only a back-compat alias"
skills/objectstack-formula — "treat conditionalRequired as a read alias", plus
a CEL-surface table row for it
skills/objectstack-data — "a deprecated compatibility alias"
skills/objectstack-ai — "topics is a deprecated alias"
data-modeling/validation-rules.mdx, field-types.mdx — table rows describing the
alias as lowered at parse time
All now say REMOVED / parse error, or are dropped where they were only a row in
a table of live keys. Historical release notes are left alone — they correctly
describe what those versions did.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
…on ledger (#3855) The dogfood expression-conformance ledger (ADR-0058 D7) still listed `data/field.zod.ts:conditionalRequired` as a CEL-bearing surface. With the key removed the ledger entry went stale and its own ratchet caught it: STALE covers — surface no longer in source: data/field.zod.ts:conditionalRequired Exactly what that ledger is for — the removal has to be acknowledged in every place that claimed the surface exists, not just in the schema. Also clarifies `rule-validator.ts`. Its `requiredWhen ?? conditionalRequired` read is NOT removed and NOT dead: the validator is handed RAW, unparsed field definitions, which includes metadata STORED under protocol <= 16. Dropping the read would silently stop enforcing those rules on existing deployments — a runtime regression this spec change does not otherwise cause, and precisely the kind of silent loss the whole line of work exists to prevent. The comment now says the key is removed from the spec (so nobody reads it as still authorable) and that the fallback retires when stored metadata is migrated, not when the spec key went away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JqC9ckaLbmhmytt65Gxi96
os-zhuang
marked this pull request as ready for review
July 28, 2026 14:42
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
…itness watched (#3855) (#3895) 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 watch 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`. Verified concretely: #3883 narrowed `defineAction`'s input by three keys and the signature snapshot did not move. And #3733 was the ACCIDENTAL version — `dataQuality` / `cached` outlived their keys, stayed discoverable in the reference docs, and were silently stripped on parse. ADR-0059 §5 deferred a deeper gate "until a narrowing actually slips both". It has, twice. `content/docs/protocol/backward-compatibility.mdx` had meanwhile been promising third parties the exact rule nothing enforced: "removing or renaming properties is a MAJOR change". `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 for free because a tombstoned key (`retiredKey()`, #3883) 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 — the whole point being that a gate can look present and be dead: 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. Audible to whoever hits it, but the change documentation is still empty. Requires a D2 conversion / D3 chain entry naming the surface — mechanizing what #3883 did by hand with nothing to enforce it. 3. An addition is left uncommitted (`--check`). An unrecorded key is invisible to the ratchet forever after, since it can only detect the disappearance of something it once saw. Failures exit before the snapshot write, so a red run never clobbers the baseline. Runs in `check:docs` (unconditional, required — the job whose own comment says the gate 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. Value-level narrowing (an enum losing a member) stays ungated per ADR-0059 §5's evidence gate; noise kills a ratchet faster than a gap does. What is fixed 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3855. Breaking — needs the protocol-17 major. Builds on #3872.
What goes
action.executeaction.targetfield.conditionalRequiredfield.requiredWhenagent.knowledge.topicsagent.knowledge.sourcesAll three are pure key renames. No runtime behaviour changes: each alias was already lowered into its canonical key at parse time and erased before any consumer saw it (#3713/#3742, #3754/#3764, #1878/#1891). What shrinks is the authorable surface — every slot now has exactly one spelling.
The change documentation is the primary channel, not the error
Three registry entries carry the removal, and between them they answer "what changed" and "fix it for me" without the consumer having to hit an error first:
toMajor: 17,retiredFromLoadPath: truefrom the day they land — 17 gives the aliases no acceptance window, deliberately). They put the renames intospec-changes.json(ADR-0087 D4). Per-major manifests compose, so a consumer jumping several majors at once gets one answer rather than N changelogs to reconcile — and the generated upgrade guide and thespec_changesMCP tool are both projections of that record.os migrate meta --from <N>rewrites the consumer's source mechanically.Verified end to end — all three rename in a single pass:
This is the same shape as
object-compactLayout-to-highlightFields, already in the registry as a retired-from-load-path entry paired with a schema tombstone — so the pattern is precedent, not invention.Why the keys reject rather than vanish
None of the three schemas is
.strict(). Deleting a key outright makes Zod silently strip it: the metadata parses clean and the setting simply never takes effect — a script action bound to nothing, a field that is never required, an agent recruiting no RAG context.FieldSchemaalready carries a comment about the last time exactly this happened (dataQuality/cachedoutlived their keys by a release, #3726 / #3733, the ADR-0104 class).A silent strip would also mean the change documentation above is never reached by someone who didn't read it in advance. So each key is tombstoned via a new
retiredKey()helper — declared asnever, which makes writing it:tscerror at the authoring site, before anything runs;migrate metapointer;[REMOVED]row in the generated reference docs, with the same prescription:UNKNOWN_KEY_GUIDANCEcould not do this job: it lives onObjectSchema.create()and governs object top-level keys only, so two of the three aliases had no mechanism at all.Two things retire alongside
lintDeprecatedAliases(#3838 / #3854) and its wiring. It warned when an author declared both an alias and its canonical key, because the parse resolved that conflict silently. The parse now rejects, which is strictly louder — the rule has no subject left. Removing it is what takes 6 exports off the api-surface (the freeze-contract trigger; snapshot regenerated in this PR per ADR-0059 §4). The conversion-notice wiring from #3872 stays — that is a different mechanism and it is what these conversions report through.lowerCallablesstops binding a function onexecute. It runs before the parse, so binding it there would have kept the removed alias quietly working for inline-function authoring while every other style rejected it — a dialect split, which is the thing the removal exists to end.Release-owner note
Per ADR-0059 §4 this is by definition breaking, and worth stating plainly: neither automated witness stops it. The api-surface snapshot records export names, not schema keys, and the frozen downstream-contract fixture does not exercise these three aliases (verified). ADR-0059 §5 acknowledges the snapshot "catches add/remove/rename but not every narrowing". The changeset text is therefore the real protection, which is why it carries the FROM → TO table, a diff, and the
migrate metainstruction.PROTOCOL_VERSIONis deliberately not touched —protocol-version.test.tslocks it to the package major, so the release bumps it, not this PR. Themajorchangeset is what drives that.Testing
@objectstack/spec6810 passed ·@objectstack/cli735 passed · all example apps typecheck and pass · frozen downstream-contract typecheck passesos migrate meta, that it is refused even when the canonical key is present, and that the canonical key still works. Plus the real authoring path (a field nested in an object), not just a bareFieldSchema.parse().conversions.test.ts(which runs withincludeRetired: true), and the full chain replays undermigrations.test.ts.lint,check:nul-bytes,check:doc-authoring,check:role-word,check:org-identifier,check:authz-resolver,check:release-notes, spectsc --noEmit,check:docs,check:skill-refs,check:react-blocks,check:api-surface,check:liveness, examples typecheck, downstream-contract typecheck.Generated by Claude Code