You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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
ActionSchema (ui/action.zod.ts:389) and AIKnowledgeSchema (ai/agent.zod.ts:35) are bare z.object, so Zod's default .strip().
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 insidenormalizeStackInput, 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 lintDeprecatedAliasesdoes 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
Delete the three keys and their transform branches (action.zod.ts:683, field.zod.ts:622, agent.zod.ts:43).
Retire lintDeprecatedAliases; repurpose its defineStack and CLI wiring as the conversion-notice sink.
Close the notice-channel gap in compile.ts and defineStack.
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.
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.
Filed per Prime Directive #10 rather than expanding #3838 / #3854.
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:
action.executeaction.targetui/action.zod.ts:683field.conditionalRequiredfield.requiredWhendata/field.zod.ts:622agent.knowledge.topicsagent.knowledge.sourcesai/agent.zod.ts:43Two 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().FieldSchema—data/field.zod.ts:503says so in a comment, and records that this exact trap already bit:dataQualityandcachedoutlived their keys by a release, stayed discoverable on the API surface and in the generated reference docs, and an author writing them got a clean parse with the key silently stripped (DataQualityRulesSchema 是 2026-06 字段剪除留下的孤儿:字段键已删,schema + 类型仍公开导出 #3726, ComputedFieldCacheSchema 是 2026-06 字段剪除留下的第二个孤儿(#3726 表格误记为「已清理」) #3733 — the ADR-0104 failure class).ActionSchema(ui/action.zod.ts:389) andAIKnowledgeSchema(ai/agent.zod.ts:35) are barez.object, so Zod's default.strip().So "delete the key from the Zod object" does not produce an error — it produces a silent strip. An author still writing
conditionalRequiredafter 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_GUIDANCEdoes not cover it. That map lives onObjectSchema.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 relevantUNKNOWN_KEY_GUIDANCEmap" 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).ConversionNotice.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 thespec_changesMCP 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/spec16.1.0 /PROTOCOL_VERSION 16.0.0, so the entries aretoMajor: 17.Two things that must be handled, not assumed
1.
renameKeygoes silent on the conflict case.conversions/walk.ts:113returnsnullwhen 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 plainrenameKeyconversion therefore re-creates the exact bug #3838/#3854 fixed. Each conversion needs a customapply()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.
lintDeprecatedAliasesgoes blind, and then has no subject left. Conversions run insidenormalizeStackInput, and the pass readsnormalized— 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,lintDeprecatedAliasesretires entirely — its wiring (warnDeprecatedAliasesindefineStack, the CLI pre-parse pass) is repurposed as the conversion-notice sink rather than deleted.3.
os buildcurrently drops conversion notices.compile.ts:96callsnormalizeStackInput(config)with no options whilevalidate.ts:85passesonConversionNotice.defineStackpasses 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/andpackages/*/src; the apparent hits are an unrelated automation-engine interface and the GitHub-connector / message-queuetopicsschemas). 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.inputfor three schemas. Per ADR-0059 §4 that is by definition a breaking change — "restore it, or bump@objectstack/specto a new major and document the migration … in the same PR". Two notes on the gates:lintDeprecatedAliasesdoes 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_VERSIONis asserted in lockstep withpackage.jsonmajor (protocol-version.test.ts), so it is bumped by the release, not by hand in a feature PR. Themajorchangeset 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
action.zod.ts:683,field.zod.ts:622,agent.zod.ts:43).toMajor: 17conversions with customapply()handling the conflict case (see Add metamodel interfaces for ObjectQL/ObjectUI contract #1 above).lintDeprecatedAliases; repurpose itsdefineStackand CLI wiring as the conversion-notice sink.compile.tsanddefineStack.api-surface.json(removed exports) and add amajorchangeset carrying FROM → TO and the one-line fix per alias — this ships asCHANGELOG.mdin the npm package and is what an upgrading agent greps.protocol/objectui/actions.mdx,data-modeling/fields.mdx,ai/agents.mdx).Note that the original suggestion "promote the lint rules from
warningtoerrorone 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.