Skip to content

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

Description

@os-zhuang

Filed per Prime Directive #10 rather than expanding #3838 / #3854.

Revised. The first version of this issue proposed deleting the keys from the Zod objects and adding UNKNOWN_KEY_GUIDANCE tombstones. That is wrong on both halves — see "The trap" below. The corrected plan is the conversion layer.

Where the three aliases stand

The spec carries three fold-and-drop aliases. Each is accepted on input, lowered into its canonical key, and dropped from the parsed output:

Alias Canonical Transform Since
action.execute action.target ui/action.zod.ts:683 #3713 / #3742
field.conditionalRequired field.requiredWhen data/field.zod.ts:622 #3754 / #3764
agent.knowledge.topics agent.knowledge.sources ai/agent.zod.ts:43 #1878 / #1891

Two things have landed for all three: the conflict is unrepresentable on the parsed output (#3742 and siblings), and the discard is no longer silent (#3838, #3854). What is missing is a retirement date — all three say "will be removed in a future version" with no version named.

The trap: deleting the key is a silent strip, not an error

None of the three schemas is .strict().

So "delete the key from the Zod object" does not produce an error — it produces a silent strip. An author still writing conditionalRequired after the removal gets: parses clean, key vanishes, field never required. That is precisely the failure mode #3713 → #3743 → #3838 → #3854 exists to eliminate, reintroduced one layer down.

And UNKNOWN_KEY_GUIDANCE does not cover it. That map lives on ObjectSchema.create() (data/object.zod.ts:1243, rejection at :1365) and only governs object top-level keys. There is no equivalent for field-level, action-level, or agent-knowledge-level keys. The AGENTS.md checklist item "add a tombstone entry in the relevant UNKNOWN_KEY_GUIDANCE map" has no relevant map for two of the three — building the unknown-key rejection would be a prerequisite, not a step.

The right mechanism: the ADR-0087 D2 conversion layer

The conversion layer exists for exactly this, and its contract is lossless / declared / loud / expiring:

  • toMajor: N → the loader accepts the N−1 shape and rewrites it to canonical at load; retiresIn = toMajor + 1 (conversions/apply.ts:71).
  • Every application emits a structured ConversionNotice.
  • The registry already carries entries for majors 11 / 13 / 14 / 15, five of them marked retiredFromLoadPath — the cycle demonstrably completes.
  • spec-changes.json (D4) is derived from this registry plus the api-surface diff (migrations/spec-changes.ts:22), so the generated upgrade guide and the spec_changes MCP tool pick the entry up with no extra authoring.

This is the same graduation Prime Directive #12 describes for filters → filter: rewritten to the canonical key at load, so no consumer-side fallback survives.

Current version is @objectstack/spec 16.1.0 / PROTOCOL_VERSION 16.0.0, so the entries are toMajor: 17.

Two things that must be handled, not assumed

1. renameKey goes silent on the conflict case. conversions/walk.ts:113 returns null when the canonical key is already present ("canonical already wins — nothing to do"), leaving the alias key in place — where the non-strict schema then strips it silently. A plain renameKey conversion therefore re-creates the exact bug #3838/#3854 fixed. Each conversion needs a custom apply() covering three cases: alias only → rewrite + notice; both with different values → drop the alias + notice naming both (this is where the #3743 warning relocates); both with the same value → drop quietly.

2. lintDeprecatedAliases goes blind, and then has no subject left. Conversions run inside normalizeStackInput, and the pass reads normalized — i.e. after conversions. The moment an alias graduates, its rule stops firing. Retire each rule together with its alias, in the same change; a rule that silently stops firing is the #3782 dead-gate class. Since all three aliases graduate together, lintDeprecatedAliases retires entirely — its wiring (warnDeprecatedAliases in defineStack, the CLI pre-parse pass) is repurposed as the conversion-notice sink rather than deleted.

3. os build currently drops conversion notices. compile.ts:96 calls normalizeStackInput(config) with no options while validate.ts:85 passes onConversionNotice. defineStack passes none either. Today that is cosmetic; once the aliases live in the conversion layer it is the only warning channel, and two of the three surfaces are deaf. Same #3782 parity class — must be closed in the same change.

Blast radius

Zero authored usage of any of the three aliases in this repo (verified by grep across examples/ and packages/*/src; the apparent hits are an unrelated automation-engine interface and the GitHub-connector / message-queue topics schemas). The frozen downstream-contract fixture does not exercise them either.

The part that needs a release owner, not a patch

Removing the keys narrows z.input for three schemas. Per ADR-0059 §4 that is by definition a breaking change — "restore it, or bump @objectstack/spec to a new major and document the migration … in the same PR". Two notes on the gates:

  • The api-surface snapshot is name+kind of exports, not schema keys, so removing an object key does not trip it. ADR-0059 §5 acknowledges the snapshot "catches add/remove/rename but not every narrowing", and the frozen fixture does not exercise these keys — so neither automated witness will stop this change. That makes the migration text the only real protection, not less important but more.
  • Retiring lintDeprecatedAliases does remove exports (lintDeprecatedAliases, formatDeprecatedAliasFinding, and the three rule-id consts), which trips the freeze contract mechanically and requires the snapshot regenerated in the same PR.
  • PROTOCOL_VERSION is asserted in lockstep with package.json major (protocol-version.test.ts), so it is bumped by the release, not by hand in a feature PR. The major changeset is what drives it.

ADR-0085 set the precedent for who decides: "Release owner decides major vs. exception at version time."

Concrete plan for 17

  1. Delete the three keys and their transform branches (action.zod.ts:683, field.zod.ts:622, agent.zod.ts:43).
  2. Add three toMajor: 17 conversions with custom apply() handling the conflict case (see Add metamodel interfaces for ObjectQL/ObjectUI contract #1 above).
  3. Retire lintDeprecatedAliases; repurpose its defineStack and CLI wiring as the conversion-notice sink.
  4. Close the notice-channel gap in compile.ts and defineStack.
  5. Regenerate api-surface.json (removed exports) and add a major changeset carrying FROM → TO and the one-line fix per alias — this ships as CHANGELOG.md in the npm package and is what an upgrading agent greps.
  6. Update the three docs surfaces that describe the aliases (protocol/objectui/actions.mdx, data-modeling/fields.mdx, ai/agents.mdx).

Note that the original suggestion "promote the lint rules from warning to error one major before removal" is dropped: the conversion notice is an equivalent signal that also expires on its own, so the manual promote-then-delete cycle buys nothing.

Refs #3713, #3742, #3743, #3754, #3764, #1878, #1891, #3838, #3854, ADR-0059 §4/§5, ADR-0087 D2/D4, ADR-0104.

Activity

  1. changed the title [-][P3] The three deprecated aliases now warn — give them a removal schedule[/-] [+][P3] Retire the three deprecated aliases — via the ADR-0087 D2 conversion layer, not by deleting the keys[/+] on Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions