|
| 1 | +# Recheck: "naming drift → silent no-ops" (§3) |
| 2 | + |
| 3 | +**Date**: 2026-07-27 · **Supersedes**: the drift list in |
| 4 | +[`README.md` §3](./README.md) (dated 2026-06-15). · **Umbrella**: #1878. |
| 5 | + |
| 6 | +The 2026-06 audit listed nine spec-key ≠ consumed-key drifts. Re-verified every |
| 7 | +one against current code in **both** repos (framework + objectui, including the |
| 8 | +`<!-- os:check -->` TypeScript blocks inside `skills/*/SKILL.md`, which compile |
| 9 | +against the spec and are real consumers). |
| 10 | + |
| 11 | +**Result: six of nine are already resolved** — the list, not the code, was |
| 12 | +stale. Three real problems remain, two of them a coupled pair that produces |
| 13 | +**visibly wrong rendering today**. |
| 14 | + |
| 15 | +> Vocabulary used below: |
| 16 | +> - **FORWARD-DRIFT** — spec declares `A`, the consumer reads `B`. Authoring the |
| 17 | +> documented key silently no-ops. The dangerous direction: the author did |
| 18 | +> everything right. |
| 19 | +> - **REVERSE-DRIFT** — the consumer reads `B`, the spec has no `B`. The |
| 20 | +> capability works but is undiscoverable (and rejected outright by a strict |
| 21 | +> schema). |
| 22 | +
|
| 23 | +## Verdicts |
| 24 | + |
| 25 | +| # | Item (as listed in §3) | Verdict | Evidence | |
| 26 | +|---|---|---|---| |
| 27 | +| 1 | field `maxLength` / `minLength` | **MOSTLY ALIGNED** — enforcement is fine; two cosmetic readers still snake-only | server: `objectql/src/validation/record-validator.ts:243-247` (rejects on the camelCase keys); client: `fields/src/index.tsx:2013,2022` dual-reads. Residual: `plugin-form/src/ObjectForm.tsx:607-608` and `fields/src/widgets/TextAreaField.tsx:39` read `max_length` only → no HTML `maxlength` cap, no character counter | |
| 28 | +| 2 | field `referenceFilters` | **RESOLVED** (item stale) | key removed in #2377/ADR-0049 (tombstone `field.zod.ts:335-337`); successor `lookupFilters` (`:418`) is read by `fields/src/widgets/LookupField.tsx:244` and scopes the picker | |
| 29 | +| 3 | field `maxRating` | **RESOLVED** (item stale) | `maxRating` was pruned; the builder emits `max` (`field.zod.ts:747-750`) and `fields/src/widgets/RatingField.tsx:13` reads `max`. Zero `maxRating` occurrences in objectui | |
| 30 | +| 4 | **page `type` → `pageType`** | **🔴 FORWARD-DRIFT — live** | spec `type` (`ui/page.zod.ts:310`); renderer reads `schema.pageType` (`components/src/renderers/layout/page.tsx:391`), falling back to `'record'`. The boundary (`app-shell/src/views/PageView.tsx:123`) passes `type` only — **no mapping exists** | |
| 31 | +| 5 | **page `label` → `title`** | **🔴 FORWARD-DRIFT — live** | spec `label` is required (`page.zod.ts:303`); the region/SDUI renderer reads `schema.title` with no fallback (`page.tsx:503-507`). Zero `schema.label` reads in `page.tsx` | |
| 32 | +| 6 | page `visibility` | **ALIGNED** (item stale) | spec folds `visibility` → `visibleWhen` (`page.zod.ts:116,126`); `react/src/SchemaRenderer.tsx:285` consumes `visibleWhen`, `:294` still honors raw `visibility` | |
| 33 | +| 7 | dashboard `title` vs `label` | **ALIGNED** (item stale) | renderer dual-reads `title \|\| label` (`plugin-dashboard/.../DashboardRenderer.tsx:782`, `DashboardGridLayout.tsx:278`). Widget-level keys additionally hard-fail via a `.strict()` schema + naming error map (`dashboard.zod.ts:273,98-123`, #1894) | |
| 34 | +| 8 | app `accentColor` / `badgeVariant` / `separator` | **SPLIT** — `accentColor` aligned; the other two **decayed** | #1894 declared all three in the spec to match objectui's `NavigationRenderer`. `accentColor` is still consumed (`app-shell/src/layout/ConsoleLayout.tsx:171`, `PageHeader.tsx:46`, `apps/console/src/hooks/useBranding.ts:25`). But **`NavigationRenderer` no longer exists** — the live nav is `UnifiedSidebar`, which renders neither `badgeVariant` **nor `badge` itself**, and has no `separator` branch (`separator` is only recognized as "no href" by `AppContent.tsx:849` and excluded by `nav-target.ts:17`) | |
| 35 | +| 9 | action `disabled` → `enabled` | **RESOLVED 2026-07** | fixed across all six rendering surfaces (objectui#2863); showcase specimen `showcase_archive_task` (#3643) | |
| 36 | +| — | flow `http` vs `http_request` | **ALIGNED** (item stale) | spec enum carries `http` marked canonical (`automation/flow.zod.ts:32`); the runtime registers `HTTP_TYPE = 'http'` (`service-automation/src/builtin/http-nodes.ts:48`) | |
| 37 | +| — | skill `requiredPermissions` vs `permissions` | **NOT A DRIFT** — mis-filed | `requiredPermissions` **never existed** on `SkillSchema`; the spec key is and was `permissions` (`ai/skill.zod.ts:121`). The original audit described prose/label drift in the docs, not a spec-vs-runtime mismatch | |
| 38 | +| — | agent `knowledge.{topics → sources}` | **RESOLVED** (item stale) | #1891 made `sources` canonical and folds the `topics` alias via `.transform()` (`ai/agent.zod.ts:35-48`); `agent.test.ts:105-123` asserts `'topics' in parsed === false` | |
| 39 | +| — | webhook `object` → `object_name`, `isActive` → `active` | **RESOLVED** | #3489 materializes declared webhooks with an explicit mapping (`plugin-webhooks/src/bootstrap-declared-webhooks.ts:22-23`) | |
| 40 | + |
| 41 | +## The one that matters: the page pair (#4 + #5) |
| 42 | + |
| 43 | +These two are **coupled and must be fixed together**. The header block is gated |
| 44 | +on `pageType !== 'record'` (`page.tsx:503`) — and because #4 pins `pageType` to |
| 45 | +its `'record'` fallback, the gate is *always false*. Wiring `label → title` |
| 46 | +alone would still render no title. |
| 47 | + |
| 48 | +Observable today in the showcase: `capability-map` (`type: 'home'`), |
| 49 | +`active-projects` (`type: 'list'`) and `command-center-jsx` (`type: 'home'`) all |
| 50 | +render as record pages — wrong `max-width` (`page.tsx:486`), wrong |
| 51 | +`data-page-type` attribute (`:494`), and their page titles suppressed. |
| 52 | + |
| 53 | +Why it survived a year: the spec's `type` **defaults to `'record'`** |
| 54 | +(`page.zod.ts:310`) and the renderer's fallback is *also* `'record'`, so the |
| 55 | +most common page kind looks correct by coincidence. |
| 56 | + |
| 57 | +## Gate coverage — the systemic finding |
| 58 | + |
| 59 | +For every item above, authoring the wrong key is **silently stripped** at |
| 60 | +runtime. `FieldSchema` and `PageSchema` are bare `z.object` (no `.strict()`), |
| 61 | +verified empirically: parsing a page with `pageType` + `title` returns neither |
| 62 | +key, with no error. |
| 63 | + |
| 64 | +What does exist: |
| 65 | +- **TypeScript authoring is a real gate** — `defineField`/`defineSkill` etc. take |
| 66 | + `z.input<typeof Schema>`, so excess-property checking rejects a wrong key in an |
| 67 | + object literal (`Field.rating(5, { maxRating: 10 })` → TS2353). Bypassed by |
| 68 | + `as any`, JSON/YAML, and API-posted metadata. |
| 69 | +- **Widget-level dashboard keys hard-fail** via `.strict()` + a naming-aware |
| 70 | + error map (#1894) — the pattern worth copying. |
| 71 | +- **`check:liveness` does not help here**: it ratchets *declared* spec props into |
| 72 | + the ledger; it says nothing about *authored unknown* keys. |
| 73 | + |
| 74 | +The dashboard-widget approach (strict + a fixable error message that names the |
| 75 | +right key) is the only mechanism in the codebase that turns a silent no-op into |
| 76 | +a teaching error. Extending it to `PageSchema` / `FieldSchema` is the structural |
| 77 | +fix for this whole class; the per-item wiring below is the tactical one. |
| 78 | + |
| 79 | +## Disposition |
| 80 | + |
| 81 | +**Fixed in this pass:** |
| 82 | +1. **page `type` → `pageType`** + **page `label` → `title`** — wired at the |
| 83 | + boundary + a `title || label` fallback (objectui). |
| 84 | +2. **field `maxLength`/`minLength`** — the two snake-only readers now dual-read, |
| 85 | + matching the `buildValidationRules` precedent already in the same repo. |
| 86 | +3. **`os doctor`** pointed `reference_filters` at the removed `referenceFilters` |
| 87 | + instead of the live `lookupFilters` — corrected (framework). |
| 88 | + |
| 89 | +**Recorded, not fixed (each needs an owner decision):** |
| 90 | +4. **app `badgeVariant` + nav `separator`** — a #1894 reverse-drift fix that |
| 91 | + **decayed** when `NavigationRenderer` was replaced by `UnifiedSidebar`. |
| 92 | + Enforce-or-prune: either the sidebar renders nav badges + separators (they are |
| 93 | + ordinary nav affordances), or both keys leave the spec. Note `badge` itself is |
| 94 | + also unrendered, so this is "nav badges are gone", not merely a variant key. |
| 95 | +5. **`skill.permissions` has no gate** — mis-filed as naming drift; it is an |
| 96 | + aspirational-config item (§4). The ledger marks it `live` on the evidence of a |
| 97 | + *preview renderer* while the identically-unenforced `tool.permissions` is |
| 98 | + `dead` + `authorWarn`. An author writing `skill.permissions` gets silence |
| 99 | + where they should get a warning. |
| 100 | +6. **`agent.knowledge` is inert at runtime** — naming is fixed, but RAG resolves |
| 101 | + its sources from the LLM tool call, not from the authored block. Wire or mark |
| 102 | + experimental. |
0 commit comments