From 04960c41f1bdda6fc0a8457ec0b2171185470353 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 07:09:44 +0000 Subject: [PATCH 1/3] =?UTF-8?q?refactor(spec)!:=20the=20#3896=20close-out?= =?UTF-8?q?=20sweep=20=E2=80=94=20fourteen=20inert=20authoring=20keys=20le?= =?UTF-8?q?ave=20the=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enforce-or-remove worklist across the remaining governed types, each key tombstoned with its prescription (retiredKey) and stripped by a protocol-17 conversion: - action shortcut/bulkEnabled — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions. - flow active/template, node outputSchema, errorHandling.fallbackNodeId — active:false never stopped a flow (status is the enforced lifecycle; the default even READ as disabled while the engine treated unset as enabled); faults route via per-node fault edges. - view list responsive/performance, form defaultSort/aria — no renderer read them. List aria/data stay live. form.data SURVIVED: the removal attempt broke the build (defineForm writes data.provider='schema' onto every metadata form; metadata-protocol serves it) — the build refuted the ledger's dead verdict, so the entry is CORRECTED to live instead. The sweep's method note: a ledger verdict is an input to removal, not a substitute for the build's own proof. - dashboard aria/performance + widget performance (+ orphaned PerformanceConfigSchema) — no renderer applied them; widget.performance had no ledger entry and was call-graph-closed fresh across both repos. - agent knowledge (+ orphaned AIKnowledgeSchema) — declaring sources never scoped retrieval (search_knowledge takes sourceIds from the LLM's tool-call args). The same-major topics→sources rename is ABSORBED into the removal pre-release: composed, its effect was unobservable, and the fixture-disjointness contract of the conversion table enforces exactly that judgment. - skill triggerPhrases — never matched; activation is triggerConditions ∩ the agent's skills[] allowlist. Deliberate keeps, recorded so they are not re-litigated: hook label/description and flow description are docs-shaped annotation fields, exempt from enforce-or-remove. Stale report aria/performance ledger entries (schema already clean) deleted as hygiene. Kit: 6 conversions + chain step (chain-replay green); retirement pins across 7 test files; forms pruned (action/flow/dashboard/agent/skill); i18n bundles regenerated (merge mode); baselines edited deliberately (authorable-surface, json-schema.manifest — AIKnowledge/PerformanceConfig unpublished); the surface guard's coverage matcher generalized to ' / '-separated multi-key conversion surfaces (exactly as strict per key); example flows drop template:true (3 sites, caught by the tombstone's tsc leg); SKILL.md + agents.mdx stop teaching the removed keys; v17 notes + upgrade checklist + README ledger rows updated; major changeset. spec 6932 / objectql 1179 / cli 830 green; all spec gates green; examples + downstream-contract typecheck clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- .changeset/close-out-sweep-inert-keys.md | 36 ++ content/docs/ai/agents.mdx | 17 +- content/docs/references/ai/agent.mdx | 21 +- content/docs/references/ai/skill.mdx | 2 +- .../docs/references/api/automation-api.mdx | 4 +- content/docs/references/automation/flow.mdx | 6 +- content/docs/references/ui/action.mdx | 4 +- content/docs/references/ui/dashboard.mdx | 4 +- content/docs/references/ui/responsive.mdx | 22 +- content/docs/references/ui/view.mdx | 12 +- content/docs/references/ui/widget.mdx | 2 +- content/docs/releases/v17.mdx | 23 +- docs/protocol-upgrade-guide.md | 9 +- .../src/automation/flows/index.ts | 3 - .../utils/lint-liveness-properties.test.ts | 52 +-- .../en.metadata-forms.generated.ts | 28 -- .../es-ES.metadata-forms.generated.ts | 28 -- .../ja-JP.metadata-forms.generated.ts | 28 -- .../zh-CN.metadata-forms.generated.ts | 28 -- packages/spec/api-surface.json | 3 - packages/spec/authorable-surface.json | 45 +-- packages/spec/json-schema.manifest.json | 2 - packages/spec/liveness/README.md | 16 +- packages/spec/liveness/action.json | 16 +- packages/spec/liveness/agent.json | 9 +- packages/spec/liveness/dashboard.json | 12 +- packages/spec/liveness/flow.json | 30 +- packages/spec/liveness/hook.json | 78 +++- packages/spec/liveness/report.json | 12 +- packages/spec/liveness/skill.json | 9 +- packages/spec/liveness/view.json | 377 +++++++++++++----- packages/spec/scripts/build-schemas.ts | 6 +- packages/spec/spec-changes.json | 84 +++- packages/spec/src/ai/agent.form.ts | 3 +- packages/spec/src/ai/agent.test.ts | 112 +----- packages/spec/src/ai/agent.zod.ts | 38 +- packages/spec/src/ai/skill.form.ts | 3 +- packages/spec/src/ai/skill.test.ts | 16 +- packages/spec/src/ai/skill.zod.ts | 14 +- packages/spec/src/automation/flow.form.ts | 3 +- packages/spec/src/automation/flow.test.ts | 48 +-- packages/spec/src/automation/flow.zod.ts | 47 ++- packages/spec/src/conversions/registry.ts | 302 ++++++++++++-- packages/spec/src/migrations/registry.ts | 21 +- packages/spec/src/ui/action.form.ts | 8 +- packages/spec/src/ui/action.test.ts | 19 + packages/spec/src/ui/action.zod.ts | 23 +- packages/spec/src/ui/dashboard.form.ts | 4 +- packages/spec/src/ui/dashboard.zod.ts | 21 +- packages/spec/src/ui/responsive.test.ts | 58 +-- packages/spec/src/ui/responsive.zod.ts | 57 +-- packages/spec/src/ui/view.test.ts | 165 ++------ packages/spec/src/ui/view.zod.ts | 55 ++- packages/spec/src/ui/widget.test.ts | 17 +- packages/spec/src/ui/widget.zod.ts | 11 +- skills/objectstack-ai/SKILL.md | 2 +- skills/objectstack-data/references/_index.md | 1 - 57 files changed, 1149 insertions(+), 927 deletions(-) create mode 100644 .changeset/close-out-sweep-inert-keys.md diff --git a/.changeset/close-out-sweep-inert-keys.md b/.changeset/close-out-sweep-inert-keys.md new file mode 100644 index 0000000000..80a8be6f69 --- /dev/null +++ b/.changeset/close-out-sweep-inert-keys.md @@ -0,0 +1,36 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: the #3896 close-out sweep — fourteen inert authoring keys leave the surface + +The enforce-or-remove worklist across the remaining metadata types, each key +tombstoned at its schema with the prescription (`retiredKey`) and stripped by +a protocol-17 conversion (`os migrate meta` rewrites sources): + +- **action** `shortcut` / `bulkEnabled` — no keydown path ever dispatched a + shortcut; the multi-select toolbar reads the view's `bulkActions`. +- **flow** `active` / `template`, node `outputSchema`, errorHandling + `fallbackNodeId` — `active: false` never stopped a flow (`status` is the + enforced lifecycle; the default even read as disabled while the engine + treated unset as enabled); faults route via per-node fault edges. +- **view** list `responsive` / `performance`, form `defaultSort` / `aria` — + no renderer read any of them. List `aria`/`data` stay live, and **form + `data` survived the sweep**: the removal attempt broke the build — + `defineForm` writes `data.provider='schema'` onto every metadata form — + which re-verified the entry; its ledger verdict is corrected instead. +- **dashboard** `aria` / `performance`, widget `performance` (+ the orphaned + `PerformanceConfigSchema`) — no renderer applied them; virtual scrolling is + the live top-level `virtualScroll`. +- **agent** `knowledge` (+ `AIKnowledgeSchema`) — declaring sources/indexes + never scoped retrieval: `search_knowledge` takes `sourceIds` from the LLM's + tool-call arguments. The protocol-17 `topics`→`sources` rename is absorbed + into the removal pre-release. +- **skill** `triggerPhrases` — phrases were never matched; activation is + `triggerConditions` ∩ the agent's `skills[]` allowlist. + +Docs-shaped annotation fields (`hook.label`/`description`, `flow.description`) +are deliberately KEPT and so noted in the ledger — they document intent for +the next reader and are exempt from enforce-or-remove. The stale report +`aria`/`performance` ledger entries (schema already clean) are deleted as +hygiene. diff --git a/content/docs/ai/agents.mdx b/content/docs/ai/agents.mdx index 5124fee4ae..7dee17ef05 100644 --- a/content/docs/ai/agents.mdx +++ b/content/docs/ai/agents.mdx @@ -258,13 +258,12 @@ Always be professional and data-driven.`, // Capability comes from skills — the only tool-bearing slot (ADR-0064). // Each skill names the platform tools and `action_` tools it needs. + // (There is no agent-level `knowledge` block — it was removed in protocol + // 17 (#3896 close-out): declaring sources never scoped retrieval. Restrict + // access at the knowledge-service/source level; describe intended + // grounding in `instructions`.) skills: ['lead_qualification', 'opportunity_coaching', 'email_drafting'], - // RAG access: sources to recruit knowledge from + vector store indexes. - knowledge: { - sources: ['sales-playbook', 'leads', 'opportunities'], - indexes: ['sales_docs'], - }, }); ``` @@ -298,10 +297,6 @@ Always be empathetic and solution-focused.`, // Protocol). Neither needs a `defineTool` record. skills: ['case_triage', 'knowledge_search', 'response_drafting'], - knowledge: { - sources: ['support-kb', 'cases'], - indexes: ['support_docs'], - }, }); ``` @@ -371,10 +366,6 @@ Use the data tools to query records and aggregate metrics.`, // names directly — see "Natural Language Queries". skills: ['revenue_forecasting'], - knowledge: { - sources: ['opportunities', 'pipeline'], - indexes: ['sales_docs'], - }, }); ``` diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index 63a297e6c9..cdfc790e65 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -14,26 +14,13 @@ AI Model Configuration ## TypeScript Usage ```typescript -import { AIKnowledge, AIModelConfig, Agent, StructuredOutputConfig, StructuredOutputFormat, TransformPipelineStep } from '@objectstack/spec/ai'; -import type { AIKnowledge, AIModelConfig, Agent, StructuredOutputConfig, StructuredOutputFormat, TransformPipelineStep } from '@objectstack/spec/ai'; +import { AIModelConfig, Agent, StructuredOutputConfig, StructuredOutputFormat, TransformPipelineStep } from '@objectstack/spec/ai'; +import type { AIModelConfig, Agent, StructuredOutputConfig, StructuredOutputFormat, TransformPipelineStep } from '@objectstack/spec/ai'; // Validate data -const result = AIKnowledge.parse(data); +const result = AIModelConfig.parse(data); ``` ---- - -## AIKnowledge - -### Properties - -| 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** | `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 | - - --- ## AIModelConfig @@ -67,7 +54,7 @@ const result = AIKnowledge.parse(data); | **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** | `any` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 (#3894) — use `skills`. An agent reaches exactly the tools its surface-compatible skills declare (ADR-0064), so move each reference into a skill: a platform tool by its registered name, or `action_` for one of your own AI-exposed Actions. Run `os migrate meta --from 16` to rewrite it automatically. | -| **knowledge** | `{ sources?: string[]; topics?: any; indexes: string[] }` | optional | RAG access | +| **knowledge** | `any` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — declaring knowledge sources/indexes on an agent never scoped retrieval: the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from the agent record. Delete the block. Restrict retrieval at the knowledge-service / source level (per-source permissions), and describe intended grounding in `instructions` so the model asks for the right sources. | | **active** | `boolean` | ✅ | | | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permission-set capabilities | diff --git a/content/docs/references/ai/skill.mdx b/content/docs/references/ai/skill.mdx index a887aae0c1..546795f65d 100644 --- a/content/docs/references/ai/skill.mdx +++ b/content/docs/references/ai/skill.mdx @@ -39,7 +39,7 @@ const result = Skill.parse(data); | **surface** | `Enum<'ask' \| 'build' \| 'both'>` | ✅ | Agent surface this skill binds to ('ask' \| 'build' \| 'both') — ADR-0063 §3 | | **instructions** | `string` | optional | LLM instructions when skill is active | | **tools** | `string[]` | ✅ | Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) | -| **triggerPhrases** | `string[]` | optional | Phrases that activate this skill | +| **triggerPhrases** | `any` | optional | [REMOVED] `skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — phrases were never matched against the user's message; skill activation is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in `triggerConditions`; describe intent in `description`/`instructions` for the LLM. | | **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions | | **active** | `boolean` | ✅ | Whether the skill is enabled | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. | diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index a14ae52bd2..819ea212d1 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -102,12 +102,12 @@ const result = AutomationApiErrorCode.parse(data); | **errorMessage** | `string` | optional | Toast shown when a screen flow fails (defaults to the raw error). | | **version** | `integer` | optional | Version number | | **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Deployment status | -| **template** | `boolean` | optional | Is logic template (Subflow) | +| **template** | `any` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | | **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | | **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables | | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | | **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | ✅ | Flow connections | -| **active** | `boolean` | optional | Is active (Deprecated: use status) | +| **active** | `any` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | | **runAs** | `Enum<'system' \| 'user'>` | optional | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | | **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; retryDelayMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index c91c5e36c1..c37afe5ed3 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -52,12 +52,12 @@ const result = Flow.parse(data); | **errorMessage** | `string` | optional | Toast shown when a screen flow fails (defaults to the raw error). | | **version** | `integer` | optional | Version number | | **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Deployment status | -| **template** | `boolean` | optional | Is logic template (Subflow) | +| **template** | `any` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | | **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | | **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables | | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | | **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | ✅ | Flow connections | -| **active** | `boolean` | optional | Is active (Deprecated: use status) | +| **active** | `any` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | | **runAs** | `Enum<'system' \| 'user'>` | optional | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | | **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; retryDelayMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | @@ -103,7 +103,7 @@ const result = Flow.parse(data); | **position** | `{ x: number; y: number }` | optional | | | **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | | **inputSchema** | `Record; required: boolean; description?: string }>` | optional | Input parameter schema for this node | -| **outputSchema** | `Record; description?: string }>` | optional | Output schema declaration for this node | +| **outputSchema** | `any` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}`}) regardless of any declaration. | | **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string; timeoutMs?: integer; … }` | optional | Configuration for wait node event resumption | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index e0a68c20f9..1e28fb4979 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -101,8 +101,8 @@ const result = Action.parse(data); | **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| 'phoneNumberOtp'>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | | **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Boolean or predicate (CEL) — action is disabled when TRUE. | | **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. | -| **shortcut** | `string` | optional | Keyboard shortcut to trigger this action (e.g., "Ctrl+S") | -| **bulkEnabled** | `boolean` | optional | Whether this action can be applied to multiple selected records | +| **shortcut** | `any` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. | +| **bulkEnabled** | `any` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / `bulkActionDefs`, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's `bulkActions` instead. | | **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. | | **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. | | **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". | diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index f1f4aa0511..f20d82b470 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -39,8 +39,8 @@ const result = Dashboard.parse(data); | **refreshInterval** | `number` | optional | Auto-refresh interval in seconds | | **dateRange** | `{ field?: string; defaultRange: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| 'this_quarter' \| 'last_quarter' \| 'this_year' \| 'last_year' \| 'last_7_days' \| 'last_30_days' \| 'last_90_days' \| 'custom'>; allowCustomRange: boolean }` | optional | Global dashboard date range filter configuration | | **globalFilters** | `{ name?: string; field: string; label?: string; type?: Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>; … }[]` | optional | Global filters that apply to all widgets in the dashboard | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | -| **performance** | `{ lazyLoad?: boolean; virtualScroll?: object; cacheStrategy?: Enum<'none' \| 'cache-first' \| 'network-first' \| 'stale-while-revalidate'>; prefetch?: boolean; … }` | optional | Performance optimization settings | +| **aria** | `any` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. | +| **performance** | `any` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dashboard. | | **_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. | diff --git a/content/docs/references/ui/responsive.mdx b/content/docs/references/ui/responsive.mdx index 935b588fc7..8ff6b24508 100644 --- a/content/docs/references/ui/responsive.mdx +++ b/content/docs/references/ui/responsive.mdx @@ -16,8 +16,8 @@ Standard Tailwind-style breakpoint names (xs–2xl). ## TypeScript Usage ```typescript -import { BreakpointColumnMap, BreakpointName, BreakpointOrderMap, PerformanceConfig, ResponsiveConfig, ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; -import type { BreakpointColumnMap, BreakpointName, BreakpointOrderMap, PerformanceConfig, ResponsiveConfig, ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; +import { BreakpointColumnMap, BreakpointName, BreakpointOrderMap, ResponsiveConfig, ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; +import type { BreakpointColumnMap, BreakpointName, BreakpointOrderMap, ResponsiveConfig, ResponsiveStyles, StyleMap } from '@objectstack/spec/ui'; // Validate data const result = BreakpointColumnMap.parse(data); @@ -73,24 +73,6 @@ Display order per breakpoint | **2xl** | `number` | optional | | ---- - -## PerformanceConfig - -Performance optimization configuration - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **lazyLoad** | `boolean` | optional | Enable lazy loading (defer rendering until visible) | -| **virtualScroll** | `{ enabled: boolean; itemHeight?: number; overscan?: number }` | optional | Virtual scrolling configuration | -| **cacheStrategy** | `Enum<'none' \| 'cache-first' \| 'network-first' \| 'stale-while-revalidate'>` | optional | Client-side data caching strategy | -| **prefetch** | `boolean` | optional | Prefetch data before component is visible | -| **pageSize** | `number` | optional | Number of items per page for pagination | -| **debounceMs** | `number` | optional | Debounce interval for user interactions in milliseconds | - - --- ## ResponsiveConfig diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 22b8e4a9ff..2ac0cd981d 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -212,12 +212,12 @@ Column footer summary configuration | **sections** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | | **groups** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | | **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `{ field: string; order?: Enum<'asc' \| 'desc'> }[]` | optional | Default sort order for related list views within this form | +| **defaultSort** | `any` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. | | **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | | **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values; framework#1894 / #2998). | -| **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the form view | +| **aria** | `any` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. | --- @@ -411,8 +411,8 @@ List chart view configuration | **allowPrinting** | `boolean` | optional | Allow users to print the view | | **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | -| **responsive** | `{ breakpoint?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>; hiddenOn?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>[]; columns?: object; order?: object }` | optional | Responsive layout configuration | -| **performance** | `{ lazyLoad?: boolean; virtualScroll?: object; cacheStrategy?: Enum<'none' \| 'cache-first' \| 'network-first' \| 'stale-while-revalidate'>; prefetch?: boolean; … }` | optional | Performance optimization settings | +| **responsive** | `any` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | +| **performance** | `any` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | --- @@ -499,8 +499,8 @@ List chart view configuration | **allowPrinting** | `boolean` | optional | Allow users to print the view | | **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | -| **responsive** | `{ breakpoint?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>; hiddenOn?: Enum<'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' \| '2xl'>[]; columns?: object; order?: object }` | optional | Responsive layout configuration | -| **performance** | `{ lazyLoad?: boolean; virtualScroll?: object; cacheStrategy?: Enum<'none' \| 'cache-first' \| 'network-first' \| 'stale-while-revalidate'>; prefetch?: boolean; … }` | optional | Performance optimization settings | +| **responsive** | `any` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | +| **performance** | `any` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: { field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[] }` | optional | | diff --git a/content/docs/references/ui/widget.mdx b/content/docs/references/ui/widget.mdx index 85fcc44b2e..9a66f24311 100644 --- a/content/docs/references/ui/widget.mdx +++ b/content/docs/references/ui/widget.mdx @@ -110,7 +110,7 @@ const result = WidgetEvent.parse(data); | **license** | `string` | optional | License (SPDX identifier) | | **tags** | `string[]` | optional | Tags for categorization | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | -| **performance** | `{ lazyLoad?: boolean; virtualScroll?: object; cacheStrategy?: Enum<'none' \| 'cache-first' \| 'network-first' \| 'stale-while-revalidate'>; prefetch?: boolean; … }` | optional | Performance optimization settings | +| **performance** | `any` | optional | [REMOVED] `widget.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime ever read it. Delete the key. Virtual scrolling is the live top-level `virtualScroll` on list-shaped views. | --- diff --git a/content/docs/releases/v17.mdx b/content/docs/releases/v17.mdx index 1b6355f563..226e4151ee 100644 --- a/content/docs/releases/v17.mdx +++ b/content/docs/releases/v17.mdx @@ -76,7 +76,9 @@ parsed-but-never-enforced spec clusters are removed rather than maintained. the capabilities-descriptor cluster, `FeatureFlagSchema`, `SkillSchema.permissions`, `tool.requiresConfirmation`, `agent.tools[]`, `object.enable.trash`/`mru`, report `aria`/`performance`, `DEFAULT_DISPATCHER_ROUTES`, the four inert tool - authoring keys (`category`/`permissions`/`active`/`builtIn`) and the last three + authoring keys (`category`/`permissions`/`active`/`builtIn`), the close-out + sweep across action/flow/view/dashboard/agent/skill (fourteen more inert + keys, `flow.active` and `agent.knowledge` among them) and the last three deprecated authorable aliases are removed — each one had been parsed and ignored. @@ -242,7 +244,7 @@ swallows flow errors. |---|---|---| | `action.execute` | `action.target` | unchanged | | `field.conditionalRequired` | `field.requiredWhen` | unchanged | -| `agent.knowledge.topics` | `agent.knowledge.sources` | unchanged | +| `agent.knowledge.topics` | ~~`agent.knowledge.sources`~~ → the whole `knowledge` block was later removed (#3896 close-out — see the dead-cluster table) | — | All three are pure key renames — each alias was already lowered into its canonical key at parse time, so what shrinks is the authorable surface, not the @@ -263,7 +265,8 @@ three plus every earlier step you skipped. Manual alternative: rename the key. + fields: { due_date: { type: 'date', requiredWhen: 'record.stage == "closed"' } } - knowledge: { topics: ['faq', 'policies'], indexes: ['docs'] } -+ knowledge: { sources: ['faq', 'policies'], indexes: ['docs'] } ++ (delete the block — `agent.knowledge` was removed outright in the #3896 ++ close-out: declaring sources never scoped retrieval) ``` Each removed key is **tombstoned** as `never` rather than simply deleted, so @@ -681,6 +684,12 @@ import or the authored key. | `DynamicLoadingConfig` / `PluginDiscoveryConfig` / `PluginDiscoverySource` | promised plugin sandboxing / integrity verification / source allow-listing / load approval; none of it was ever wired (#3950) | | `RowLevelSecurityPolicy.priority` | conflict-resolution semantics that cannot exist under OR-combination (#3990) | | `tool.category` / `.permissions` / `.active` / `.builtIn` (+ `ToolCategorySchema`) | authorable and inert — `permissions` gated nothing, `active: false` withdrew nothing; strict-rejected with prescriptions (#3896 close-out) | +| `action.shortcut` / `.bulkEnabled` | no keydown path dispatches shortcuts; the multi-select toolbar reads the view's `bulkActions` (#3896 close-out) | +| `flow.active` / `.template` / node `outputSchema` / `errorHandling.fallbackNodeId` | `active: false` never stopped a flow — `status` is the enforced lifecycle; faults route via per-node fault edges (#3896 close-out) | +| view: list `responsive`/`performance`, form `data`/`defaultSort`/`aria` | no renderer read any of them; list `aria`/`data` stay live (#3896 close-out) | +| `dashboard.aria` / `.performance` / widget `performance` (+ `PerformanceConfigSchema`) | no renderer applied them; virtual scrolling is the live top-level `virtualScroll` (#3896 close-out) | +| `agent.knowledge` (+ `AIKnowledgeSchema`) | declaring sources/indexes never scoped retrieval — `search_knowledge` takes `sourceIds` from the LLM's tool-call arguments (#3896 close-out) | +| `skill.triggerPhrases` | phrases were never matched; activation is `triggerConditions` + the agent's `skills[]` allowlist (#3896 close-out) | | `DEFAULT_DISPATCHER_ROUTES` | dead route table | | Aspirational config on Theme / Translation / Webhook | still-dead after #3494 | | `ChartInteraction.zoom` / `.clickAction` | never implemented (#3752) | @@ -1024,7 +1033,7 @@ objectui commits on top of the pin 16.1.0 shipped. six remain. Watch for a legacy-only whitelist stripping to deny-all. - **Metadata renames:** run `os migrate meta --from ` — it applies `execute`→`target`, `conditionalRequired`→`requiredWhen`, - `knowledge.topics`→`knowledge.sources`, sharing `full`→`edit`, and every + the retired-key deletions (`os migrate meta` handles all of them), sharing `full`→`edit`, and every earlier step you skipped, in one pass. - **Flows:** declare `runAs: 'system'` on any flow that reacts to system writes and must act beyond one user's grants; otherwise ensure the trigger supplies a @@ -1053,6 +1062,12 @@ objectui commits on top of the pin 16.1.0 shipped. metadata (`os migrate meta` does it; none ever had an effect). To gate a tool, gate the underlying action (`action.requiredPermissions`); to withdraw one, remove it from the skills/agents that reference it. +- **Close-out sweep:** run `os migrate meta` once more — it also strips + `action.shortcut`/`bulkEnabled`, `flow.active`/`template`/node + `outputSchema`/`errorHandling.fallbackNodeId`, the inert view keys, + dashboard/widget `aria`/`performance`, `agent.knowledge` and + `skill.triggerPhrases`. If you set `flow.active: false` expecting a flow to + stop, set `status: 'obsolete'` — the flag never worked. - **Required fields:** run `os migrate meta` — it stamps `storage: { notNull: true }` onto every pre-17 `required: true` field so your columns keep their exact constraints. New fields: `required: true` alone diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index 279d0aecc4..0d2adebb5d 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -132,13 +132,14 @@ ADR-0113 splits the `required` tri-binding: post-17, `required` is ONLY the writ On the wire contract it also retires the `/analytics/query` request ENVELOPE (#3878): `AnalyticsQueryRequestSchema` used to describe `{ cube, query: {...}, format }` — the dialect of the retired degraded analytics shim (#3891) that the real engine never understood (an envelope body inferred a column-less cube and died as an SQL syntax error). The canonical request body is now the BARE AnalyticsQuery — `cube` + `measures` at the top level — which is what every real caller already sends; the schema tombstones `query`/`format`, and the dispatcher entry validates bodies and answers 400 with the prescription. No stored metadata carries this shape (it was HTTP-only), so the change is two semantic TODOs for API callers rather than a stack conversion. +The close-out sweep finishes the enforce-or-remove worklist across the remaining types: action `shortcut`/`bulkEnabled` (no keydown path; the multi-select toolbar reads the view's bulkActions), flow `active`/`template`/node `outputSchema`/errorHandling `fallbackNodeId` (`active: false` never stopped a flow — `status` is the enforced lifecycle; faults route via per-node fault edges), the inert view keys (list `responsive`/`performance`, form `data`/`defaultSort`/`aria` — list aria/data stay live), dashboard and widget `aria`/`performance`, `agent.knowledge` (declaring sources never scoped retrieval — absorbs the former topics→sources rename), and `skill.triggerPhrases` (phrases were never matched; routing is triggerConditions + the agent allowlist). All pure lossless deletes, each tombstoned at its schema with the prescription. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | |---|---|---|---| | `action-execute-to-target` | `action.execute` | action key 'execute' → 'target' (the deprecated handler alias, #3713) | retired — `migrate meta` only | | `field-conditionalRequired-to-requiredWhen` | `field.conditionalRequired` | field key 'conditionalRequired' → 'requiredWhen' (the deprecated predicate alias, #3754) | retired — `migrate meta` only | -| `agent-knowledge-topics-to-sources` | `agent.knowledge.topics` | agent knowledge key 'topics' → 'sources' (the deprecated RAG-source alias, #1891) | retired — `migrate meta` only | | `agent-tools-to-skills` | `agent.tools` | agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894) | retired — `migrate meta` only | | `sharing-rule-access-level-full-to-edit` | `sharingRule.accessLevel` | sharing-rule accessLevel 'full' → 'edit' (#3865 — `full` never granted more than `edit`) | live — protocol 17 loader accepts the old shape | | `flow-node-crud-object-alias` | `flow.node.config.objectName` | CRUD flow-node config key 'object' → 'objectName' (#3796 — `readAliasedConfig` shim graduation) | live — protocol 17 loader accepts the old shape | @@ -147,6 +148,12 @@ On the wire contract it also retires the `/analytics/query` request ENVELOPE (#3 | `permission-rls-priority-removed` | `permission.rowLevelSecurity.priority` | RLS-policy key 'priority' removed (#3896 audit — policies OR-combine, so the promised conflict-resolution semantics cannot exist; dropping it changes no outcome) | retired — `migrate meta` only | | `tool-inert-authoring-keys-removed` | `tool.category / tool.permissions / tool.active / tool.builtIn` | tool keys 'category'/'permissions'/'active'/'builtIn' removed (#3896 close-out — authorable and inert; permissions gated nothing, active:false withdrew nothing) | retired — `migrate meta` only | | `field-required-notnull-explicit` | `object.fields.*.required / object.fields.*.storage.notNull` | required fields gain explicit 'storage.notNull: true' (ADR-0113 — pre-17 'required' implied the column constraint; post-17 it is only the write contract) | retired — `migrate meta` only | +| `action-inert-keys-removed` | `action.shortcut / action.bulkEnabled` | action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | retired — `migrate meta` only | +| `flow-inert-keys-removed` | `flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId` | flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle) | retired — `migrate meta` only | +| `view-inert-keys-removed` | `view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria` | view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live) | retired — `migrate meta` only | +| `dashboard-inert-keys-removed` | `dashboard.aria / dashboard.performance / dashboard.widgets[].performance` | dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them) | retired — `migrate meta` only | +| `agent-knowledge-removed` | `agent.knowledge` | agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level) | retired — `migrate meta` only | +| `skill-trigger-phrases-removed` | `skill.triggerPhrases` | skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection) | retired — `migrate meta` only | ### Semantic (delegated to you, with acceptance criteria) diff --git a/examples/app-showcase/src/automation/flows/index.ts b/examples/app-showcase/src/automation/flows/index.ts index 49176ba157..8fa2e28b1f 100644 --- a/examples/app-showcase/src/automation/flows/index.ts +++ b/examples/app-showcase/src/automation/flows/index.ts @@ -587,7 +587,6 @@ export const NotifyOwnerSubflow = defineFlow({ label: 'Notify Owner (reusable subflow)', description: 'Reusable subflow: notifies a record owner. Invoked by other flows via a subflow node.', type: 'autolaunched', - template: true, variables: [ { name: 'ownerId', type: 'text', isInput: true }, { name: 'message', type: 'text', isInput: true }, @@ -672,7 +671,6 @@ export const ClosureSignoffSubflow = defineFlow({ label: 'Closure Sign-off (approval subflow)', description: 'Reusable subflow: requests a manager sign-off and outputs the decision. Demonstrates approval inside a subflow (nested durable pause).', type: 'autolaunched', - template: true, variables: [ { name: 'reason', type: 'text', isInput: true }, { name: 'decision', type: 'text', isOutput: true }, @@ -1183,7 +1181,6 @@ export const OneTaskSignoffSubflow = defineFlow({ label: 'One Task Sign-off (per-item subflow)', description: 'Reusable subflow: requests sign-off on a single task. Invoked per item by the batch sign-off map.', type: 'autolaunched', - template: true, variables: [{ name: 'decision', type: 'text', isOutput: true }], nodes: [ { id: 'start', type: 'start', label: 'Start' }, diff --git a/packages/cli/src/utils/lint-liveness-properties.test.ts b/packages/cli/src/utils/lint-liveness-properties.test.ts index 1701287557..2500ab101b 100644 --- a/packages/cli/src/utils/lint-liveness-properties.test.ts +++ b/packages/cli/src/utils/lint-liveness-properties.test.ts @@ -68,29 +68,21 @@ describe('lintLivenessProperties', () => { // block. These run against the REAL ledgers, so they double as contract // tests for those markings. - it('warns on flow.errorHandling.fallbackNodeId (engine uses fault edges)', () => { - const findings = lintLivenessProperties({ - flows: [{ name: 'f1', errorHandling: { fallbackNodeId: 'n2' } }], - }); - const f = findings.find((x) => x.message.includes('errorHandling.fallbackNodeId')); - expect(f).toBeDefined(); - expect(f!.where).toBe("flow 'f1'"); - }); - - it('fans out array containers: flow.nodes[].outputSchema warns once per flow', () => { + // flow.errorHandling.fallbackNodeId and nodes[].outputSchema left the warn + // list with the #3896 close-out sweep: the keys were REMOVED from the + // schema (retiredKey tombstones carry the prescription at parse), so the + // advisory warn's job is done by a hard error and the ledger entries this + // lint keyed on are gone. Same shape as tool.permissions below. + it('retired flow keys no longer warn — the strict parse owns them now', () => { const findings = lintLivenessProperties({ flows: [{ name: 'f1', - nodes: [ - { id: 'n1' }, - { id: 'n2', outputSchema: { type: 'object' } }, - { id: 'n3', outputSchema: { type: 'object' } }, - ], + errorHandling: { fallbackNodeId: 'n2' }, + nodes: [{ id: 'n2', outputSchema: { type: 'object' } }], }], }); - const hits = findings.filter((x) => x.message.includes('nodes.outputSchema')); - expect(hits.length).toBe(1); // one finding per (flow, path), not per node - expect(hits[0].where).toBe("flow 'f1'"); + expect(findings.some((x) => x.message.includes('errorHandling.fallbackNodeId'))).toBe(false); + expect(findings.some((x) => x.message.includes('nodes.outputSchema'))).toBe(false); }); it('warns on an experimental prop with no authorWarn of its own (agent.memory)', () => { @@ -144,20 +136,16 @@ describe('lintLivenessProperties', () => { // ── view (#2998 Track B) ────────────────────────────────────────────────── - it('warns on dead list-level responsive config (view ledger, list.responsive)', () => { - const findings = lintLivenessProperties({ - views: [{ object: 'task', list: { type: 'grid', responsive: { breakpoint: 'md' } } }], - }); - const f = findings.find((x) => x.message.includes('list.responsive')); - expect(f).toBeDefined(); - expect(f!.where).toBe("view 'task'"); // container binds via `object`, not `name` - expect(f!.hint.length).toBeGreaterThan(0); - }); - - it('warns on form.data and form.defaultSort but not on live form config', () => { + // list.responsive / form.defaultSort left the warn list with the #3896 + // close-out sweep (keys REMOVED, strict parse owns them). form.data is the + // sweep's one CORRECTION: the removal attempt broke the build — defineForm + // writes data.provider='schema' on every metadata form — so its ledger + // entry flipped to live and it must not warn either. + it('retired/corrected view keys no longer warn', () => { const findings = lintLivenessProperties({ views: [{ object: 'task', + list: { type: 'grid', responsive: { breakpoint: 'md' } }, form: { type: 'wizard', sections: [{ fields: ['title'] }], @@ -167,9 +155,9 @@ describe('lintLivenessProperties', () => { }], }); const msgs = paths(findings); - expect(msgs.some((m) => m.includes('form.data'))).toBe(true); - expect(msgs.some((m) => m.includes('form.defaultSort'))).toBe(true); - expect(msgs.some((m) => m.includes('form.sections') || m.includes('form.type'))).toBe(false); + expect(msgs.some((m) => m.includes('list.responsive'))).toBe(false); + expect(msgs.some((m) => m.includes('form.defaultSort'))).toBe(false); + expect(msgs.some((m) => m.includes('form.data'))).toBe(false); }); it('stays silent on a clean grid view', () => { diff --git a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts index 7aaa9723eb..fa1e2aed99 100644 --- a/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/en.metadata-forms.generated.ts @@ -936,14 +936,6 @@ export const enMetadataForms: NonNullable = { globalFilters: { label: "Global Filters", helpText: "Filters applied to all widgets" - }, - aria: { - label: "Aria", - helpText: "Accessibility labels" - }, - performance: { - label: "Performance", - helpText: "Caching and optimization config" } } }, @@ -1151,14 +1143,6 @@ export const enMetadataForms: NonNullable = { label: "Disabled", helpText: "CEL expression: disable when condition is true" }, - shortcut: { - label: "Shortcut", - helpText: "Keyboard shortcut (e.g., \"Ctrl+S\", \"Cmd+Enter\")" - }, - bulkEnabled: { - label: "Bulk Enabled", - helpText: "Allow applying to multiple selected records" - }, ai: { label: "Ai", helpText: "AI exposure (opt-in): set ai.exposed=true and write ai.description (≥40 chars) to make this callable by agents." @@ -1347,10 +1331,6 @@ export const enMetadataForms: NonNullable = { label: "Type", helpText: "How the flow starts (autolaunched, record_change, schedule, screen, api)" }, - template: { - label: "Template", - helpText: "Is this a reusable subflow (can be called from other flows)" - }, description: { label: "Description", helpText: "What this flow does" @@ -1622,10 +1602,6 @@ export const enMetadataForms: NonNullable = { label: "Skills", helpText: "Skill names (Agent→Skill→Tool architecture)" }, - knowledge: { - label: "Knowledge", - helpText: "RAG knowledge access configuration" - }, access: { label: "Access", helpText: "User IDs or role names who can chat with this agent" @@ -1720,10 +1696,6 @@ export const enMetadataForms: NonNullable = { label: "Tools", helpText: "Tool names (supports wildcard: action_*)" }, - triggerPhrases: { - label: "Trigger Phrases", - helpText: "Natural language phrases that activate this skill" - }, triggerConditions: { label: "Trigger Conditions", helpText: "Programmatic conditions (e.g., objectName == \"case\")" diff --git a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts index cb7ea495b1..2971e5b629 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.metadata-forms.generated.ts @@ -936,14 +936,6 @@ export const esESMetadataForms: NonNullable = globalFilters: { label: "Filtros globales", helpText: "Filtros aplicados a todos los widgets" - }, - aria: { - label: "Accesibilidad", - helpText: "Etiquetas de accesibilidad" - }, - performance: { - label: "Rendimiento", - helpText: "Configuración de caché y optimización" } } }, @@ -1151,14 +1143,6 @@ export const esESMetadataForms: NonNullable = label: "Deshabilitado", helpText: "Expresión CEL: desactivar cuando la condición sea true" }, - shortcut: { - label: "Atajo", - helpText: "Atajo de teclado (p. ej., \"Ctrl+S\", \"Cmd+Enter\")" - }, - bulkEnabled: { - label: "Acción masiva", - helpText: "Permite aplicar a varios registros seleccionados" - }, ai: { label: "Ai", helpText: "AI exposure (opt-in): set ai.exposed=true and write ai.description (≥40 chars) to make this callable by agents." @@ -1347,10 +1331,6 @@ export const esESMetadataForms: NonNullable = label: "Tipo", helpText: "Cómo se inicia el flujo (autolaunched, record_change, schedule, screen, api)" }, - template: { - label: "Plantilla", - helpText: "Indica si es un subflujo reutilizable (puede llamarse desde otros flujos)" - }, description: { label: "Descripción", helpText: "Qué hace este flujo" @@ -1622,10 +1602,6 @@ export const esESMetadataForms: NonNullable = label: "Habilidades", helpText: "Nombres de skill (arquitectura Agent→Skill→Tool)" }, - knowledge: { - label: "Conocimiento", - helpText: "Configuración de acceso a conocimiento RAG" - }, access: { label: "Acceso", helpText: "IDs de usuario o nombres de rol que pueden chatear con este agente" @@ -1720,10 +1696,6 @@ export const esESMetadataForms: NonNullable = label: "Herramientas", helpText: "Nombres de herramienta (admite comodín: action_*)" }, - triggerPhrases: { - label: "Frases disparadoras", - helpText: "Frases de lenguaje natural que activan esta skill" - }, triggerConditions: { label: "Condiciones disparadoras", helpText: "Condiciones programáticas (p. ej., objectName == \"case\")" diff --git a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts index 0a8189b86f..2d397e42bf 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.metadata-forms.generated.ts @@ -936,14 +936,6 @@ export const jaJPMetadataForms: NonNullable = globalFilters: { label: "グローバルフィルター", helpText: "全ウィジェットに適用するフィルター" - }, - aria: { - label: "アクセシビリティ", - helpText: "アクセシビリティラベル" - }, - performance: { - label: "パフォーマンス", - helpText: "キャッシュと最適化設定" } } }, @@ -1151,14 +1143,6 @@ export const jaJPMetadataForms: NonNullable = label: "無効", helpText: "CEL 式: 条件が true の場合に無効化" }, - shortcut: { - label: "ショートカット", - helpText: "キーボードショートカット(例: \"Ctrl+S\", \"Cmd+Enter\")" - }, - bulkEnabled: { - label: "一括操作有効", - helpText: "選択した複数レコードへの適用を許可" - }, ai: { label: "Ai", helpText: "AI exposure (opt-in): set ai.exposed=true and write ai.description (≥40 chars) to make this callable by agents." @@ -1347,10 +1331,6 @@ export const jaJPMetadataForms: NonNullable = label: "型", helpText: "フローの開始方法(autolaunched, record_change, schedule, screen, api)" }, - template: { - label: "テンプレート", - helpText: "再利用可能なサブフローか(他のフローから呼び出し可)" - }, description: { label: "説明", helpText: "このフローの処理内容" @@ -1622,10 +1602,6 @@ export const jaJPMetadataForms: NonNullable = label: "スキル", helpText: "スキル名(Agent→Skill→Tool アーキテクチャ)" }, - knowledge: { - label: "ナレッジ", - helpText: "RAG ナレッジアクセス設定" - }, access: { label: "アクセス", helpText: "このエージェントとチャット可能なユーザー ID またはロール名" @@ -1720,10 +1696,6 @@ export const jaJPMetadataForms: NonNullable = label: "ツール", helpText: "ツール名(ワイルドカード対応: action_*)" }, - triggerPhrases: { - label: "トリガーフレーズ", - helpText: "このスキルを起動する自然言語フレーズ" - }, triggerConditions: { label: "トリガー条件", helpText: "プログラム条件(例: objectName == \"case\")" diff --git a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts index 0cf9568d0e..bc567dd234 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.metadata-forms.generated.ts @@ -936,14 +936,6 @@ export const zhCNMetadataForms: NonNullable = globalFilters: { label: "全局筛选", helpText: "应用到所有组件的筛选条件" - }, - aria: { - label: "无障碍", - helpText: "无障碍标签" - }, - performance: { - label: "性能", - helpText: "懒加载、虚拟滚动、缓存等" } } }, @@ -1151,14 +1143,6 @@ export const zhCNMetadataForms: NonNullable = label: "禁用", helpText: "CEL 表达式:满足条件时禁用" }, - shortcut: { - label: "快捷键", - helpText: "键盘快捷键(如 \"Ctrl+S\"、\"Cmd+Enter\")" - }, - bulkEnabled: { - label: "批量启用", - helpText: "允许对多条选中记录执行" - }, ai: { label: "Ai", helpText: "AI exposure (opt-in): set ai.exposed=true and write ai.description (≥40 chars) to make this callable by agents." @@ -1347,10 +1331,6 @@ export const zhCNMetadataForms: NonNullable = label: "类型", helpText: "流程如何启动(autolaunched / record_change / schedule / screen / api)" }, - template: { - label: "模板", - helpText: "是否为可复用子流程(可被其他流程调用)" - }, description: { label: "描述", helpText: "此流程做什么" @@ -1622,10 +1602,6 @@ export const zhCNMetadataForms: NonNullable = label: "技能", helpText: "技能名称(Agent→Skill→Tool 架构)" }, - knowledge: { - label: "知识", - helpText: "RAG 知识访问配置" - }, access: { label: "访问", helpText: "可以与此代理对话的用户 ID 或角色名" @@ -1720,10 +1696,6 @@ export const zhCNMetadataForms: NonNullable = label: "工具", helpText: "工具名(支持通配符:action_*)" }, - triggerPhrases: { - label: "触发短语", - helpText: "激活此技能的自然语言短语" - }, triggerConditions: { label: "触发条件", helpText: "程序化条件(如 objectName == \"case\")" diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index c1841144d3..c6b6f0c95c 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1837,7 +1837,6 @@ "validateObjectNamespacePrefix (function)" ], "./ai": [ - "AIKnowledgeSchema (const)", "AIModelConfigSchema (const)", "AIUsageRecord (type)", "AIUsageRecordSchema (const)", @@ -3348,8 +3347,6 @@ "PageVariableSchema (const)", "PaginationConfig (type)", "PaginationConfigSchema (const)", - "PerformanceConfig (type)", - "PerformanceConfigSchema (const)", "PersistStorage (type)", "PersistStorageSchema (const)", "PinchGestureConfig (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 9f5d4883a6..e79f705434 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -1,9 +1,6 @@ { "description": "Ratchet of every AUTHORABLE key in the spec — what a metadata author may write, which for this platform IS the third-party API. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 §5.", "keys": [ - "ai/AIKnowledge:indexes", - "ai/AIKnowledge:sources", - "ai/AIKnowledge:topics [RETIRED]", "ai/AIModelConfig:maxTokens", "ai/AIModelConfig:model", "ai/AIModelConfig:provider", @@ -26,7 +23,7 @@ "ai/Agent:avatar", "ai/Agent:guardrails", "ai/Agent:instructions", - "ai/Agent:knowledge", + "ai/Agent:knowledge [RETIRED]", "ai/Agent:label", "ai/Agent:lifecycle", "ai/Agent:memory", @@ -299,7 +296,7 @@ "ai/Skill:surface", "ai/Skill:tools", "ai/Skill:triggerConditions", - "ai/Skill:triggerPhrases", + "ai/Skill:triggerPhrases [RETIRED]", "ai/SkillTriggerCondition:field", "ai/SkillTriggerCondition:operator", "ai/SkillTriggerCondition:value", @@ -769,7 +766,7 @@ "api/CreateFlowRequest:_packageId", "api/CreateFlowRequest:_packageVersion", "api/CreateFlowRequest:_provenance", - "api/CreateFlowRequest:active", + "api/CreateFlowRequest:active [RETIRED]", "api/CreateFlowRequest:description", "api/CreateFlowRequest:edges", "api/CreateFlowRequest:errorHandling", @@ -781,7 +778,7 @@ "api/CreateFlowRequest:runAs", "api/CreateFlowRequest:status", "api/CreateFlowRequest:successMessage", - "api/CreateFlowRequest:template", + "api/CreateFlowRequest:template [RETIRED]", "api/CreateFlowRequest:type", "api/CreateFlowRequest:variables", "api/CreateFlowRequest:version", @@ -2377,7 +2374,7 @@ "automation/Flow:_packageId", "automation/Flow:_packageVersion", "automation/Flow:_provenance", - "automation/Flow:active", + "automation/Flow:active [RETIRED]", "automation/Flow:description", "automation/Flow:edges", "automation/Flow:errorHandling", @@ -2389,7 +2386,7 @@ "automation/Flow:runAs", "automation/Flow:status", "automation/Flow:successMessage", - "automation/Flow:template", + "automation/Flow:template [RETIRED]", "automation/Flow:type", "automation/Flow:variables", "automation/Flow:version", @@ -2406,7 +2403,7 @@ "automation/FlowNode:id", "automation/FlowNode:inputSchema", "automation/FlowNode:label", - "automation/FlowNode:outputSchema", + "automation/FlowNode:outputSchema [RETIRED]", "automation/FlowNode:position", "automation/FlowNode:timeoutMs", "automation/FlowNode:type", @@ -7425,7 +7422,7 @@ "ui/Action:body", "ui/Action:bodyExtra", "ui/Action:bodyShape", - "ui/Action:bulkEnabled", + "ui/Action:bulkEnabled [RETIRED]", "ui/Action:component", "ui/Action:confirmText", "ui/Action:disabled", @@ -7449,7 +7446,7 @@ "ui/Action:requiredPermissions", "ui/Action:requiresFeature", "ui/Action:resultDialog", - "ui/Action:shortcut", + "ui/Action:shortcut [RETIRED]", "ui/Action:successMessage", "ui/Action:target", "ui/Action:type", @@ -7661,7 +7658,7 @@ "ui/Dashboard:_packageId", "ui/Dashboard:_packageVersion", "ui/Dashboard:_provenance", - "ui/Dashboard:aria", + "ui/Dashboard:aria [RETIRED]", "ui/Dashboard:columns", "ui/Dashboard:dateRange", "ui/Dashboard:description", @@ -7670,7 +7667,7 @@ "ui/Dashboard:header", "ui/Dashboard:label", "ui/Dashboard:name", - "ui/Dashboard:performance", + "ui/Dashboard:performance [RETIRED]", "ui/Dashboard:protection", "ui/Dashboard:refreshInterval", "ui/Dashboard:widgets", @@ -7899,11 +7896,11 @@ "ui/FormSection:visibleOn", "ui/FormSection:visibleWhen", "ui/FormView:allowSkip", - "ui/FormView:aria", + "ui/FormView:aria [RETIRED]", "ui/FormView:buttons", "ui/FormView:columns", "ui/FormView:data", - "ui/FormView:defaultSort", + "ui/FormView:defaultSort [RETIRED]", "ui/FormView:defaultTab", "ui/FormView:defaults", "ui/FormView:description", @@ -8077,9 +8074,9 @@ "ui/ListView:name", "ui/ListView:navigation", "ui/ListView:pagination", - "ui/ListView:performance", + "ui/ListView:performance [RETIRED]", "ui/ListView:resizable", - "ui/ListView:responsive", + "ui/ListView:responsive [RETIRED]", "ui/ListView:rowActions", "ui/ListView:rowColor", "ui/ListView:rowHeight", @@ -8182,9 +8179,9 @@ "ui/ObjectListView:name", "ui/ObjectListView:navigation", "ui/ObjectListView:pagination", - "ui/ObjectListView:performance", + "ui/ObjectListView:performance [RETIRED]", "ui/ObjectListView:resizable", - "ui/ObjectListView:responsive", + "ui/ObjectListView:responsive [RETIRED]", "ui/ObjectListView:rowActions", "ui/ObjectListView:rowColor", "ui/ObjectListView:rowHeight", @@ -8305,12 +8302,6 @@ "ui/PageVariable:type", "ui/PaginationConfig:pageSize", "ui/PaginationConfig:pageSizeOptions", - "ui/PerformanceConfig:cacheStrategy", - "ui/PerformanceConfig:debounceMs", - "ui/PerformanceConfig:lazyLoad", - "ui/PerformanceConfig:pageSize", - "ui/PerformanceConfig:prefetch", - "ui/PerformanceConfig:virtualScroll", "ui/PinchGestureConfig:maxScale", "ui/PinchGestureConfig:minScale", "ui/PluralRule:few", @@ -8570,7 +8561,7 @@ "ui/WidgetManifest:license", "ui/WidgetManifest:lifecycle", "ui/WidgetManifest:name", - "ui/WidgetManifest:performance", + "ui/WidgetManifest:performance [RETIRED]", "ui/WidgetManifest:properties", "ui/WidgetManifest:screenshots", "ui/WidgetManifest:tags", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 1faebb7fad..6eee13a9d9 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1,7 +1,6 @@ { "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema — remove a key ONLY for a deliberate retirement. See #2978.", "schemas": [ - "ai/AIKnowledge", "ai/AIModelConfig", "ai/AIUsageRecord", "ai/Agent", @@ -1709,7 +1708,6 @@ "ui/PageType", "ui/PageVariable", "ui/PaginationConfig", - "ui/PerformanceConfig", "ui/PersistStorage", "ui/PinchGestureConfig", "ui/PluralRule", diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 2db316063a..84ced134bb 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -465,20 +465,20 @@ EOF |---|---|---|---|---|---| | object | 40 | – | 0 | 1 | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) + tags/active/abstract REMOVED (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; `enable.trash`/`mru` REMOVED (#2377 close-out) — tombstoned in the now-`.strict()` ObjectCapabilities; `isSystem` + `enable.searchable` CORRECTED to live (#2377 — sharing default-model + global-search opt-out; 2026-06 audit missed both readers); `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | | field | 55 | – | 0 | – | healthy — full dead set (vectorConfig/fileAttachmentConfig/dependencies, then referenceFilters/columnName/index) REMOVED (#2377); columnName also dropped the ADR-0062 D7 lint + StorageNameMapping column helpers | -| flow | 26 | – | 5 | – | dead = description/template/nodes.outputSchema/errorHandling.fallbackNodeId (engine uses fault edges) + `active` CORRECTED to dead 2026-07 (deprecated no-op — `status` is what gates binding/execution since 497bda853; the file `_note` claiming otherwise is fixed) | -| action | 34 | 0 | 2 | – | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live for real since objectui#2863 (six surfaces); `shortcut` + `bulkEnabled` CORRECTED to dead 2026-07 — registered into ActionEngine but their accessors have no non-test caller (#3686 sweep); `undoable` CORRECTED to live 2026-07 — understated, two objectui readers gate the toast's Undo and the record restore (#3714) | -| hook | 11 | – | 2 | – | model-healthy; only label/description dead (benign) | +| flow | 26 | – | 1 | – | `active`/`template`/nodes.`outputSchema`/errorHandling.`fallbackNodeId` REMOVED 2026-07-30 (#3896 close-out sweep — `active: false` never stopped a flow, `status` is the enforced lifecycle; faults route via per-node fault edges); remaining dead = `description`, KEPT deliberately: docs-shaped, exempt from enforce-or-remove | +| action | 34 | 0 | 0 | – | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live since objectui#2863; `undoable` CORRECTED to live (#3714); `shortcut` + `bulkEnabled` REMOVED 2026-07-30 (#3896 close-out sweep — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | +| hook | 11 | – | 2 | – | model-healthy; label/description dead but KEPT deliberately (2026-07-30 sweep) — docs-shaped annotation fields, exempt from enforce-or-remove | | permission | 29 | – | 4 | – | CRUD/FLS/RLS live; dead `contextVariables` REMOVED (ADR-0105 D11 — RLS resolves only the `current_user.*` built-ins plus runtime-staged `rlsMembership` sets). 2026-07-30 security-subset re-verification (all 33 entries `verifiedAt`-stamped): `rowLevelSecurity.enabled` was live-with-wrong-evidence and UNREAD — a disabled policy kept contributing its OR-branch grant; ENFORCED same day in rls-compiler (`getApplicablePolicies`), the `positions` ADR-0049 resolution repeated. `rowLevelSecurity.priority` CORRECTED to dead+authorWarn — semantically void under OR-combination (no conflict exists to order), a REMOVE candidate. `rls.label`/`description`/`tags` CORRECTED to dead (benign display, no consumer in either repo). `tabPermissions` was UNDERSTATED ("only hidden read" → the rank merge reads all four values; me-apps dogfood test exercises it). `allowExport` re-verified TRUE end-to-end (server-side 403 gate, not just the /me projection) | | position | 4 | – | – | – | (role's ADR-0090 successor) fully live; all 4 `verifiedAt`-stamped 2026-07-30 | -| agent | 13 | 5 | 1 | – | dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377) — only `planning.maxIterations` live; autonomy tier experimental; `knowledge` CORRECTED to dead 2026-07 — `search_knowledge` takes `sourceIds` from the LLM's tool-call args, never from the agent record (#1878 §3 recheck) | +| agent | 13 | 4 | 0 | – | dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377); autonomy tier experimental; `knowledge` REMOVED 2026-07-30 (#3896 close-out sweep — declaring sources never scoped retrieval; AIKnowledgeSchema removed with it, the topics→sources rename absorbed pre-release) | | tool | 5 | 1 | 0 | – | the inert authoring surface is now REMOVED, not merely marked: `category`/`permissions`/`active`/`builtIn` retired 2026-07-30 (#3896 close-out) after `requiresConfirmation` set the precedent (#3715, ADR-0033 §2). `permissions` promised an invocation gate nothing enforced and `active:false` withdrew nothing — false compliance, same shape as rls.enabled. The `.strict()` ToolSchema rejects each retired key with its prescription; the `tool-inert-authoring-keys-removed` conversion strips them from authored sources | -| skill | 8 | – | 1 | – | `permissions` REMOVED 2026-07 (never gated anything — owner call was prune, #3704); `triggerPhrases` CORRECTED to dead — phrases are never matched against user messages; activation is `triggerConditions` + the agent's `skills[]` + explicit /skill-name pinning (#3686 sweep) | +| skill | 8 | – | 0 | – | `permissions` REMOVED 2026-07 (#3704); `triggerPhrases` REMOVED 2026-07-30 (#3896 close-out sweep — phrases were never matched; activation is `triggerConditions` + the agent's `skills[]` + /skill-name pinning) | | dataset | 19 | – | 0 | – | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | 16 | – | – | 1 | fully live + one planned | -| view | 70 | 0 | 5 | – | list/form drilled via `children` (#2998 Track B); dead = list.{responsive,performance} + form.{data,defaultSort,aria}, all but aria authorWarn'd; form.{buttons,defaults} now live — objectui ObjectForm folds them onto its flat props (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification (submitBehavior, sharing.lockedBy, list ViewData providers, and the ADR-0021 chart shape — all live now); level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | +| view | 70 | 0 | 0 | – | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{data,defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification; level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | -| report | 20 | 0 | 2 | – | dataset-bound (ADR-0021); dead = aria + performance (perf authorWarn'd); audit-era `chart` DEAD superseded — DatasetReportChart plots `chart.xAxis`/`yAxis` via useDatasetRows (framework#1890 / #3441), `groupBy` stays experimental (describe marker) | -| dashboard | 18 | 0 | 2 | – | ADR-0021 dataset widgets (WidgetConfigPanel + DashboardRenderer migrated #3251; DashboardWidgetSchema `.strict()`); dead = aria + performance (perf authorWarn'd); audit-era `globalFilters`/`dateRange` DEAD superseded — LIVE via framework#2501; `title`↔`label` fixed (objectui#2806) | +| report | 13 | 0 | 0 | – | dataset-bound (ADR-0021); the aria/performance LEDGER entries were stale — the keys left the schema in the report-liveness close-out; deleted 2026-07-30 as hygiene. Audit-era `chart` DEAD superseded (framework#1890 / #3441) | +| dashboard | 10 | 0 | 0 | – | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) | | webhook | 0 | 1 | 16 | – | **not a registered metadata type** — governed via the gate's spec-only schema override (`SPEC_ONLY_SCHEMAS`), not `getMetadataTypeSchema` (#3461/#3462). The ENTIRE authoring surface is dead: nothing materializes an authored `webhooks:` entry into a `sys_webhook` dispatcher row (#3461, enforce-or-remove pending). `url` carries the single per-webhook `authorWarn` (one no-op heads-up per artifact, not per-prop); `authentication` experimental (HMAC-`secret`-only); `isActive` unmarked (default(true)). Notes cite the sys_webhook column map as the future materializer's mapping table | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index 4a11fb4375..4a3b0d1f15 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -1,6 +1,6 @@ { "type": "action", - "_note": "ActionSchema. Seeded from docs/audits/2026-06-actionschema-property-liveness.md. Renderers live in objectui (evidence cited as prose, not framework paths); framework-side: service-ai action-tools, runtime body-runner/http-dispatcher. Containers (params/resultDialog/ai/aria) classified at top level — no divergent sub-statuses in the audit. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary.", + "_note": "ActionSchema. Seeded from docs/audits/2026-06-actionschema-property-liveness.md. Renderers live in objectui (evidence cited as prose, not framework paths); framework-side: service-ai action-tools, runtime body-runner/http-dispatcher. Containers (params/resultDialog/ai/aria) classified at top level — no divergent sub-statuses in the audit. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent.", "props": { "name": { "status": "live", @@ -107,20 +107,6 @@ "evidence": "objectui: components action-button/-group/-icon/-menu + app-shell DeclaredActionsBar + plugin-detail record-quick-actions — all six rendering surfaces gate the Button on the predicate", "note": "EVIDENCE CORRECTED 2026-07 (#1878 §3 recheck): previously cited the metadata-admin PREVIEW renderer, which only echoes the authored value. At that time the predicate was actually enforced on ONE of six surfaces (#1885 wired action:button alone) — the 'live' verdict was right for the wrong reason and hid a real silent no-op on the other five. objectui#2863 wired the rest; showcase specimen `showcase_archive_task` (#3643) dogfoods it." }, - "shortcut": { - "status": "dead", - "evidence": "registered into ActionEngine.shortcuts[] (objectui packages/core/src/actions/ActionEngine.ts:150) but getShortcuts()/handleShortcut() have no non-test caller and no keydown listener feeds them", - "authorWarn": true, - "authorHint": "Not wired — nothing dispatches keydown events to actions. objectui's keyboard stack (useKeyboardShortcuts / KeyboardProtocol) is hand-registered and never touches ActionEngine; register the key there and have its handler invoke the action by name.", - "note": "RE-VERIFIED 2026-07 (#3686 preview-claim sweep): the prior `live` verdict cited only a metadata-admin PREVIEW panel, which echoes what the author typed. Also declared in action.form.ts:69, so pruning would need a form change + a 4-locale i18n regen." - }, - "bulkEnabled": { - "status": "dead", - "evidence": "registered into ActionEngine (objectui ActionEngine.ts:151) but getBulkActions()/executeBulk() have no non-test caller; the multi-select toolbar is driven by the LIST VIEW's bulkActions/bulkActionDefs (plugin-grid ObjectGrid.tsx:1602-1612)", - "authorWarn": true, - "authorHint": "Not wired — the multi-select toolbar reads the list view's `bulkActions` / `bulkActionDefs`, not this flag. Declare the action on the view instead of setting bulkEnabled here.", - "note": "RE-VERIFIED 2026-07 (#3686 preview-claim sweep): the prior `live` verdict cited only a metadata-admin PREVIEW panel, which echoes what the author typed. Distinct from the live bulk fast-path (core/src/actions/bulkFastPath.ts executeBulkBatch), which never consults this flag. Also in action.form.ts:79." - }, "ai": { "status": "live", "evidence": "packages/services/service-ai/src/tools/action-tools.ts", diff --git a/packages/spec/liveness/agent.json b/packages/spec/liveness/agent.json index 8ec2682972..b6961f318a 100644 --- a/packages/spec/liveness/agent.json +++ b/packages/spec/liveness/agent.json @@ -1,6 +1,6 @@ { "type": "agent", - "_note": "AgentSchema. Seeded from docs/audits/2026-06-agentschema-property-liveness.md. agent-runtime.ts is the runtime consumer; AgentPreview is display-only. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary.", + "_note": "AgentSchema. Seeded from docs/audits/2026-06-agentschema-property-liveness.md. agent-runtime.ts is the runtime consumer; AgentPreview is display-only. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent. agent.knowledge (and AIKnowledgeSchema) removed; the topics→sources rename was absorbed into the removal pre-release.", "props": { "name": { "status": "live", @@ -64,13 +64,6 @@ "evidence": "packages/services/service-ai/src/routes/agent-access.ts:50", "note": "evaluateAgentAccess() — caller must hold ALL required permissions/roles; enforced at the chat route. #1884." }, - "knowledge": { - "status": "dead", - "evidence": "agent.form.ts + objectui AgentPreview.tsx:69 (both authoring-side echo only); the RAG path never reads it", - "authorWarn": true, - "authorHint": "Declaring `knowledge.sources` does NOT scope retrieval — the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments (cloud service-ai/src/tools/knowledge-tools.ts:96), never from the agent record. Restrict retrieval at the knowledge-service / source level instead.", - "note": "CORRECTED 2026-07 (#1878 §3 recheck): was marked live on the evidence of the metadata-admin PREVIEW renderer — an echo of what the author typed, not runtime consumption. The `topics`→`sources` NAMING drift is genuinely fixed (#1891 alias fold); the block is nonetheless behaviourally inert. Wiring it would also close a scope gap (an agent can search sources it was never scoped to) — tracked separately." - }, "lifecycle": { "status": "experimental", "evidence": "no runtime reader (StateMachine)", diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index 48c78b003a..8a12b26ea0 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -1,6 +1,6 @@ { "type": "dashboard", - "_note": "DashboardSchema (UI, ADR-0021 dataset-bound). Live path: objectui DashboardView → DashboardRenderer → DatasetWidget. Seeded from docs/audits/2026-06-dashboardschema-property-liveness.md and re-verified against objectui HEAD — several audit-era findings are superseded: the ADR-0021 widget migration shipped (Studio WidgetConfigPanel + DashboardRenderer on dataset/dimensions/values, framework#3251; DashboardWidgetSchema is now `.strict()`); `globalFilters`/`dateRange` are LIVE (dashboard-level filters, framework#2501); the `title`↔`label` drift is fixed (renderer falls back to `label`, objectui#2806); the undeclared widget props were reconciled (#1894). objectui paths cited as prose in `note` (not `evidence`). Framework provenance/lock fields auto-classify live (ADR-0010). Widget-level props are classified in the DashboardWidgetSchema subtree, not drilled here.", + "_note": "DashboardSchema (UI, ADR-0021 dataset-bound). Live path: objectui DashboardView → DashboardRenderer → DatasetWidget. Seeded from docs/audits/2026-06-dashboardschema-property-liveness.md and re-verified against objectui HEAD — several audit-era findings are superseded: the ADR-0021 widget migration shipped (Studio WidgetConfigPanel + DashboardRenderer on dataset/dimensions/values, framework#3251; DashboardWidgetSchema is now `.strict()`); `globalFilters`/`dateRange` are LIVE (dashboard-level filters, framework#2501); the `title`↔`label` drift is fixed (renderer falls back to `label`, objectui#2806); the undeclared widget props were reconciled (#1894). objectui paths cited as prose in `note` (not `evidence`). Framework provenance/lock fields auto-classify live (ADR-0010). Widget-level props are classified in the DashboardWidgetSchema subtree, not drilled here. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent.", "props": { "name": { "status": "live", @@ -41,16 +41,6 @@ "dateRange": { "status": "live", "note": "objectui: DashboardRenderer.tsx:279,988 — dashboard-level date-range filter (framework#2501). Supersedes the 2026-06 audit's DEAD finding." - }, - "aria": { - "status": "dead", - "note": "ARIA attributes — authorable (dashboard.form.ts) but no DashboardRenderer path applies them." - }, - "performance": { - "status": "dead", - "authorWarn": true, - "authorHint": "Dashboard performance tuning is not implemented — this config has no runtime consumer.", - "note": "Perf-optimization config — authorable but no renderer/runtime reads it (audit; re-verified — no consumer in plugin-dashboard)." } } } diff --git a/packages/spec/liveness/flow.json b/packages/spec/liveness/flow.json index ffca156e88..d488036387 100644 --- a/packages/spec/liveness/flow.json +++ b/packages/spec/liveness/flow.json @@ -1,6 +1,6 @@ { "type": "flow", - "_note": "FlowSchema. Seeded from docs/audits/2026-06-flowschema-property-liveness.md. Consumers: service-automation engine + node executors. runAs is marker-driven experimental (spec). CORRECTED 2026-07: the previous note claimed 'status/active gate nothing' — that was true when the audit ran but became FALSE with commit 497bda853 (2026-07-05, 'honor flow status for enable/disable'). `status` now gates binding AND execution; `active` remains a deprecated no-op.", + "_note": "FlowSchema. Seeded from docs/audits/2026-06-flowschema-property-liveness.md. Consumers: service-automation engine + node executors. runAs is marker-driven experimental (spec). CORRECTED 2026-07: the previous note claimed 'status/active gate nothing' — that was true when the audit ran but became FALSE with commit 497bda853 (2026-07-05, 'honor flow status for enable/disable'). `status` now gates binding AND execution; `active` remains a deprecated no-op. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent.", "props": { "name": { "status": "live", @@ -20,7 +20,8 @@ }, "description": { "status": "dead", - "evidence": "no reader either layer" + "evidence": "no reader either layer", + "note": "KEPT deliberately (2026-07-30 sweep): docs-shaped, not capability-shaped — an annotation field documents intent for the next reader (per ADR-0033, often a model) even without a runtime consumer. Exempt from enforce-or-remove; do not re-litigate." }, "version": { "status": "live", @@ -31,25 +32,12 @@ "evidence": "packages/services/service-automation/src/engine.ts:1374-1382 (obsolete/invalid -> flowEnabled=false), :1387 (gates activateFlowTrigger), :1704 + :3097 (execute-time 'Flow is disabled' guard)", "note": "RE-VERIFIED 2026-07 (#3686 preview-claim sweep): the prior `live` verdict cited only a metadata-admin PREVIEW panel, which echoes what the author typed. Verdict stands, evidence replaced: it is genuinely engine-consumed since 497bda853. The previously cited FlowPreview line number did not even contain a status read." }, - "active": { - "status": "dead", - "evidence": "spec already marks it '(Deprecated: use status)'; the only reader anywhere is a display fallback in objectui FlowPreview.tsx:157 (`d.status ?? (d.active ? ...)`)", - "authorWarn": true, - "authorHint": "Deprecated no-op — writing `active: false` does NOT stop a flow. Use `status: 'obsolete'` (or 'invalid') to unbind and disable it, `status: 'active'` to arm it.", - "note": "RE-VERIFIED 2026-07 (#3686 preview-claim sweep): the prior `live` verdict cited only a metadata-admin PREVIEW panel, which echoes what the author typed. Sharper than a plain no-op: the spec default is `false` while the engine treats an unset flow as enabled, so the key reads as if it disabled the flow when it does nothing." - }, "runAs": { "status": "live", "proof": "packages/qa/dogfood/test/flow-runas.dogfood.test.ts#flow-runas-identity", "evidence": "packages/services/service-automation/src/builtin/crud-nodes.ts: CRUD nodes pass the run identity to ObjectQL per flow.runAs; the engine sets context.runAs at run setup; resolveRunDataContext maps system->isSystem / user->trigger user (#1888)", "note": "ENFORCED 2026-06-24 (#1888, ADR-0049): was marker-driven experimental; the engine now switches the data-layer execution identity and restores the caller context. Proof-backed by the dogfood gate." }, - "template": { - "status": "dead", - "evidence": "no reader either layer", - "authorWarn": true, - "authorHint": "No reader in designer or engine — flagging a flow as a template/subflow does nothing. Invoke shared logic via a subflow node referencing the flow by name." - }, "type": { "status": "live", "evidence": "packages/services/service-automation/src/engine.ts:637", @@ -102,12 +90,6 @@ "evidence": "packages/services/service-automation/src/engine.ts", "note": "runtime-validated." }, - "outputSchema": { - "status": "dead", - "evidence": "declared, never validated", - "authorWarn": true, - "authorHint": "Declared but never validated — node outputs are NOT checked against this schema at runtime. Do not rely on it for contract enforcement." - }, "waitEventConfig": { "status": "live", "evidence": "packages/services/service-automation/src/engine.ts" @@ -144,12 +126,6 @@ "jitter": { "status": "live", "evidence": "packages/services/service-automation/src/engine.ts" - }, - "fallbackNodeId": { - "status": "dead", - "evidence": "engine uses per-node fault edges, not this", - "authorWarn": true, - "authorHint": "The engine routes failures via per-node fault edges, not errorHandling.fallbackNodeId — wire a fault edge from the failing node instead." } } } diff --git a/packages/spec/liveness/hook.json b/packages/spec/liveness/hook.json index c59e707427..d1dd157d58 100644 --- a/packages/spec/liveness/hook.json +++ b/packages/spec/liveness/hook.json @@ -2,18 +2,70 @@ "type": "hook", "_note": "HookSchema. Seeded from docs/audits/2026-06-hookschema-property-liveness.md — a model-healthy schema (near-total liveness). Consumers: objectql hook-binder/engine + runtime sandbox.", "props": { - "name": { "status": "live", "evidence": "packages/objectql/src/hook-binder.ts:182", "note": "registration key + log identity." }, - "object": { "status": "live", "evidence": "packages/objectql/src/hook-binder.ts:171", "note": "single/array/'*' wildcard honored." }, - "events": { "status": "live", "evidence": "packages/objectql/src/hook-binder.ts:171", "note": "per-event registration." }, - "body": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "body.{language,source,capabilities,timeoutMs} wired (L1/L2); body.memoryMb is advisory/soft under QuickJS." }, - "handler": { "status": "live", "evidence": "packages/objectql/src/hook-binder.ts:237", "note": "deprecated but fully wired; body takes precedence." }, - "priority": { "status": "live", "evidence": "packages/objectql/src/hook-binder.ts:177", "note": "genuinely orders hooks (lower first)." }, - "async": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "fire-and-forget, after* only." }, - "condition": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "CEL gate; falsy/error → skip." }, - "retryPolicy": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "{maxRetries,backoffMs} linear backoff." }, - "timeout": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "wall-clock abort, independent of body.timeoutMs." }, - "onError": { "status": "live", "evidence": "packages/objectql/src/hook-wrappers.ts", "note": "'log' suppresses+continues; 'abort' rethrows." }, - "label": { "status": "dead", "evidence": "pure docs, zero runtime readers (runtime uses only name)" }, - "description": { "status": "dead", "evidence": "pure docs, zero runtime readers; redundant with label" } + "name": { + "status": "live", + "evidence": "packages/objectql/src/hook-binder.ts:182", + "note": "registration key + log identity." + }, + "object": { + "status": "live", + "evidence": "packages/objectql/src/hook-binder.ts:171", + "note": "single/array/'*' wildcard honored." + }, + "events": { + "status": "live", + "evidence": "packages/objectql/src/hook-binder.ts:171", + "note": "per-event registration." + }, + "body": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "body.{language,source,capabilities,timeoutMs} wired (L1/L2); body.memoryMb is advisory/soft under QuickJS." + }, + "handler": { + "status": "live", + "evidence": "packages/objectql/src/hook-binder.ts:237", + "note": "deprecated but fully wired; body takes precedence." + }, + "priority": { + "status": "live", + "evidence": "packages/objectql/src/hook-binder.ts:177", + "note": "genuinely orders hooks (lower first)." + }, + "async": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "fire-and-forget, after* only." + }, + "condition": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "CEL gate; falsy/error → skip." + }, + "retryPolicy": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "{maxRetries,backoffMs} linear backoff." + }, + "timeout": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "wall-clock abort, independent of body.timeoutMs." + }, + "onError": { + "status": "live", + "evidence": "packages/objectql/src/hook-wrappers.ts", + "note": "'log' suppresses+continues; 'abort' rethrows." + }, + "label": { + "status": "dead", + "evidence": "pure docs, zero runtime readers (runtime uses only name)", + "note": "KEPT deliberately (2026-07-30 sweep): docs-shaped, not capability-shaped — an annotation field documents intent for the next reader even without a runtime consumer. Exempt from enforce-or-remove; do not re-litigate." + }, + "description": { + "status": "dead", + "evidence": "pure docs, zero runtime readers; redundant with label", + "note": "KEPT deliberately (2026-07-30 sweep): docs-shaped, not capability-shaped — an annotation field documents intent for the next reader even without a runtime consumer. Exempt from enforce-or-remove; do not re-litigate." + } } } diff --git a/packages/spec/liveness/report.json b/packages/spec/liveness/report.json index b4feda4747..32d88130b7 100644 --- a/packages/spec/liveness/report.json +++ b/packages/spec/liveness/report.json @@ -1,6 +1,6 @@ { "type": "report", - "_note": "ReportSchema (analytics report, ADR-0021 single-form). Live path: objectui ReportRenderer → DatasetReportRenderer (dataset-bound). Seeded from docs/audits/2026-06-reportschema-property-liveness.md and re-verified against objectui HEAD — the audit's `chart` = DEAD finding is superseded: DatasetReportRenderer's DatasetReportChart now plots `chart.xAxis`/`yAxis` (dataset dimension/measure) via useDatasetRows (framework#1890/#3441), so `chart` is LIVE (only its `groupBy` sub-key stays experimental, per its own describe marker). objectui paths cited as prose in `note` (not `evidence`, which resolves framework file:line). Framework provenance/lock fields (protection/_lock*) auto-classify live (ADR-0010).", + "_note": "ReportSchema (analytics report, ADR-0021 single-form). Live path: objectui ReportRenderer → DatasetReportRenderer (dataset-bound). Seeded from docs/audits/2026-06-reportschema-property-liveness.md and re-verified against objectui HEAD — the audit's `chart` = DEAD finding is superseded: DatasetReportRenderer's DatasetReportChart now plots `chart.xAxis`/`yAxis` (dataset dimension/measure) via useDatasetRows (framework#1890/#3441), so `chart` is LIVE (only its `groupBy` sub-key stays experimental, per its own describe marker). objectui paths cited as prose in `note` (not `evidence`, which resolves framework file:line). Framework provenance/lock fields (protection/_lock*) auto-classify live (ADR-0010). 2026-07-30: the aria/performance entries were STALE — the keys had already been removed from the schema in the report-liveness close-out; deleted as ledger hygiene.", "props": { "name": { "status": "live", @@ -55,16 +55,6 @@ "blocks": { "status": "live", "note": "objectui: joined-report sub-reports, each dataset-bound (dataset/rows/columns/values/runtimeFilter/chart) — DatasetReportRenderer joined branch." - }, - "aria": { - "status": "dead", - "note": "ARIA attributes — authorable (report.form.ts) but no report renderer applies them; the dataset renderer ignores `report.aria`." - }, - "performance": { - "status": "dead", - "authorWarn": true, - "authorHint": "Report performance tuning is not implemented — this config has no runtime consumer.", - "note": "Perf-optimization config — authorable but no renderer/runtime reads it (audit L; re-verified — no consumer in plugin-report)." } } } diff --git a/packages/spec/liveness/skill.json b/packages/spec/liveness/skill.json index 1bc349b904..2d45a91a8a 100644 --- a/packages/spec/liveness/skill.json +++ b/packages/spec/liveness/skill.json @@ -1,6 +1,6 @@ { "type": "skill", - "_note": "SkillSchema. Seeded from docs/audits/2026-06-skillschema-property-liveness.md. skill-registry.ts + agent-runtime.ts are the runtime consumers. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary.", + "_note": "SkillSchema. Seeded from docs/audits/2026-06-skillschema-property-liveness.md. skill-registry.ts + agent-runtime.ts are the runtime consumers. ⚠ EVIDENCE LIVES IN CLOUD/EE: the `packages/services/service-ai/...` paths cited below are the closed `@objectstack/service-ai` runtime in the CLOUD repo, NOT git-tracked framework code (the framework's own service-ai tree is a stale build artifact with no src/). These props are `live` because that cloud runtime consumes them; the OPEN framework edition does not — see content/docs/ai for the open/cloud boundary. 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent.", "props": { "name": { "status": "live", @@ -40,13 +40,6 @@ "status": "live", "evidence": "packages/services/service-ai/src/skill-registry.ts:93", "note": "inactive skills dropped." - }, - "triggerPhrases": { - "status": "dead", - "evidence": "cloud service-ai/src/skill-registry.ts:266 copies it into toSummary() and assistant-routes.ts:98,129 serve it — but nothing consumes the field; skill activation is triggerConditions + the agent's skills[] allowlist + explicit /skill-name pinning (skill-registry.ts:128-175, assistant-routes.ts:189-196)", - "authorWarn": true, - "authorHint": "Phrases are never matched against the user's message. Routing is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning; put routing intent in triggerConditions, and describe intent in description/instructions (the LLM does see those).", - "note": "RE-VERIFIED 2026-07 (#3686 preview-claim sweep): the prior `live` verdict cited only a metadata-admin PREVIEW panel, which echoes what the author typed. A runtime path does read it, but only to hand it back to API clients — a dead-end projection, not consumption." } } } diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index a4fe305745..125ee4dfef 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -1,110 +1,293 @@ { "type": "view", - "_note": "ViewSchema (object view container: list/form/listViews/formViews). Seeded from docs/audits/2026-06-viewschema-property-liveness.md (objectui consumer cross-reference) and re-verified against objectui@fb35e48 (2026-07-16) — several audit-era DEAD findings have since gone LIVE (submitBehavior, sharing.lockedBy, ViewData providers on the list path) and are classified from the current reads, not the stale audit lines. objectui paths cited as 'objectui: ' prose (not resolved against this repo). Sub-sub-key divergences (userActions.buttons, addRecord.mode/formView, tabs[].order) can't be drilled (one level only) — captured in the parent entry's note and tracked in the enforce-or-remove worklist (#2998 Track B / ADR-0049).", + "_note": "ViewSchema (object view container: list/form/listViews/formViews). Seeded from docs/audits/2026-06-viewschema-property-liveness.md (objectui consumer cross-reference) and re-verified against objectui@fb35e48 (2026-07-16) — several audit-era DEAD findings have since gone LIVE (submitBehavior, sharing.lockedBy, ViewData providers on the list path) and are classified from the current reads, not the stale audit lines. objectui paths cited as 'objectui: ' prose (not resolved against this repo). Sub-sub-key divergences (userActions.buttons, addRecord.mode/formView, tabs[].order) can't be drilled (one level only) — captured in the parent entry's note and tracked in the enforce-or-remove worklist (#2998 Track B / ADR-0049). 2026-07-30 (#3896 close-out sweep): the dead authoring keys were REMOVED — tombstoned at the schema with prescriptions (retiredKey) and stripped by the protocol-17 close-out conversions; entries deleted per the #3715 precedent. widget.performance (no ledger file of its own) was verified dead the corrected way — zero readers in either repo; objectui virtual scrolling reads the LIVE top-level virtualScroll — and removed with them.", "props": { "list": { "children": { - "name": { "status": "live", "evidence": "packages/spec/src/ui/view.zod.ts:1432", "note": "view identity — expandViewContainer key; objectui ListView/ViewTabBar." }, - "label": { "status": "live", "note": "objectui: view switcher tab + toolbar title (ListView.tsx / ViewTabBar)." }, - "type": { "status": "live", "note": "objectui: ListView.tsx routes grid/kanban/calendar/gantt/gallery/timeline/chart/tree renderers (audit L16)." }, - "data": { "status": "live", "note": "objectui: ListView.tsx:841-850 fetch effect supports ViewDataSchema provider modes ('value' inline, 'api'; gantt owns api-provided data at :476). Supersedes the 2026-06 audit's 'api/schema dead in list path'. The 'schema' provider remains form/metadata-admin-only — not honored on lists." }, - "columns": { "status": "live", "note": "objectui: ObjectGrid.tsx / ListView.tsx — legacy + rich column shapes (audit L15)." }, - "filter": { "status": "live", "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note)." }, - "sort": { "status": "live", "note": "objectui: ListView.tsx (audit L15)." }, - "searchableFields": { "status": "live", "note": "objectui: ListView.tsx (audit L15)." }, - "filterableFields": { "status": "live", "note": "objectui: ListView.tsx / UserFilters.tsx (audit L15)." }, - "resizable": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "striped": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "bordered": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "compactToolbar": { "status": "live", "note": "objectui: ListView.tsx:370 + toolbar gating at :1776-1988 (View-settings popover collapse). Post-audit key, verified objectui@fb35e48." }, - "selection": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "navigation": { "status": "live", "note": "objectui: ObjectView.tsx:987 — activeView.navigation ?? objectDef.navigation resolves the record-detail overlay (mode/size buckets, #2578/#2604, ADR-0085)." }, - "pagination": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "kanban": { "status": "live", "note": "objectui: KanbanImpl.tsx:682 — field-by-field wired (audit L16)." }, - "calendar": { "status": "live", "note": "objectui: calendar-view-renderer.tsx:147 (audit L16)." }, - "gantt": { "status": "live", "note": "objectui: ObjectGantt.tsx:236 (audit L16)." }, - "gallery": { "status": "live", "note": "objectui: ObjectGallery.tsx:209 (audit L16)." }, - "timeline": { "status": "live", "note": "objectui: ObjectTimeline.tsx:179 (audit L16)." }, - "chart": { "status": "live", "note": "objectui: dataset-first branches at ListView.tsx:1460 and ObjectView.tsx:1185 route the post-ADR-0021 shape (dataset/dimensions/values) into ObjectChart, which fetches the dataset definition and runs the semantic-layer query (ObjectChart.tsx:298-325); the legacy xAxisField/yAxisFields translation at ListView.tsx:1476/ObjectView.tsx:1209 is only the back-compat fallback. Supersedes audit L9 (its rec 1 — migrate the chart renderers — has since been implemented); exercised by the showcase task chart view." }, - "tree": { "status": "live", "note": "objectui: @object-ui/plugin-tree ObjectTree.tsx (spec CHANGELOG: 'Renderer ships in objectui @object-ui/plugin-tree')." }, - "description": { "status": "live", "note": "objectui: ListView.tsx:1728 renders it under the toolbar (gated by appearance.showDescription)." }, - "sharing": { "status": "live", "note": "objectui: ListView sharing tooltip incl. lockedBy (ListView.test.tsx:2276 'should display lockedBy in sharing tooltip when set'). Supersedes the audit's 'sharing.lockedBy dead' line." }, - "rowHeight": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "grouping": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "rowColor": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "hiddenFields": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "fieldOrder": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "rowActions": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "bulkActions": { "status": "live", "note": "objectui: live only via ListView.tsx:1318 remapping to ObjectGrid's real 'batchActions' key — a direct object-grid caller using bulkActions silently no-ops (audit L23 drift; normalize at one boundary per audit rec 3)." }, - "bulkActionDefs": { "status": "live", "note": "objectui: ListView.tsx:1343 forwards rich defs to ObjectGrid (BulkActionDialog). Post-audit key, verified objectui@fb35e48." }, - "virtualScroll": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "conditionalFormatting": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "inlineEdit": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "exportOptions": { "status": "live", "note": "objectui: ObjectGrid.tsx (audit L15)." }, - "userActions": { "status": "live", "note": "objectui: ListView.tsx:374 toolbarFlags (search/sort/filters/refresh/density/hide-fields/group/color + addRecordForm). Sub-key userActions.refresh drives the manual toolbar Refresh button (objectui#2634). Sub-key userActions.buttons has NO reader (audit L20, re-verified objectui@fb35e48) — dead sub-surface, enforce-or-remove worklist." }, - "appearance": { "status": "live", "note": "objectui: allowedVisualizations gates the view-type switcher (audit L15); showDescription gates ListView.tsx:1728." }, - "tabs": { "status": "live", "note": "objectui: TabBar.tsx — icon/visible/pinned/filter wired (audit L15). Sub-key tabs[].order is NOT used for sorting (audit L20) — dead sub-surface." }, - "addRecord": { "status": "live", "note": "objectui: ListView.tsx:374 (addRecord + userActions.addRecordForm drive the add-record flow). Sub-keys addRecord.mode / addRecord.formView have NO reader (audit L20, re-verified objectui@fb35e48) — dead sub-surface." }, - "showRecordCount": { "status": "live", "note": "objectui: ListView toolbar (audit L15)." }, - "allowPrinting": { "status": "live", "note": "objectui: ListView toolbar (audit L15)." }, - "emptyState": { "status": "live", "evidence": "packages/cli/src/utils/i18n-extract.ts", "note": "objectui: ListView empty-state panel (audit L15); CLI i18n extract emits _views..emptyState keys (spec CHANGELOG)." }, - "aria": { "status": "live", "note": "objectui: ListView.tsx:1712-1714 applies aria-label/-describedby/-live." }, - "responsive": { - "status": "dead", - "evidence": "no reader in either repo (audit L20; objectui@fb35e48 re-grepped 2026-07-16)", - "authorWarn": true, - "authorHint": "List-level responsive config does nothing — renderers adapt automatically. Remove the block." - }, - "performance": { - "status": "dead", - "evidence": "no reader in either repo (audit L20; objectui@fb35e48 re-grepped 2026-07-16)", - "authorWarn": true, - "authorHint": "List-level performance hints are not consumed — virtualScroll is the wired knob. Remove the block." - }, - "userFilters": { "status": "live", "note": "objectui: UserFilters.tsx — element/fields/tabs/showAllRecords wired (audit L15); object views narrow to ObjectUserFiltersSchema (ADR-0047 amendment, #2679/objectui#2338)." } + "name": { + "status": "live", + "evidence": "packages/spec/src/ui/view.zod.ts:1432", + "note": "view identity — expandViewContainer key; objectui ListView/ViewTabBar." + }, + "label": { + "status": "live", + "note": "objectui: view switcher tab + toolbar title (ListView.tsx / ViewTabBar)." + }, + "type": { + "status": "live", + "note": "objectui: ListView.tsx routes grid/kanban/calendar/gantt/gallery/timeline/chart/tree renderers (audit L16)." + }, + "data": { + "status": "live", + "note": "objectui: ListView.tsx:841-850 fetch effect supports ViewDataSchema provider modes ('value' inline, 'api'; gantt owns api-provided data at :476). Supersedes the 2026-06 audit's 'api/schema dead in list path'. The 'schema' provider remains form/metadata-admin-only — not honored on lists." + }, + "columns": { + "status": "live", + "note": "objectui: ObjectGrid.tsx / ListView.tsx — legacy + rich column shapes (audit L15)." + }, + "filter": { + "status": "live", + "note": "objectui: ListView.tsx client-side filter shaping (audit L15); no list-render prop reaches the server query (audit data-flow note)." + }, + "sort": { + "status": "live", + "note": "objectui: ListView.tsx (audit L15)." + }, + "searchableFields": { + "status": "live", + "note": "objectui: ListView.tsx (audit L15)." + }, + "filterableFields": { + "status": "live", + "note": "objectui: ListView.tsx / UserFilters.tsx (audit L15)." + }, + "resizable": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "striped": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "bordered": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "compactToolbar": { + "status": "live", + "note": "objectui: ListView.tsx:370 + toolbar gating at :1776-1988 (View-settings popover collapse). Post-audit key, verified objectui@fb35e48." + }, + "selection": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "navigation": { + "status": "live", + "note": "objectui: ObjectView.tsx:987 — activeView.navigation ?? objectDef.navigation resolves the record-detail overlay (mode/size buckets, #2578/#2604, ADR-0085)." + }, + "pagination": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "kanban": { + "status": "live", + "note": "objectui: KanbanImpl.tsx:682 — field-by-field wired (audit L16)." + }, + "calendar": { + "status": "live", + "note": "objectui: calendar-view-renderer.tsx:147 (audit L16)." + }, + "gantt": { + "status": "live", + "note": "objectui: ObjectGantt.tsx:236 (audit L16)." + }, + "gallery": { + "status": "live", + "note": "objectui: ObjectGallery.tsx:209 (audit L16)." + }, + "timeline": { + "status": "live", + "note": "objectui: ObjectTimeline.tsx:179 (audit L16)." + }, + "chart": { + "status": "live", + "note": "objectui: dataset-first branches at ListView.tsx:1460 and ObjectView.tsx:1185 route the post-ADR-0021 shape (dataset/dimensions/values) into ObjectChart, which fetches the dataset definition and runs the semantic-layer query (ObjectChart.tsx:298-325); the legacy xAxisField/yAxisFields translation at ListView.tsx:1476/ObjectView.tsx:1209 is only the back-compat fallback. Supersedes audit L9 (its rec 1 — migrate the chart renderers — has since been implemented); exercised by the showcase task chart view." + }, + "tree": { + "status": "live", + "note": "objectui: @object-ui/plugin-tree ObjectTree.tsx (spec CHANGELOG: 'Renderer ships in objectui @object-ui/plugin-tree')." + }, + "description": { + "status": "live", + "note": "objectui: ListView.tsx:1728 renders it under the toolbar (gated by appearance.showDescription)." + }, + "sharing": { + "status": "live", + "note": "objectui: ListView sharing tooltip incl. lockedBy (ListView.test.tsx:2276 'should display lockedBy in sharing tooltip when set'). Supersedes the audit's 'sharing.lockedBy dead' line." + }, + "rowHeight": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "grouping": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "rowColor": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "hiddenFields": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "fieldOrder": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "rowActions": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "bulkActions": { + "status": "live", + "note": "objectui: live only via ListView.tsx:1318 remapping to ObjectGrid's real 'batchActions' key — a direct object-grid caller using bulkActions silently no-ops (audit L23 drift; normalize at one boundary per audit rec 3)." + }, + "bulkActionDefs": { + "status": "live", + "note": "objectui: ListView.tsx:1343 forwards rich defs to ObjectGrid (BulkActionDialog). Post-audit key, verified objectui@fb35e48." + }, + "virtualScroll": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "conditionalFormatting": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "inlineEdit": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "exportOptions": { + "status": "live", + "note": "objectui: ObjectGrid.tsx (audit L15)." + }, + "userActions": { + "status": "live", + "note": "objectui: ListView.tsx:374 toolbarFlags (search/sort/filters/refresh/density/hide-fields/group/color + addRecordForm). Sub-key userActions.refresh drives the manual toolbar Refresh button (objectui#2634). Sub-key userActions.buttons has NO reader (audit L20, re-verified objectui@fb35e48) — dead sub-surface, enforce-or-remove worklist." + }, + "appearance": { + "status": "live", + "note": "objectui: allowedVisualizations gates the view-type switcher (audit L15); showDescription gates ListView.tsx:1728." + }, + "tabs": { + "status": "live", + "note": "objectui: TabBar.tsx — icon/visible/pinned/filter wired (audit L15). Sub-key tabs[].order is NOT used for sorting (audit L20) — dead sub-surface." + }, + "addRecord": { + "status": "live", + "note": "objectui: ListView.tsx:374 (addRecord + userActions.addRecordForm drive the add-record flow). Sub-keys addRecord.mode / addRecord.formView have NO reader (audit L20, re-verified objectui@fb35e48) — dead sub-surface." + }, + "showRecordCount": { + "status": "live", + "note": "objectui: ListView toolbar (audit L15)." + }, + "allowPrinting": { + "status": "live", + "note": "objectui: ListView toolbar (audit L15)." + }, + "emptyState": { + "status": "live", + "evidence": "packages/cli/src/utils/i18n-extract.ts", + "note": "objectui: ListView empty-state panel (audit L15); CLI i18n extract emits _views..emptyState keys (spec CHANGELOG)." + }, + "aria": { + "status": "live", + "note": "objectui: ListView.tsx:1712-1714 applies aria-label/-describedby/-live." + }, + "userFilters": { + "status": "live", + "note": "objectui: UserFilters.tsx — element/fields/tabs/showAllRecords wired (audit L15); object views narrow to ObjectUserFiltersSchema (ADR-0047 amendment, #2679/objectui#2338)." + } } }, "form": { "children": { - "type": { "status": "live", "note": "objectui: ObjectForm.tsx:135-242 variant routing (simple/tabbed/wizard/split/drawer/modal) + SchemaForm.tsx:625 (tabbed). Mainstream RecordFormPage path hardcodes formType:'simple' (audit L12) — variants are fully honored on the plugin-form/showcase and metadata-admin paths only." }, - "layout": { "status": "live", "note": "objectui: ObjectForm.tsx:826, ModalForm.tsx:468, DrawerForm.tsx:384 (vertical/horizontal)." }, - "columns": { "status": "live", "note": "objectui: ModalForm.tsx:247-258 / DrawerForm.tsx:478 applyAutoLayout column count." }, - "title": { "status": "live", "note": "objectui: ModalForm.tsx:662 / DrawerForm.tsx:528 header; ObjectView form adapter (audit L25)." }, - "description": { "status": "live", "note": "objectui: ModalForm.tsx:662-681 / DrawerForm.tsx:528-558 render it under the title." }, - "defaultTab": { "status": "live", "note": "objectui: ObjectForm.tsx:185 → tabbed variant." }, - "tabPosition": { "status": "live", "note": "objectui: ObjectForm.tsx:186 → tabbed variant." }, - "allowSkip": { "status": "live", "note": "objectui: WizardForm.tsx:397-444 (step navigation gating)." }, - "showStepIndicator": { "status": "live", "note": "objectui: WizardForm.tsx:438." }, - "splitDirection": { "status": "live", "note": "objectui: SplitForm.tsx:205." }, - "splitSize": { "status": "live", "note": "objectui: SplitForm.tsx:206." }, - "splitResizable": { "status": "live", "note": "objectui: SplitForm.tsx:275." }, - "drawerSide": { "status": "live", "note": "objectui: DrawerForm.tsx:185." }, - "drawerWidth": { "status": "live", "note": "objectui: DrawerForm.tsx:377-382 still reads it. DEPRECATED in spec (#2578 → size buckets; a pixel width can't be authored blind) — live-but-deprecated, remove the read when the deprecation completes." }, - "modalSize": { "status": "live", "note": "objectui: ModalForm.tsx:263." }, "data": { - "status": "dead", - "evidence": "no form-path reader in either repo (objectui@fb35e48 grepped 2026-07-16)", - "note": "ViewDataSchema provider modes are honored on the LIST path (ListView.tsx:841) but nothing on the form path reads form.data — form data binding comes from the record route/object context (RecordFormPage / ObjectForm dataSource).", - "authorWarn": true, - "authorHint": "form.data is not consumed — a form binds to its object/record via the route context. Remove the block (list views do honor data providers)." - }, - "sections": { "status": "live", "note": "objectui: form renderers + spec-bridge form-view.ts:169 (audit L17)." }, - "groups": { "status": "live", "note": "objectui: legacy alias of sections, normalized at ObjectForm.tsx:90 and spec-bridge form-view.ts:169 (spec.sections ?? spec.groups). Audit L24 drift resolved by alias-at-one-boundary." }, - "subforms": { "status": "live", "note": "objectui: deriveMasterDetail.ts:338, LineItemsPanel.tsx:45 — master-detail fully wired (audit L17)." }, - "defaultSort": { - "status": "dead", - "evidence": "no reader in either repo (audit L20; objectui@fb35e48 re-grepped 2026-07-16)", - "authorWarn": true, - "authorHint": "defaultSort on a form view sorts nothing — related-list ordering is not read from here. Configure sorting on the related list view instead." - }, - "sharing": { "status": "live", "evidence": "packages/rest/src/rest-server.ts:4384", "note": "Framework-side consumer: public (anonymous) form endpoints opt in via FormView.sharing — /forms/:slug resolves sharing.publicLink across form + formViews (rest-server.ts:1716, :4366-4405) and grants publicFormGrant. Renderer-side the config is not read (audit L20) — the public form is served, not re-parsed client-side." }, - "submitBehavior": { "status": "live", "note": "objectui: WizardForm.tsx:315-316 + ObjectForm.tsx dispatch thank-you/redirect/continue/next-record; covered by ObjectForm.submitBehavior.test.tsx / WizardForm.successBehavior.test.tsx. Supersedes the 2026-06 audit's DEAD line (L20) — wiring landed with the objectui#2545/#2552 reconciliation." }, - "buttons": { "status": "live", "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.buttons and folds submit/cancel/reset {show,label} onto the flat showSubmit/submitText/showCancel/cancelText/showReset props (framework#1894 / #2998); ObjectView + RecordFormPage forward it. Flat keys kept as deprecated back-compat (explicit flat key wins)." }, - "defaults": { "status": "live", "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.defaults and folds it into create-mode initialValues (framework#1894 / #2998); an explicit initialValues (URL prefill) still wins." }, - "aria": { - "status": "dead", - "evidence": "no reader in either repo (objectui@fb35e48 grepped 2026-07-16: no schema.aria read in plugin-form/SchemaForm; only ListView applies aria)", - "note": "Benign a11y metadata — not authorWarn'd (README rule 1), but a candidate for wiring alongside the objectui#2545 form pass." + "status": "live", + "verifiedAt": "2026-07-30", + "evidence": "packages/spec/src/ui/view.zod.ts defineForm (writes data.provider=schema + schemaId onto every metadata form); packages/metadata-protocol/src/protocol.ts:1580,1598 (serves schemaId with the form to the metadata-admin pipeline)", + "note": "CORRECTED 2026-07-30: the #3896 sweep attempted removal on the prior dead verdict and the build itself refuted it — defineForm authors the key on every *.form.ts. The record-form provider VALUES (object/api/value) may still lack readers; that is a value-subset audit, not a key removal." + }, + "type": { + "status": "live", + "note": "objectui: ObjectForm.tsx:135-242 variant routing (simple/tabbed/wizard/split/drawer/modal) + SchemaForm.tsx:625 (tabbed). Mainstream RecordFormPage path hardcodes formType:'simple' (audit L12) — variants are fully honored on the plugin-form/showcase and metadata-admin paths only." + }, + "layout": { + "status": "live", + "note": "objectui: ObjectForm.tsx:826, ModalForm.tsx:468, DrawerForm.tsx:384 (vertical/horizontal)." + }, + "columns": { + "status": "live", + "note": "objectui: ModalForm.tsx:247-258 / DrawerForm.tsx:478 applyAutoLayout column count." + }, + "title": { + "status": "live", + "note": "objectui: ModalForm.tsx:662 / DrawerForm.tsx:528 header; ObjectView form adapter (audit L25)." + }, + "description": { + "status": "live", + "note": "objectui: ModalForm.tsx:662-681 / DrawerForm.tsx:528-558 render it under the title." + }, + "defaultTab": { + "status": "live", + "note": "objectui: ObjectForm.tsx:185 → tabbed variant." + }, + "tabPosition": { + "status": "live", + "note": "objectui: ObjectForm.tsx:186 → tabbed variant." + }, + "allowSkip": { + "status": "live", + "note": "objectui: WizardForm.tsx:397-444 (step navigation gating)." + }, + "showStepIndicator": { + "status": "live", + "note": "objectui: WizardForm.tsx:438." + }, + "splitDirection": { + "status": "live", + "note": "objectui: SplitForm.tsx:205." + }, + "splitSize": { + "status": "live", + "note": "objectui: SplitForm.tsx:206." + }, + "splitResizable": { + "status": "live", + "note": "objectui: SplitForm.tsx:275." + }, + "drawerSide": { + "status": "live", + "note": "objectui: DrawerForm.tsx:185." + }, + "drawerWidth": { + "status": "live", + "note": "objectui: DrawerForm.tsx:377-382 still reads it. DEPRECATED in spec (#2578 → size buckets; a pixel width can't be authored blind) — live-but-deprecated, remove the read when the deprecation completes." + }, + "modalSize": { + "status": "live", + "note": "objectui: ModalForm.tsx:263." + }, + "sections": { + "status": "live", + "note": "objectui: form renderers + spec-bridge form-view.ts:169 (audit L17)." + }, + "groups": { + "status": "live", + "note": "objectui: legacy alias of sections, normalized at ObjectForm.tsx:90 and spec-bridge form-view.ts:169 (spec.sections ?? spec.groups). Audit L24 drift resolved by alias-at-one-boundary." + }, + "subforms": { + "status": "live", + "note": "objectui: deriveMasterDetail.ts:338, LineItemsPanel.tsx:45 — master-detail fully wired (audit L17)." + }, + "sharing": { + "status": "live", + "evidence": "packages/rest/src/rest-server.ts:4384", + "note": "Framework-side consumer: public (anonymous) form endpoints opt in via FormView.sharing — /forms/:slug resolves sharing.publicLink across form + formViews (rest-server.ts:1716, :4366-4405) and grants publicFormGrant. Renderer-side the config is not read (audit L20) — the public form is served, not re-parsed client-side." + }, + "submitBehavior": { + "status": "live", + "note": "objectui: WizardForm.tsx:315-316 + ObjectForm.tsx dispatch thank-you/redirect/continue/next-record; covered by ObjectForm.submitBehavior.test.tsx / WizardForm.successBehavior.test.tsx. Supersedes the 2026-06 audit's DEAD line (L20) — wiring landed with the objectui#2545/#2552 reconciliation." + }, + "buttons": { + "status": "live", + "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.buttons and folds submit/cancel/reset {show,label} onto the flat showSubmit/submitText/showCancel/cancelText/showReset props (framework#1894 / #2998); ObjectView + RecordFormPage forward it. Flat keys kept as deprecated back-compat (explicit flat key wins)." + }, + "defaults": { + "status": "live", + "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.defaults and folds it into create-mode initialValues (framework#1894 / #2998); an explicit initialValues (URL prefill) still wins." } } }, diff --git a/packages/spec/scripts/build-schemas.ts b/packages/spec/scripts/build-schemas.ts index 808210746e..486972adb2 100644 --- a/packages/spec/scripts/build-schemas.ts +++ b/packages/spec/scripts/build-schemas.ts @@ -431,7 +431,11 @@ if (surfaceDoc) { .filter(([k, retired]) => retired && prev.get(k) === false) .map(([k]) => k); if (newlyRetired.length > 0) { - const surfaces = registeredRetirementSurfaces(); + // A multi-key conversion names its keys as ' / '-separated clauses + // (`'flow.active / flow.template / …'` — the house style since the tool + // sweep), so split before matching: every clause must still END with the + // key, which keeps the check exactly as strict per key as before. + const surfaces = registeredRetirementSurfaces().flatMap((s) => s.split(' / ')); const unregistered = newlyRetired.filter( (k) => !surfaces.some((s) => s.endsWith('.' + k.split(':')[1])), ); diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 6d2827666c..19484a05b8 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -80,12 +80,6 @@ "conversionId": "field-conditionalRequired-to-requiredWhen", "toMajor": 17 }, - { - "surface": "agent.knowledge.topics", - "to": "agent knowledge key 'topics' → 'sources' (the deprecated RAG-source alias, #1891)", - "conversionId": "agent-knowledge-topics-to-sources", - "toMajor": 17 - }, { "surface": "agent.tools", "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894)", @@ -133,6 +127,42 @@ "to": "required fields gain explicit 'storage.notNull: true' (ADR-0113 — pre-17 'required' implied the column constraint; post-17 it is only the write contract)", "conversionId": "field-required-notnull-explicit", "toMajor": 17 + }, + { + "surface": "action.shortcut / action.bulkEnabled", + "to": "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + "conversionId": "action-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId", + "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + "conversionId": "flow-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria", + "to": "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + "conversionId": "view-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", + "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + "conversionId": "dashboard-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "agent.knowledge", + "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + "conversionId": "agent-knowledge-removed", + "toMajor": 17 + }, + { + "surface": "skill.triggerPhrases", + "to": "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + "conversionId": "skill-trigger-phrases-removed", + "toMajor": 17 } ], "migrated": [ @@ -428,12 +458,6 @@ "conversionId": "field-conditionalRequired-to-requiredWhen", "toMajor": 17 }, - { - "surface": "agent.knowledge.topics", - "to": "agent knowledge key 'topics' → 'sources' (the deprecated RAG-source alias, #1891)", - "conversionId": "agent-knowledge-topics-to-sources", - "toMajor": 17 - }, { "surface": "agent.tools", "to": "agent key 'tools' removed — declare capability in a skill (ADR-0064, #3894)", @@ -481,6 +505,42 @@ "to": "required fields gain explicit 'storage.notNull: true' (ADR-0113 — pre-17 'required' implied the column constraint; post-17 it is only the write contract)", "conversionId": "field-required-notnull-explicit", "toMajor": 17 + }, + { + "surface": "action.shortcut / action.bulkEnabled", + "to": "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + "conversionId": "action-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId", + "to": "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + "conversionId": "flow-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria", + "to": "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + "conversionId": "view-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "dashboard.aria / dashboard.performance / dashboard.widgets[].performance", + "to": "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + "conversionId": "dashboard-inert-keys-removed", + "toMajor": 17 + }, + { + "surface": "agent.knowledge", + "to": "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + "conversionId": "agent-knowledge-removed", + "toMajor": 17 + }, + { + "surface": "skill.triggerPhrases", + "to": "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + "conversionId": "skill-trigger-phrases-removed", + "toMajor": 17 } ], "migrated": [ diff --git a/packages/spec/src/ai/agent.form.ts b/packages/spec/src/ai/agent.form.ts index e9aa023997..5123cf400e 100644 --- a/packages/spec/src/ai/agent.form.ts +++ b/packages/spec/src/ai/agent.form.ts @@ -42,7 +42,8 @@ export const agentForm = defineForm({ description: 'Skills and knowledge sources the agent can use.', fields: [ { field: 'skills', widget: 'string-tags', helpText: 'Skill names (Agent→Skill→Tool architecture)' }, - { field: 'knowledge', type: 'composite', helpText: 'RAG knowledge access configuration' }, + // `knowledge` input removed with the block (#3896 close-out): the RAG + // path never read the agent record — a form for it was false compliance. ], }, { diff --git a/packages/spec/src/ai/agent.test.ts b/packages/spec/src/ai/agent.test.ts index 90d090de2a..ff485377a3 100644 --- a/packages/spec/src/ai/agent.test.ts +++ b/packages/spec/src/ai/agent.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest'; import { AgentSchema, AIModelConfigSchema, - AIKnowledgeSchema, StructuredOutputFormatSchema, StructuredOutputConfigSchema, defineAgent, @@ -96,61 +95,9 @@ describe('agent.tools retirement (ADR-0064 / #3820, tombstoned in #3894)', () => }); }); -describe('AIKnowledgeSchema', () => { - it('should accept knowledge config (canonical sources)', () => { - const knowledge = { - sources: ['product_docs', 'faq', 'troubleshooting'], - indexes: ['vector_store_main', 'vector_store_archive'], - }; - - expect(() => AIKnowledgeSchema.parse(knowledge)).not.toThrow(); - }); - - it('rejects the removed `topics` alias instead of silently stripping it (#3855)', () => { - // #1891 folded `topics` into `sources` and dropped it; protocol 17 removes - // it from the spec. Tombstoned rather than deleted because - // `AIKnowledgeSchema` is not `.strict()` — a silent strip would leave the - // agent recruiting no RAG context at all, which is #1878's original bug. - expect(() => AIKnowledgeSchema.parse({ - topics: ['product_docs', 'faq'], - indexes: ['vector_store_main'], - })).toThrow(/`knowledge.topics` was removed/); - }); - - it('carries the upgrade prescription in the rejection itself', () => { - let message = ''; - try { - AIKnowledgeSchema.parse({ topics: ['legacy'], indexes: [] }); - } catch (err) { - message = String(err); - } - expect(message).toContain('use `knowledge.sources`'); - expect(message).toContain('os migrate meta'); - }); - - it('rejects `topics` even when the canonical `sources` is also present', () => { - expect(() => AIKnowledgeSchema.parse({ - sources: ['canonical'], - topics: ['legacy'], - indexes: [], - })).toThrow(/`knowledge.topics` was removed/); - }); - - it('accepts the canonical `sources` on its own', () => { - const parsed = AIKnowledgeSchema.parse({ sources: ['canonical'], indexes: [] }); - expect(parsed.sources).toEqual(['canonical']); - expect('topics' in parsed).toBe(false); - }); - - it('should accept empty arrays', () => { - const knowledge = { - sources: [], - indexes: [], - }; +// AIKnowledgeSchema tests removed with the schema (#3896 close-out). The +// retirement itself is pinned below in "retired `knowledge`". - expect(() => AIKnowledgeSchema.parse(knowledge)).not.toThrow(); - }); -}); describe('AgentSchema', () => { describe('Basic Properties', () => { @@ -237,35 +184,28 @@ describe('AgentSchema', () => { expect(result).not.toHaveProperty('tools'); }); - it('should accept agent with knowledge base', () => { - const agent: Agent = { + // ── #3896 close-out — the retired `knowledge` block ── + // Declaring sources/indexes never scoped retrieval: `search_knowledge` + // takes `sourceIds` from the LLM's tool-call arguments, never the agent + // record. The rejection must carry the prescription. + it('REJECTS the retired `knowledge` block instead of silently stripping it', () => { + expect(() => AgentSchema.parse({ name: 'knowledge_bot', label: 'Knowledge Bot', - role: 'Documentation Assistant', - instructions: 'Answer questions using the knowledge base.', - knowledge: { - sources: ['api_docs', 'user_guide', 'faq'], - indexes: ['main_index', 'legacy_index'], - }, - }; - - expect(() => AgentSchema.parse(agent)).not.toThrow(); + instructions: 'x', + knowledge: { sources: ['api_docs'], indexes: ['main_index'] }, + })).toThrow(/knowledge/); }); - it('should accept agent with both skills and knowledge', () => { - const agent: Agent = { - name: 'full_agent', - label: 'Complete Agent', - role: 'Full-Stack Assistant', - instructions: 'Comprehensive assistant with all capabilities.', - skills: ['record_management', 'reporting'], - knowledge: { - sources: ['everything'], - indexes: ['master_index'], - }, - }; - - expect(() => AgentSchema.parse(agent)).not.toThrow(); + it('the rejection names where retrieval scoping actually lives', () => { + let message = ''; + try { + AgentSchema.parse({ name: 'a', label: 'A', instructions: 'x', knowledge: { sources: ['s'] } }); + } catch (e) { + message = String((e as Error).message); + } + expect(message).toMatch(/knowledge-service|source level/); + expect(message).toMatch(/#3896/); }); it('should accept agent with skills (Agent→Skill→Tool architecture)', () => { @@ -386,10 +326,6 @@ Always be polite, empathetic, and solution-oriented.`, maxTokens: 2048, }, skills: ['record_management', 'reporting'], - knowledge: { - sources: ['product_docs', 'faq', 'troubleshooting', 'api_reference'], - indexes: ['support_kb_v2'], - }, access: ['support_team', 'customers'], active: true, }; @@ -419,10 +355,6 @@ Be persuasive but honest. Focus on value creation.`, temperature: 0.8, }, skills: ['record_management', 'reporting'], - knowledge: { - sources: ['sales_playbooks', 'product_features', 'case_studies', 'competitor_analysis'], - indexes: ['sales_intelligence'], - }, access: ['sales_team'], active: true, }; @@ -451,10 +383,6 @@ Be precise, data-driven, and clear in your explanations.`, maxTokens: 4096, }, skills: ['record_management', 'reporting'], - knowledge: { - sources: ['sql_guides', 'metrics_definitions'], - indexes: ['analytics_kb'], - }, access: ['analysts', 'executives'], active: true, }; diff --git a/packages/spec/src/ai/agent.zod.ts b/packages/spec/src/ai/agent.zod.ts index 38c7aabe42..e17e1df690 100644 --- a/packages/spec/src/ai/agent.zod.ts +++ b/packages/spec/src/ai/agent.zod.ts @@ -18,25 +18,13 @@ export const AIModelConfigSchema = lazySchema(() => z.object({ topP: z.number().optional(), })); -/** - * AI Knowledge Base - * RAG configuration. +/* + * REMOVED — `AIKnowledgeSchema` / `AIKnowledge` (#3896 audit close-out). It + * typed `agent.knowledge`, tombstoned below: the RAG path never read the + * agent record, so the whole block was a grounding claim nothing enforced. + * The protocol-17 `agent-knowledge-topics-to-sources` conversion remains in + * the chain (it rewrites historical SOURCES and imports nothing from here). */ -export const AIKnowledgeSchema = lazySchema(() => z.object({ - sources: z.array(z.string()).optional().describe('Knowledge sources/tags to recruit RAG context from. Canonical key consumed by the agent renderer (objectui AgentPreview KnowledgeSummary).'), - /** - * [REMOVED in protocol 17 — #3855] The deprecated alias of `sources`. - * Tombstoned rather than deleted: `AIKnowledgeSchema` is not `.strict()`, so a - * plain deletion would silently strip the key and the agent would quietly - * recruit no RAG context (#1878's original failure, restored). - */ - topics: retiredKey( - "`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: z.array(z.string()).describe('Vector Store Indexes'), -})); /** * Structured Output Format @@ -167,7 +155,19 @@ export const AgentSchema = lazySchema(() => z.object({ ), /** Knowledge */ - knowledge: AIKnowledgeSchema.optional().describe('RAG access'), + // `knowledge` REMOVED (#3896 audit close-out) — a GROUNDING claim nothing + // enforced: the RAG path never reads the agent record; `search_knowledge` + // takes `sourceIds` from the LLM's own tool-call arguments (cloud + // knowledge-tools.ts:96). An author who "scoped" retrieval here scoped + // nothing — the dangerous direction, same shape as tool.permissions. + knowledge: retiredKey( + '`agent.knowledge` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'declaring knowledge sources/indexes on an agent never scoped retrieval: the ' + + "`search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from " + + 'the agent record. Delete the block. Restrict retrieval at the knowledge-service / ' + + 'source level (per-source permissions), and describe intended grounding in ' + + '`instructions` so the model asks for the right sources.', + ), /** Interface */ active: z.boolean().default(true), diff --git a/packages/spec/src/ai/skill.form.ts b/packages/spec/src/ai/skill.form.ts index d91a6e311f..db86f76af6 100644 --- a/packages/spec/src/ai/skill.form.ts +++ b/packages/spec/src/ai/skill.form.ts @@ -36,7 +36,8 @@ export const skillForm = defineForm({ collapsible: true, collapsed: true, fields: [ - { field: 'triggerPhrases', widget: 'string-tags', helpText: 'Natural language phrases that activate this skill' }, + // `triggerPhrases` input removed with the key (#3896 close-out): phrases + // were never matched; routing is triggerConditions + the agent allowlist. { field: 'triggerConditions', type: 'repeater', helpText: 'Programmatic conditions (e.g., objectName == "case")' }, ], }, diff --git a/packages/spec/src/ai/skill.test.ts b/packages/spec/src/ai/skill.test.ts index 778ae25c35..8b5c19a519 100644 --- a/packages/spec/src/ai/skill.test.ts +++ b/packages/spec/src/ai/skill.test.ts @@ -59,7 +59,6 @@ describe('SkillSchema', () => { description: 'Handles order lifecycle operations', instructions: 'Use these tools to manage customer orders. Always verify order ownership first.', tools: ['create_order', 'update_order', 'cancel_order', 'query_orders'], - triggerPhrases: ['place an order', 'cancel my order', 'check order status'], triggerConditions: [ { field: 'objectName', operator: 'eq' as const, value: 'order' }, { field: 'userRole', operator: 'in' as const, value: ['sales', 'support'] }, @@ -70,7 +69,6 @@ describe('SkillSchema', () => { const result = SkillSchema.parse(skill); expect(result.name).toBe('order_management'); expect(result.tools).toHaveLength(4); - expect(result.triggerPhrases).toHaveLength(3); expect(result.triggerConditions).toHaveLength(2); }); @@ -156,7 +154,6 @@ describe('defineSkill', () => { description: 'Handles support case lifecycle', instructions: 'Use these tools to create, update, and resolve support cases.', tools: ['create_case', 'update_case', 'resolve_case'], - triggerPhrases: ['create a case', 'open a ticket'], }); expect(skill.name).toBe('case_management'); @@ -182,3 +179,16 @@ describe('defineSkill', () => { })).toThrow(); }); }); + +describe('#3896 close-out — retired `triggerPhrases`', () => { + it('REJECTS the retired key with the routing prescription', () => { + let message = ''; + try { + SkillSchema.parse({ name: 'case_mgmt', label: 'Cases', triggerPhrases: ['open a ticket'] }); + } catch (e) { + message = String((e as Error).message); + } + expect(message).toMatch(/triggerConditions/); + expect(message).toMatch(/#3896/); + }); +}); diff --git a/packages/spec/src/ai/skill.zod.ts b/packages/spec/src/ai/skill.zod.ts index 246fc20186..5da777154f 100644 --- a/packages/spec/src/ai/skill.zod.ts +++ b/packages/spec/src/ai/skill.zod.ts @@ -15,6 +15,7 @@ import { MetadataProtectionFields } from '../kernel/metadata-protection.zod'; * Allows context-aware activation based on object type, user role, etc. */ import { lazySchema } from '../shared/lazy-schema'; +import { retiredKey } from '../shared/retired-key'; export const SkillTriggerConditionSchema = lazySchema(() => z.object({ /** Condition field (e.g. 'objectName', 'userRole', 'channel') */ field: z.string().describe('Context field to evaluate'), @@ -116,7 +117,18 @@ export const SkillSchema = lazySchema(() => z.object({ * Natural language phrases that trigger skill activation. * Used for intent matching and skill routing. */ - triggerPhrases: z.array(z.string()).optional().describe('Phrases that activate this skill'), + // `triggerPhrases` REMOVED (#3896 audit close-out): phrases were NEVER + // matched against the user's message — activation is `triggerConditions` ∩ + // the agent's `skills[]` allowlist (+ explicit /skill-name pinning). The + // cloud API served the field back to clients, a dead-end projection that + // made the false capability look extra real. + triggerPhrases: retiredKey( + '`skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) ' + + "— phrases were never matched against the user's message; skill activation is " + + "`triggerConditions` (AND of context field/operator/value) intersected with the agent's " + + '`skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in ' + + '`triggerConditions`; describe intent in `description`/`instructions` for the LLM.', + ), /** * Programmatic conditions for skill activation. diff --git a/packages/spec/src/automation/flow.form.ts b/packages/spec/src/automation/flow.form.ts index 38642eead9..d86dffa941 100644 --- a/packages/spec/src/automation/flow.form.ts +++ b/packages/spec/src/automation/flow.form.ts @@ -20,7 +20,8 @@ export const flowForm = defineForm({ { field: 'name', required: true, colSpan: 1, helpText: 'Unique identifier (snake_case)' }, { field: 'label', required: true, colSpan: 1, helpText: 'Display name for users' }, { field: 'type', required: true, colSpan: 1, helpText: 'How the flow starts (autolaunched, record_change, schedule, screen, api)' }, - { field: 'template', colSpan: 1, helpText: 'Is this a reusable subflow (can be called from other flows)' }, + // `template` input removed with the key (#3896 close-out): no designer or + // engine path read it — subflows are invoked via a subflow NODE by name. { field: 'description', widget: 'textarea', colSpan: 2, helpText: 'What this flow does' }, ], }, diff --git a/packages/spec/src/automation/flow.test.ts b/packages/spec/src/automation/flow.test.ts index 80817c9403..ab63ec4ea1 100644 --- a/packages/spec/src/automation/flow.test.ts +++ b/packages/spec/src/automation/flow.test.ts @@ -147,7 +147,7 @@ describe('FlowNodeSchema', () => { } }); - it('should accept node with inputSchema and outputSchema', () => { + it('should accept node with inputSchema (outputSchema retired, #3896)', () => { const result = FlowNodeSchema.safeParse({ id: 'script_1', type: 'script', @@ -156,14 +156,10 @@ describe('FlowNodeSchema', () => { name: { type: 'string', required: true, description: 'User name' }, age: { type: 'number', required: false }, }, - outputSchema: { - greeting: { type: 'string', description: 'Generated greeting' }, - }, }); expect(result.success).toBe(true); if (result.success) { expect(result.data.inputSchema).toBeDefined(); - expect(result.data.outputSchema).toBeDefined(); } }); }); @@ -231,7 +227,6 @@ describe('FlowSchema', () => { }; const result = FlowSchema.parse(flow); - expect(result.active).toBe(false); expect(result.runAs).toBe('user'); }); @@ -274,17 +269,17 @@ describe('FlowSchema', () => { }); }); - it('should default active to false', () => { - const flow = { - name: 'test_flow', - label: 'Test', - type: 'autolaunched' as const, - nodes: [], - edges: [], - }; - - const result = FlowSchema.parse(flow); - expect(result.active).toBe(false); + it('REJECTS the retired `active` with the status prescription (#3896)', () => { + let message = ''; + try { + FlowSchema.parse({ + name: 'test_flow', label: 'Test', type: 'autolaunched', nodes: [], edges: [], active: false, + }); + } catch (e) { + message = String((e as Error).message); + } + expect(message).toMatch(/status/); + expect(message).toMatch(/#3896/); }); it('should default runAs to user', () => { @@ -411,7 +406,6 @@ describe('FlowSchema', () => { { id: 'e5', source: 'auto_approve', target: 'end' }, { id: 'e6', source: 'send_approval_request', target: 'end' }, ], - active: true, runAs: 'system', }; @@ -460,7 +454,6 @@ describe('FlowSchema', () => { { id: 'e2', source: 'create_contact', target: 'assign_output' }, { id: 'e3', source: 'assign_output', target: 'end' }, ], - active: true, }; expect(() => FlowSchema.parse(screenFlow)).not.toThrow(); @@ -508,7 +501,6 @@ describe('FlowSchema', () => { { id: 'e4', source: 'delete_record', target: 'loop_records' }, { id: 'e5', source: 'loop_records', target: 'end', label: 'Done' }, ], - active: true, runAs: 'system', }; @@ -555,7 +547,6 @@ describe('FlowSchema', () => { { id: 'e2', source: 'call_external_api', target: 'process_response' }, { id: 'e3', source: 'process_response', target: 'end' }, ], - active: true, }; expect(() => FlowSchema.parse(apiFlow)).not.toThrow(); @@ -604,8 +595,8 @@ describe('FlowSchema - errorHandling', () => { expect(result.errorHandling?.maxRetries).toBe(0); }); - it('should accept continue strategy with fallback node', () => { - const result = FlowSchema.parse({ + it('REJECTS the retired errorHandling.fallbackNodeId — faults route via fault edges (#3896)', () => { + expect(() => FlowSchema.parse({ name: 'fallback_flow', label: 'Fallback', type: 'autolaunched', @@ -614,13 +605,8 @@ describe('FlowSchema - errorHandling', () => { { id: 'fallback', type: 'end', label: 'Fallback' }, ], edges: [], - errorHandling: { - strategy: 'continue', - fallbackNodeId: 'fallback', - }, - }); - expect(result.errorHandling?.strategy).toBe('continue'); - expect(result.errorHandling?.fallbackNodeId).toBe('fallback'); + errorHandling: { strategy: 'continue', fallbackNodeId: 'fallback' }, + })).toThrow(/fault edge/); }); it('should accept flow without errorHandling (optional)', () => { @@ -711,7 +697,6 @@ describe('defineFlow', () => { }); expect(result.version).toBe(1); expect(result.status).toBe('draft'); - expect(result.active).toBe(false); expect(result.runAs).toBe('user'); }); @@ -803,7 +788,6 @@ describe('BPMN — Parallel Gateway & Join Gateway', () => { { id: 'e6', source: 'join', target: 'final_approve' }, { id: 'e7', source: 'final_approve', target: 'end' }, ], - active: true, runAs: 'system', }; diff --git a/packages/spec/src/automation/flow.zod.ts b/packages/spec/src/automation/flow.zod.ts index 56f3425c1b..54262ea0cc 100644 --- a/packages/spec/src/automation/flow.zod.ts +++ b/packages/spec/src/automation/flow.zod.ts @@ -19,6 +19,7 @@ import { ExpressionInputSchema } from '../shared/expression.zod'; * no longer constrains authored flows — plugins extend the vocabulary. */ import { lazySchema } from '../shared/lazy-schema'; +import { retiredKey } from '../shared/retired-key'; export const FlowNodeAction = z.enum([ 'start', // Trigger 'end', // Return/Stop @@ -124,11 +125,14 @@ export const FlowNodeSchema = lazySchema(() => z.object({ description: z.string().optional().describe('Parameter description'), })).optional().describe('Input parameter schema for this node'), - /** Node output schema declaration */ - outputSchema: z.record(z.string(), z.object({ - type: z.enum(['string', 'number', 'boolean', 'object', 'array']).describe('Output type'), - description: z.string().optional().describe('Output description'), - })).optional().describe('Output schema declaration for this node'), + // `outputSchema` REMOVED (#3896 audit close-out): declared, never validated — + // no engine path checked node outputs against it (ledger: dead). + outputSchema: retiredKey( + '`flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit ' + + 'close-out) — it was never validated: the engine does not check node outputs against ' + + 'it, so it documented a contract nothing enforced. Delete the key. Downstream nodes ' + + "read prior outputs via expressions ({{nodeId.field}}) regardless of any declaration.", + ), /** * Wait Event Configuration (for 'wait' nodes) @@ -247,7 +251,13 @@ export const FlowSchema = lazySchema(() => z.object({ /** Metadata & Versioning */ version: z.number().int().default(1).describe('Version number'), status: z.enum(['draft', 'active', 'obsolete', 'invalid']).default('draft').describe('Deployment status'), - template: z.boolean().default(false).describe('Is logic template (Subflow)'), + // `template` REMOVED (#3896 audit close-out): no reader in designer or engine. + template: retiredKey( + '`flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'no designer or engine path ever read it, so flagging a flow as a template/subflow did ' + + 'nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the ' + + 'flow by name.', + ), /** Trigger Type */ type: z.enum(['autolaunched', 'record_change', 'schedule', 'screen', 'api']).describe('Flow type'), @@ -259,8 +269,18 @@ export const FlowSchema = lazySchema(() => z.object({ nodes: z.array(FlowNodeSchema).describe('Flow nodes'), edges: z.array(FlowEdgeSchema).describe('Flow connections'), - /** Execution Config */ - active: z.boolean().default(false).describe('Is active (Deprecated: use status)'), + // `active` REMOVED (#3896 audit close-out) — the rls.enabled shape, in the + // over-permissive direction: the spec default was `false` while the engine + // treated an unset flow as ENABLED, and `active: false` never stopped a flow + // (`status` is the enforced lifecycle). An author who "disabled" a flow here + // left it firing. + active: retiredKey( + '`flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it ' + + 'never had an effect: the engine arms flows from `status`, and `active: false` did NOT ' + + 'stop a flow (worse, the default read as disabled while the engine treated unset as ' + + "enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and " + + "disable a flow, `status: 'active'` to arm it.", + ), // ADR-0049 / #1888 — ENFORCED. The service-automation engine establishes the // declared identity for the run's data operations and restores the caller's // context afterward: `system` runs elevated (a full-access, RLS-bypassing @@ -291,7 +311,16 @@ export const FlowSchema = lazySchema(() => z.object({ backoffMultiplier: z.number().min(1).default(1).describe('Multiplier for exponential backoff between retries'), maxRetryDelayMs: z.number().int().min(0).default(30000).describe('Maximum delay between retries in milliseconds'), jitter: z.boolean().default(false).describe('Add random jitter to retry delay to avoid thundering herd'), - fallbackNodeId: z.string().optional().describe('Node ID to jump to on unrecoverable error'), + // `fallbackNodeId` REMOVED (#3896 audit close-out): the engine routes + // unrecoverable errors via per-node FAULT EDGES, never this — an author + // who configured a fallback here had none. + fallbackNodeId: retiredKey( + '`flow.errorHandling.fallbackNodeId` was removed in @objectstack/spec 17.0.0 (#3896 ' + + 'audit close-out) — the engine routes unrecoverable node errors via per-node fault ' + + "edges (an edge with condition 'fault'), and never read this key: a fallback " + + 'configured here silently did not exist. Delete the key and draw a fault edge from ' + + 'the failing node to the handler node instead.', + ), }).optional().describe('Flow-level error handling configuration'), /** * ADR-0010 §3.7 — Package-level protection envelope. Package diff --git a/packages/spec/src/conversions/registry.ts b/packages/spec/src/conversions/registry.ts index 43a75ba664..98e137e342 100644 --- a/packages/spec/src/conversions/registry.ts +++ b/packages/spec/src/conversions/registry.ts @@ -739,37 +739,15 @@ const fieldConditionalRequiredToRequiredWhen: MetadataConversion = { }, }; -/** - * Agent `knowledge.topics` → `knowledge.sources` (protocol 17, #1891 / #3855). - * - * A pure key rename — the value (a list of RAG source tags) is unchanged. +/* + * ABSORBED — `agent-knowledge-topics-to-sources` (protocol 17, #3855). The + * rename lived inside the same unreleased major that now REMOVES the whole + * `agent.knowledge` block (`agent-knowledge-removed` below): composed, its + * effect is unobservable — any pre-17 `knowledge` ends deleted regardless of + * its inner spelling — and two notices for one dead block would only confuse + * the author. Folded pre-release per the ADR-0090 discipline; the removal's + * prescription covers the historical `topics` spelling. */ -const agentKnowledgeTopicsToSources: MetadataConversion = { - id: 'agent-knowledge-topics-to-sources', - toMajor: 17, - retiredFromLoadPath: true, - surface: 'agent.knowledge.topics', - summary: "agent knowledge key 'topics' → 'sources' (the deprecated RAG-source alias, #1891)", - apply(stack, emit) { - return mapCollection(stack, 'agents', (agent, path) => { - const knowledge = agent.knowledge; - if (!isDict(knowledge)) return agent; - const renamed = renameKey(knowledge, 'topics', 'sources'); - if (!renamed) return agent; - emit({ from: 'topics', to: 'sources', path: `${path}.knowledge.sources` }); - return { ...agent, knowledge: renamed }; - }); - }, - fixture: { - before: { - agents: [{ name: 'support_bot', knowledge: { topics: ['faq', 'policies'], indexes: ['docs'] } }], - }, - after: { - agents: [{ name: 'support_bot', knowledge: { sources: ['faq', 'policies'], indexes: ['docs'] } }], - }, - expectedNotices: 1, - }, -}; /** * Agent `tools` → dropped (protocol 17, #3894 / #3820). @@ -1309,6 +1287,263 @@ const fieldRequiredNotNullExplicit: MetadataConversion = { }, }; + +/** + * The #3896 close-out sweep, part 2 (protocol 17): the remaining inert + * authoring keys leave the surface, one conversion per metadata type. All are + * pure lossless deletes — none of these keys ever had a runtime effect to + * lose — and all are retired from the load path: each schema tombstones its + * keys with the prescription (`retiredKey`), so the loader rejects loudly and + * only `os migrate meta` rewrites sources. + */ +const stripKeys = ( + item: Record, + keys: readonly string[], + emit: (n: { from: string; to: string; path: string }) => void, + path: string, +): Record => { + let touched = false; + const next: Record = { ...item }; + for (const key of keys) { + if (!(key in next)) continue; + delete next[key]; + emit({ from: key, to: '(removed)', path: `${path}.${key}` }); + touched = true; + } + return touched ? next : item; +}; + +/** action.shortcut / action.bulkEnabled — capability claims nothing enforced. */ +const actionInertKeysRemoved: MetadataConversion = { + id: 'action-inert-keys-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'action.shortcut / action.bulkEnabled', + summary: "action keys 'shortcut'/'bulkEnabled' removed (#3896 close-out — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions)", + apply(stack, emit) { + return mapCollection(stack, 'actions', (a, path) => stripKeys(a, ['shortcut', 'bulkEnabled'], emit, path)); + }, + fixture: { + before: { actions: [{ name: 'close_case', label: 'Close', type: 'script', shortcut: 'Ctrl+K', bulkEnabled: true }] }, + after: { actions: [{ name: 'close_case', label: 'Close', type: 'script' }] }, + expectedNotices: 2, + }, +}; + +/** + * flow.active / flow.template / nodes[].outputSchema / + * errorHandling.fallbackNodeId — `active` is the rls.enabled shape (writing + * `active: false` never stopped a flow; `status` is the enforced lifecycle), + * the rest were never read. Deleting `active` preserves behavior exactly — + * the flow was governed by `status` all along; the notice tells the author + * where the real switch is. + */ +const flowInertKeysRemoved: MetadataConversion = { + id: 'flow-inert-keys-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'flow.active / flow.template / flow.nodes[].outputSchema / flow.errorHandling.fallbackNodeId', + summary: "flow keys 'active'/'template', node 'outputSchema' and errorHandling 'fallbackNodeId' removed (#3896 close-out — active:false never stopped a flow; status is the enforced lifecycle)", + apply(stack, emit) { + let out = mapCollection(stack, 'flows', (f, path) => { + let next = stripKeys(f, ['active', 'template'], emit, path); + const eh = next.errorHandling; + if (eh && typeof eh === 'object' && !Array.isArray(eh) && 'fallbackNodeId' in (eh as Record)) { + const nextEh = { ...(eh as Record) }; + delete nextEh.fallbackNodeId; + emit({ from: 'fallbackNodeId', to: '(removed)', path: `${path}.errorHandling.fallbackNodeId` }); + next = next === f ? { ...f } : next; + next.errorHandling = nextEh; + } + return next; + }); + out = mapFlowNodes(out, (node, path) => { + if (!('outputSchema' in node)) return node; + const next = { ...node }; + delete next.outputSchema; + emit({ from: 'outputSchema', to: '(removed)', path: `${path}.outputSchema` }); + return next; + }); + return out; + }, + fixture: { + before: { + flows: [{ + name: 'escalate', + type: 'autolaunched', + active: false, + template: true, + errorHandling: { strategy: 'retry', fallbackNodeId: 'n9' }, + nodes: [{ id: 'n1', type: 'start', outputSchema: { ok: { type: 'boolean' } } }], + edges: [], + }], + }, + after: { + flows: [{ + name: 'escalate', + type: 'autolaunched', + errorHandling: { strategy: 'retry' }, + nodes: [{ id: 'n1', type: 'start' }], + edges: [], + }], + }, + expectedNotices: 4, + }, +}; + +/** + * View container: list-shaped entries lose `responsive`/`performance`, + * form-shaped entries lose `data`/`defaultSort`/`aria`. Deliberately + * SHAPE-SCOPED — the LIST view's `aria` and `data` are live and untouched. + */ +const viewInertKeysRemoved: MetadataConversion = { + id: 'view-inert-keys-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'view.list.responsive / view.list.performance / view.form.defaultSort / view.form.aria', + summary: "view keys removed (#3896 close-out): list 'responsive'/'performance', form 'defaultSort'/'aria' — no renderer read them (list aria/data and form data stay live)", + apply(stack, emit) { + const LIST_KEYS = ['responsive', 'performance'] as const; + // NOT 'data': the sweep's removal attempt was refuted by the build — + // defineForm writes data.provider='schema' on every metadata form. + const FORM_KEYS = ['defaultSort', 'aria'] as const; + return mapCollection(stack, 'views', (view, path) => { + let touched = false; + const next: Record = { ...view }; + const fix = (keys: readonly string[], sub: Record, subPath: string) => { + const cleaned = stripKeys(sub, keys, emit, subPath); + if (cleaned !== sub) { touched = true; return cleaned; } + return sub; + }; + for (const [slot, keys] of [['list', LIST_KEYS], ['form', FORM_KEYS]] as const) { + const v = next[slot]; + if (v && typeof v === 'object' && !Array.isArray(v)) next[slot] = fix(keys, v as Record, `${path}.${slot}`); + } + for (const [slot, keys] of [['listViews', LIST_KEYS], ['formViews', FORM_KEYS]] as const) { + const named = next[slot]; + if (named && typeof named === 'object' && !Array.isArray(named)) { + const rebuilt: Record = { ...(named as Record) }; + let subTouched = false; + for (const [name, v] of Object.entries(rebuilt)) { + if (v && typeof v === 'object' && !Array.isArray(v)) { + const cleaned = stripKeys(v as Record, keys, emit, `${path}.${slot}.${name}`); + if (cleaned !== v) { rebuilt[name] = cleaned; subTouched = true; } + } + } + if (subTouched) { next[slot] = rebuilt; touched = true; } + } + } + return touched ? next : view; + }); + }, + fixture: { + before: { + views: [{ + object: 'crm_lead', + list: { type: 'grid', columns: ['name'], responsive: { sm: {} }, performance: { lazyLoad: true }, aria: { label: 'Leads' } }, + form: { type: 'simple', data: { provider: 'object', object: 'crm_lead' }, defaultSort: [{ field: 'created_at', order: 'desc' }], aria: { label: 'Lead form' } }, // data survives + }], + }, + after: { + views: [{ + object: 'crm_lead', + list: { type: 'grid', columns: ['name'], aria: { label: 'Leads' } }, + form: { type: 'simple', data: { provider: 'object', object: 'crm_lead' } }, + }], + }, + expectedNotices: 4, + }, +}; + +/** dashboard.aria / dashboard.performance / widgets[].performance. */ +const dashboardInertKeysRemoved: MetadataConversion = { + id: 'dashboard-inert-keys-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'dashboard.aria / dashboard.performance / dashboard.widgets[].performance', + summary: "dashboard keys 'aria'/'performance' and widget 'performance' removed (#3896 close-out — no renderer applied any of them)", + apply(stack, emit) { + return mapCollection(stack, 'dashboards', (d, path) => { + let next = stripKeys(d, ['aria', 'performance'], emit, path); + const widgets = next.widgets; + if (Array.isArray(widgets)) { + let subTouched = false; + const rebuilt = widgets.map((w, i) => { + if (w && typeof w === 'object' && !Array.isArray(w) && 'performance' in w) { + const cleaned = { ...(w as Record) }; + delete cleaned.performance; + emit({ from: 'performance', to: '(removed)', path: `${path}.widgets[${i}].performance` }); + subTouched = true; + return cleaned; + } + return w; + }); + if (subTouched) { + next = next === d ? { ...d } : next; + next.widgets = rebuilt; + } + } + return next; + }); + }, + fixture: { + before: { + dashboards: [{ + name: 'sales_kpis', + aria: { label: 'Sales' }, + performance: { lazyLoad: true }, + widgets: [{ id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'], performance: { prefetch: true } }], + }], + }, + after: { + dashboards: [{ + name: 'sales_kpis', + widgets: [{ id: 'w1', type: 'kpi', dataset: 'orders', values: ['total'] }], + }], + }, + expectedNotices: 3, + }, +}; + +/** + * agent.knowledge — a grounding claim nothing enforced (the RAG path reads + * `sourceIds` from the LLM's tool-call arguments, never the agent record). + * Absorbs the former `agent-knowledge-topics-to-sources` rename (#3855): + * both spellings of the block end deleted, in one notice. + */ +const agentKnowledgeRemoved: MetadataConversion = { + id: 'agent-knowledge-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'agent.knowledge', + summary: "agent key 'knowledge' removed (#3896 close-out — declaring sources/indexes never scoped retrieval; restrict at the knowledge-service level)", + apply(stack, emit) { + return mapCollection(stack, 'agents', (a, path) => stripKeys(a, ['knowledge'], emit, path)); + }, + fixture: { + before: { agents: [{ name: 'support_agent', label: 'Support', knowledge: { sources: ['faq'], indexes: ['docs'] } }] }, + after: { agents: [{ name: 'support_agent', label: 'Support' }] }, + expectedNotices: 1, + }, +}; + +/** skill.triggerPhrases — phrases were never matched against the user's message. */ +const skillTriggerPhrasesRemoved: MetadataConversion = { + id: 'skill-trigger-phrases-removed', + toMajor: 17, + retiredFromLoadPath: true, + surface: 'skill.triggerPhrases', + summary: "skill key 'triggerPhrases' removed (#3896 close-out — activation is triggerConditions + the agent's skills[] allowlist; phrases were a dead-end projection)", + apply(stack, emit) { + return mapCollection(stack, 'skills', (sk, path) => stripKeys(sk, ['triggerPhrases'], emit, path)); + }, + fixture: { + before: { skills: [{ name: 'case_mgmt', label: 'Cases', triggerPhrases: ['open a ticket'] }] }, + after: { skills: [{ name: 'case_mgmt', label: 'Cases' }] }, + expectedNotices: 1, + }, +}; + /** * All conversions, keyed by the protocol major that introduced the canonical * shape. Newest majors last; ordering within a major is application order. @@ -1321,7 +1556,6 @@ export const CONVERSIONS_BY_MAJOR: Readonly { expect(action.requiredPermissions).toBeUndefined(); }); }); + +describe('#3896 close-out — retired shortcut/bulkEnabled', () => { + it('REJECTS the retired `shortcut` and names the real keyboard stack', () => { + let message = ''; + try { + ActionSchema.parse({ name: 'save_now', label: 'Save', type: 'script', target: 'h', shortcut: 'Ctrl+S' }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/keyboard stack/); + expect(message).toMatch(/#3896/); + }); + it('REJECTS the retired `bulkEnabled` and points at the view bulkActions', () => { + let message = ''; + try { + ActionSchema.parse({ name: 'close_all', label: 'Close', type: 'script', target: 'h', bulkEnabled: true }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/bulkActions/); + expect(message).toMatch(/#3896/); + }); +}); diff --git a/packages/spec/src/ui/action.zod.ts b/packages/spec/src/ui/action.zod.ts index 1457b0f003..4c53316a88 100644 --- a/packages/spec/src/ui/action.zod.ts +++ b/packages/spec/src/ui/action.zod.ts @@ -598,10 +598,25 @@ export const ActionSchema = lazySchema(() => z.object({ requiredPermissions: z.array(z.string()).optional().describe('[ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there.'), /** Keyboard Shortcut */ - shortcut: z.string().optional().describe('Keyboard shortcut to trigger this action (e.g., "Ctrl+S")'), - - /** Bulk Operations */ - bulkEnabled: z.boolean().optional().describe('Whether this action can be applied to multiple selected records'), + // `shortcut` and `bulkEnabled` REMOVED by the 2026-07 #3896 audit close-out — + // both were authorable capability claims nothing enforced (ledger: dead, + // #3686 re-verification): ActionEngine registered them but no keydown path + // ever dispatched a shortcut, and the multi-select toolbar reads the LIST + // VIEW's bulkActions, never this flag. Tombstoned with the prescription; + // `action-inert-keys-removed` strips them from authored sources. + shortcut: retiredKey( + '`action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and ' + + "objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults " + + 'action metadata. Delete the key. For a real shortcut, register the key in the Console ' + + 'keyboard stack and have its handler invoke the action by name.', + ), + bulkEnabled: retiredKey( + '`action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'the multi-select toolbar is driven by the LIST VIEW\'s `bulkActions` / `bulkActionDefs`, ' + + 'never by this flag, so setting it changed nothing. Delete the key and declare the action ' + + "in the view's `bulkActions` instead.", + ), /** * AI exposure block (ADR-0011). Opt-in, default off: an action is exposed diff --git a/packages/spec/src/ui/dashboard.form.ts b/packages/spec/src/ui/dashboard.form.ts index 62bbc9f89e..e0add16bac 100644 --- a/packages/spec/src/ui/dashboard.form.ts +++ b/packages/spec/src/ui/dashboard.form.ts @@ -50,8 +50,8 @@ export const dashboardForm = defineForm({ collapsible: true, collapsed: true, fields: [ - { field: 'aria', type: 'composite', helpText: 'Accessibility labels' }, - { field: 'performance', type: 'composite', helpText: 'Caching and optimization config' }, + // `aria` / `performance` inputs removed with the keys (#3896 close-out): + // no dashboard renderer ever applied either. ], }, ], diff --git a/packages/spec/src/ui/dashboard.zod.ts b/packages/spec/src/ui/dashboard.zod.ts index 8fd18f409e..386f3272b9 100644 --- a/packages/spec/src/ui/dashboard.zod.ts +++ b/packages/spec/src/ui/dashboard.zod.ts @@ -8,12 +8,13 @@ import { DateGranularity } from '../data/query.zod'; import { ChartTypeSchema, ChartConfigSchema } from './chart.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; -import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zod'; +import { ResponsiveConfigSchema } from './responsive.zod'; /** * Color variant for dashboard widgets (e.g., KPI cards). */ import { lazySchema } from '../shared/lazy-schema'; +import { retiredKey } from '../shared/retired-key'; export const WidgetColorVariantSchema = lazySchema(() => z.enum([ 'default', 'blue', @@ -451,11 +452,19 @@ export const DashboardSchema = lazySchema(() => z.object({ /** Global Filters */ globalFilters: z.array(GlobalFilterSchema).optional().describe('Global filters that apply to all widgets in the dashboard'), - /** ARIA accessibility attributes */ - aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), - - /** Performance optimization settings */ - performance: PerformanceConfigSchema.optional().describe('Performance optimization settings'), + // `aria` / `performance` REMOVED (#3896 audit close-out): authorable and + // inert — no DashboardRenderer path applied either (ledger: dead; the + // report twins were removed in the report-liveness close-out). + aria: retiredKey( + '`dashboard.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no ' + + 'dashboard renderer ever applied it, so declared ARIA attributes silently did not reach ' + + 'the DOM. Delete the key.', + ), + performance: retiredKey( + '`dashboard.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit ' + + 'close-out) — no renderer or runtime read it; dashboard performance tuning was never ' + + 'implemented. Delete the key.', + ), /** * ADR-0010 §3.7 — Package-level protection envelope. Package * authors declare lock policy here; the loader translates it diff --git a/packages/spec/src/ui/responsive.test.ts b/packages/spec/src/ui/responsive.test.ts index d9a14df82e..ba5f522894 100644 --- a/packages/spec/src/ui/responsive.test.ts +++ b/packages/spec/src/ui/responsive.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from 'vitest'; import { ResponsiveConfigSchema, - PerformanceConfigSchema, BreakpointName, type ResponsiveConfig, type PerformanceConfig, @@ -88,59 +87,6 @@ describe('ResponsiveConfigSchema', () => { }); }); -describe('PerformanceConfigSchema', () => { - it('should accept empty config', () => { - expect(() => PerformanceConfigSchema.parse({})).not.toThrow(); - }); - - it('should accept lazy load config', () => { - const config: PerformanceConfig = { - lazyLoad: true, - }; - - const result = PerformanceConfigSchema.parse(config); - expect(result.lazyLoad).toBe(true); - }); - - it('should accept virtual scroll config', () => { - const config: PerformanceConfig = { - virtualScroll: { - enabled: true, - itemHeight: 40, - overscan: 5, - }, - }; - - const result = PerformanceConfigSchema.parse(config); - expect(result.virtualScroll?.enabled).toBe(true); - expect(result.virtualScroll?.itemHeight).toBe(40); - expect(result.virtualScroll?.overscan).toBe(5); - }); - - it('should accept cache strategy config', () => { - const strategies = ['none', 'cache-first', 'network-first', 'stale-while-revalidate'] as const; +// PerformanceConfigSchema tests removed with the schema (#3896 close-out): +// every carrier of a `performance` block was authorable and inert. - strategies.forEach(strategy => { - expect(() => PerformanceConfigSchema.parse({ cacheStrategy: strategy })).not.toThrow(); - }); - }); - - it('should accept full performance config', () => { - const config: PerformanceConfig = { - lazyLoad: true, - virtualScroll: { enabled: true, itemHeight: 48, overscan: 3 }, - cacheStrategy: 'stale-while-revalidate', - prefetch: true, - pageSize: 50, - debounceMs: 300, - }; - - expect(() => PerformanceConfigSchema.parse(config)).not.toThrow(); - }); - - it('should reject invalid cache strategy', () => { - expect(() => PerformanceConfigSchema.parse({ - cacheStrategy: 'invalid', - })).toThrow(); - }); -}); diff --git a/packages/spec/src/ui/responsive.zod.ts b/packages/spec/src/ui/responsive.zod.ts index 25c9317328..d8822e402d 100644 --- a/packages/spec/src/ui/responsive.zod.ts +++ b/packages/spec/src/ui/responsive.zod.ts @@ -106,53 +106,12 @@ export const ResponsiveStylesSchema = lazySchema(() => z.object({ export type ResponsiveStyles = z.infer; -/** - * Performance Configuration Schema - * - * Defines performance optimization settings for UI components - * such as lazy loading, virtual scrolling, and caching. - * - * @example - * ```typescript - * const perf: PerformanceConfig = { - * lazyLoad: true, - * virtualScroll: { enabled: true, itemHeight: 40, overscan: 5 }, - * cacheStrategy: 'stale-while-revalidate', - * prefetch: true, - * }; - * ``` +/* + * REMOVED — `PerformanceConfigSchema` / `PerformanceConfig` (#3896 audit + * close-out). It typed the `performance` keys on report (removed in the + * report-liveness close-out), list view and dashboard (removed in this + * sweep): every carrier was authorable and inert — no renderer or runtime + * ever read a performance block. An exported schema with no consumer is read + * as a capability by whoever finds it (#3950 precedent). */ -export const PerformanceConfigSchema = lazySchema(() => z.object({ - /** Enable lazy loading for this component */ - lazyLoad: z.boolean().optional() - .describe('Enable lazy loading (defer rendering until visible)'), - - /** Virtual scrolling configuration for large datasets */ - virtualScroll: z.object({ - enabled: z.boolean().default(false).describe('Enable virtual scrolling'), - itemHeight: z.number().optional().describe('Fixed item height in pixels (for estimation)'), - overscan: z.number().optional().describe('Number of extra items to render outside viewport'), - }).optional().describe('Virtual scrolling configuration'), - - /** Client-side caching strategy */ - cacheStrategy: z.enum([ - 'none', - 'cache-first', - 'network-first', - 'stale-while-revalidate', - ]).optional().describe('Client-side data caching strategy'), - - /** Enable data prefetching */ - prefetch: z.boolean().optional() - .describe('Prefetch data before component is visible'), - - /** Maximum number of items to render before pagination */ - pageSize: z.number().optional() - .describe('Number of items per page for pagination'), - - /** Debounce interval for user interactions (ms) */ - debounceMs: z.number().optional() - .describe('Debounce interval for user interactions in milliseconds'), -}).describe('Performance optimization configuration')); - -export type PerformanceConfig = z.infer; + diff --git a/packages/spec/src/ui/view.test.ts b/packages/spec/src/ui/view.test.ts index 10c9f8fe22..cb5a0bd782 100644 --- a/packages/spec/src/ui/view.test.ts +++ b/packages/spec/src/ui/view.test.ts @@ -574,63 +574,22 @@ describe('FormViewSchema', () => { expect(() => FormViewSchema.parse(wizardView)).not.toThrow(); }); - it('should accept form view with object provider', () => { - const formView: FormView = { - type: 'simple', - data: { - provider: 'object', - object: 'account', - }, - sections: [ - { - label: 'Account Information', - fields: ['name', 'industry', 'revenue'], - }, - ], - }; - - expect(() => FormViewSchema.parse(formView)).not.toThrow(); - }); - - it('should accept form view with api provider', () => { - const formView: FormView = { + // form `data` SURVIVED the #3896 sweep: the removal attempt broke the build + // — defineForm writes data.provider='schema' onto every metadata form — so + // the ledger verdict was corrected instead. These pin the surviving shapes. + it('accepts form view with object provider', () => { + expect(() => FormViewSchema.parse({ type: 'simple', - data: { - provider: 'api', - read: { - url: '/api/accounts/:id', - method: 'GET', - }, - write: { - url: '/api/accounts/:id', - method: 'PUT', - }, - }, - sections: [ - { - fields: ['name', 'email', 'phone'], - }, - ], - }; - - expect(() => FormViewSchema.parse(formView)).not.toThrow(); + data: { provider: 'object', object: 'account' }, + sections: [{ fields: ['name'] }], + })).not.toThrow(); }); - - it('should accept form view with value provider', () => { - const formView: FormView = { + it('accepts the schema provider defineForm writes', () => { + expect(() => FormViewSchema.parse({ type: 'simple', - data: { - provider: 'value', - items: [{ name: 'Default Account', type: 'Customer' }], - }, - sections: [ - { - fields: ['name', 'type'], - }, - ], - }; - - expect(() => FormViewSchema.parse(formView)).not.toThrow(); + data: { provider: 'schema', schemaId: 'flow' }, + sections: [{ fields: ['name'] }], + })).not.toThrow(); }); }); @@ -1948,43 +1907,23 @@ describe('Airtable-style ListView enhancements', () => { // Protocol Improvement Tests: FormView defaultSort // ============================================================================ -describe('FormViewSchema - defaultSort', () => { - it('should accept defaultSort configuration', () => { - const result = FormViewSchema.parse({ - type: 'simple', - sections: [{ fields: ['name'] }], - defaultSort: [ - { field: 'created_at', order: 'desc' }, - { field: 'name', order: 'asc' }, - ], - }); - expect(result.defaultSort).toHaveLength(2); - expect(result.defaultSort![0].field).toBe('created_at'); - expect(result.defaultSort![0].order).toBe('desc'); - }); - - it('should default sort order to desc', () => { - const result = FormViewSchema.parse({ - type: 'simple', - sections: [{ fields: ['name'] }], - defaultSort: [{ field: 'updated_at' }], - }); - expect(result.defaultSort![0].order).toBe('desc'); - }); - - it('should accept form view without defaultSort (optional)', () => { - const result = FormViewSchema.parse({ - type: 'simple', - sections: [{ fields: ['name'] }], - }); - expect(result.defaultSort).toBeUndefined(); +describe('FormViewSchema — retired defaultSort (#3896 close-out)', () => { + it('REJECTS the retired `defaultSort` and points at the related list view', () => { + let message = ''; + try { + FormViewSchema.parse({ + type: 'simple', sections: [{ fields: ['name'] }], + defaultSort: [{ field: 'created_at', order: 'desc' }], + }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/list view/); + expect(message).toMatch(/#3896/); + }); + it('a form without it still parses', () => { + expect(() => FormViewSchema.parse({ type: 'simple', sections: [{ fields: ['name'] }] })).not.toThrow(); }); }); -// ============================================================================ -// FormView structured buttons + defaults (#2998; live now the objectui ObjectForm folds them, framework#1894) -// ============================================================================ - describe('FormViewSchema - buttons & defaults', () => { it('should accept structured button config with visibility and labels', () => { const result = FormViewSchema.parse({ @@ -2596,49 +2535,21 @@ describe('ListViewSchema filter field', () => { // ============================================================================ // Issue #7: ListView responsive and performance config // ============================================================================ -describe('ListViewSchema - responsive and performance', () => { - it('should accept list view with responsive config', () => { - const view = ListViewSchema.parse({ - type: 'grid', - columns: ['name', 'status'], - responsive: { - hiddenOn: ['xs'], - columns: { xs: 12, md: 6, lg: 4 }, - }, - }); - expect(view.responsive?.hiddenOn).toEqual(['xs']); - expect(view.responsive?.columns?.md).toBe(6); - }); - - it('should accept list view with performance config', () => { - const view = ListViewSchema.parse({ - type: 'grid', - columns: ['name'], - performance: { - lazyLoad: true, - virtualScroll: { enabled: true, itemHeight: 40, overscan: 5 }, - cacheStrategy: 'stale-while-revalidate', - prefetch: true, - }, - }); - expect(view.performance?.lazyLoad).toBe(true); - expect(view.performance?.virtualScroll?.enabled).toBe(true); - expect(view.performance?.cacheStrategy).toBe('stale-while-revalidate'); +describe('ListViewSchema — retired responsive/performance (#3896 close-out)', () => { + it('REJECTS the retired `responsive` with the prescription', () => { + expect(() => ListViewSchema.parse({ + type: 'grid', columns: ['name'], responsive: { hiddenOn: ['xs'] }, + })).toThrow(/responsive.*removed|removed.*responsive/s); }); - - it('should accept list view without responsive/performance (optional)', () => { - const view = ListViewSchema.parse({ - type: 'grid', - columns: ['name'], - }); - expect(view.responsive).toBeUndefined(); - expect(view.performance).toBeUndefined(); + it('REJECTS the retired `performance` with the prescription', () => { + let message = ''; + try { + ListViewSchema.parse({ type: 'grid', columns: ['name'], performance: { lazyLoad: true } }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/#3896/); }); }); -// ============================================================================ -// Issue #8: HttpMethodSchema/HttpRequestSchema re-exported from view.zod -// ============================================================================ describe('HttpMethodSchema/HttpRequestSchema backward compat', () => { it('should still be importable from view.zod', () => { expect(HttpMethodSchema).toBeDefined(); diff --git a/packages/spec/src/ui/view.zod.ts b/packages/spec/src/ui/view.zod.ts index 7ac0ecf8ce..fc824ffa11 100644 --- a/packages/spec/src/ui/view.zod.ts +++ b/packages/spec/src/ui/view.zod.ts @@ -8,7 +8,7 @@ import { ExpressionInputSchema } from '../shared/expression.zod'; import { normalizeVisibleWhen, strictVisibilityError } from '../shared/visibility'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; import { SharingConfigSchema } from './sharing.zod'; -import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zod'; +import { retiredKey } from '../shared/retired-key'; import { FieldType, SelectOptionSchema } from '../data/field.zod'; /** @@ -774,11 +774,18 @@ export const ListViewSchema = lazySchema(() => z.object({ /** ARIA accessibility attributes */ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the list view'), - /** Responsive layout overrides per breakpoint */ - responsive: ResponsiveConfigSchema.optional().describe('Responsive layout configuration'), - - /** Performance optimization settings */ - performance: PerformanceConfigSchema.optional().describe('Performance optimization settings'), + // `responsive` / `performance` REMOVED (#3896 audit close-out): authorable + // and inert — no renderer in either repo read them (re-grepped 2026-07-16, + // objectui@fb35e48; ledger: dead). + responsive: retiredKey( + '`view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'no renderer ever read it; the grid is responsive by its own layout rules. Delete the key.', + ), + performance: retiredKey( + '`view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'no renderer or runtime read it; list-view performance tuning was never implemented. ' + + 'Delete the key.', + ), })); /** @@ -995,9 +1002,17 @@ export const FormViewSchema = lazySchema(() => z.object({ /** Modal (`type: 'modal'`). */ modalSize: z.enum(['sm', 'default', 'lg', 'xl', 'full']).optional().describe('Modal size (modal forms)'), - /** Data Source Configuration */ + /** + * Data Source Configuration. NOT removed in the #3896 sweep despite the + * ledger's dead verdict — the removal attempt broke the build and thereby + * re-verified the entry: `defineForm` (below) writes + * `data: { provider: 'schema', schemaId }` onto EVERY metadata form, and + * `metadata-protocol` serves it to the metadata-admin pipeline. The + * record-form provider values (`object`/`api`/`value`) may still be + * unread — that is a narrower value-subset audit, not a key removal. + */ data: ViewDataSchema.optional().describe('Data source configuration (defaults to "object" provider)'), - + sections: z.array(FormSectionSchema).optional(), // For simple layout groups: z.array(FormSectionSchema).optional(), // Legacy support -> alias to sections @@ -1022,11 +1037,13 @@ export const FormViewSchema = lazySchema(() => z.object({ maxRows: z.number().optional(), })).optional().describe('Inline master-detail child collections'), - /** Default Sort for Related Lists (e.g., sort child records by date) */ - defaultSort: z.array(z.object({ - field: z.string().describe('Field name to sort by'), - order: z.enum(['asc', 'desc']).default('desc').describe('Sort direction'), - })).optional().describe('Default sort order for related list views within this form'), + // `defaultSort` REMOVED (#3896 audit close-out): no reader in either repo — + // related lists sort by their OWN list view's sort, never by this. + defaultSort: retiredKey( + '`form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'nothing read it: a related list inside a form sorts by its own list view\'s `sort`. ' + + 'Delete the key and set the sort on the related list view instead.', + ), /** Public form sharing configuration */ sharing: SharingConfigSchema.optional().describe('Public sharing configuration for this form'), @@ -1080,8 +1097,16 @@ export const FormViewSchema = lazySchema(() => z.object({ defaults: z.record(z.string(), z.unknown()).optional() .describe('Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values; framework#1894 / #2998).'), - /** ARIA accessibility attributes */ - aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the form view'), + // `aria` REMOVED from the FORM view (#3896 audit close-out): no form + // renderer applied it (plugin-form/SchemaForm never read schema.aria) — an + // ACCESSIBILITY claim that is merely accepted is false compliance, the + // requiresConfirmation shape. + aria: retiredKey( + '`form.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no form ' + + 'renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. ' + + 'Delete the key. The form renderer emits its own semantic markup; report gaps as ' + + 'renderer issues rather than per-view attribute overrides.', + ), })); /** diff --git a/packages/spec/src/ui/widget.test.ts b/packages/spec/src/ui/widget.test.ts index 1c23dc4f77..b05ef5515f 100644 --- a/packages/spec/src/ui/widget.test.ts +++ b/packages/spec/src/ui/widget.test.ts @@ -354,12 +354,15 @@ describe('Widget ARIA Integration', () => { }); }); -describe('Widget Performance Integration', () => { - it('should accept widget with performance config', () => { - expect(() => WidgetManifestSchema.parse({ - name: 'perf_widget', - label: 'Performance Widget', - performance: { lazyLoad: true, virtualScroll: { enabled: true } }, - })).not.toThrow(); +describe('Widget — retired performance (#3896 close-out)', () => { + it('REJECTS the retired `performance` and names the live alternative', () => { + let message = ''; + try { + WidgetManifestSchema.parse({ + name: 'perf_widget', label: 'Performance Widget', performance: { lazyLoad: true }, + }); + } catch (e) { message = String((e as Error).message); } + expect(message).toMatch(/virtualScroll/); + expect(message).toMatch(/#3896/); }); }); diff --git a/packages/spec/src/ui/widget.zod.ts b/packages/spec/src/ui/widget.zod.ts index e9b35f0515..dedc9e7a3e 100644 --- a/packages/spec/src/ui/widget.zod.ts +++ b/packages/spec/src/ui/widget.zod.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { FieldSchema } from '../data/field.zod'; import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod'; import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod'; -import { PerformanceConfigSchema } from './responsive.zod'; +import { retiredKey } from '../shared/retired-key'; /** * Widget Lifecycle Hooks Schema @@ -371,7 +371,14 @@ export const WidgetManifestSchema = lazySchema(() => z.object({ aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'), /** Performance optimization settings */ - performance: PerformanceConfigSchema.optional().describe('Performance optimization settings'), + // `performance` REMOVED (#3896 audit close-out): call-graph closed across + // both repos — zero readers (objectui's virtual scrolling reads the LIVE + // top-level `virtualScroll` key, never performance.virtualScroll). + performance: retiredKey( + '`widget.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — ' + + 'no renderer or runtime ever read it. Delete the key. Virtual scrolling is the live ' + + 'top-level `virtualScroll` on list-shaped views.', + ), })); export type WidgetManifest = z.infer; diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index 71757ac1ec..4ab0c8db9f 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -169,7 +169,7 @@ To grant data exploration to your own (platform-internal) agent, add | `tools` | Direct tool references — legacy fallback | | `surface` | `'ask' \| 'build'` — the product surface this agent is (default `'ask'`) | | `model` | LLM model configuration — `provider`, `model`, `temperature`, `maxTokens`, `topP` | -| `knowledge` | RAG access — `sources` + `indexes`. The `topics` alias was REMOVED in protocol 17; emitting it is a parse error | +| ~~`knowledge`~~ | REMOVED in protocol 17 (#3896 close-out) — declaring sources/indexes on an agent never scoped retrieval (`search_knowledge` takes `sourceIds` from the LLM's tool-call arguments). Restrict at the knowledge-service/source level; describe intended grounding in `instructions` | | `guardrails` | `maxTokensPerInvocation`, `maxExecutionTimeSec`, `blockedTopics` | | `structuredOutput` | Output format (JSON schema, regex, etc.) | | `planning` | Autonomous reasoning — `maxIterations` (default 10) | diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index e1c19ed139..037b4abc63 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -30,7 +30,6 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities - `node_modules/@objectstack/spec/src/ui/action.zod.ts` — Action Parameter Schema - `node_modules/@objectstack/spec/src/ui/i18n.zod.ts` — I18n Object Schema -- `node_modules/@objectstack/spec/src/ui/responsive.zod.ts` — Breakpoint Name Enum - `node_modules/@objectstack/spec/src/ui/sharing.zod.ts` — Sharing & Embedding Protocol - `node_modules/@objectstack/spec/src/ui/view.zod.ts` — HTTP Method Enum & HTTP Request Schema From 4f585f9ed164e2f6270e0bba407586a59851e912 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 07:14:38 +0000 Subject: [PATCH 2/3] fix(liveness,skills): tombstone entries stay in the ledger; the SKILL.md defineSkill example drops triggerPhrases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's liveness gate caught the sweep's ledger discipline error: retiredKey keeps a key in the walked shape, so its entry must STAY (dead + REMOVED note — the rls.priority precedent), not be deleted (that is the strict- removal discipline, used for the tool batch where keys left the shape). All 14 entries restored with verifiedAt + per-key prescriptions; README counts corrected to match. The local 'pass' before push was a masked exit code ('| tail -1' swallows the gate's status) — gates now re-run with explicit exit checks, which also surfaced the SKILL.md defineSkill example still authoring triggerPhrases (a head-truncated grep miss; the os:check harness is the authoritative sweeper and is green again). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- packages/spec/liveness/README.md | 12 ++++++------ packages/spec/liveness/action.json | 10 ++++++++++ packages/spec/liveness/agent.json | 5 +++++ packages/spec/liveness/dashboard.json | 10 ++++++++++ packages/spec/liveness/flow.json | 20 ++++++++++++++++++++ packages/spec/liveness/skill.json | 5 +++++ packages/spec/liveness/view.json | 20 ++++++++++++++++++++ skills/objectstack-ai/SKILL.md | 6 ------ 8 files changed, 76 insertions(+), 12 deletions(-) diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 84ced134bb..4a0aece274 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -465,20 +465,20 @@ EOF |---|---|---|---|---|---| | object | 40 | – | 0 | 1 | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) + tags/active/abstract REMOVED (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; `enable.trash`/`mru` REMOVED (#2377 close-out) — tombstoned in the now-`.strict()` ObjectCapabilities; `isSystem` + `enable.searchable` CORRECTED to live (#2377 — sharing default-model + global-search opt-out; 2026-06 audit missed both readers); `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | | field | 55 | – | 0 | – | healthy — full dead set (vectorConfig/fileAttachmentConfig/dependencies, then referenceFilters/columnName/index) REMOVED (#2377); columnName also dropped the ADR-0062 D7 lint + StorageNameMapping column helpers | -| flow | 26 | – | 1 | – | `active`/`template`/nodes.`outputSchema`/errorHandling.`fallbackNodeId` REMOVED 2026-07-30 (#3896 close-out sweep — `active: false` never stopped a flow, `status` is the enforced lifecycle; faults route via per-node fault edges); remaining dead = `description`, KEPT deliberately: docs-shaped, exempt from enforce-or-remove | -| action | 34 | 0 | 0 | – | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live since objectui#2863; `undoable` CORRECTED to live (#3714); `shortcut` + `bulkEnabled` REMOVED 2026-07-30 (#3896 close-out sweep — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | +| flow | 26 | – | 5 | – | dead count = 4 tombstone entries + the kept docs field: `active`/`template`/nodes.`outputSchema`/errorHandling.`fallbackNodeId` REMOVED 2026-07-30 (#3896 close-out sweep — `active: false` never stopped a flow, `status` is the enforced lifecycle; faults route via per-node fault edges); remaining dead = `description`, KEPT deliberately: docs-shaped, exempt from enforce-or-remove | +| action | 34 | 0 | 2 | – | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live since objectui#2863; `undoable` CORRECTED to live (#3714); `shortcut` + `bulkEnabled` REMOVED 2026-07-30 (#3896 close-out sweep — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions) | | hook | 11 | – | 2 | – | model-healthy; label/description dead but KEPT deliberately (2026-07-30 sweep) — docs-shaped annotation fields, exempt from enforce-or-remove | | permission | 29 | – | 4 | – | CRUD/FLS/RLS live; dead `contextVariables` REMOVED (ADR-0105 D11 — RLS resolves only the `current_user.*` built-ins plus runtime-staged `rlsMembership` sets). 2026-07-30 security-subset re-verification (all 33 entries `verifiedAt`-stamped): `rowLevelSecurity.enabled` was live-with-wrong-evidence and UNREAD — a disabled policy kept contributing its OR-branch grant; ENFORCED same day in rls-compiler (`getApplicablePolicies`), the `positions` ADR-0049 resolution repeated. `rowLevelSecurity.priority` CORRECTED to dead+authorWarn — semantically void under OR-combination (no conflict exists to order), a REMOVE candidate. `rls.label`/`description`/`tags` CORRECTED to dead (benign display, no consumer in either repo). `tabPermissions` was UNDERSTATED ("only hidden read" → the rank merge reads all four values; me-apps dogfood test exercises it). `allowExport` re-verified TRUE end-to-end (server-side 403 gate, not just the /me projection) | | position | 4 | – | – | – | (role's ADR-0090 successor) fully live; all 4 `verifiedAt`-stamped 2026-07-30 | -| agent | 13 | 4 | 0 | – | dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377); autonomy tier experimental; `knowledge` REMOVED 2026-07-30 (#3896 close-out sweep — declaring sources never scoped retrieval; AIKnowledgeSchema removed with it, the topics→sources rename absorbed pre-release) | +| agent | 13 | 4 | 1 | – | dead `tenantId` + `planning.strategy`/`allowReplan` REMOVED (#2377); autonomy tier experimental; `knowledge` REMOVED 2026-07-30 (#3896 close-out sweep — declaring sources never scoped retrieval; AIKnowledgeSchema removed with it, the topics→sources rename absorbed pre-release) | | tool | 5 | 1 | 0 | – | the inert authoring surface is now REMOVED, not merely marked: `category`/`permissions`/`active`/`builtIn` retired 2026-07-30 (#3896 close-out) after `requiresConfirmation` set the precedent (#3715, ADR-0033 §2). `permissions` promised an invocation gate nothing enforced and `active:false` withdrew nothing — false compliance, same shape as rls.enabled. The `.strict()` ToolSchema rejects each retired key with its prescription; the `tool-inert-authoring-keys-removed` conversion strips them from authored sources | -| skill | 8 | – | 0 | – | `permissions` REMOVED 2026-07 (#3704); `triggerPhrases` REMOVED 2026-07-30 (#3896 close-out sweep — phrases were never matched; activation is `triggerConditions` + the agent's `skills[]` + /skill-name pinning) | +| skill | 8 | – | 1 | – | `permissions` REMOVED 2026-07 (#3704); `triggerPhrases` REMOVED 2026-07-30 (#3896 close-out sweep — phrases were never matched; activation is `triggerConditions` + the agent's `skills[]` + /skill-name pinning) | | dataset | 19 | – | 0 | – | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | 16 | – | – | 1 | fully live + one planned | -| view | 70 | 0 | 0 | – | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{data,defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification; level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | +| view | 70 | 0 | 4 | – | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{data,defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification; level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) noted on parents — one drill level only | | report | 13 | 0 | 0 | – | dataset-bound (ADR-0021); the aria/performance LEDGER entries were stale — the keys left the schema in the report-liveness close-out; deleted 2026-07-30 as hygiene. Audit-era `chart` DEAD superseded (framework#1890 / #3441) | -| dashboard | 10 | 0 | 0 | – | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) | +| dashboard | 10 | 0 | 2 | – | ADR-0021 dataset widgets (#3251; DashboardWidgetSchema `.strict()`); `aria`/`performance` (and widget `performance` + PerformanceConfigSchema) REMOVED 2026-07-30 (#3896 close-out sweep — no renderer applied any of them); audit-era `globalFilters`/`dateRange` DEAD superseded (framework#2501) | | webhook | 0 | 1 | 16 | – | **not a registered metadata type** — governed via the gate's spec-only schema override (`SPEC_ONLY_SCHEMAS`), not `getMetadataTypeSchema` (#3461/#3462). The ENTIRE authoring surface is dead: nothing materializes an authored `webhooks:` entry into a `sys_webhook` dispatcher row (#3461, enforce-or-remove pending). `url` carries the single per-webhook `authorWarn` (one no-op heads-up per artifact, not per-prop); `authentication` experimental (HMAC-`secret`-only); `isActive` unmarked (default(true)). Notes cite the sys_webhook column map as the future materializer's mapping table | The `dead` set across types is the enforce-or-remove worklist (ADR-0049); every diff --git a/packages/spec/liveness/action.json b/packages/spec/liveness/action.json index 4a3b0d1f15..c3e83ff3e7 100644 --- a/packages/spec/liveness/action.json +++ b/packages/spec/liveness/action.json @@ -153,6 +153,16 @@ "aria": { "status": "live", "note": "PARTIAL — honored by a few objectui renderers, not the core action buttons/menus." + }, + "shortcut": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); register keys in the Console keyboard stack and invoke the action by name." + }, + "bulkEnabled": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); declare the action in the view's `bulkActions` instead." } } } diff --git a/packages/spec/liveness/agent.json b/packages/spec/liveness/agent.json index b6961f318a..c08a0c7a12 100644 --- a/packages/spec/liveness/agent.json +++ b/packages/spec/liveness/agent.json @@ -83,6 +83,11 @@ "status": "experimental", "evidence": "no runtime reader", "note": "aspirational." + }, + "knowledge": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); restrict retrieval at the knowledge-service/source level; describe grounding in `instructions`. AIKnowledgeSchema removed; the topics→sources rename absorbed pre-release." } } } diff --git a/packages/spec/liveness/dashboard.json b/packages/spec/liveness/dashboard.json index 8a12b26ea0..1333e6ce58 100644 --- a/packages/spec/liveness/dashboard.json +++ b/packages/spec/liveness/dashboard.json @@ -41,6 +41,16 @@ "dateRange": { "status": "live", "note": "objectui: DashboardRenderer.tsx:279,988 — dashboard-level date-range filter (framework#2501). Supersedes the 2026-06 audit's DEAD finding." + }, + "aria": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); no dashboard renderer applied it." + }, + "performance": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); dashboard performance tuning was never implemented." } } } diff --git a/packages/spec/liveness/flow.json b/packages/spec/liveness/flow.json index d488036387..0cbaea6b90 100644 --- a/packages/spec/liveness/flow.json +++ b/packages/spec/liveness/flow.json @@ -98,6 +98,11 @@ "status": "live", "evidence": "objectui: packages/app-shell/src/views/metadata-admin/inspectors/flow-node-config.ts:480", "note": "LIVE via objectui metadata-admin authoring UI — the 2026-06 audit missed objectui; verified by reading the file." + }, + "outputSchema": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); node outputs were never validated; downstream nodes read prior outputs via expressions regardless." } } }, @@ -126,8 +131,23 @@ "jitter": { "status": "live", "evidence": "packages/services/service-automation/src/engine.ts" + }, + "fallbackNodeId": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); faults route via per-node fault edges — draw a fault edge to the handler node." } } + }, + "active": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); the real switch is `status` ('obsolete'/'invalid' disable, 'active' arms)." + }, + "template": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); shared logic is invoked via a subflow NODE referencing the flow by name." } } } diff --git a/packages/spec/liveness/skill.json b/packages/spec/liveness/skill.json index 2d45a91a8a..560ea64af0 100644 --- a/packages/spec/liveness/skill.json +++ b/packages/spec/liveness/skill.json @@ -40,6 +40,11 @@ "status": "live", "evidence": "packages/services/service-ai/src/skill-registry.ts:93", "note": "inactive skills dropped." + }, + "triggerPhrases": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); routing intent belongs in `triggerConditions`; describe intent in `description`/`instructions`." } } } diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index 125ee4dfef..9f32c2b11f 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -189,6 +189,16 @@ "userFilters": { "status": "live", "note": "objectui: UserFilters.tsx — element/fields/tabs/showAllRecords wired (audit L15); object views narrow to ObjectUserFiltersSchema (ADR-0047 amendment, #2679/objectui#2338)." + }, + "responsive": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); the grid is responsive by its own layout rules." + }, + "performance": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); list performance tuning was never implemented." } } }, @@ -288,6 +298,16 @@ "defaults": { "status": "live", "note": "objectui: ObjectForm.tsx foldFormButtons() reads schema.defaults and folds it into create-mode initialValues (framework#1894 / #2998); an explicit initialValues (URL prefill) still wins." + }, + "defaultSort": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); a related list sorts by its own list view's `sort`." + }, + "aria": { + "status": "dead", + "verifiedAt": "2026-07-30", + "note": "REMOVED 2026-07-30 (#3896 close-out sweep) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and stripped from sources by the protocol-17 close-out conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); no form renderer applied it; report gaps as renderer issues." } } }, diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index 4ab0c8db9f..3d6c736d44 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -269,12 +269,6 @@ export default defineSkill({ 'update_support_case', 'escalate_case', ], - triggerPhrases: [ - 'I need help with a case', - 'Create a support ticket', - 'What is the status of my case', - 'Escalate this issue', - ], triggerConditions: [ { field: 'objectName', operator: 'eq', value: 'support_case' }, ], From f75a231c3af20d9271b7d6a140e7a94a9f436808 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 07:15:33 +0000 Subject: [PATCH 3/3] docs(flows): the flow reference stops teaching the retired template/fallbackNodeId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by the corrected sweep after the SKILL.md catch — flows.mdx's defineFlow example authored template: false, and the error-handling section documented fallbackNodeId in both the example and the property table. The :208 'template' hit is the notify node's email template (a different, live config key); the sharing-rule and app 'active' hits are live keys on other types — all three verified before leaving them alone. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QuViRSR1j6GJjf9qGbnqFX --- content/docs/automation/flows.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/content/docs/automation/flows.mdx b/content/docs/automation/flows.mdx index d782100a7e..b866045dac 100644 --- a/content/docs/automation/flows.mdx +++ b/content/docs/automation/flows.mdx @@ -17,7 +17,6 @@ const approvalFlow = { type: 'record_change', version: 1, status: 'active', - template: false, runAs: 'system', variables: [ @@ -74,7 +73,6 @@ const approvalFlow = { | `version` | `number` | optional | Version number (defaults to `1`) | | `status` | `enum` | optional | `'draft'`, `'active'`, `'obsolete'`, `'invalid'` (defaults to `'draft'`) | | `type` | `FlowType` | ✅ | Flow trigger type (see below) | -| `template` | `boolean` | optional | Is this a reusable subflow template (defaults to `false`) | | `runAs` | `enum` | optional | `'system'` or `'user'` execution context (defaults to `'user'`) | | `variables` | `FlowVariable[]` | optional | Input/output variables | | `nodes` | `FlowNode[]` | ✅ | Flow nodes | @@ -670,7 +668,6 @@ errorHandling: { strategy: 'retry', // 'fail' | 'retry' | 'continue' maxRetries: 3, retryDelayMs: 5000, - fallbackNodeId: 'error_handler', } ``` @@ -679,7 +676,6 @@ errorHandling: { | `strategy` | `enum` | `'fail'` (stop), `'retry'` (retry), `'continue'` (skip) | | `maxRetries` | `number` | Maximum retry attempts (0-10) | | `retryDelayMs` | `number` | Delay between retries (ms) | -| `fallbackNodeId` | `string` | Node to execute on failure | ## Discovery & Registration