From b3d1970d87704b475e950d95b1f55406e4b49eff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 16:39:29 +0000 Subject: [PATCH] docs(spec): fix AgentSchema's @example and four stale `.strict()` tombstone rationales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AgentSchema`'s own `@example` wrote `knowledge: { sources, indexes }` — a key the same file declares as `retiredKey()` about 110 lines below. The canonical example an upgrading author copies taught a key the schema rejects, and typed `never` fails `tsc` at the authoring site. Drop the line; the example keeps `skills`, which is the whole point of the block. Four tombstone rationales still argued from "the schema is not `.strict()`, so a plain deletion would silently strip the key". The #4001 `strictObject` conversion made that false for `AgentSchema`, `FieldSchema` and `ActionSchema`, and for the premise in `shared/retired-key.ts`'s own module docblock. Each now rests on the reason that is load-bearing today: the prescription is the payload, because an unknown-key rejection carries neither the FROM -> TO mapping nor the migration command, and the key is typed `never` so the mistake still fails `tsc` first. Every tombstone stays; only the stated reason changes. Prose only — no schema shape, acceptance behaviour or `.describe()` semantic moves. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B4h3medzvhB9rpfoja9jcw --- .changeset/olive-pumas-repeat.md | 21 +++++++++++++++++++++ packages/spec/src/ai/agent.test.ts | 7 ++++--- packages/spec/src/ai/agent.zod.ts | 14 +++++++++----- packages/spec/src/data/field.zod.ts | 15 +++++++++------ packages/spec/src/shared/retired-key.ts | 22 +++++++++++++++------- packages/spec/src/ui/action.zod.ts | 9 ++++++--- 6 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 .changeset/olive-pumas-repeat.md diff --git a/.changeset/olive-pumas-repeat.md b/.changeset/olive-pumas-repeat.md new file mode 100644 index 0000000000..637381a744 --- /dev/null +++ b/.changeset/olive-pumas-repeat.md @@ -0,0 +1,21 @@ +--- +'@objectstack/spec': patch +--- + +Docs accuracy: correct the `AgentSchema` example and four stale `.strict()` tombstone rationales + +`AgentSchema`'s own `@example` wrote `knowledge: { sources: …, indexes: … }`, a key the +same schema declares as `retiredKey()` — so the canonical example an author (very often an +AI, ADR-0033) copies taught a key the schema rejects, and typed `never` fails `tsc` at the +authoring site. The line is dropped; the example keeps `skills`, which is the block's point. + +Four tombstone rationales still argued from "the schema is not `.strict()`, so a plain +deletion would silently strip the key". The #4001 `strictObject` conversion made that false +for the schemas named: `AgentSchema` (`agent.tools`), `FieldSchema` +(`field.conditionalRequired`), `ActionSchema` (`action.execute`), and the module docblock of +`shared/retired-key.ts` itself. Each now rests on the reason that is load-bearing today — +the prescription is the payload, since an unknown-key rejection carries neither the +FROM → TO mapping nor the migration command, and the key is typed `never` so the mistake +still fails `tsc` first. Every tombstone stays; only the stated reason changes. + +Prose only — no schema shape, acceptance behaviour or `.describe()` semantic is touched. diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index ff485377a3..57769b06ee 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -68,9 +68,10 @@ describe('AIModelConfigSchema', () => { describe('agent.tools retirement (ADR-0064 / #3820, tombstoned in #3894)', () => { it('REJECTS a legacy inline tools array, with the fix in the message', () => { - // Tombstoned, not deleted: AgentSchema is not `.strict()`, so a plain - // deletion would silently strip the key and the agent would quietly reach - // none of the tools its author listed. `retiredKey()` makes it audible. + // Tombstoned, not deleted: AgentSchema is `strictObject`, so a plain + // deletion would reject the key with a generic unknown-key error. + // `retiredKey()` is what makes the rejection carry the prescription — + // which is the payload, and what this assertion pins. expect(() => AgentSchema.parse({ name: 'legacy', diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index bc1093e695..5dff87dfaf 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -136,7 +136,6 @@ export type StructuredOutputConfigParsed = z.infer strictObject({ * resolved `agent.tools[].name` against the FULL registry with no surface * check, so an `ask`-surface agent could name an authoring tool and get it. * - * Tombstoned rather than deleted: `AgentSchema` is not `.strict()`, so a - * plain deletion would silently strip the key and the agent would quietly - * reach none of the tools its author listed — the same silent-capability-loss - * shape this whole issue is about (#3820), restored one layer down. + * Tombstoned rather than deleted: `AgentSchema` is `strictObject`, so a plain + * deletion would already REJECT the key — but only with a generic unknown-key + * error. The prescription is the payload. An author who wrote `tools` has to + * be told the specific thing this key's removal means: the capability moves + * into a skill, and ADR-0064's union is the only path from an agent to a tool + * — which this tombstone says and an unknown-key rejection cannot. It also + * types the key `never`, so the same mistake fails `tsc` at the authoring site + * before any parse runs. Those are the two channels an upgrading author, very + * often an AI (ADR-0033), actually reads (`shared/retired-key.ts`). */ tools: retiredKey( '`agent.tools` was removed in @objectstack/spec 17 (#3894) — use `skills`. ' + diff --git a/packages/spec/src/data/field.zod.ts b/packages/spec/src/data/field.zod.ts index 0998cf90a2..70bb2afde2 100644 --- a/packages/spec/src/data/field.zod.ts +++ b/packages/spec/src/data/field.zod.ts @@ -1177,8 +1177,9 @@ export const FieldSchema = lazySchema(() => strictObject({ // types, #3726) and `cached`'s (`ComputedFieldCacheSchema` + `ComputedFieldCache`, // #3733). Each key was gone from this object while its schema stayed on the // published API surface and in the generated reference docs, so an author could - // still discover the shape and write it. This object is NOT `.strict()`, so that - // write did not fail loudly: it parsed clean and the key was silently stripped — + // still discover the shape and write it. This object was NOT `.strict()` then + // (#4001 has since closed it), so that write did not fail loudly: it parsed + // clean and the key was silently stripped — // the same ADR-0104 failure class as the pre-declaration `accept` / `maxSize` // above (accepted in source, dropped in the contract, no feedback). Both schemas // are removed as of #3733; all five keys are now dead in both layers, as the @@ -1205,10 +1206,12 @@ export const FieldSchema = lazySchema(() => strictObject({ /** * [REMOVED in protocol 17 — #3855] The deprecated alias of `requiredWhen`. - * Tombstoned rather than deleted: `FieldSchema` is deliberately not - * `.strict()`, so a plain deletion would silently strip the key and the field - * would never be required — the ADR-0104 / #3733 failure class this object - * already carries a comment about. + * Tombstoned rather than deleted: `FieldSchema` is `strictObject` (#4001), so + * a plain deletion would reject the key — but with a generic unknown-key + * error that does not name `requiredWhen`. The prescription is the payload: + * it carries the rename, so an author who wrote `conditionalRequired` is told + * where the CEL predicate goes. It also types the key `never`, so the mistake + * fails `tsc` at the authoring site before any parse runs. */ conditionalRequired: retiredKey( '`conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. ' + diff --git a/packages/spec/src/shared/retired-key.ts b/packages/spec/src/shared/retired-key.ts index d57b862acd..689c48cb52 100644 --- a/packages/spec/src/shared/retired-key.ts +++ b/packages/spec/src/shared/retired-key.ts @@ -4,13 +4,21 @@ * Tombstones for RETIRED authorable keys (#3855). * * Removing a key an author can write has one hard requirement: the removal must - * be **audible**. None of the schemas that carried a deprecated alias is - * `.strict()` — `FieldSchema` says so in a comment and records that the trap - * already bit once (`dataQuality` / `cached` outlived their keys by a release - * and were silently stripped, #3726 / #3733, the ADR-0104 class). So simply - * deleting the key from the Zod object does not produce an error; it produces a - * **silent strip**, which is the exact failure mode #3713 → #3743 → #3838 → - * #3854 spent four PRs eliminating, reintroduced one layer down. + * be **audible**. When these tombstones were introduced, none of the schemas + * carrying a deprecated alias was `.strict()`, so deleting the key from the Zod + * object produced no error at all — it produced a **silent strip**, the exact + * failure mode #3713 → #3743 → #3838 → #3854 spent four PRs eliminating, + * reintroduced one layer down. `FieldSchema` recorded the trap biting once + * (`dataQuality` / `cached` outlived their keys by a release and were silently + * stripped, #3726 / #3733, the ADR-0104 class). + * + * The #4001 campaign has since closed many of those shapes with `strictObject`, + * and on a closed shape a bare deletion is no longer silent — but it is still + * not enough, which is why tombstones stay. An unknown-key rejection reports + * only that the key is unrecognised; it cannot carry the FROM → TO mapping, the + * ADR the removal rests on, or the migration command. **The prescription is the + * payload**, so both channels below survive the conversion — and on the shapes + * that are still non-strict, the silent strip above is still the alternative. * * A tombstone keeps the key declared but makes it unwritable, so the removal * lands in the two channels an upgrading author — very often an AI (ADR-0033) — diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 413e86f8c7..1b7e4a4c21 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -973,9 +973,12 @@ const actionObject = () => strictObject({ /** * [REMOVED in protocol 17 — #3855] The deprecated alias of `target`. - * Tombstoned rather than deleted: `ActionSchema` is not `.strict()`, so a - * plain deletion would silently strip the key and the action would bind no - * handler at all — the #2169 "Mark Done does nothing" shape, restored. + * Tombstoned rather than deleted: `ActionSchema` is `strictObject`, so a + * plain deletion would reject the key — but with a generic unknown-key error + * that does not name `target`. The prescription is the payload: it carries + * the rename, so an author who wrote `execute` is told where the handler ref + * goes instead of only that the key is unknown. It also types the key + * `never`, so the mistake fails `tsc` before any parse runs. */ execute: retiredKey( '`execute` was removed in @objectstack/spec 17 (#3855) — use `target`. ' +