From dfba56ec44ac89ddc9eb3cf9adfb3c7eaa33189b Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:25:29 +0500 Subject: [PATCH 1/8] docs(audit): FieldSchema property liveness & necessity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evidence-backed cross-reference (file:line) of every core FieldSchema property against its runtime + renderer consumers. ~half are DEAD (defined in the protocol but read by neither layer → silent no-ops for authors). Surfaces three high-value classes: camelCase↔snake_case naming drift (maxLength/minLength/referenceFilters/maxRating read under a different key), nested-config-vs-flat duplication (currencyConfig/vectorConfig/ fileAttachmentConfig dead, flat siblings read), and redundant field-level flags superseded by object/dataset-level config (searchable/index/externalId/ columnName). Intended to seed a spec-hygiene ADR. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-fieldschema-property-liveness.md | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 docs/audits/2026-06-fieldschema-property-liveness.md diff --git a/docs/audits/2026-06-fieldschema-property-liveness.md b/docs/audits/2026-06-fieldschema-property-liveness.md new file mode 100644 index 0000000000..5c26fb2ee3 --- /dev/null +++ b/docs/audits/2026-06-fieldschema-property-liveness.md @@ -0,0 +1,101 @@ +# Audit: FieldSchema property liveness & necessity + +**Date**: 2026-06-15 +**Scope**: `packages/spec/src/data/field.zod.ts` — the ~60 core properties of `FieldSchema`. +**Method**: cross-reference each property's **spec definition** against its **consumers** by `file:line`, in two layers — (1) framework runtime (`@objectstack/objectql` engine + validators, `driver-sql` DDL, `@objectstack/rest`, formula/rollup, secret/encryption services) and (2) objectui renderers (`plugin-form`, `plugin-grid`, `fields` widgets, `app-shell` metadata-admin). A property is **LIVE** when at least one layer reads it and changes behavior; **DEAD** when only the spec definition / `*.form.ts` UI hint / lint / tests reference it. Browser observation was deliberately *not* used as the liveness signal — an unreactive UI can't distinguish "property is dead" from "data didn't trigger it". + +> This is an evidence catalog intended to seed an ADR (spec hygiene). It makes no decisions; it states what is wired and proposes options. + +## Headline + +Of ~60 core `FieldSchema` properties, **roughly half are DEAD** — defined in the protocol but consumed by neither the runtime nor the renderers. Authors who set them get **silent no-ops**. There are also **camelCase↔snake_case naming-drift bugs** where a documented property is read under a *different* key, and **nested-config-vs-flat duplication** where the nested object is dead and only flat siblings are read. + +| Bucket | Count (approx) | Action | +|---|---|---| +| LIVE & necessary (core) | ~26 | keep | +| LIVE with caveat | 4 | document / complete | +| LIVE but "fake-alive" (naming drift) | 3 | **rename-fix** (normalize keys) | +| DEAD — redundant (superseded by object/dataset-level) | 4 | **remove or wire** | +| DEAD — nested config duplicating live flat props | 3 | **二选一 (pick one)** | +| DEAD — aspirational enhanced-type / governance config | ~20 | **remove or mark experimental** | + +--- + +## 1. "Fake-alive" — naming drift (highest priority; looks valid, silently no-ops) + +The spec exports camelCase; the main object-form / widgets read legacy snake_case. Authoring per the protocol fails silently. + +| Property (spec) | Read instead as | Net effect | Evidence | +|---|---|---|---| +| `maxLength` / `minLength` | `max_length` / `min_length` | server validation honors camelCase, but the **client form length attrs do not** | `plugin-form/src/ObjectForm.tsx:489-490`; `fields/src/index.tsx:1684,1691` (UI reads snake) vs `objectql/src/validation/record-validator.ts:127,130` (runtime reads camel) | +| `referenceFilters` | `lookup_filters` | lookup dialog filter **entirely dead** as authored | `fields/src/widgets/LookupField.tsx:171` | +| `maxRating` | `max` | dead, and redundant with `max` | `fields/src/widgets/RatingField.tsx:13` | + +**Recommendation**: normalize at build time (snake→camel or vice-versa) **or** have renderers accept both keys. Until then these are broken promises. `maxRating` should simply be removed (use `max`). + +## 2. Nested config objects vs flat siblings — config dead, duplication + +The nested config objects are read by **nobody**; runtime and renderers read flat siblings instead. + +| Nested config (DEAD) | What is actually read | Evidence | +|---|---|---| +| `currencyConfig` | flat `currency`, `precision` | `fields/src/widgets/CurrencyField.tsx:40` (flat); no `currencyConfig` consumer | +| `vectorConfig` | flat `dimensions` | `fields/src/widgets/VectorField.tsx` (flat); no `vectorConfig` consumer, no vector-index DDL | +| `fileAttachmentConfig` | flat `multiple`, `accept`, `maxSize` | `fields/src/widgets/FileField.tsx:16`; no config-object consumer, no size/type/virus enforcement in write path | + +**Recommendation**: pick one shape per field type. Either delete the nested config from the spec (it misleads) or move consumers onto it. Today setting the nested config is a silent no-op. + +## 3. DEAD — redundant field-level flags superseded elsewhere + +| Property | Real mechanism | Evidence | +|---|---|---| +| `searchable` (field-level) | object-level `searchable` / view `searchableFields` | no field-level DDL/query consumer; UI hit `react/src/hooks/useRecordSearch.ts:195` is **object**-level | +| `index` (field-level) | object-level `indexes[]` | `driver-sql/src/sql-driver.ts:1252` reads object `indexes[]`; field bool unused | +| `externalId` (field-level) | dataset-level `externalId` | `objectql/.../seed-loader.ts:175` keys upsert off dataset externalId | +| `columnName` | — (broken) | `resolveColumnName` helper in `spec/.../system-names.ts:182` has **zero call sites**; the SQL driver hardcodes column = field key | + +**Recommendation**: remove these field-level flags or actually wire them. `columnName` is the most dangerous — it advertises custom physical column names that the driver never honors. + +## 4. DEAD — aspirational enhanced-type & governance config (remove or mark experimental) + +No consumer in either layer (only spec / `field.form.ts` hints / lint / tests): + +- **Code**: `theme`, `lineNumbers` (only `language` is live — `CodeField.tsx:13`) +- **Rating**: `allowHalf` (and `maxRating`, see §1) +- **Location**: `displayMap`, `allowGeocoding` +- **Address**: `addressFormat` +- **Color**: `colorFormat`, `allowAlpha`, `presetColors` (ColorField uses a fixed hex ``) +- **Slider**: `showValue`, `marks` (only `min`/`max`/`step` are live — `SliderField.tsx:12-14,32`) +- **Barcode/QR**: `barcodeFormat`, `qrErrorCorrection`, `displayValue`, `allowScanning` +- **Governance/security**: `encryptionConfig`, `maskingRule`, `auditTrail`, `dataQuality`, `cached`, `dependencies`, `trackFeedHistory`, `caseSensitive`, `writeRequiresMasterRead` +- **Master-detail explicit overrides**: `inlineTitle`, `inlineColumns`, `inlineAmountField`, `relatedList`, `relatedListTitle`, `relatedListColumns` (the **auto-derivation** in `plugin-form/src/deriveMasterDetail.ts` works; the explicit overrides are unread; detail-page related lists come from view metadata, not FieldSchema) + +The only working at-rest protection is the separate `type: 'secret'` channel (`objectql/src/engine.ts` `encryptSecretFields`) — `encryptionConfig` is not it. + +**Recommendation**: delete from the protocol, or gate behind an explicit `experimental` marker so authors aren't misled. Each is a silent no-op today. + +## 5. LIVE with caveats (works, but incompletely) + +| Property | Caveat | Evidence | +|---|---|---| +| `unique` | **DDL-only** — emits a UNIQUE constraint but is **not validated on the write path**, so violations surface as raw driver errors | `driver-sql/src/sql-driver.ts:1853`; absent from `record-validator.ts` | +| `precision` | UI display formatting live; **DDL never sizes** — number/currency/percent all map to `table.float()` | `fields/src/widgets/NumberField.tsx:16` (UI) vs `sql-driver.ts:1810` (no sizing). `scale` is even thinner — grid formatting only | +| `reference` | live via `$expand` / cascade / seed in `engine.ts`; **FK DDL reads `reference_to`**, which nothing maps from `reference` → driver-level FK constraints are effectively absent for spec-authored fields | `engine.ts:1672-1675,2218-2219` (live) vs `sql-driver.ts:1835` (reads `reference_to`) | +| `autonumberFormat` | runtime sequence formatting live; the UI `AutoNumberField` ignores it (shows raw value) | `engine.ts:765-767` (runtime) vs `fields/.../AutoNumberField.tsx` (no read) | + +## 6. LIVE & necessary (core — keep) + +`name`, `label`, `type`, `description`, `required`, `multiple`, `defaultValue`, `min`, `max`, `options`, `deleteBehavior`, `expression`, `summaryOperations`, `requiredWhen`, `readonlyWhen`, `visibleWhen`, `readonly`, `hidden`, `system`, `sortable`, `format`, `language` (code), `step` (slider), `inlineEdit`, and `conditionalRequired` (live only as the deprecated alias of `requiredWhen`; plan removal). + +Primary live-consumer hot-spots: `packages/objectql/src/engine.ts`, `packages/objectql/src/validation/record-validator.ts`, `packages/objectql/src/validation/rule-validator.ts`, `packages/plugins/driver-sql/src/sql-driver.ts` (runtime); `objectui` `packages/plugin-form/src/ObjectForm.tsx`, `packages/plugin-grid/src/ObjectGrid.tsx`, `packages/fields/src/index.tsx` + `widgets/*` (renderers). + +--- + +## Proposed follow-up (for an ADR) + +1. **Rename-fix the naming drift** (§1) — normalize `maxLength`/`minLength`/`referenceFilters`/`maxRating` so the documented camelCase works. Net user-visible bug fix. +2. **Resolve nested-vs-flat** (§2) — one shape per field type. +3. **Prune or wire** the redundant flags (§3) and aspirational config (§4). Default to prune; anything kept gets an `experimental` marker and a tracking issue. +4. **Complete the caveated four** (§5) — write-path `unique` validation, DDL sizing for `precision`/`scale`, `reference`→`reference_to` mapping (or unify the key), UI honoring `autonumberFormat`. + +This audit covers **1 of ~15 metadata types** (the densest). The same method applies to ObjectSchema, ViewSchema, etc. From 594a88f64e6270d2de7dbc265590a13011015b6e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:33:04 +0500 Subject: [PATCH 2/8] docs(audit): ObjectSchema top-level property liveness Entire enable/ObjectCapabilities block dead (apiEnabled/apiMethods NOT enforced by REST). versioning/partitioning/cdc/softDelete/search/recordTypes/ defaultDetailForm aspirational. recordName duplicated by field autonumber; tenancy only .enabled live. Core load-bearing set documented with file:line. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-objectschema-property-liveness.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/audits/2026-06-objectschema-property-liveness.md diff --git a/docs/audits/2026-06-objectschema-property-liveness.md b/docs/audits/2026-06-objectschema-property-liveness.md new file mode 100644 index 0000000000..291b19048a --- /dev/null +++ b/docs/audits/2026-06-objectschema-property-liveness.md @@ -0,0 +1,40 @@ +# Audit: ObjectSchema (top-level) property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: top-level `ObjectSchema` props in `packages/spec/src/data/object.zod.ts` (per-field props audited separately). **Method**: consumer cross-reference (runtime: objectql/driver-sql/rest/security/sharing; renderers: objectui app-shell/plugin-form/grid/list/detail). LIVE = a non-spec/non-test consumer reads it. + +## Headline +Object schema carries **roughly as many dead props as live ones**. Unlike the field layer there's **no camelCase naming drift**, but a large tier of **aspirational "enterprise data-management" config** has zero runtime. + +## DEAD — the entire `enable` / `ObjectCapabilities` block +`trackHistory`, `searchable`, `apiEnabled`, `apiMethods`, `files`, `feeds`, `activities`, `trash`, `mru`, `clone` — **all 10 flags have zero behavior-changing readers** in either repo. Most serious: **`apiEnabled` / `apiMethods` are NOT enforced by REST** — an object cannot be hidden from the API via these flags (false sense of security). `database-loader.trackHistory` is a separate ctor option, not `enable.trackHistory`. + +## DEAD — aspirational enterprise blocks (no runtime) +`versioning`, `partitioning`, `cdc`, `softDelete`, `search` (`SearchConfigSchema`), `recordTypes`, `defaultDetailForm`, `keyPrefix`, `tags`, `abstract`, `isSystem` (object-level), `active` (object-level). Several carry elaborate docstrings describing behavior that is unimplemented (`defaultDetailForm` fallback chain, `recordTypes`). + +## DEAD — duplication traps +- `recordName` (object-level autonumber: type/displayFormat/startNumber) — superseded by a **field** of `type:'autonumber'` + `autonumberFormat` (`objectql/.../engine.ts:757,767`). +- `softDelete` ↔ `enable.trash` — duplicated, both dead. +- `search` block ↔ `enable.searchable` — duplicated, both dead. +- `tenancy`: only `tenancy.enabled` is read (`driver-sql/sql-driver.ts:1081`, `plugin-security:790`); `strategy`/`tenantField`/`crossTenantAccess` are inert. + +## LIVE & necessary (the load-bearing core) +| property | layer | evidence | +|---|---|---| +| `name` | both | `driver-sql/sql-driver.ts` (table), `objectql/registry.ts` keys | +| `fields` | both | engine/DDL + `plugin-form/SplitForm.tsx:162` | +| `datasource` | fw | `objectql/engine.ts:1147,1368` (driver routing) | +| `external` | fw | `runtime/external-validation-plugin.ts`, `engine.ts` (remote table + write gate) | +| `indexes` | fw | `driver-sql/sql-driver.ts:1181` (DDL) | +| `validations` | fw | `objectql/validation/rule-validator.ts:154,256,581` (incl. state_machine) | +| `actions` | fw | `runtime/app-plugin.ts:929` (served on /meta/objects/:name) | +| `protection` | fw | `objectql/registry.ts:562` → `_lock` envelope | +| `managedBy` | both | `registry.ts:208` (default perms); ui `crudAffordances.ts:60` | +| `userActions` | ui | `plugin-grid/ObjectGrid.tsx:1307` (per-object CRUD) | +| `sharingModel` / `publicSharing` | fw | `plugin-sharing/sharing-service.ts:54`, `share-link-service.ts:56` | +| `systemFields` | fw | `registry.ts` (organization_id auto-inject gate) | +| `label`/`pluralLabel`/`icon`/`displayNameField`/`titleFormat`/`compactLayout`/`fieldGroups`/`listViews`/`detail.renderViaSchema` | ui | `plugin-grid/ObjectGrid.tsx:830,1101`; `app-shell/RecordDetailView.tsx:193,1423`; `metadataConverters.ts:79` | + +## Recommendation (for ADR) +1. **Decide the fate of `enable`/ObjectCapabilities** — either enforce (esp. `apiEnabled`/`apiMethods` in REST, a real security expectation) or remove. Shipping a non-enforcing `apiEnabled` is a latent security bug. +2. **Prune the aspirational tier** (`versioning`/`partitioning`/`cdc`/`softDelete`/`search`/`recordTypes`/`defaultDetailForm`) or mark `experimental`. +3. **De-duplicate**: drop object-level `recordName` (use field autonumber); collapse `tenancy` to the one live flag or wire the rest. From c981c6e12fe377a598d25367e5a8c102272dfc01 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:33:43 +0500 Subject: [PATCH 3/8] docs(audit): ViewSchema property liveness chart list variant dead-on-arrival vs its own spec (renderers read removed legacy xAxisField/yAxisFields; spec exposes dataset/dimensions/values) = same ADR-0021 debt as dashboard/report seeds. Form variants wizard/split/ drawer/modal aspirational on mainstream path (RecordFormPage hardcodes simple). List family + grid/kanban/calendar/gantt/gallery/timeline fully wired. bulkActions->batchActions drift documented. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-viewschema-property-liveness.md | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 docs/audits/2026-06-viewschema-property-liveness.md diff --git a/docs/audits/2026-06-viewschema-property-liveness.md b/docs/audits/2026-06-viewschema-property-liveness.md new file mode 100644 index 0000000000..cb39b8531d --- /dev/null +++ b/docs/audits/2026-06-viewschema-property-liveness.md @@ -0,0 +1,31 @@ +# Audit: ViewSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/view.zod.ts` (list family common + per-variant blocks + form family). **Method**: consumer cross-reference in objectui renderers (`plugin-{grid,list,view,kanban,calendar,gantt,charts,timeline,form}`, `fields`, `app-shell`) + framework `rest`/`objectql` for query effects. File:line below are in `objectui/packages/` unless prefixed `framework/`. + +## Data-flow note +`plugin-list/src/ListView.tsx` normalizes spec (camelCase) into `plugin-grid`'s internal `object-grid` schema, then `ObjectGrid.tsx` consumes it. The server (`framework/packages/rest`) only persists/serves view items — **no** list-render prop reaches the query server-side; all filter/sort/column shaping is client-side. + +## 🔴 Highest-impact: the `chart` list variant is dead-on-arrival vs its own spec +Post-ADR-0021 the spec exposes only `dataset` / `dimensions` / `values`, but **both** renderers (`ListView.tsx:1413-1428`, `app-shell/.../ObjectView.tsx:754-768`) still read the **removed legacy** `xAxisField` / `yAxisFields` / `valueField` / `aggregation` inline shape. A chart view authored to the *current* spec renders nothing meaningful (`chartType` is the only surviving key). This is the same ADR-0021 migration debt that left dashboard/report seeds invalid — **the chart renderers were never migrated either**. + +## Form variants are mostly aspirational on the mainstream path +Variant routing (`tabbed/wizard/split/drawer/modal`) exists in `plugin-form/ObjectForm.tsx:135-242`, but the real entry points bypass it: `app-shell/.../RecordFormPage.tsx:273` **hardcodes `formType:'simple'`**, and `ObjectView.tsx`'s form adapter never forwards `form.type`/`sections`. The spec `FormView` is genuinely honored only on the metadata-admin **schema-provider** path (`SchemaForm.tsx:539`), and even there only `simple` vs `tabbed` differ — wizard/split/drawer/modal **degrade to stacked sections**. Net: **only `grid` and `simple`-form are fully wired end-to-end; `tabbed` partial; the other form variants are showcase-only.** + +## Fully-wired (thick, correct) — keep +- **List common**: `name/label/type`, `columns` (legacy + rich), `filter`, `sort`, `searchableFields`, `filterableFields`, `userFilters{element,fields,tabs,showAllRecords}`, `selection`, `pagination`, `grouping`, `rowColor`, `rowHeight`, `hiddenFields/fieldOrder`, `rowActions`, `conditionalFormatting`, `inlineEdit`, `exportOptions`, `virtualScroll`, `tabs{icon,visible,pinned,filter}`, `emptyState`, `aria`, `appearance.allowedVisualizations`, `resizable/striped/bordered`, `showRecordCount`, `allowPrinting`. (evidence: `ObjectGrid.tsx`/`ListView.tsx`/`UserFilters.tsx`/`TabBar.tsx`) +- **List variants** (all field-by-field wired, with deprecated flat aliases as fallback): **kanban** (`KanbanImpl.tsx:682`), **calendar** (`calendar-view-renderer.tsx:147`), **gantt** (`ObjectGantt.tsx:236`), **gallery** (`ObjectGallery.tsx:209`), **timeline** (`ObjectTimeline.tsx:179`). +- **Form (where reachable)**: `sections`, FormField {`field,type,label,placeholder,required,readonly,hidden,options,reference,widget,colSpan,helpText,language,dependsOn,visibleOn,keyField,immutable`}, FormSection {`collapsible,collapsed,columns,visibleOn`}, `subforms` (master-detail, fully wired: `deriveMasterDetail.ts:338`, `LineItemsPanel.tsx:45`). + +## DEAD — no consumer anywhere +`userActions.buttons`, `addRecord.mode`, `addRecord.formView`, `sharing.lockedBy`, list-level `responsive`, list-level `performance`, FormView `submitBehavior{thankYou,redirect,continue,nextRecord}`, FormView `defaultSort`, FormView `sharing` (renderer side), `ViewData` providers `api` & `schema` in the **list** path, `tab.order` (not used for sorting). + +## Drift / structure issues (silent no-op risk) +- `bulkActions` works **only** because `ListView.tsx:1318` remaps it to `batchActions` (ObjectGrid's real key) — a direct `object-grid` caller using `bulkActions` silently no-ops. +- `groups` vs `sections` — both alive but on different code paths, not aliased in one place. +- Inverse problem: `ObjectView.tsx:809-823`'s form adapter reads keys (`layout,showSubmit,submitText,customFields,title,initialValues,className`) that **don't exist in `FormViewSchema`** — renderer-invented surface with no spec backing. + +## Recommendation (for ADR) +1. **Migrate the chart renderers to `dataset`/`dimensions`/`values`** (ADR-0021) — pairs with the dashboard/report seed migration; currently the entire chart view variant is dead against the spec. +2. **Decide form-variant scope**: either wire `wizard/split/drawer/modal` through `RecordFormPage`/`ObjectView`, or demote them in the spec to "metadata-admin / showcase only" so authors aren't misled. +3. Normalize the `bulkActions`/`batchActions` and `groups`/`sections` key drift at one boundary; reconcile `ObjectView` form-adapter keys with `FormViewSchema`. +4. Prune the confirmed-dead props. From aa5e2e335b42e9452892e313cdb801c49678f414 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:39:48 +0500 Subject: [PATCH 4/8] docs(audit): App/Page/Dashboard/Report property liveness Recurring themes: ADR-0021 migration debt (dashboard renderer+Studio still on legacy object/valueField shape the spec says was removed; report chart dead), orphaned spec-bridge (page/dashboard bridges have no src caller -> their exclusive props dead), spec<->renderer naming drift (page type->pageType & label->title break layouts/headers; dashboard title vs label; app reads accentColor/badgeVariant/separator not in spec), and aspirational config (app mobileNavigation/sharing/embed; page recordReview/blankLayout required by superRefine yet unrendered; report chart/aria/performance). Evidence file:line in each doc. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-appschema-property-liveness.md | 22 +++++++++++++++++++ ...26-06-dashboardschema-property-liveness.md | 20 +++++++++++++++++ .../2026-06-pageschema-property-liveness.md | 21 ++++++++++++++++++ .../2026-06-reportschema-property-liveness.md | 21 ++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 docs/audits/2026-06-appschema-property-liveness.md create mode 100644 docs/audits/2026-06-dashboardschema-property-liveness.md create mode 100644 docs/audits/2026-06-pageschema-property-liveness.md create mode 100644 docs/audits/2026-06-reportschema-property-liveness.md diff --git a/docs/audits/2026-06-appschema-property-liveness.md b/docs/audits/2026-06-appschema-property-liveness.md new file mode 100644 index 0000000000..77ff9f4dd3 --- /dev/null +++ b/docs/audits/2026-06-appschema-property-liveness.md @@ -0,0 +1,22 @@ +# Audit: AppSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/app.zod.ts`. **Renderers**: `objectui` `layout/NavigationRenderer.tsx`, `app-shell` `{AppSidebar,ConsoleLayout,AppContent}`. **Method**: consumer cross-reference. + +## 🔴 Spec↔renderer drift — renderer reads fields the spec doesn't declare (would be stripped by `AppSchema.parse()`) +- `branding.accentColor` — consumed `AppShell.tsx:160-165`, **absent from `AppBrandingSchema`** +- nav-item `badgeVariant` — consumed `NavigationRenderer.tsx:850/890`, absent from `BaseNavItemSchema` +- nav-item `type:'separator'` — rendered `NavigationRenderer.tsx:574/774`, **not a union member** in app.zod.ts + +These are the highest-priority fixes (authoring them per spec fails; they only work because parse is bypassed somewhere). + +## DEAD — aspirational, no consumer in either repo +`version`, `aria`, `objects[]`, `apis[]`, `App.sharing`, `App.embed`, `mobileNavigation`/`bottomNavItems` (fully unimplemented — even `packages/mobile` ignores it), `branding.logo` (passed at `ConsoleLayout.tsx:117` but never read in `AppShell`). +- **Misleading**: `App.sharing`/`App.embed` attach Sharing/Embed config to apps, but the only live sharing/embed path is `FormView.sharing` (`framework/.../rest-server.ts:3282`) — no public-app or iframe route reads the app-level versions. +- The spec itself labels `objects[]`/`apis[]` "config convenience"; the chatbot's object list comes from nav items (`AppHeader.tsx:500 collectNavObjects`), not `App.objects`. + +## LIVE & necessary (the sidebar core — all camelCase, no snake drift) +`name`, `label`, `description`, `icon`, `active`, `isDefault`, `hidden`, `navigation` (whole tree), `areas` (precedence over navigation), `contextSelectors` (all sub-fields), `homePageId`, `defaultAgent` (dual consumer: framework `agent-runtime.ts:341` + objectui chatbot), `branding.{primaryColor,favicon}`, `protection`. Nav-item union fully live: `id/label/icon/order/badge/visible(CEL)/requiredPermissions/requiresObject/requiresService` + per-type payloads (`objectName/viewName/recordId/recordMode/dashboardName/pageName/url/target/reportName/componentRef/params/children/expanded`). NavigationContribution (ADR-0029) live via `objectql/engine.ts:912`. +- PARTIAL: `App.requiredPermissions` (app-entry gate not observed; only nav-item perms enforced), nav `type:'action'` `actionDef` (fires `onAction(item)`; `actionDef.{actionName,params}` shape read loosely in the action runtime, 0 direct grep in shell). + +## Recommendation +Add the 3 drift fields to the spec (`accentColor`, `badgeVariant`, `separator`) **or** stop the renderer reading them. Prune the aspirational block; `App.sharing`/`App.embed`/`apiEnabled`-style props create a false security/feature impression. diff --git a/docs/audits/2026-06-dashboardschema-property-liveness.md b/docs/audits/2026-06-dashboardschema-property-liveness.md new file mode 100644 index 0000000000..0a0ba63369 --- /dev/null +++ b/docs/audits/2026-06-dashboardschema-property-liveness.md @@ -0,0 +1,20 @@ +# Audit: DashboardSchema / DashboardWidgetSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/dashboard.zod.ts`. **Live path**: `objectui` `DashboardView`→`DashboardRenderer`→`DatasetWidget` (ADR-0021) / legacy inline `getComponentSchema()`. The `spec-bridge/bridges/dashboard.ts` path is **orphaned** (no `src` caller; its node types have no renderer). + +## 🔴 The ADR-0021 cutover is half-done and the spec contradicts the code +`dashboard.zod.ts:153-156` declares `dataset`+`values` **required** and says the legacy `object/categoryField/valueField/aggregate` query "was removed." But `DashboardRenderer.getComponentSchema()` (`:423-723`) is built almost entirely on that **legacy inline shape**, and Studio's `WidgetConfigPanel.tsx:161-285` still authors `object/categoryField/valueField/aggregate` with **no dataset/dimensions/values controls**. → Studio emits widgets the current spec **rejects**; the dataset-required rule cannot hold. (Same debt that left the HotCRM dashboard seeds invalid.) + +## DEAD — only the orphaned spec-bridge references them +Dashboard: `globalFilters`, `dateRange`, `aria`, `performance`. Widget: `responsive`, `aria`, `actionUrl`/`actionType`/`actionIcon`, `requiresService` (nav uses it; widgets don't). `chartConfig` is read by **CLI lint only** (`validate-widget-bindings.ts:239`), no runtime renderer. They appear in `dashboard.form.ts` so Studio shows editable fields that render nothing. + +## Drift +- **`title` vs `label`**: renderer reads `schema.title` (`DashboardRenderer.tsx:856`, `DashboardView.tsx:358`), but `DashboardSchema` has only `label`. `header.showTitle/showDescription` gate on a non-spec field. +- **`refreshInterval` effectively dead**: interval logic exists (`:332`) but only fires if `onRefresh` is passed; `DashboardView` never passes it. +- **Renderer depends on undeclared props**: `component`, `data`, `rowField`, `columnField`, `searchable`, `pagination`, `categoryGranularity` are read but **not in `DashboardWidgetSchema`** (inverse of dead-spec-prop). + +## LIVE & well-wired +`name`, `label`, `description`, `widgets`, `columns`, `gap`, `header.actions`, widget `id/title/description/type/colorVariant/filter→runtimeFilter/compareTo/layout.{w,h}/options/requiresObject`, and the canonical `dataset`/`dimensions`/`values` (DatasetWidget + framework `build-probes.ts` runtime probe + CLI lint). Plus the legacy `object/valueField/categoryField/aggregate` (LIVE-drift; spec says removed but renderer+Studio still depend on them). + +## Recommendation +Finish ADR-0021: migrate `DashboardRenderer` + `WidgetConfigPanel` to dataset/dimensions/values, then remove the legacy inline shape (or un-deprecate it in the spec). Fix `title`↔`label`. Prune the orphaned-bridge-only props. Declare the undeclared-but-read props. diff --git a/docs/audits/2026-06-pageschema-property-liveness.md b/docs/audits/2026-06-pageschema-property-liveness.md new file mode 100644 index 0000000000..09369aad59 --- /dev/null +++ b/docs/audits/2026-06-pageschema-property-liveness.md @@ -0,0 +1,21 @@ +# Audit: PageSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/page.zod.ts` (+ PageComponent/InterfaceConfig/PageVariable). **Renderers**: `objectui` `PageView`→`SchemaRenderer`→`components/.../layout/page.tsx`, `plugin-detail` (record path), `InterfaceListPage`. + +## 🔴 `bridgePage` (spec-bridge) is entirely dead +`PageView.tsx` spreads the **raw** spec into `SchemaRenderer`; nothing calls `SpecBridge.toPage`. So every prop whose only reader is the bridge is DEAD: component `events`, `style`, `visibility`, element `dataSource`, `responsive`, page `icon`. + +## 🔴 `type` → `pageType` naming drift breaks page-type layouts +The custom-page renderer switches on `schema.pageType` (`page.tsx:349`) but the spec emits `type`; only `usePageAssignment` normalizes `pageType ?? type`. So `app`/`home`/`utility` pages rendered via `PageView` **silently collapse to the `record` layout**. Only `template`-named pages route correctly. Same drift: renderer reads `title` (`page.tsx:429`), spec emits `label` → the page header never shows the spec label. Non-spec `priority` is also read for page selection (`usePageAssignment.ts:139`). + +## 🔴 Component-level `visibility` (CEL) silently does nothing +`SchemaRenderer` gates on `visible`/`hidden`/`visibleOn`/`hiddenOn` (`:251-262`), **never the spec's `visibility`** — an author-written visibility predicate on a page block is ignored (correctness footgun). + +## DEAD — whole config subtrees with zero renderers +`recordReview` (`record_review`), `blankLayout` (`blank`) — and the zod `superRefine` **requires** these for types that have no renderer (validation enforces config for dead features). InterfaceConfig `levels` + `allowPrinting`. `PageVariable.source`. `assignedProfiles`, `isDefault`, `icon`. + +## LIVE & well-wired +`name`, `description`, `variables{name,type,defaultValue}`, `object`, `template` (default/full-width/header-sidebar-main/three-column/dashboard), `regions`, `kind`+`slots` (slotted composition via `usePageAssignment`+`buildDefaultPageSchema`), `interfaceConfig` (10/12 sub-props: source/sourceView/appearance/userFilters/userActions/addRecord/filterBy/showRecordCount). PageComponent `type/id/label/properties/className/aria`. Component types: record:* (details/related_list/highlights/activity/chatter/path/quick_actions/history/reference_rail/alert), page:* containers, element:text/number/image/divider/button/repeater all wired. **Aspirational (render "Unknown component type")**: `element:filter`, `element:form`, `element:record_picker`, `ai:chat_window`. + +## Recommendation +Fix `type`↔`pageType` and `label`↔`title` at one boundary; route component `visibility` into the live gate (or rename). Either delete `bridgePage` (dead) or wire it. Prune `recordReview`/`blankLayout`/`levels`/`allowPrinting` and stop `superRefine` requiring config for unrendered types. diff --git a/docs/audits/2026-06-reportschema-property-liveness.md b/docs/audits/2026-06-reportschema-property-liveness.md new file mode 100644 index 0000000000..e17f2b5e73 --- /dev/null +++ b/docs/audits/2026-06-reportschema-property-liveness.md @@ -0,0 +1,21 @@ +# Audit: ReportSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/report.zod.ts` (ADR-0021 single-form). **Live path**: `objectui` `ReportRenderer`→`DatasetReportRenderer` (dataset-bound). Pre-9.0 object/columns-query renderers retired; old JSON limps through the lossy `specReportToPresentation` bridge. + +## LIVE & well-wired (dataset shape — the canonical path) +`name`, `label`, `description`, `type` (summary/tabular/matrix/joined), `dataset`, `rows`, `columns` (matrix-across only, matches spec), `values`, `runtimeFilter` (`?? filter`, ANDed via `mergeFilters`), `drilldown` (default-on; cells clickable), `blocks` (joined, per-block dataset/rows/columns/values/runtimeFilter). Evidence: `DatasetReportRenderer.tsx:486-575`, `:493-543`. Wired types: tabular/summary/matrix (true cross-tab + server totals + drill)/joined. + +## 🔴 DEAD on the live path — aspirational +- **`chart`** (top-level and per-block) — in the spec + `report.form.ts:62` (editable in Studio) but **no renderer reads `report.chart`**. The dataset renderer ignores it entirely; legacy `ReportViewer` only handles a different `section.chart`/`xAxisField` shape via the bridge, which never populates from spec `report.chart`. **Editing chart config produces no output.** +- **`aria`**, **`performance`** — `report.form.ts:71-72` only; no renderer. + +## 🔴 Obsolete sub-schemas (type-only re-exports, fully superseded by ADR-0021) +- `ReportColumnSchema` (`field/label/aggregate/responsive`) — replaced by plain `string[]` `values`; per-column aggregate/label now live in the **dataset** definition. +- `ReportGroupingSchema` (`field/sortOrder/dateGranularity`) — replaced by `string[]` `rows`; sort/granularity now in the **dataset**. +- `ReportChartSchema` (`xAxis/yAxis/groupBy`) — **naming drift**: the only chart code (`ReportViewer.tsx:329`) reads legacy `xAxisField/yAxisFields`, never the spec's `xAxis/yAxis` → spec chart field names are dead. + +## Studio gap (PARTIAL) +`ReportPreview.tsx:34` branches on top-level `draft.dataset` only — a **`joined`** report (legitimately has no top-level `dataset`; data on `blocks`) falls to the "bind a dataset" empty state instead of previewing, though the runtime renders it. Joined previews are effectively unwired in Studio. + +## Recommendation +Remove `chart`/`aria`/`performance` from ReportSchema (or wire `chart` for dataset reports). Delete the obsolete `ReportColumn/ReportGrouping/ReportChart` re-exports. Fix `ReportPreview` to preview joined reports (branch on `dataset || blocks?.length`). From e324ee0a309a84c714a0b14b57107d86d73cce0d Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:45:56 +0500 Subject: [PATCH 5/8] docs(audit): Dataset/Action/Flow/Agent property liveness Cluster of security-relevant parsed-but-UNENFORCED props: agent permissions/visibility/access (who-can-chat is a no-op), flow runAs (never switches identity), action disabled-CEL (silently ignored). Plus: flow FlowNodeAction enum out of sync with executors; http vs http_request drift; agent model.provider dead (provider comes from adapter); agent autonomy surface (memory/guardrails/structuredOutput/lifecycle) aspirational except planning.maxIterations; dataset description/certified dead + Studio under-covers live props; action type:form/shortcut/bulkEnabled half-built. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-actionschema-property-liveness.md | 22 +++++++++++++++++ .../2026-06-agentschema-property-liveness.md | 21 ++++++++++++++++ ...2026-06-datasetschema-property-liveness.md | 20 ++++++++++++++++ .../2026-06-flowschema-property-liveness.md | 24 +++++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 docs/audits/2026-06-actionschema-property-liveness.md create mode 100644 docs/audits/2026-06-agentschema-property-liveness.md create mode 100644 docs/audits/2026-06-datasetschema-property-liveness.md create mode 100644 docs/audits/2026-06-flowschema-property-liveness.md diff --git a/docs/audits/2026-06-actionschema-property-liveness.md b/docs/audits/2026-06-actionschema-property-liveness.md new file mode 100644 index 0000000000..cae3194cc8 --- /dev/null +++ b/docs/audits/2026-06-actionschema-property-liveness.md @@ -0,0 +1,22 @@ +# Audit: ActionSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/action.zod.ts`. **Consumers**: objectui action renderers (`components/.../action/*`, `RowActionMenu`, `record-quick-actions`), runtime dispatcher (`useConsoleActionRuntime`, `ActionRunner`), server (`runtime/http-dispatcher`→`engine.executeAction`→`body-runner`), AI bridge (`service-ai/tools/action-tools.ts`). + +## 🔴 `disabled` → `enabled` naming drift (CEL form silently ignored) +Spec's canonical field is `disabled` (bool | CEL), but the primary renderers (`action-button.tsx:56,116`, action-menu, action-group) read a **non-spec `schema.enabled`** and invert it. Only detail/quick-action toolbars read spec `disabled`, and only its **boolean** form. **The CEL-predicate form of `disabled` has zero consumers** — authoring `disabled: ""` is silently ignored. + +## 🔴 Aspirational / half-built +- **`type:'form'`** — in the enum, marks `target` required, documents a `/console/forms/:name` route, but **no renderer or runtime consumes it** (AI bridge classifies it unsupported). Dead action type. +- **`shortcut`** — `ActionEngine` registers it + exposes `handleShortcut`, but **no keydown listener** pumps events → never fires. +- **`bulkEnabled`** — engine has `getBulkActions`/`executeBulk`, but no spec-driven view path calls `executeBulk`. +- **`timeout`** (action-level) — DEAD; server uses `body.timeoutMs`, no UI consumer. +- **`mode`** — consumed only by the AI HITL heuristic, never by UI. **`aria`** — honored by a few renderers but not the core action buttons/menus. + +## LIVE & well-wired +`name`, `label`, `objectName`, `icon`, `type`, `target` (+`${param}`/`${ctx}` interpolation), `body` (server script only), `params` (+ field/objectOverride/defaultFromRow/options/placeholder/helpText/defaultValue/required/name), `variant`, `component`, `locations`, `confirmText`, `successMessage`, `refreshAfter`, `resultDialog` (one-shot reveal), `visible` (CEL, fail-closed), `recordIdParam`/`recordIdField`, `bodyShape{wrap}`, `bodyExtra`, `method`, `opensInNewTab`/`newTabUrl`, the full `ai.*` bridge. **Action types**: api/script/flow fully wired; url thinner; **modal PARTIAL** (console maps modal→serverActionHandler, not a real modal opener); form DEAD. + +## 🟠 Two parallel prop-readers with different coverage +The **AI bridge** (`action-tools.ts`) reads almost every advanced prop; the **visual button** (`action-button.tsx`) forwards only a fixed subset and relies on the grid path (`ObjectGrid.tsx:1333 ...rest`) to carry the full def into handlers. Divergent coverage = maintenance risk. + +## Recommendation +Fix `disabled`↔`enabled` and wire the CEL form (correctness + matches `visible`). Decide `type:'form'`/`shortcut`/`bulkEnabled`/`timeout`/`mode`/`aria` — wire or remove. Make the modal action a real modal opener or drop it. Unify the two prop-readers. diff --git a/docs/audits/2026-06-agentschema-property-liveness.md b/docs/audits/2026-06-agentschema-property-liveness.md new file mode 100644 index 0000000000..f35f539a09 --- /dev/null +++ b/docs/audits/2026-06-agentschema-property-liveness.md @@ -0,0 +1,21 @@ +# Audit: AgentSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ai/agent.zod.ts`. **Consumers**: framework `service-ai/agent-runtime.ts` (+ agent/assistant routes, eval-runner); objectui `AgentPreview` (display) + chatbot (picker). + +## 🔴 Model provider drift — `model.provider` is DEAD +Runtime applies only `model.{model,temperature,maxTokens}` (`agent-runtime.ts:264-266`). **`model.provider` and `model.topP` are never applied** — the provider/model comes from the configured AI adapter (`plugin.ts`), so an agent setting `provider:'anthropic'` has **zero effect**. (`AgentPreview.tsx:124` displays provider, reinforcing the false impression.) + +## 🔴 The entire "autonomy" surface is aspirational except one knob +**DEAD** (no runtime reader; only authored + displayed in AgentPreview): `memory.*` (shortTerm/longTerm/reflectionInterval), `guardrails.*` (maxTokensPerInvocation/maxExecutionTimeSec/blockedTopics — real limits enforced by an unrelated quota service), `structuredOutput.*`, `lifecycle` (StateMachine), `planning.strategy`, `planning.allowReplan`. **The only LIVE planning sub-field is `planning.maxIterations`** (3 call sites). + +## 🔴 Access control declared but UNENFORCED (security gap) +`access`, `visibility`, `tenantId` — none read at runtime. `permissions` is display-only; the chat route **hardcodes `['ai:chat','ai:agents']`** (`agent-routes.ts:109`) regardless of the agent's declared `permissions`. So **"who can chat with this agent" is currently a no-op** — a latent access-control gap. (`visibility` defaults `organization` but gates nothing.) + +## Drift +**`knowledge`** — spec defines `{topics,indexes}` but the only consumer (`AgentPreview.tsx:213`) reads `knowledge.sources`/`indexes`, and **no runtime reads it at all** — RAG is wired via `service-knowledge`, not `agent.knowledge`. Chatbot picker `description` is derived from `role` (not an AgentSchema field). + +## LIVE & necessary +`name`, `label`, `role`, `avatar`, `instructions` (core), `model.{model,temperature,maxTokens}`, `skills` (→ tools+prompt), `tools` (legacy fallback), `active` (gates listing + 403 on chat), `planning.maxIterations`, `protection` (loader). + +## Recommendation +Either route `model.provider`/`topP` into the LLM call or remove them (currently misleading). **Enforce `permissions`/`visibility`/`access`** at the chat route, or stop accepting them (security). Prune the aspirational autonomy surface (memory/guardrails/structuredOutput/lifecycle/planning.{strategy,allowReplan}) or mark `experimental`. Fix the `knowledge` shape drift. diff --git a/docs/audits/2026-06-datasetschema-property-liveness.md b/docs/audits/2026-06-datasetschema-property-liveness.md new file mode 100644 index 0000000000..93cff0c356 --- /dev/null +++ b/docs/audits/2026-06-datasetschema-property-liveness.md @@ -0,0 +1,20 @@ +# Audit: DatasetSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ui/dataset.zod.ts`. **Consumers**: framework `service-analytics` (`dataset-compiler`/`dataset-executor`/`analytics-service`), objectui `DatasetWidget`/`DatasetReportRenderer`/`DatasetDefaultInspector`. + +## LIVE & well-wired (the analytics query path) +Dataset: `name`, `label`, `object` (FROM), `include` (join allowlist), `filter`, `dimensions[]`, `measures[]`. Dimension: `name`, `field`, `type`, `dateGranularity`. Measure: `name`, `aggregate`, `field`, `filter`, `format`, `label`, `derived{op,of}`. Evidence: `dataset-compiler.ts:137-191`, `dataset-executor.ts:184-278`, `analytics-service.ts:464-472`. Report/widget renderers bind by **name only** + a `{dimensions,measures}` selection → all sub-prop resolution happens server-side in the compiler/executor. + +## DEAD +- **`description`** (dataset) — only a Studio form field; no runtime reader. +- **`certified`** (measure) — has a Studio checkbox (`DatasetDefaultInspector.tsx:160`) but **no runtime gate** (aspirational ADR-0021 governance checkpoint with zero enforcement). + +## PARTIAL / drift +- **Dimension `label`** is compiled but surfaces only via `getMeta` discovery titles; charts render the raw server-resolved dimension *value*, never the declared `label`. (Measure `label` fully flows to the renderer.) +- **Dual vocabulary**: spec says `measures`/`dimensions`; presentations select via `values` (measures) and `rows`/`columns` (dimensions); the compile renames `DatasetMeasure→Metric`, `aggregate→type`. Two parallel naming layers (ADR-0021 Phase-1). + +## 🟠 Studio under-covers the live schema (authoring gap) +`DatasetDefaultInspector` edits only name/label/description/object/include + dim(name/field/type) + measure(name/aggregate/field/certified). It exposes **no editor** for the LIVE, behavior-changing props: dataset `filter`, measure `filter`, measure `format`, measure `derived`, dimension `dateGranularity`, dim/measure `label`. These must be hand-authored in `.dataset.ts`. + +## Recommendation +Remove `description`/`certified` or wire them. Surface the live-but-uneditable props in the Studio dataset designer. Reconcile the measures/dimensions ↔ values/rows/columns vocabulary in one documented place. diff --git a/docs/audits/2026-06-flowschema-property-liveness.md b/docs/audits/2026-06-flowschema-property-liveness.md new file mode 100644 index 0000000000..558032ea37 --- /dev/null +++ b/docs/audits/2026-06-flowschema-property-liveness.md @@ -0,0 +1,24 @@ +# Audit: FlowSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/automation/flow.zod.ts`. **Consumers**: framework `service-automation` engine + node executors; objectui Studio flow designer (canvas/inspector/palette). Per ADR-0018 `node.type` is an open string validated against the live registry, not the enum. + +## 🔴 The `FlowNodeAction` enum is significantly out of sync with reality +- **Lists (but DEAD — no executor)**: `parallel_gateway`, `join_gateway`, `boundary_event` (BPMN-interop, "import/export-only"). `boundary_event` is still editable in the inspector (`flow-node-config.ts:479`) and drives the otherwise-dead node prop `boundaryConfig`. +- **Omits (but LIVE — real executors)**: `loop`, `parallel`, `try_catch`, `map`, `approval` (plugin-contributed). The enum is now misleading documentation. + +## 🔴 http vs http_request drift +Engine canonical type is `http` (`HTTP_TYPE`); `http_request` is a registered **deprecated alias** (`engine.ts:486`). The Studio palette/config/type-picker author **`http_request`** and never offer `http` → new Studio flows bake in the deprecated alias. + +## 🟠 Execution-config props that are display-only at runtime (incl. security-relevant) +- **`runAs`** — engine **never switches execution identity** on it (`FlowPreview` shows it; execution always runs as-is). Security-relevant: a flow declaring `runAs: system` does not actually elevate/de-elevate. +- **`status` / `active`** — engine gates on its in-memory `flowEnabled` map (`toggleFlow`), **not** on `status`/`active`. `active` is spec-flagged Deprecated and redundant with `status`. +- **`errorHandling.fallbackNodeId`** — DEAD (engine uses per-node fault edges). Node **`outputSchema`** — DEAD (declared, never validated). `flow.template`, `flow.description` — no reader either layer. + +## LIVE & well-wired +Top-level: `name`, `label`, `version`, `variables[]{name,isInput,isOutput}`, `nodes[]`, `edges[]{source,target,condition(CEL),label}`, `errorHandling.{strategy,maxRetries,retryDelayMs,backoffMultiplier,...}`. Node common: `id`, `type`, `label`, `config`, `connectorConfig`, `timeoutMs`, `inputSchema` (runtime-validated), `waitEventConfig`. **Executors (LIVE)**: start/end/decision/assignment/get|create|update|delete_record/script/screen/http(+alias)/notify/connector_action/wait/subflow/map/loop/parallel/try_catch/approval. + +## 🟠 `notify` invisible in the static designer +Full executor + descriptor (`paradigms:['flow']`) but reaches the palette only via the server-driven `/automation/actions` overlay — absent from the hardcoded fallback palette + `flow-node-config.ts`. Against an older/offline backend it can't be authored and renders only generic JSON config. + +## Recommendation +Resync `FlowNodeAction` enum with the live registry (add loop/parallel/try_catch/map/approval; remove or mark import-only the 3 gateways). Make the Studio palette author `http` (canonical). **Enforce `runAs`** (or remove — a non-enforcing identity switch is a security footgun). Collapse `status`/`active`. Prune `fallbackNodeId`/`outputSchema`/`template`. Add `notify` to the static palette. From 2b80dccc908429505161bc877c5e62c492bb549a Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:51:08 +0500 Subject: [PATCH 6/8] docs(audit): Tool/Skill/Hook/Validation property liveness Tool metadata is WRITE-ONLY (projected one-way for Studio display; runtime uses code-built AIToolDefinition; cannot author a working tool as metadata). Skill triggerPhrases display-only (no intent matcher); permissions dead + requiredPermissions naming drift; triggerConditions not editable in designer. Hook + Validation are the healthiest schemas: near-total liveness; hook only label/description dead, memoryMb advisory; all 6 validation types enforced, only events:[delete] is a silent no-op + label/description/tags dead. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-hookschema-property-liveness.md | 29 +++++++++++++++++++ .../2026-06-skillschema-property-liveness.md | 20 +++++++++++++ .../2026-06-toolschema-property-liveness.md | 22 ++++++++++++++ ...6-06-validationschema-property-liveness.md | 21 ++++++++++++++ 4 files changed, 92 insertions(+) create mode 100644 docs/audits/2026-06-hookschema-property-liveness.md create mode 100644 docs/audits/2026-06-skillschema-property-liveness.md create mode 100644 docs/audits/2026-06-toolschema-property-liveness.md create mode 100644 docs/audits/2026-06-validationschema-property-liveness.md diff --git a/docs/audits/2026-06-hookschema-property-liveness.md b/docs/audits/2026-06-hookschema-property-liveness.md new file mode 100644 index 0000000000..b904294445 --- /dev/null +++ b/docs/audits/2026-06-hookschema-property-liveness.md @@ -0,0 +1,29 @@ +# Audit: HookSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/data/hook.zod.ts`. **Consumers**: framework `objectql` (`hook-binder`, `engine`) + `runtime` sandbox (`quickjs-runner`, `hook-wrappers`, `body-runner`). Several behaviors confirmed empirically (condition gating, onError=log, retry, async all observed firing in showcase server logs). + +## LIVE & necessary (this schema is healthy — almost everything is wired) +| property | evidence | note | +|---|---|---| +| `name` | `hook-binder.ts:182`, `hook-wrappers.ts:67` | registration key, ref source, log identity | +| `object` (single/array/`*`) | `hook-binder.ts:171,207`; `engine.ts:531` | wildcard honored | +| `events` | `hook-binder.ts:171`; `engine.ts:516` | per-event registration | +| `body.{language,source,capabilities,timeoutMs}` | `quickjs-runner.ts:72,88-90,136,239` | L1/L2 branch; caps gate host fns; timeout = min of default/opts/body | +| `handler` (deprecated) | `hook-binder.ts:237-248` | still fully wired; `body` takes precedence | +| `priority` | `hook-binder.ts:177`; `engine.ts:387` (sort asc) | **genuinely orders hooks** (lower first) | +| `async` | `hook-wrappers.ts:111` | fire-and-forget, after* only | +| `condition` | `hook-wrappers.ts:75-103,179-190` | CEL gate; falsy/error → skip | +| `retryPolicy.{maxRetries,backoffMs}` | `hook-wrappers.ts:105,137-147` | linear backoff | +| `timeout` (top-level) | `hook-wrappers.ts:113-131` | wall-clock abort, **independent** of body.timeoutMs; works for L1/L2/legacy handler | +| `onError` | `hook-wrappers.ts:160-175` | `log` suppresses+continues; `abort` rethrows | + +Note: top-level `timeout` and `body.timeoutMs` are two distinct, both-live timeouts at different layers (declarative wrapper vs sandbox-internal). + +## DEAD +- **`label`** + **`description`** — pure docs, zero runtime readers, redundant with each other (runtime uses only `name`). + +## PARTIAL +- **`body.memoryMb`** — read and passed to `runtime.setMemoryLimit` but "advisory/best-effort" under QuickJS (no hard MB enforcement). Caps + timeout are hard; memory is soft. + +## Recommendation +Drop `label` (keep `description`, or vice-versa). Document `memoryMb` as advisory (or enforce). Otherwise this is a model schema — near-total liveness. (Designer gap fixed separately: `retryPolicy`/`timeout` were missing from the hook form — added in a prior PR.) diff --git a/docs/audits/2026-06-skillschema-property-liveness.md b/docs/audits/2026-06-skillschema-property-liveness.md new file mode 100644 index 0000000000..77f28c49e1 --- /dev/null +++ b/docs/audits/2026-06-skillschema-property-liveness.md @@ -0,0 +1,20 @@ +# Audit: SkillSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ai/skill.zod.ts`. **Consumers**: framework `service-ai` (`skill-registry`, `agent-runtime`); objectui `SkillPreview`, `default-schemas`. + +## LIVE & necessary +`name`, `label`, `description`, `instructions` (all injected into the agent system prompt — `skill-registry.ts:247-249`), `tools[]` (the real tool-contribution path incl. `action_*` wildcard expand — `:206-227`, merged at `agent-runtime.ts:287`), `active` (inactive dropped — `:93`), and **`triggerConditions[]`** — the **sole** activation gate (AND of `{field,operator,value}`, operators eq/neq/in/not_in/contains — `:153-189`). + +## 🔴 `triggerPhrases` is display-only +Despite the schema/doc framing it as "phrases that activate this skill," **nothing matches user text against them** — no intent/phrase matcher exists. They only populate the slash-command palette summary (`toSummary`). Intent routing is unimplemented. + +## 🔴 `permissions` is dead at runtime + naming drift +No code restricts a skill (or its tools/prompt) by `skill.permissions`. The spec field is `permissions`, but docs/preview call it **`requiredPermissions`** and label it "required perm" (`SkillPreview.tsx:80`) — a field that exists by a different name and is enforced nowhere. + +## 🟠 Studio preview & form out of sync (operators get a misleading view) +- `SkillPreview.tsx:151` renders `cond.type`/`cond.expression` (CEL shape) but the spec is `{field,operator,value}` → real conditions show raw/blank. +- `SkillPreview` reads a non-existent `model` field (`:50`) — never renders for valid skills. +- The schema-driven create/edit form (`default-schemas.ts:176-185`) exposes only name/label/description/instructions/tools — **`triggerConditions` (the one activation-critical field), `triggerPhrases`, `permissions`, `active` are not editable**. Correct skill gating is code-only (`defineSkill`/raw JSON). + +## Recommendation +Implement phrase/intent matching for `triggerPhrases` or remove it. Enforce `permissions` (and fix the `requiredPermissions` naming) or drop it. Add a `triggerConditions` editor to the skill designer; fix the preview's condition shape + phantom `model`. diff --git a/docs/audits/2026-06-toolschema-property-liveness.md b/docs/audits/2026-06-toolschema-property-liveness.md new file mode 100644 index 0000000000..4b5407dcda --- /dev/null +++ b/docs/audits/2026-06-toolschema-property-liveness.md @@ -0,0 +1,22 @@ +# Audit: ToolSchema property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/ai/tool.zod.ts`. **Consumers**: framework `service-ai` (`tool-registry`, `action-tools`, `vercel-adapter`, `plugin.ts`), objectui `ToolPreview`. + +## 🔴 `tool` metadata is write-only +The metadata `ToolSchema` is **not** the runtime tool contract — the runtime uses a separate `AIToolDefinition` (`contracts/ai-service.ts:158`). Tools are **built imperatively in code** (built-ins + Action-derived `action_*`), registered into `ToolRegistry`, then **projected one-way** into `tool` metadata (`plugin.ts:813`) so Studio can display them. **No code reads `tool` metadata back** to build an LLM call or execute (zero `metadataService.get/find/list('tool')` in service-ai). **You cannot author a working tool by writing `*.tool.ts` alone** — there's no `implementation`/`handler` field and no executor that loads metadata; tools are agent-resolved by name and executed by a code-registered handler closure (`tool-registry.ts:116`). + +## LIVE (the LLM-facing subset) +`name` (selection + function key), `description` (sent to LLM), `parameters` (LLM function schema), `objectName` (action-tool dispatch). Evidence: `vercel-adapter.ts:46-48`, `action-tools.ts:535`. + +## DEAD / cosmetic on the definition +- `requiresConfirmation` — populated but **never read off the def**; the real HITL gate re-derives from `action.ai.requiresConfirmation` (`action-tools.ts:239`). Redundant mirror. +- `active`, `builtIn` — **never set** by any registration path; ToolPreview shows defaults only. +- `permissions` — in `tool.form.ts` only; not on `AIToolDefinition`, not persisted, no consumer. +- `outputSchema` — aspirational: docstring claims output validation + chaining, but the only use folds its **keys** into the LLM description string (`action-tools.ts:437`); no output validation anywhere. +- `category` — enum drift: spec closed enum vs `AIToolDefinition.category` free string, "not sent to the model"; listing/preview tag only. + +## Bug +ToolPreview's API-Console link targets `/ai/tools/{name}/invoke` but the route is `/ai/tools/:toolName/execute` (`tool-routes.ts:70`) — broken affordance. + +## Recommendation +Decide the model: either make `tool` metadata authoritative (add `implementation`/`handler`, load+execute from metadata) **or** stop projecting a schema that implies authorability. Remove `active`/`builtIn`/`permissions`/`requiresConfirmation` from the definition (cosmetic). Wire `outputSchema` or drop it. Fix the ToolPreview route. diff --git a/docs/audits/2026-06-validationschema-property-liveness.md b/docs/audits/2026-06-validationschema-property-liveness.md new file mode 100644 index 0000000000..cad7c99815 --- /dev/null +++ b/docs/audits/2026-06-validationschema-property-liveness.md @@ -0,0 +1,21 @@ +# Audit: Validation-rule property liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/data/validation.zod.ts` (object-embedded rules; discriminatedUnion on `type`). **Consumers**: framework `objectql/validation/rule-validator.ts` (+ `record-validator.ts`), wired into `engine.ts` insert (`:1973`) + update (`:2107`) write paths. + +## LIVE — all 6 validation TYPES are enforced +`script` (`.condition` CEL), `state_machine` (`.field`/`.transitions`, update-only), `format` (`.field`/`.regex`/`.format` email/url/phone/json), `cross_field` (`.condition`), `json_schema` (`.field`/`.schema` via ajv), `conditional` (`.when`/`.then`/`.otherwise`, recursive). Evidence: `rule-validator.ts:332-347,367-566`. No aspirational variant — the spec's "nothing is a silent no-op" claim holds for the type set (with the exceptions below). Un-evaluable predicates **fail-open** (treated as pass) across the board. + +Common props LIVE: `name` (diagnostics), `type` (discriminator), `message` (error envelope), `active` (`!== false`), `priority` (sort low-first), `severity` (only `error` blocks; `warning`/`info` logged, never throw). + +## 🔴 `events: ['delete']` is a silent no-op +Spec enum admits `delete` (`:88`) but the runtime `Mode` is `'insert' | 'update'` (`rule-validator.ts:72`) and `engine.ts` never invokes validation with `delete`. A rule scoped only to `delete` **never fires**. The header even notes delete isn't a write-payload context → drop the enum value. + +## DEAD +`label`, `description`, `tags` — governance/reporting metadata, never read by either validator. + +## PARTIAL / redundancy +- `cross_field.fields` — does **not** scope evaluation; `cross_field` shares the exact `checkPredicate` path with `script`. Only `fields[0]` is used as the error's field label; `fields[1..n]` decorative. `cross_field` is functionally `script` + a cosmetic field hint. +- **Two redundant paths to the same guarantee**: a `script` rule `amount < 0` overlaps field-level `min`; a `conditional`→required rule overlaps field-level `requiredWhen`. Both LIVE, enforced in different places (`rule-validator` vs `record-validator`). + +## Recommendation +Remove `delete` from the events enum (dead). Drop `label`/`description`/`tags` or document as governance-only. Consider merging `cross_field` into `script` (it adds only a label). Document the rule-vs-field-prop overlap so authors pick one path. From 749be7db13e37e2e17caed80f97266243ef32a4e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:56:02 +0500 Subject: [PATCH 7/8] docs(audit): Security/Identity + System/Integration property liveness MOST SERIOUS findings: PolicySchema 100% dead (password/session/mfa/ip/audit parsed-but-unenforced; not even registered) -> false compliance; permission allowTransfer/allowRestore/allowPurge dead (destructive ops ungated); Role parent dead (no manager rollup); SharingRuleSchema disconnected from the live sys_sharing_rule engine. System: EmailTemplateSchema registered but runtime reads differently-shaped sys_email_template; PortalSchema fully aspirational; Job retryPolicy/timeout dead; Webhook non-HMAC auth/payloadFields dead; TranslationConfig messageFormat(ICU)/supportedLocales/cache dead; Theme spacing/breakpoints/logo merged-but-never-emitted. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...-06-security-identity-property-liveness.md | 26 +++++++++++++++++++ ...06-system-integration-property-liveness.md | 24 +++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 docs/audits/2026-06-security-identity-property-liveness.md create mode 100644 docs/audits/2026-06-system-integration-property-liveness.md diff --git a/docs/audits/2026-06-security-identity-property-liveness.md b/docs/audits/2026-06-security-identity-property-liveness.md new file mode 100644 index 0000000000..d90e64fe9a --- /dev/null +++ b/docs/audits/2026-06-security-identity-property-liveness.md @@ -0,0 +1,26 @@ +# Audit: Security/Identity metadata liveness & necessity + +**Date**: 2026-06-15 · **Scope**: `packages/spec/src/{identity,security}/*.zod.ts` — RoleSchema, PermissionSetSchema, PolicySchema, SharingRuleSchema. **Consumers**: `plugin-security`, `plugin-sharing`, `plugin-auth`, `objectql`. No dedicated Studio designers — generic metadata-admin forms only. **⚠️ This layer is security-critical: "parsed but unenforced" = latent access-control gap.** + +## 🔴🔴 PolicySchema is 100% DEAD (highest impact) +Every prop — `password.*` (minLength/requireUppercase/…), `session.*` (idle/absolute timeout), `forceMfa`, `network.*` (`trustedRanges`/`blockUnknown`/`vpnRequired`), `audit.*` (retention/redaction), `isDefault`, `assignedProfiles` — has **zero runtime consumers**, and `PolicySchema` **isn't even registered as a metadata type** (absent from `metadata-type-schemas.ts`). `better-auth` (`auth-manager.ts:458`) runs its own **hardcoded** session/scrypt config, fully independent. **Authoring a security/compliance Policy gives a false sense of compliance with zero enforcement.** + +## 🔴 PermissionSet — destructive lifecycle ops not gated +- `objects.*.allowTransfer` / `allowRestore` / `allowPurge` — **DEAD**: omitted from `OPERATION_TO_PERMISSION` (`permission-evaluator.ts:8-16`). Ownership transfer, undelete, and hard-delete/GDPR purge (the most destructive ops) are **not gated by RBAC**. +- `isProfile` — DEAD (profile-vs-permset never gates anything). +- `systemPermissions` — PARTIAL: enforced only for **app-entry/nav visibility** (`hono-plugin.ts:741`), **not** as a general capability gate (e.g. `manage_users` is checked nowhere in the data path). +- `tabPermissions` — PARTIAL: only `'hidden'` is read; `default_on`/`default_off` never read; UI-only, not a boundary. +- `contextVariables` (RLS) — **DEAD**: `rls-compiler.ts` never reads it (doc claims runtime evaluation; RLS uses only `current_user.*` built-ins). +- **LIVE & fail-closed**: `objects.*` CRUD, `viewAllRecords`/`modifyAllRecords`, `fields.*` FLS (read-mask **and** write-deny), `rowLevelSecurity` (find + analytics raw-SQL). + +## 🔴 Role.parent is DEAD +`team-graph.ts:27` explicitly does **not** walk a hierarchy. The schema's documented "managers see subordinates' data" rollup is **unimplemented** everywhere. `label`/`description` display-only. + +## 🔴 SharingRuleSchema is disconnected from the live engine +The runtime enforces a **separate, divergent** `sys_sharing_rule`/`sys_record_share` model: `criteria_json` is a **JSON ObjectQL filter, not** the spec's CEL `condition` (unparsable CEL → "match nothing", `sharing-rule-service.ts:33`); `recipient_type ∈ user/team/department/role/queue`, **not** the spec enum (`role_and_subordinates`/`group`/`guest`). The spec `SharingRuleSchema` has **no runtime consumer** — authoring it has no effect. (Runtime sharing itself IS live + enforced via `sharing-plugin.ts:227`, just under a different contract; role expansion is flat — no subordinate rollup.) + +## Recommendation (security ADR — high priority) +1. **PolicySchema**: enforce (wire into better-auth + audit) or delete — shipping unenforced security policy is a compliance liability. +2. **Gate `allowTransfer`/`allowRestore`/`allowPurge`** (add to `OPERATION_TO_PERMISSION`) — destructive ops must be permissioned. +3. **Reconcile SharingRuleSchema with `sys_sharing_rule`** (one contract) or document the spec as non-authoritative. +4. Implement or remove Role `parent` hierarchy and RLS `contextVariables`. diff --git a/docs/audits/2026-06-system-integration-property-liveness.md b/docs/audits/2026-06-system-integration-property-liveness.md new file mode 100644 index 0000000000..a5e7240e83 --- /dev/null +++ b/docs/audits/2026-06-system-integration-property-liveness.md @@ -0,0 +1,24 @@ +# Audit: System/Integration metadata liveness & necessity + +**Date**: 2026-06-15 · **Scope**: EmailTemplateSchema, TranslationBundle/Config, ThemeSchema, JobSchema, WebhookSchema, PortalSchema. **Consumers**: email/messaging/i18n/job/webhook services + objectui theme engine. + +## 🔴 EmailTemplateSchema — worst drift case +Registered as a live metadata type (`email_template`, `metadata-plugin.zod.ts:106`) **but the entire runtime email path reads a structurally different object** `sys_email_template` (columns `body_html`/`body_text`/`from_name`/`from_address`/`variables_json`/`locale`) — not the spec's `body`/`bodyType`/`variables`. The spec `EmailTemplateSchema` has **zero importers**. Reconcile with `sys_email_template` or delete. + +## 🔴 PortalSchema — 100% aspirational +Not registered as a metadata type (no `'portal'` in `metadata-plugin.zod.ts`), no server route wiring (`routePrefix`/`anonymousEntry` have no consumer), no objectui renderer. 326 lines of richly-documented schema with **no consumer** (the file's own header describes consumers as *future*). Every prop DEAD. + +## ThemeSchema — `core/theme/ThemeEngine.ts` +LIVE: `name`, `mode`, `colors`, `typography`, `borderRadius`, `shadows`, `animation`, `zIndex`, `customVars`, `extends` (inheritance). **DEAD**: `spacing`, `breakpoints`, `logo` (merged by `mergeThemes` but **never emitted** as CSS vars), `density`, `rtl`, `touchTarget`, `keyboardNavigation`. PARTIAL: `wcagContrast` (`meetsContrastLevel` helper exists but isn't driven by the prop). `label`/`description` display-only. + +## TranslationConfig — 5 dead knobs +LIVE: `defaultLocale` (`app-plugin.ts:838`), `fallbackLocale` (i18n adapter). **DEAD**: `supportedLocales`, `messageFormat` (**no ICU engine exists anywhere** — `messageFormat:'icu'` is an unkept promise), `fileOrganization`, `lazyLoad`, `cache`. TranslationData groups (objects/apps/messages) resolve via generic dot-path — convention, not validated keys. HTTP locale fallback uses a heuristic `resolveLocale()`, not the configured `fallbackLocale`. + +## JobSchema — `app-plugin.ts:382` → service-job adapters +LIVE: `name`, `schedule` (cron/interval/once: expression/timezone/intervalMs/at), `handler`, `enabled`. **DEAD**: `id`, `label`, `description`, **`retryPolicy`**, **`timeout`** — the cron adapter's `execute()` only try/catch-logs; no retry/backoff/timeout despite the detailed `RetryPolicySchema`. + +## WebhookSchema (outbound) — `sys_webhook` + `auto-enqueuer.ts` +LIVE: `name`, `object` (→ row `object_name`, **naming drift**), `triggers`, `url`, `method`, `headers`, `authentication.secret` (HMAC), `timeoutMs`, `isActive` (→ row `active`, **drift**). **DEAD**: `body`, `payloadFields`, `includeSession`, `authentication` (bearer/basic/api-key block — only `secret` read), `retryPolicy` (owned by the outbox), `description`, `tags`. `WebhookReceiverSchema` (inbound) — entirely DEAD. + +## Headline +EmailTemplate (live type, dead shape) and Portal (fully aspirational) are the two worst; both should be reconciled-or-deleted. Job `retryPolicy`/`timeout`, Webhook auth (non-HMAC) + `payloadFields`, Theme `spacing`/`breakpoints`, and TranslationConfig `messageFormat`-ICU are richly-specced features with no runtime — classic aspirational config that misleads authors. From d6125004da29b7480d5a06a9de4294d6c6414588 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:57:09 +0500 Subject: [PATCH 8/8] docs(audit): cross-type synthesis + index (README) Ties 21 schemas into 7 cross-cutting patterns, priority-ordered: (1) parsed- but-unenforced security props (policy/permission-lifecycle/agent-access/flow- runAs/object-apiEnabled/action-disabled) = latent access-control gaps; (2) ADR-0021 analytics migration debt; (3) naming drift silent no-ops; (4) aspirational config; (5) write-only/disconnected metadata; (6) renderer reads undeclared props; (7) designer authoring gaps. Suggests 3 ADRs. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/audits/README.md | 65 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 docs/audits/README.md diff --git a/docs/audits/README.md b/docs/audits/README.md new file mode 100644 index 0000000000..0fdb329ea9 --- /dev/null +++ b/docs/audits/README.md @@ -0,0 +1,65 @@ +# Metadata Property Liveness & Necessity Audit + +A protocol-level audit of **every ObjectStack metadata type**: for each schema property, cross-reference its spec definition against its **actual consumers** (`file:line`) in the runtime and renderers, to answer two questions in order — **(1) does the property take effect?** and **(2) is it necessary / reasonable?** + +Liveness is judged by **code consumption**, not browser observation (an unreactive UI can't distinguish "dead property" from "data didn't trigger it"). "DEAD" = parsed but read by no non-spec/non-test consumer → a **silent no-op** for authors. + +## Per-type reports +| Type | Doc | Health | +|---|---|---| +| field | `2026-06-fieldschema-property-liveness.md` | ~half dead | +| object | `2026-06-objectschema-property-liveness.md` | ~half dead | +| view | `2026-06-viewschema-property-liveness.md` | list-family healthy; chart/form-variants broken | +| app | `2026-06-appschema-property-liveness.md` | nav core healthy; aspirational tail | +| page | `2026-06-pageschema-property-liveness.md` | drift breaks layouts | +| dashboard | `2026-06-dashboardschema-property-liveness.md` | ADR-0021 half-done | +| report | `2026-06-reportschema-property-liveness.md` | dataset live; chart dead | +| dataset | `2026-06-datasetschema-property-liveness.md` | live; Studio under-covers | +| action | `2026-06-actionschema-property-liveness.md` | disabled-CEL ignored; form/shortcut dead | +| flow | `2026-06-flowschema-property-liveness.md` | enum out of sync; runAs unenforced | +| agent | `2026-06-agentschema-property-liveness.md` | autonomy + access-control aspirational | +| tool | `2026-06-toolschema-property-liveness.md` | **write-only metadata** | +| skill | `2026-06-skillschema-property-liveness.md` | triggerPhrases display-only | +| hook | `2026-06-hookschema-property-liveness.md` | **healthy** (near-total) | +| validation | `2026-06-validationschema-property-liveness.md` | **healthy** (all 6 types enforced) | +| security/identity (role, permission, policy, sharing) | `2026-06-security-identity-property-liveness.md` | **policy 100% dead** | +| system/integration (email/i18n/theme/job/webhook/portal) | `2026-06-system-integration-property-liveness.md` | email + portal dead | + +## Cross-cutting patterns (in priority order) + +### 1. 🔴 Parsed-but-UNENFORCED security props (latent access-control gaps) +The most serious cluster — properties that imply a security boundary but enforce nothing: +- **PolicySchema** — 100% dead (password complexity, session timeout, `forceMfa`, IP allow-list, audit retention); not even registered. better-auth's hardcoded defaults govern. **False compliance.** +- **Permission `allowTransfer`/`allowRestore`/`allowPurge`** — destructive ops (transfer, undelete, GDPR purge) not gated by RBAC. +- **Agent `permissions`/`visibility`/`access`** — "who can chat with this agent" is a no-op (route hardcodes `['ai:chat','ai:agents']`). +- **Flow `runAs`** — never switches execution identity. +- **Object `apiEnabled`/`apiMethods`** — not enforced by REST (object can't be hidden from the API). +- **Action `disabled`** (CEL form) — silently ignored (renderer reads non-spec `enabled`). +- **Role `parent`** / **SharingRuleSchema** — manager rollup & spec sharing rules disconnected from the live engine. + +### 2. 🔴 ADR-0021 analytics migration debt +Spec moved to `dataset`+`values`/`dimensions`, but: the **chart view variant** + **dashboard renderer + Studio WidgetConfigPanel** still read the *removed* legacy `object/valueField/categoryField/aggregate` shape; **report `chart`** is dead; `ReportColumn`/`ReportGrouping` are obsolete re-exports. (Same debt that invalidated the showcase dashboard/report seeds.) + +### 3. 🟠 Naming drift → silent no-ops (spec key ≠ consumed key) +field `maxLength`/`minLength`/`referenceFilters`/`maxRating`; page `type`→`pageType` & `label`→`title` & `visibility`; dashboard `title` vs `label`; app `accentColor`/`badgeVariant`/`separator` (renderer reads, **not in spec**); action `disabled`→`enabled`; flow `http` vs `http_request`; skill `requiredPermissions` vs `permissions`; agent `knowledge.{topics→sources}`; webhook `object`→`object_name`, `isActive`→`active`. + +### 4. 🟠 Aspirational config (rich spec, zero runtime) — prune or mark `experimental` +field enhanced-type configs (barcode/qr/slider/rating/color/location) + governance (encryption/masking/audit/dataQuality); object `enable`/versioning/partitioning/cdc/softDelete/search; agent autonomy (memory/guardrails/structuredOutput/lifecycle); tool `outputSchema`; job `retryPolicy`/`timeout`; theme rtl/density/touchTarget; translation `messageFormat:'icu'` (no ICU engine); **portal (entire)**; webhook non-HMAC auth. + +### 5. 🟠 Write-only / disconnected metadata (type implies authorability it lacks) +**tool** (write-only projection — can't author a working tool as metadata); **EmailTemplateSchema** (registered, but runtime reads differently-shaped `sys_email_template`); **SharingRuleSchema** (runtime uses `sys_sharing_rule`); **spec-bridge** page/dashboard bridges orphaned (their exclusive props dead). + +### 6. Inverse drift — renderer depends on UNDECLARED props +dashboard `component`/`data`/`rowField`/`columnField`; view `ObjectView` form-adapter keys; app `accentColor`/`badgeVariant`/`separator`. These break a strict `Schema.parse()`. + +### 7. Designer authoring gaps (live prop, no Studio editor) +dataset `filter`/`format`/`derived`/`dateGranularity`; skill `triggerConditions` (the activation-critical field); flow `notify` (absent from static palette). + +## Healthiest vs worst +- **Healthiest** (near-total liveness, model schemas): **hook**, **validation**. +- **Worst**: **policy** (100% dead), **portal** (100% dead), **tool** (write-only). + +## Suggested ADRs +1. **Security enforcement** (cluster #1) — highest priority; either enforce or remove every parsed-but-unenforced security prop. +2. **Finish ADR-0021** (cluster #2) — migrate chart/dashboard/report renderers + Studio off the legacy inline shape. +3. **Spec hygiene** (clusters #3–#7) — normalize naming drift, prune aspirational config, reconcile write-only/disconnected types, declare the undeclared-but-read props, close designer authoring gaps.