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
76 changes: 76 additions & 0 deletions .changeset/retire-three-deprecated-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
"@objectstack/spec": major
"@objectstack/cli": patch
---

feat(spec)!: retire the last three deprecated authorable aliases (#3855)

Protocol 17 removes the three keys that a schema transform used to fold into a
canonical slot and drop from the parsed output. Every slot now has exactly one
spelling.

## Migration

| Removed | Use instead | Value shape |
|---|---|---|
| `action.execute` | `action.target` | unchanged — a handler / flow / URL ref |
| `field.conditionalRequired` | `field.requiredWhen` | unchanged — a CEL predicate |
| `agent.knowledge.topics` | `agent.knowledge.sources` | unchanged — a list of source tags |

All three are **pure key renames**. Nothing about the value changes, and no
runtime behaviour changes: each alias was already lowered into its canonical key
at parse time and erased before any consumer saw it, so what shrinks is the
authorable surface, not the semantics.

**Run `os migrate meta --from <your current major>`.** It rewrites your source
mechanically — these renames are registered as protocol-17 chain steps, so the
tool applies all three (and every earlier step you skipped) in one pass. Manual
alternative: rename the key. That is the entire fix.

```diff
- actions: [{ name: 'convert', type: 'script', execute: 'convertHandler' }]
+ actions: [{ name: 'convert', type: 'script', target: 'convertHandler' }]

- fields: { due_date: { type: 'date', conditionalRequired: 'record.stage == "closed"' } }
+ fields: { due_date: { type: 'date', requiredWhen: 'record.stage == "closed"' } }

- knowledge: { topics: ['faq', 'policies'], indexes: ['docs'] }
+ knowledge: { sources: ['faq', 'policies'], indexes: ['docs'] }
```

## Why these reject instead of being ignored

None of the three schemas is `.strict()`, so deleting a key outright makes Zod
**silently strip** it: the metadata would parse clean and the setting would
simply never take effect — a script action bound to nothing, a field that is
never required, an agent recruiting no RAG context. `FieldSchema` already
carries a comment about the last time that happened (`dataQuality` / `cached`,
#3726 / #3733).

So each removed key is **tombstoned**: it stays declared as `never`, which makes
writing it a `tsc` error at the authoring site *and* a parse error carrying the
rename. You cannot lose the setting quietly.

## Where to find this if you missed it

The removal is in the machine-readable change manifest (`spec-changes.json`,
ADR-0087 D4) as three protocol-17 conversions. Per-major manifests **compose**,
so jumping several majors at once still yields a single answer rather than N
changelogs to reconcile — the generated upgrade guide and the `spec_changes` MCP
tool are both projections of that record.

## Also removed

`lintDeprecatedAliases` and its rule-id exports (`ACTION_TARGET_EXECUTE_CONFLICT`,
`FIELD_REQUIREDWHEN_CONDITIONALREQUIRED_CONFLICT`,
`AGENT_KNOWLEDGE_SOURCES_TOPICS_CONFLICT`, `DeprecatedAliasFinding`,
`formatDeprecatedAliasFinding`). That pass existed to warn when an author
declared both an alias and its canonical key, because the parse resolved the
conflict silently. With the aliases gone the parse **rejects** instead, which is
strictly louder — the rule has no subject left. If you imported any of these,
delete the import; there is no replacement because the condition it reported can
no longer occur.

The CLI's inline-handler lowering also stops binding a function on `execute`. It
runs before the parse, so binding it there would have kept the removed alias
quietly working for one authoring style while every other style rejected it.
2 changes: 1 addition & 1 deletion content/docs/ai/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ own `ask` / `build` records use exactly these fields):
| `model` | Provider + model config (`provider`: `openai` \| `azure_openai` \| `anthropic` \| `local`) |
| `skills` | Skill names to attach (the primary Agent → Skill → Tool capability model) |
| `tools` | Direct tool **references** `{ type, name, description }` — `type` is `action` \| `flow` \| `query` \| `vector_search`; `name` points at an existing Action/Flow/query |
| `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 |
| `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 |

There is no `type` field and no fixed agent "type" taxonomy — behaviour comes from
persona, instructions, skills, and tools. There are no `triggers` / `schedule`
Expand Down
1 change: 0 additions & 1 deletion content/docs/data-modeling/field-types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -642,7 +642,6 @@ These properties are available on **all** field types:
| `visibleWhen` | `Expression` | — | Show field only when predicate is true |
| `readonlyWhen` | `Expression` | — | Make field read-only when predicate is true |
| `requiredWhen` | `Expression` | — | Require field when predicate is true |
| `conditionalRequired` | `Expression` | — | Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped from the parsed metadata |

---

Expand Down
16 changes: 4 additions & 12 deletions content/docs/data-modeling/fields.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ For values that must be encrypted at rest (API keys, tokens, DB passwords), use
| `visibleWhen` | `string \| Expression` | CEL predicate; field is shown only when `TRUE` |
| `readonlyWhen` | `string \| Expression` | CEL predicate; field is read-only when `TRUE` |
| `requiredWhen` | `string \| Expression` | CEL predicate; field is required when `TRUE` |
| `conditionalRequired` | `string \| Expression` | Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped |
| `multiple` | `boolean` | Allow multiple values (for select, lookup, file) |

```typescript
Expand Down Expand Up @@ -343,17 +342,10 @@ visibility, read-only state, and required markers live as the draft record
changes, including row-scoped rules inside inline master-detail grids. The
server enforces `requiredWhen` on submit and ignores writes to fields whose
`readonlyWhen` predicate is `TRUE`, so client-side affordances are not the
only guard. Use `requiredWhen` for new metadata; keep `conditionalRequired`
only for older packages that already emitted it. The alias is still accepted on
input, but it is lowered into `requiredWhen` at parse time and then removed from
the parsed metadata, so every consumer reads one canonical slot — if a field
declares both, `requiredWhen` wins and the alias is discarded. Declaring both
with *different* predicates means the `conditionalRequired` one never gates the
field, so it raises an advisory `field-requiredwhen-conditionalrequired-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 `conditionalRequired`. The same predicate
in both slots is harmless duplication and stays quiet.
only guard. Use `requiredWhen`. The deprecated `conditionalRequired` alias was removed in
protocol 17 (#3855): authoring it is rejected with an error naming the
replacement, not silently stripped, and `os migrate meta --from 16` rewrites
existing sources automatically.

## Naming Conventions

Expand Down
1 change: 0 additions & 1 deletion content/docs/data-modeling/validation-rules.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ These properties apply to **all** field types and are validated by the base `Fie
| `visibleWhen` | `string \| Expression` | — | CEL predicate; field is shown only when `TRUE` |
| `readonlyWhen` | `string \| Expression` | — | CEL predicate; field is read-only when `TRUE` |
| `requiredWhen` | `string \| Expression` | — | CEL predicate; field is required when `TRUE` |
| `conditionalRequired` | `string \| Expression` | — | Deprecated alias of `requiredWhen` — lowered into it at parse time, then dropped from the parsed metadata |

---

Expand Down
4 changes: 1 addition & 3 deletions content/docs/protocol/objectui/actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ The `type` field selects how an action is dispatched. The complete enum is **`sc
| `api` | Call an API endpoint (`method` defaults to `POST`). | Yes |
| `form` | Open a FormView by name, routed to `/console/forms/:name`. | Yes |

`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.

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.
`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.

### Script Actions

Expand Down
14 changes: 7 additions & 7 deletions content/docs/references/ai/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const result = AIKnowledge.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **sources** | `string[]` | optional | Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary). |
| **topics** | `string[]` | optional | Deprecated alias for `sources` (spec key ≠ consumed key drift, liveness audit #1878/#1891). Folded into `sources` at parse time; prefer `sources`. |
| **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. |
| **indexes** | `string[]` | ✅ | Vector Store Indexes |


Expand Down Expand Up @@ -75,19 +75,19 @@ const result = AIKnowledge.parse(data);
| **avatar** | `string` | optional | |
| **role** | `string` | ✅ | The persona/role (e.g. "Senior Support Engineer") |
| **instructions** | `string` | ✅ | System Prompt / Prime Directives |
| **model** | `{ provider?: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature?: number; maxTokens?: number; … }` | optional | |
| **model** | `{ provider: Enum<'openai' \| 'azure_openai' \| 'anthropic' \| 'local'>; model: string; temperature: number; maxTokens?: number; … }` | optional | |
| **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). |
| **surface** | `Enum<'ask' \| 'build'>` | optional | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
| **surface** | `Enum<'ask' \| 'build'>` | | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
| **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) |
| **tools** | `{ type: Enum<'action' \| 'flow' \| 'query' \| 'vector_search'>; name: string; description?: string }[]` | optional | Direct tool references (legacy fallback) |
| **knowledge** | `{ sources?: string[]; topics?: string[]; indexes: string[] }` | optional | RAG access |
| **active** | `boolean` | optional | |
| **knowledge** | `{ sources?: string[]; topics?: any; indexes: string[] }` | optional | RAG access |
| **active** | `boolean` | | |
| **access** | `string[]` | optional | Who can chat with this agent |
| **permissions** | `string[]` | optional | Required permission-set capabilities |
| **planning** | `{ maxIterations?: integer }` | optional | Autonomous reasoning and planning configuration |
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | [EXPERIMENTAL — not enforced] Agent memory management. Parsed but no runtime consumer yet (liveness #1878/#1893). |
| **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). |
| **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). |
| **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). |
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this agent. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/data/field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ const result = Address.parse(data);
| **group** | `string` | optional | Field group name for organizing fields in forms and layouts (e.g., "contact_info", "billing", "system") |
| **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'` |
| **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'` |
| **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. |
| **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. |
| **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). |
| **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. |
| **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". |
| **hidden** | `boolean` | optional | Hidden from default UI |
| **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. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/action.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const result = Action.parse(data);
| **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. |
| **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). |
| **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`. |
| **execute** | `string` | optional | @deprecated — Use target instead. Lowered into target during parsing and dropped from the parsed output; when both are set, target wins. |
| **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. |
| **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string; … }[]` | optional | Input parameters required from user |
| **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) |
| **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. |
Expand Down
Loading
Loading