Skip to content

Commit acbf364

Browse files
authored
feat(spec)!: retire the last three deprecated authorable aliases (#3855) (#3883)
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 D2 conversion entries (`toMajor: 17`, `retiredFromLoadPath: true` from the day they land — 17 gives the aliases no acceptance window, deliberately) put the renames 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 it. A step-17 migration chain entry references them, so `os migrate meta --from <N>` rewrites the consumer's source mechanically — verified end to end, all three in one pass. Same shape as `object-compactLayout-to-highlightFields`, 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 never required, an agent recruiting no RAG context. `FieldSchema` already carries a comment about the last time that happened (#3726 / #3733). Each key is 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, and it renders as a `[REMOVED]` row in the generated reference docs. `lintDeprecatedAliases` retires with its subject — it warned about a conflict the parse resolved silently; the parse now rejects, which is strictly louder. `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. Also purges the authoring corpus. Seven places still presented the aliases as usable — four 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 dogfood expression-conformance ledger caught its own stale row for `data/field.zod.ts:conditionalRequired` — fixed. `rule-validator.ts` keeps its `requiredWhen ?? conditionalRequired` read on purpose: it is handed RAW, unparsed field definitions including metadata STORED under protocol <= 16, and dropping it would silently stop enforcing those rules on existing deployments.
1 parent 5cfd4d5 commit acbf364

34 files changed

Lines changed: 595 additions & 1189 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/cli": patch
4+
---
5+
6+
feat(spec)!: retire the last three deprecated authorable aliases (#3855)
7+
8+
Protocol 17 removes the three keys that a schema transform used to fold into a
9+
canonical slot and drop from the parsed output. Every slot now has exactly one
10+
spelling.
11+
12+
## Migration
13+
14+
| Removed | Use instead | Value shape |
15+
|---|---|---|
16+
| `action.execute` | `action.target` | unchanged — a handler / flow / URL ref |
17+
| `field.conditionalRequired` | `field.requiredWhen` | unchanged — a CEL predicate |
18+
| `agent.knowledge.topics` | `agent.knowledge.sources` | unchanged — a list of source tags |
19+
20+
All three are **pure key renames**. Nothing about the value changes, and no
21+
runtime behaviour changes: each alias was already lowered into its canonical key
22+
at parse time and erased before any consumer saw it, so what shrinks is the
23+
authorable surface, not the semantics.
24+
25+
**Run `os migrate meta --from <your current major>`.** It rewrites your source
26+
mechanically — these renames are registered as protocol-17 chain steps, so the
27+
tool applies all three (and every earlier step you skipped) in one pass. Manual
28+
alternative: rename the key. That is the entire fix.
29+
30+
```diff
31+
- actions: [{ name: 'convert', type: 'script', execute: 'convertHandler' }]
32+
+ actions: [{ name: 'convert', type: 'script', target: 'convertHandler' }]
33+
34+
- fields: { due_date: { type: 'date', conditionalRequired: 'record.stage == "closed"' } }
35+
+ fields: { due_date: { type: 'date', requiredWhen: 'record.stage == "closed"' } }
36+
37+
- knowledge: { topics: ['faq', 'policies'], indexes: ['docs'] }
38+
+ knowledge: { sources: ['faq', 'policies'], indexes: ['docs'] }
39+
```
40+
41+
## Why these reject instead of being ignored
42+
43+
None of the three schemas is `.strict()`, so deleting a key outright makes Zod
44+
**silently strip** it: the metadata would parse clean and the setting would
45+
simply never take effect — a script action bound to nothing, a field that is
46+
never required, an agent recruiting no RAG context. `FieldSchema` already
47+
carries a comment about the last time that happened (`dataQuality` / `cached`,
48+
#3726 / #3733).
49+
50+
So each removed key is **tombstoned**: it stays declared as `never`, which makes
51+
writing it a `tsc` error at the authoring site *and* a parse error carrying the
52+
rename. You cannot lose the setting quietly.
53+
54+
## Where to find this if you missed it
55+
56+
The removal is in the machine-readable change manifest (`spec-changes.json`,
57+
ADR-0087 D4) as three protocol-17 conversions. Per-major manifests **compose**,
58+
so jumping several majors at once still yields a single answer rather than N
59+
changelogs to reconcile — the generated upgrade guide and the `spec_changes` MCP
60+
tool are both projections of that record.
61+
62+
## Also removed
63+
64+
`lintDeprecatedAliases` and its rule-id exports (`ACTION_TARGET_EXECUTE_CONFLICT`,
65+
`FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT`,
66+
`AGENT_KNOWLEDGE_SOURCES_TOPICS_CONFLICT`, `DeprecatedAliasFinding`,
67+
`formatDeprecatedAliasFinding`). That pass existed to warn when an author
68+
declared both an alias and its canonical key, because the parse resolved the
69+
conflict silently. With the aliases gone the parse **rejects** instead, which is
70+
strictly louder — the rule has no subject left. If you imported any of these,
71+
delete the import; there is no replacement because the condition it reported can
72+
no longer occur.
73+
74+
The CLI's inline-handler lowering also stops binding a function on `execute`. It
75+
runs before the parse, so binding it there would have kept the removed alias
76+
quietly working for one authoring style while every other style rejected it.

content/docs/ai/agents.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ own `ask` / `build` records use exactly these fields):
137137
| `model` | Provider + model config (`provider`: `openai` \| `azure_openai` \| `anthropic` \| `local`) |
138138
| `skills` | Skill names to attach (the primary Agent → Skill → Tool capability model) |
139139
| `tools` | Direct tool **references** `{ type, name, description }``type` is `action` \| `flow` \| `query` \| `vector_search`; `name` points at an existing Action/Flow/query |
140-
| `knowledge` | RAG access: `{ sources: string[], indexes: string[] }`. `sources` is the canonical key the renderer reads; `topics` is a deprecated alias folded into it at parse time |
140+
| `knowledge` | RAG access: `{ sources: string[], indexes: string[] }`. `sources` is the only key; the `topics` alias was removed in protocol 17 (#3855) — `os migrate meta --from 16` rewrites it |
141141

142142
There is no `type` field and no fixed agent "type" taxonomy — behaviour comes from
143143
persona, instructions, skills, and tools. There are no `triggers` / `schedule`

content/docs/data-modeling/field-types.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,6 @@ These properties are available on **all** field types:
642642
| `visibleWhen` | `Expression` | — | Show field only when predicate is true |
643643
| `readonlyWhen` | `Expression` | — | Make field read-only when predicate is true |
644644
| `requiredWhen` | `Expression` | — | Require field when predicate is true |
645-
| `conditionalRequired` | `Expression` | — | Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped from the parsed metadata |
646645
647646
---
648647

content/docs/data-modeling/fields.mdx

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,6 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
315315
| `visibleWhen` | `string \| Expression` | CEL predicate; field is shown only when `TRUE` |
316316
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
317317
| `requiredWhen` | `string \| Expression` | CEL predicate; field is required when `TRUE` |
318-
| `conditionalRequired` | `string \| Expression` | Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped |
319318
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |
320319

321320
```typescript
@@ -344,17 +343,10 @@ visibility, read-only state, and required markers live as the draft record
344343
changes, including row-scoped rules inside inline master-detail grids. The
345344
server enforces `requiredWhen` on submit and ignores writes to fields whose
346345
`readonlyWhen` predicate is `TRUE`, so client-side affordances are not the
347-
only guard. Use `requiredWhen` for new metadata; keep `conditionalRequired`
348-
only for older packages that already emitted it. The alias is still accepted on
349-
input, but it is lowered into `requiredWhen` at parse time and then removed from
350-
the parsed metadata, so every consumer reads one canonical slot — if a field
351-
declares both, `requiredWhen` wins and the alias is discarded. Declaring both
352-
with *different* predicates means the `conditionalRequired` one never gates the
353-
field, so it raises an advisory `field-requiredwhen-conditionalrequired-conflict`
354-
warning naming both — from `defineStack` when it parses your config, and from
355-
`os build` / `os validate` for stacks that skip strict `defineStack`. It never
356-
fails the build; the fix is to delete `conditionalRequired`. The same predicate
357-
in both slots is harmless duplication and stays quiet.
346+
only guard. Use `requiredWhen`. The deprecated `conditionalRequired` alias was removed in
347+
protocol 17 (#3855): authoring it is rejected with an error naming the
348+
replacement, not silently stripped, and `os migrate meta --from 16` rewrites
349+
existing sources automatically.
358350

359351
## Naming Conventions
360352

content/docs/data-modeling/validation-rules.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ These properties apply to **all** field types and are validated by the base `Fie
3434
| `visibleWhen` | `string \| Expression` || CEL predicate; field is shown only when `TRUE` |
3535
| `readonlyWhen` | `string \| Expression` || CEL predicate; field is read-only when `TRUE` |
3636
| `requiredWhen` | `string \| Expression` || CEL predicate; field is required when `TRUE` |
37-
| `conditionalRequired` | `string \| Expression` || Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped from the parsed metadata |
3837

3938
---
4039

content/docs/protocol/objectui/actions.mdx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,7 @@ The `type` field selects how an action is dispatched. The complete enum is **`sc
5151
| `api` | Call an API endpoint (`method` defaults to `POST`). | Yes |
5252
| `form` | Open a FormView by name, routed to `/console/forms/:name`. | Yes |
5353

54-
`target` is the canonical binding for every non-`script` type. The deprecated `execute` field is lowered into `target` during parsing and then **removed from the parsed metadata**, so every consumer — server dispatch and renderer alike — reads exactly one slot. If an action declares both, `target` wins and `execute` is discarded.
55-
56-
Declaring both with *different* values means one of the two handlers you wrote never runs, so it raises an advisory `action-target-execute-conflict` warning naming both — from `defineStack` when it parses your config, and from `os build` / `os validate` for stacks that skip strict `defineStack`. It never fails the build; the fix is to delete `execute`. The same value in both slots is harmless duplication and stays quiet.
54+
`target` is the canonical binding for every non-`script` type, and since protocol 17 it is the **only** one. The deprecated `execute` alias was removed (#3855): authoring it is rejected with an error naming the replacement, not silently stripped. Run `os migrate meta --from 16` to rewrite existing sources automatically — the removal also ships in the machine-readable change manifest (`spec-changes.json`), which composes across however many majors you are jumping.
5755

5856
### Script Actions
5957

content/docs/references/ai/agent.mdx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const result = AIKnowledge.parse(data);
3030
| Property | Type | Required | Description |
3131
| :--- | :--- | :--- | :--- |
3232
| **sources** | `string[]` | optional | Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary). |
33-
| **topics** | `string[]` | optional | Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Folded into `sources` at parse time; prefer `sources`. |
33+
| **topics** | `any` | optional | [REMOVED] `knowledge.topics` was removed in @objectstack/spec 17 (#3855) — use `knowledge.sources`. Rename the key; the value (a list of source tags) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. |
3434
| **indexes** | `string[]` || Vector Store Indexes |
3535

3636

@@ -75,19 +75,19 @@ const result = AIKnowledge.parse(data);
7575
| **avatar** | `string` | optional | |
7676
| **role** | `string` || The persona/role (e.g. "Senior Support Engineer") |
7777
| **instructions** | `string` || System Prompt / Prime Directives |
78-
| **model** | `{ provider?: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature?: number; maxTokens?: number; … }` | optional | |
78+
| **model** | `{ provider: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature: number; maxTokens?: number; … }` | optional | |
7979
| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record<string, any>; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). |
80-
| **surface** | `Enum<'ask' \| 'build'>` | optional | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
80+
| **surface** | `Enum<'ask' \| 'build'>` | | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
8181
| **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) |
8282
| **tools** | `{ type: Enum<'action' \| 'flow' \| 'query' \| 'vector_search'>; name: string; description?: string }[]` | optional | Direct tool references (legacy fallback) |
83-
| **knowledge** | `{ sources?: string[]; topics?: string[]; indexes: string[] }` | optional | RAG access |
84-
| **active** | `boolean` | optional | |
83+
| **knowledge** | `{ sources?: string[]; topics?: any; indexes: string[] }` | optional | RAG access |
84+
| **active** | `boolean` | | |
8585
| **access** | `string[]` | optional | Who can chat with this agent |
8686
| **permissions** | `string[]` | optional | Required permission-set capabilities |
87-
| **planning** | `{ maxIterations?: integer }` | optional | Autonomous reasoning and planning configuration |
87+
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
8888
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | [EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893). |
8989
| **guardrails** | `{ maxTokensPerInvocation?: integer; maxExecutionTimeSec?: integer; blockedTopics?: string[] }` | optional | [EXPERIMENTAL — not enforced] Safety guardrails for the agent. Parsed but not enforced — real limits come from the quota service (liveness #1878/#1893). |
90-
| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record<string, any>; strict?: boolean; retryOnValidationFailure?: boolean; … }` | optional | [EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893). |
90+
| **structuredOutput** | `{ format: Enum<'json_object' \| 'json_schema' \| 'regex' \| 'grammar' \| 'xml'>; schema?: Record<string, any>; strict: boolean; retryOnValidationFailure: boolean; … }` | optional | [EXPERIMENTAL — not enforced] Structured output format and validation configuration. Parsed but no runtime consumer yet (liveness #1878/#1893). |
9191
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this agent. |
9292
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
9393
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |

content/docs/references/data/field.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ const result = Address.parse(data);
117117
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
118118
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` |
119119
| **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` |
120-
| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. Canonical slot; the deprecated `conditionalRequired` alias is lowered into this one at parse time. |
121-
| **conditionalRequired** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | @deprecated — Use requiredWhen instead. Lowered into requiredWhen during parsing and dropped from the parsed output; when both are set, requiredWhen wins. |
120+
| **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). |
121+
| **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. |
122122
| **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:<widget>`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". |
123123
| **hidden** | `boolean` | optional | Hidden from default UI |
124124
| **readonly** | `boolean` | optional | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip: `isSystem` writes (seed replay, migration), and an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). A normal (non-system) import is NOT system-context and still strips. |

content/docs/references/ui/action.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ const result = Action.parse(data);
8787
| **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. |
8888
| **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). |
8989
| **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'crypto.hash' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. |
90-
| **execute** | `string` | optional | @deprecated — Use target instead. Lowered into target during parsing and dropped from the parsed output; when both are set, target wins. |
90+
| **execute** | `any` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. |
9191
| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string; … }[]` | optional | Input parameters required from user |
9292
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
9393
| **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. |

0 commit comments

Comments
 (0)