Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .changeset/olive-pumas-repeat.md
Original file line numberDiff line numberDiff line change
@@ -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.
7 changes: 4 additions & 3 deletions packages/spec/src/ai/agent.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
14 changes: 9 additions & 5 deletions packages/spec/src/ai/agent.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -136,7 +136,6 @@ export type StructuredOutputConfigParsed = z.infer<typeof StructuredOutputConfig
* role: 'Help Desk Assistant',
* instructions: 'You are a helpful assistant. Always verify user identity first.',
* skills: ['case_management', 'knowledge_search'],
* knowledge: { sources: ['faq', 'policies'], indexes: ['support_docs'] },
* });
* ```
*/
Expand DownExpand Up@@ -223,10 +222,15 @@ export const AgentSchema = lazySchema(() => 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`. ' +
Expand Down
15 changes: 9 additions & 6 deletions packages/spec/src/data/field.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand All@@ -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`. ' +
Expand Down
22 changes: 15 additions & 7 deletions packages/spec/src/shared/retired-key.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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) —
Expand Down
9 changes: 6 additions & 3 deletions packages/spec/src/ui/action.zod.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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`. ' +
Expand Down
Loading