Skip to content

feat(spec)!: retire the last three deprecated authorable aliases (#3855) - #3883

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

feat(spec)!: retire the last three deprecated authorable aliases (#3855)#3883
os-zhuang merged 3 commits into
mainfrom
claude/action-target-alias-discard-ahpgl5

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3855. Breaking — needs the protocol-17 major. Builds on #3872.

What goes

Removed Use instead Value shape
action.execute action.target unchanged — a handler / flow / URL ref
field.conditionalRequired field.requiredWhen unchanged — a CEL predicate
agent.knowledge.topics agent.knowledge.sources unchanged — a list of source tags

All 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:

  • Three D2 conversions (toMajor: 17, retiredFromLoadPath: true from the day they land — 17 gives the aliases no acceptance window, deliberately). They put the renames into spec-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 the spec_changes MCP tool are both projections of that record.
  • A step-17 migration chain entry referencing them by id, so os migrate meta --from <N> rewrites the consumer's source mechanically.

Verified end to end — all three rename in a single pass:

step 17 conversionIds: action-execute-to-target, field-conditionalRequired-to-requiredWhen, agent-knowledge-topics-to-sources
--- after migrate ---
action : {"name":"convert","label":"Convert","type":"script","target":"convertHandler"}
field  : {"type":"date","requiredWhen":"record.stage == \"closed\""}
agent  : {"indexes":["docs"],"sources":["faq"]}

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. FieldSchema already carries a comment about the last time exactly this happened (dataQuality / cached outlived 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 as never, which makes writing it:

  1. a tsc error at the authoring site, before anything runs;
  2. a parse error carrying the rename and the migrate meta pointer;
  3. a [REMOVED] row in the generated reference docs, with the same prescription:
| **conditionalRequired** | `any` | optional | [REMOVED] `conditionalRequired` was removed in
@objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate)
is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. |

UNKNOWN_KEY_GUIDANCE could not do this job: it lives on ObjectSchema.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.

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 — 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 meta instruction.

PROTOCOL_VERSION is deliberately not touched — protocol-version.test.ts locks it to the package major, so the release bumps it, not this PR. The major changeset is what drives that.

Testing

  • @objectstack/spec 6810 passed · @objectstack/cli 735 passed · all example apps typecheck and pass · frozen downstream-contract typecheck passes
  • Alias tests rewritten rather than deleted — they now assert the rejection: that the key is refused instead of stripped, that the message names the replacement and os 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 bare FieldSchema.parse().
  • Conversion fixtures round-trip under conversions.test.ts (which runs with includeRetired: true), and the full chain replays under migrations.test.ts.
  • Every ESLint-job and typecheck-job gate run locally: lint, check:nul-bytes, check:doc-authoring, check:role-word, check:org-identifier, check:authz-resolver, check:release-notes, spec tsc --noEmit, check:docs, check:skill-refs, check:react-blocks, check:api-surface, check:liveness, examples typecheck, downstream-contract typecheck.

Generated by Claude Code

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
@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 2:25pm

Request Review

@github-actions

github-actions Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cli, @objectstack/objectql, packages/qa, @objectstack/spec.

114 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 packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @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/cli, 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 @objectstack/objectql, 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 packages/objectql, @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/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • 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/cli, @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/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, 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/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via packages/qa, @objectstack/spec)
  • content/docs/permissions/delegated-administration.mdx (via packages/qa)
  • 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/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/objectql, @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/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • 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/objectql, @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/cli, @objectstack/objectql, @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/cli, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @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.

claude added 2 commits July 28, 2026 14:11
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
os-zhuang marked this pull request as ready for review July 28, 2026 14:42
@os-zhuang
os-zhuang merged commit acbf364 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/action-target-alias-discard-ahpgl5 branch 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys

2 participants