diff --git a/.changeset/20301-lint-list-view-tabs-walk-deleted.md b/.changeset/20301-lint-list-view-tabs-walk-deleted.md new file mode 100644 index 00000000000..bff9e102ee3 --- /dev/null +++ b/.changeset/20301-lint-list-view-tabs-walk-deleted.md @@ -0,0 +1,11 @@ +--- +"@objectstack/lint": patch +--- + +The list-view field-reference rule no longer walks a list view's own `tabs[].filter` + +Clause-②: no + +The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape, and this rule judges the parsed stack, so the key could never reach the walk: the parse refuses it first, with its prescription. The dead branch is deleted. The rule still judges `filter` and `userFilters.tabs[].filter` exactly as before. + +No finding changes for any stack that `os validate`, `os lint` or `os build` accepts. diff --git a/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md b/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md new file mode 100644 index 00000000000..925a0d39212 --- /dev/null +++ b/.changeset/20301-metadata-protocol-list-view-tabs-walk-deleted.md @@ -0,0 +1,9 @@ +--- +"@objectstack/metadata-protocol": patch +--- + +`computeViewReferenceDiagnostics` no longer walks a list view's own `tabs[].filter` + +Clause-②: no + +The list view's own `tabs` is a `retiredKey` tombstone on every list-view shape. The write door refuses it, and a stored or artifact-shipped body has it stripped by the conversion replay before it is served, so the read could never see it. A served body that still carries it is already badged by the spec diagnostics (`computeMetadataDiagnostics`), with the tombstone's prescription. The `userFilters.tabs[].filter`, `filterableFields` and `kanban` checks are unchanged. diff --git a/.changeset/20301-spec-view-container-name-ledger-note.md b/.changeset/20301-spec-view-container-name-ledger-note.md new file mode 100644 index 00000000000..4a26724d0f0 --- /dev/null +++ b/.changeset/20301-spec-view-container-name-ledger-note.md @@ -0,0 +1,15 @@ +--- +"@objectstack/spec": patch +--- + +Liveness ledger: the view container's body `name` row stays `dead`, and its note now states what the platform actually does with the key + +Clause-②: no + +- The old note said the body copy was "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body that has none, containers included (`normalizeViewMetadata` in `@objectstack/metadata-protocol`). Its overlay paths key on that stamped copy: `hydrateOverlayIntoRegistry` registers no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by it. +- The verdict is unchanged, because the ledger's `live` means that authoring the key changes runtime behaviour. An authored container `name` only restates the key the container already registers under, or contradicts it. `os validate` and `os lint` keep warning `liveness-dead-property` ("drop it"). +- The note records why the key is kept rather than tombstoned: the door's own saves stamp it, so a tombstone would refuse the platform's own writes. A maintainer ruling also refused a spec-level forbid of a container's `name`. +- It corrects the old attribution too. Artifact-shipped containers and the metadata-validation sweep author no `name`; what was read as theirs is the door's stamp. +- The ledger README's `view` cell says the same. The `view.list.tabs` row's note now records that the two author-time walks that still read a list view's own `tabs` are deleted. +- A comment in `system/i18n-resolver.ts` that still called the list view's own `tabs` a live carrier now says the key is a tombstone and `UserFiltersSchema.tabs` is the one carrier. +- ⛔ No schema, parse, export, status or accept-set change. diff --git a/packages/lint/src/validate-list-view-field-refs.test.ts b/packages/lint/src/validate-list-view-field-refs.test.ts index 2561d2c9574..c4ccdeb7529 100644 --- a/packages/lint/src/validate-list-view-field-refs.test.ts +++ b/packages/lint/src/validate-list-view-field-refs.test.ts @@ -83,7 +83,6 @@ const FULL_LIST_VIEW: AnyRec = { fields: [{ field: 'status' }], tabs: [{ name: 'open', filter: [{ field: 'status', operator: 'equals', value: 'open' }] }], }, - tabs: [{ name: 'mine', filter: [{ field: 'business_unit', operator: 'equals', value: 'x' }] }], kanban: { groupByField: 'status', summarizeField: 'estimate', columns: ['title'], titleField: 'title' }, calendar: { startDateField: 'due_at', @@ -241,23 +240,22 @@ function positionAsserted(path: string, walked: ReadonlySet): string | u } /** - * [#18836] The rule's three hard-coded filter walks, spelled as + * [#18836] The rule's two hard-coded filter walks, spelled as * {@link casePath} normalises the paths they report at. * * They are open code, not table rows — `checkListView` calls `checkFilter` on - * `listView.filter`, on `tabs[]` and on `userFilters.tabs[]` — so the position + * `listView.filter` and on `userFilters.tabs[]` — so the position * set derived from the rule cannot reach them, and the completeness assertion * below would let their rows be deleted in silence. That is precisely the hole * the floor this card replaced DID cover, by counting rows. * * So they are declared here and asserted EXACTLY: delete one of their rows and - * the list comes up short; give a fourth hard-coded walk a row without adding + * the list comes up short; give a third hard-coded walk a row without adding * it here and the list comes up long. Together with the derived assertion, every * row in both tables is then accounted for by one criterion or the other. */ const HARD_CODED_FILTER_WALKS = [ 'filter.field', - 'tabs.filter.field', 'userFilters.tabs.filter.field', ]; @@ -292,11 +290,6 @@ describe('#14107 — every other walked position', () => { 'views[0].list.userFilters.tabs[0].filter[0].field', 'error', ], - [ - { tabs: [{ name: 'a', filter: [{ field: BAD, operator: 'equals', value: 1 }] }] }, - 'views[0].list.tabs[0].filter[0].field', - 'error', - ], [{ kanban: { summarizeField: BAD } }, 'views[0].list.kanban.summarizeField', 'warning'], [{ kanban: { columns: [BAD] } }, 'views[0].list.kanban.columns[0]', 'warning'], // [#18565] Calendar's level, not the required siblings' — see the row's @@ -426,15 +419,15 @@ describe('#14107 — every other walked position', () => { // filter walks — nothing else. A set, compared in both directions, which // holds three things the completeness assertion does not: // - // - SHORT ⇒ RED. Delete a filter-walk row and the set loses a member. All - // three rows measured, one by one. That is exactly the coverage the + // - SHORT ⇒ RED. Delete a filter-walk row and the set loses a member. Every + // row measured, one by one. That is exactly the coverage the // row-counting floor had and the derived assertion cannot reach. - // - LONG ⇒ RED, measured two ways. Remove one of the three declarations + // - LONG ⇒ RED, measured two ways. Remove one of the declarations // below and the rows outnumber them. Leave a row behind for a position the // rule has DROPPED and it matches neither side, so it arrives here as an // extra — red here as well as in that row's own per-case assertion, which // is how this assertion ends up carrying the direction its sibling above - // cannot. A fourth hard-coded walk given a row without being declared + // cannot. A third hard-coded walk given a row without being declared // below lands in the same place by the same comparison. it('accounts for every asserted path, as a walked position or a declared filter walk', () => { const walkedSet = new Set(listViewWalkedPositions()); @@ -707,11 +700,10 @@ describe('#14282 — a dotted key the FILTER door refuses, and the ones it serve expect(validateListViewFieldRefs(stackWith(mutate(filterOn('id.x'))))).toEqual([]); }); - it('the tab and user-filter tab presets are judged on the same axis', () => { + it('the user-filter tab presets are judged on the same axis', () => { const findings = validateListViewFieldRefs( stackWith( mutate({ - tabs: [{ name: 'mine', filter: [{ field: 'owner.name', operator: 'equals', value: 'x' }] }], userFilters: { fields: [{ field: 'status' }], tabs: [{ name: 'open', filter: [{ field: 'parent.title', operator: 'equals', value: 'x' }] }], @@ -720,7 +712,6 @@ describe('#14282 — a dotted key the FILTER door refuses, and the ones it serve ), ); expect(idsOf(findings).sort()).toEqual([ - 'views[0].list.tabs[0].filter[0].field', 'views[0].list.userFilters.tabs[0].filter[0].field', ]); expect(findings.every((f) => f.rule === LIST_VIEW_FIELD_DOTTED)).toBe(true); diff --git a/packages/lint/src/validate-list-view-field-refs.ts b/packages/lint/src/validate-list-view-field-refs.ts index 2590220fc83..dcf199e4088 100644 --- a/packages/lint/src/validate-list-view-field-refs.ts +++ b/packages/lint/src/validate-list-view-field-refs.ts @@ -134,9 +134,9 @@ * `INVALID_FIELD` / 400 (`packages/objectql/src/engine.ts`, #7589), and * `assertProjectionFieldsExist` answers the same at the REST ingress * (#7532). So every dotted column is reported, whatever its head's type. - * - **Filter** — the view's own `filter`, its `tabs[].filter`, its - * `userFilters.tabs[].filter`, and the two positions that DECLARE which - * names the end user may filter on (`filterableFields`, spelled by the + * - **Filter** — the view's own `filter`, its `userFilters.tabs[].filter`, + * and the two positions that DECLARE which names the end user may + * filter on (`filterableFields`, spelled by the * spec as "bare field names enabled for end-user filtering", and * `userFilters.fields`; objectui folds the resulting conditions into the * fetched query through `buildEffectiveFilter`). Here the door does NOT @@ -201,9 +201,10 @@ * owned by `validateActionNameRefs`. * - **`conditionalFormatting[].condition`** — a CEL predicate, owned by the * expression rules. - * - **`tabs[].view` / `addRecord.formView`** — view names, owned by - * `lintViewRefs`. (`pageName` was here too until #17063 retired the - * `type: 'page'` view mount; a list view carries no page reference now.) + * - **`addRecord.formView`** — a view name, owned by `lintViewRefs`. + * (`pageName` was here too until #17063 retired the `type: 'page'` view + * mount, and `tabs[].view` until the list view's own `tabs` was retired; a + * list view carries neither reference now.) * - **The `data.object` binding itself** — `validateObjectReferences` owns * object-name reference sites, with the curated cross-package severity * ladder a local "not in this stack ⇒ error" would not have. When the bound @@ -520,11 +521,11 @@ const COLUMN_ENTRY_POSITIONS: Array<{ block: string; key: string; severity: Sev * It names positions only — no severity, no shape — so reading it can never * stand in for reading the tables. * - * The three filter walks further down (`listView.filter`, `tabs[].filter`, + * The two filter walks further down (`listView.filter`, * `userFilters.tabs[].filter`) are deliberately absent: they are open code, not * table rows, so there is nothing HERE to derive them from. ⛔ Absent from this * list is not absent from the test's account of itself — the test declares those - * three as an explicit list and asserts it exactly, because a row of theirs + * two as an explicit list and asserts it exactly, because a row of theirs * deleted in silence is the one thing the row-counting floor this replaced did * cover. */ @@ -768,12 +769,16 @@ export function validateListViewFieldRefs(stack: AnyRec): ListViewFieldRefFindin } } - // ── Filter KEYS: the view's own filter, its tabs' filters, and the tab - // presets inside `userFilters`. `walkFilterFieldKeys` handles all three - // authored filter shapes (Mongo condition object, `{ field, operator, - // value }` rules, `[field, op, value]` triples) so a filter authored one - // way is not judged while another is silently skipped (#3574's own - // failure mode). + // ── Filter KEYS: the view's own filter and the tab presets inside + // `userFilters`. `walkFilterFieldKeys` handles all three authored filter + // shapes (Mongo condition object, `{ field, operator, value }` rules, + // `[field, op, value]` triples) so a filter authored one way is not judged + // while another is silently skipped (#3574's own failure mode). + // + // The list view's OWN `tabs` is not walked: it is a `retiredKey` tombstone + // on every list-view shape, and this rule judges the PARSED stack + // (`input: 'parsed'` in the authoring-rule registry), where the key can + // never arrive — the parse refuses it with its prescription first. const checkFilter = (filter: unknown, filterWhere: string, filterPath: string): void => { if (filter === undefined || filter === null) return; walkFilterFieldKeys(filter, filterPath, ({ field, path: at }) => { @@ -792,7 +797,6 @@ export function validateListViewFieldRefs(stack: AnyRec): ListViewFieldRefFindin }); }; - checkTabs(listView.tabs, `${where} › tabs`, `${path}.tabs`); if (isRec(listView.userFilters)) { checkTabs( listView.userFilters.tabs, diff --git a/packages/metadata-protocol/src/metadata-diagnostics.ts b/packages/metadata-protocol/src/metadata-diagnostics.ts index 36b7a9be201..e7c03cb8b36 100644 --- a/packages/metadata-protocol/src/metadata-diagnostics.ts +++ b/packages/metadata-protocol/src/metadata-diagnostics.ts @@ -228,8 +228,11 @@ export function computeViewReferenceDiagnostics( userFilters?.tabs?.forEach((t, i) => t?.filter?.forEach((r, j) => requireField(r?.field, `userFilters.tabs.${i}.filter.${j}.field`))); - (view?.tabs as Array<{ filter?: Array<{ field?: string }> }> | undefined)?.forEach((t, i) => - t?.filter?.forEach((r, j) => requireField(r?.field, `tabs.${i}.filter.${j}.field`))); + // A list view's OWN `tabs` is not walked: it is a `retiredKey` tombstone on + // every list-view shape. The write door refuses it, and a stored or + // artifact-shipped body has it stripped by the conversion replay before it + // is served. A body that still carries it is already badged by the spec + // diagnostics, with the tombstone's prescription. (view?.filterableFields as string[] | undefined)?.forEach((f, i) => requireField(f, `filterableFields.${i}`)); diff --git a/packages/objectql/src/metadata-diagnostics.test.ts b/packages/objectql/src/metadata-diagnostics.test.ts index 7bffa8f1c2a..78b19c22ecb 100644 --- a/packages/objectql/src/metadata-diagnostics.test.ts +++ b/packages/objectql/src/metadata-diagnostics.test.ts @@ -30,7 +30,6 @@ describe('computeViewReferenceDiagnostics (ADR-0047)', () => { fields: [{ field: 'industry' }, { field: 'is_active' }], tabs: [{ name: 't', filter: [{ field: 'status', operator: 'equals', value: 'x' }] }], }, - tabs: [{ name: 'a', filter: [{ field: 'industry', operator: 'equals', value: 'technology' }] }], filterableFields: ['status'], kanban: { groupByField: 'status', columns: ['name'] }, }, objectDef); @@ -48,12 +47,12 @@ describe('computeViewReferenceDiagnostics (ADR-0047)', () => { }); }); - it('flags tab filter rules pointing at unknown fields', () => { + it('flags user-filter tab preset rules pointing at unknown fields', () => { const result = computeViewReferenceDiagnostics({ - tabs: [{ name: 'bad', filter: [{ field: 'ghost', operator: 'equals', value: 1 }] }], + userFilters: { element: 'tabs', tabs: [{ name: 'bad', filter: [{ field: 'ghost', operator: 'equals', value: 1 }] }] }, }, objectDef); expect(result.valid).toBe(false); - expect(result.errors?.[0].path).toBe('tabs.0.filter.0.field'); + expect(result.errors?.[0].path).toBe('userFilters.tabs.0.filter.0.field'); }); it('flags kanban groupBy on a non-select-like field', () => { diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index 26c6a70cd32..a6649238a42 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -933,7 +933,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | skill | `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 | `measures.certified` (declared-but-unenforced governance flag) REMOVED in 16.0 (#2377) | | page | live + one planned; dead `assignedProfiles` REMOVED 2026-09-12 (ADR-0090 D2 + ADR-0049 — a per-page audience list named for the concept D2 deleted, with zero readers in either repo, so the page was open to everyone who could reach it). The row stays because `retiredKey` keeps the key in the walked shape (the `rls.priority` precedent). Its prior `live` verdict is the #12516 class twice over: the objectui bridge it cited never existed (lit control — two sibling objectui citations in the same file resolve), and the entry carried no `verifiedAt`, so nothing ever re-asked | -| view | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); **form.data was that sweep's one CORRECTION** — the removal attempt broke the build (`defineForm` writes `data.provider='schema'` onto every metadata form, `metadata-protocol` serves it), so it stands `live` with re-verified evidence; form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification. **The dead set is six, not the four removals above**: #4534 (the last #4001 batch, batch 6e) declared three CONTAINER-level keys this row had never classified — `name` and `label`, both `dead`, and `object`, `live`. All three are properties of the `views: [...]` *container*, not of a view: `name` is dead as a BODY key because the live one is the `sys_metadata` row column the door supplies, and `label` is container display metadata with no reader. Neither is `authorWarn`'d and both are deliberately KEPT — the platform's own writers send `name` (artifact-shipped containers, the metadata-validation sweep), so tombstoning it would reject shapes we write ourselves. `object` is the container's object binding, and it was *stripped on every parse* until #4534 declared it. Separately, the level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) is noted on parents and is **not** in the counts — one drill level only **#9340**: `list.map` declared — the eighth visualization block (`ListMapConfigSchema`), keys mirroring objectui plugin-map's documented read set. FLIPPED `planned` → `live` 2026-08-24 (#11442): objectui#5908 landed `resolveListMapConfig`, which merges the view-level `map` block over the legacy `options.map` bag before `ListView.tsx`'s `case 'map'` forwards it into `ObjectMap`, with the same merged config also feeding the visualization-switcher's capability gate so a view binding coordinates only in the spec block is no longer filtered out of `allowedVisualizations` either (objectui#5042) | +| view | list/form drilled via `children` (#2998 Track B); list.{responsive,performance} + form.{defaultSort,aria} REMOVED 2026-07-30 (#3896 close-out sweep — list aria/data stay live); **form.data was that sweep's one CORRECTION** — the removal attempt broke the build (`defineForm` writes `data.provider='schema'` onto every metadata form, `metadata-protocol` serves it), so it stands `live` with re-verified evidence; form.{buttons,defaults} live (framework#1894 / #2998); audit-era DEAD lines superseded by re-verification. **The dead set is six, not the four removals above**: #4534 (the last #4001 batch, batch 6e) declared three CONTAINER-level keys this row had never classified — `name` and `label`, both `dead`, and `object`, `live`. All three are properties of the `views: [...]` *container*, not of a view: `name` is dead because authoring it changes nothing — an authored value restates the key the container already registers under or contradicts it — and `label` is container display metadata with no reader. Neither is `authorWarn`'d and both are deliberately KEPT — the metadata door itself stamps the save name into every saved view body (`normalizeViewMetadata`) and its overlay paths key on that copy, so tombstoning `name` would reject the platform's own saves (re-measured 2026-10-02, #20301 stage 2: the earlier attribution to artifact-shipped containers and the metadata-validation sweep was the door's stamp misread; neither carries one). `object` is the container's object binding, and it was *stripped on every parse* until #4534 declared it. Separately, the level-2 dead residue (userActions.buttons, addRecord.mode/formView, tabs[].order) is noted on parents and is **not** in the counts — one drill level only **#9340**: `list.map` declared — the eighth visualization block (`ListMapConfigSchema`), keys mirroring objectui plugin-map's documented read set. FLIPPED `planned` → `live` 2026-08-24 (#11442): objectui#5908 landed `resolveListMapConfig`, which merges the view-level `map` block over the legacy `options.map` bag before `ListView.tsx`'s `case 'map'` forwards it into `ObjectMap`, with the same merged config also feeding the visualization-switcher's capability gate so a view binding coordinates only in the spec block is no longer filtered out of `allowedVisualizations` either (objectui#5042) | | report | 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) — live on non-joined reports only: a `joined` report's container `chart` is refused and `blocks[].chart` was removed (#20161, 2026-09-27; nothing drew either) | | dashboard | 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) **#4956**: `widgets` DRILLED — the row jumps 20 → 41 classified because all 22 widget-level keys enter the count at once. They had never been classified at all: the entry carried one blanket `live` plus a `note` asserting they were classified "in the DashboardWidgetSchema subtree", and no such subtree existed in any of the 28 ledger files. That gap, not any evidence, is what carried `widgets[].responsive` through the #3896 sweep that removed both its sibling `widgets[].performance` and its literal namesake `view.responsive` — `view` is drilled, so `list.responsive` got asked and went out. New dead 6 = `responsive` (retired #4876/#4995, tombstone keeps the row) + `colorVariant` + `actionUrl`/`actionType`/`actionIcon` + `aria`. The action trio is the sharpest: no renderer draws a per-widget action button at all (every `actionUrl` read in DashboardRenderer is scoped to `header.actions[]`), yet `validate-dashboard-action-refs.ts` enforces reference integrity on it and its docblock calls it "the per-widget button" — a lint guarding an affordance that does not exist. `requiresService` is the counter-example worth remembering: dead by every objectui measurement, and LIVE server-side (`filterDashboardForUser`, ADR-0057 D10) — judging a widget key from the renderer repo alone would have retired an enforced gate. `compareTo` is `live` on ONE path only (inline object-provider charts); on the ADR-0021 dataset path the string arms are dropped and `{ offset }` throws in the executor. **#6774** moves the row 33/8 → 34/7: `colorVariant` CORRECTED dead → live 2026-08-09, the enforce leg of #5010 ruling B landing from the renderer side (objectui#3359 / PR objectui#3799, absorbed by pin `09987b68`). Worth reading beside `requiresService` above, because it is the same lesson from the other end — that row warns against judging a widget key from the renderer repo alone, and this one is a `dead` verdict that was correct in this repo AND correct in the renderer repo on the day it was measured, and stopped being either when a cross-repo decision was implemented. A ledger row is a claim with a timestamp; `verifiedAt` is what makes the claim re-askable. It also empties the dashboard warn set, so the author-side lint now says nothing about any widget key — `dashboard` stays in the lint's TYPE_COLLECTIONS all the same (the `webhook`/`email_template` resolved state). **#17385** DRILLS `widgets.chartConfig` — 14 per-key verdicts where the row had carried one blanket `live`, re-measured against `.objectui-sha` pin `53ded82bf7a4`: 12 live (the nine chrome keys `chartConfigPresentation` lowers, plus `xAxis`/`yAxis`/`series`, whose PRESENTATION merges onto the derived bindings while `ChartAxis.field` and `ChartSeries.name` are dropped so membership stays with the dataset) and dead 2 — `type`, which parses and does nothing because the widget's own `type` owns the chart family, and `aria`, which has no reader on either face. Both are pinned as NEGATIVES in objectui, which is what makes them re-askable rather than merely asserted. ⚠️ The drill made SIX containers one level further down visible for the first time (`xAxis`/`yAxis`/`series`/`annotations`/`interaction`/`aria`, 39 child keys); they are RECORDED, not drilled — fanning this row's verdicts down over them would manufacture verdicts, and the evidence work is a separate measurement. Note the cell's previous last stated position (`34/7`) had already drifted one `dead` behind the generated artifact before this change; the counts columns are generated and are the authority | | query | **not a metadata type** — the REQUEST surface (`QuerySchema`: client SDK QueryBuilder output; the `POST /data/:object/query` body), governed via `SPEC_ONLY_SCHEMAS` (#4286). The gate resolves 1 experimental at the depth this ledger drills; the 7 marker-experimental search affordances sit one level deeper, below what this ledger declares (the walk recurses since #17424, but only where a `children` map is written, and none is written here) — resolved from `[EXPERIMENTAL — not enforced]` describe markers, not ledger entries (search `fuzzy`/`operator`/`boost`/`minScore`/`language`/`highlight` + `aggregations[].filter` — declared engine affordances no executor receives). The #4286 sweep closed out same-release: `having` ENFORCED 2026-07-31 (engine-side post-aggregation filter, both paths; was finding 1); dead 4 = the tombstoned removals `joins`/`windowFunctions`/`cursor`/`distinct` — REMOVED 2026-07-31 (retiredKey keeps each in the walked shape so the rows stay; protocol-17 semantic migrations; the JoinNode + WindowFunctionNode clusters and the `QueryBuilder.cursor()`/`.distinct()` producers deleted with their keys; `distinct`'s mis-wired REST count suppression deleted too — finding 2). **#6815** adds the 5th dead: `aggregations[].distinct` REMOVED 2026-08-09 (live → dead, `-1` live). It is the one member of this ledger the #4286 sweep could not have caught with the question it asked — that sweep looked for keys NO executor reads, and this one had a reader: the objectql in-memory fallback deduplicated before applying the function while all five other faces (driver-sql, driver-turso, driver-mongodb, driver-memory, service-analytics' `AGGREGATE_SQL`) ignored it, so one query answered two plausible NUMBERS depending on which backend served it. The lesson for the next audit is the question, not the key: a per-key `live` verdict is only as good as the count of faces it was measured across, and this row's 2026-07-31 evidence (`in-memory-aggregation.ts:167,204-206`) was TRUE and still the wrong verdict. `count_distinct` is the surviving spelling (enforce leg, #6409) | diff --git a/packages/spec/liveness/view.json b/packages/spec/liveness/view.json index 8ef422da524..02fa2030d16 100644 --- a/packages/spec/liveness/view.json +++ b/packages/spec/liveness/view.json @@ -5,8 +5,8 @@ "name": { "status": "dead", "evidenceScope": "cross-repo", - "verifiedAt": "2026-08-10", - "note": "Container item identity, declared in #4001 batch 6e and dead as a BODY key — the honest reading of a copy nobody reads. The live one is the sys_metadata row column: the door supplies it (saveMetaItem({type:'view', name})) and readers that resolve a view by name use the row, or a ViewItem's own name (rest-server.ts:5422 reads view.name on a ViewItem, not on a container). Declared anyway, and deliberately not authorWarn'd, because the platform's own writers send it — artifact-shipped containers (service-ai/ai_traces) and the metadata-validation sweep both do, so tombstoning it rejected shapes we write ourselves. Exactly the correction translation/name needed in batch 5. VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) under the previews ruling (#7131): `translation.name` re-graded on that ruling precisely because its preview read the BODY copy as a display fallback, so this twin was measured for the same shape and does not have it. At objectui @e9ab52f9 ViewPreview reads the item name from its `name` PROP (ViewPreview.tsx:110, `String(name) || 'default'`) — the saved identity the registry passes in, i.e. the sys_metadata row column this note already calls the live one — and never touches `draft.name`. The body copy stays unread, which is what `dead` claims here." + "verifiedAt": "2026-10-02", + "note": "Container item identity, declared in #4001 batch 6e. `dead` because AUTHORING it changes nothing: an authored body `name` either restates the key the container is already registered under (the sys_metadata row name at the metadata door, the derived object key at the two source registrars) or contradicts it, so `drop it` is the right advice. RE-MEASURED 2026-10-02 (#20301 stage 2), which CORRECTS this note's old reason: the body copy is not unread. The metadata door WRITES it: `saveMetaItem` runs `normalizeViewMetadata` (packages/metadata-protocol/src/protocol.ts) ahead of the schema gate, which stamps the save name onto every view body that has none, containers included (pinned by view-container-runtime-expansion.test.ts: the stored row is the container plus the `name` the write door has always stamped). The door's overlay paths then KEY on that stamped copy: `hydrateOverlayIntoRegistry` registers (and expands) no body without a `name`, and `mergePackageAwareOverlay` slots an overlay row by its body `name`. The ObjectQL boot loop (`registerMetadataCollections`) likewise mints the derived key onto every stack container it registers. That is the platform's own identity copy, not an authored value. A CONTRADICTING authored value is refused at both source registrars under the 2026-09-03 ruling (PR #15319: objectql `viewContainerNameRefusal` and the artifact/HMR door's register contract) and is accepted at the runtime save door (#21412). The key is KEPT deliberately and is exempt from enforce-or-remove, for three recorded reasons. First, the door's own writes stamp it, so a `retiredKey` tombstone would refuse the platform's own saves. Second, the 2026-09-03 ruling refused direction 3, forbidding `ViewSchema.name` on containers in spec. Third, triage's guard on #20301 forbids retiring a key the platform's own writer still sends. Who does NOT author it, measured the same day: no example or production container (0 of 10 in examples/ and platform-objects, 0 of 27 in packages/ sources; the control is that 24 of those 27 carry `object`); cloud's service-ai view artifacts (the repo:cloud reading on #20301); objectui (no writer of its own at the pin 89cad75d55); and the metadata-validation sweep, whose view fixture is nameless. This note's former claim that artifact-shipped containers and that sweep send it was the door's stamp misattributed. objectui side upheld at the pin 89cad75d55: ViewPreview reads the item name from its `name` PROP (`String(name) || 'default'`), never `draft.name`. History: VERDICT RE-TESTED AND UPHELD 2026-08-10 (#7427) under the previews ruling (#7131)." }, "label": { "status": "dead", @@ -211,7 +211,7 @@ "status": "dead", "verifiedAt": "2026-09-27", "evidenceScope": "cross-repo", - "note": "REMOVED 2026-09-27 (#20301, ADR-0049 enforce-or-remove; triage verdict RETIRE under the maintainer's #18900 criterion — mainstream named-view switching is already delivered here, by `listViews` rendered in ViewTabBar) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error at every list-view door built from the shape: ListViewSchema, ObjectListViewSchema and the flattened overlay arm) and stripped from stored rows and sources by the protocol-18 view-list-tabs-removed conversion. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); to give end users one tab per preset, declare each tab as a named entry under the object's `listViews` — every entry renders as a tab in the saved-view switcher. RE-MEASURED 2026-09-27 before removal, with lit controls: objectui at the pinned sha f8a9d0fb — every `( * the page has no tab bar or nothing resolved, so `translatePage` leaves the * key off the copy entirely. * - * **This is where `ViewTabSchema` is actually rendered.** The schema has two - * carriers — `UserFiltersSchema.tabs` (page-only preset bar, ADR-0047) and - * `ListViewSchema.tabs` ("multi-tab view interface") — and only the first has a - * renderer: objectui's `TabFilters` draws it from a page's `interfaceConfig`, - * while nothing in either repo reads the ListView carrier. Translating the - * carrier nothing draws would declare a capability no user can see, so this - * covers the live one and stops there. + * **This is where `ViewTabSchema` is actually rendered.** Its one carrier is + * `UserFiltersSchema.tabs` (page-only preset bar, ADR-0047), which objectui's + * `TabFilters` draws from a page's `interfaceConfig`. The list view's own + * `tabs` is a `retiredKey` tombstone (@objectstack/spec 17.5.0): nothing ever + * drew it, and a named list-view preset is a `listViews` entry, which the + * saved-view switcher renders as a tab. So there is no second carrier to + * translate. * * The object comes from `interfaceConfig.source` — the page's own binding for * the records these presets filter — falling back to the page-level `object`. diff --git a/packages/spec/src/ui/view-list-tabs-retirement.test.ts b/packages/spec/src/ui/view-list-tabs-retirement.test.ts index b325384bd60..a2507b20a5e 100644 --- a/packages/spec/src/ui/view-list-tabs-retirement.test.ts +++ b/packages/spec/src/ui/view-list-tabs-retirement.test.ts @@ -339,19 +339,16 @@ describe('tree-scoped absence: no list-view payload inside the declared radius s 'packages/spec/liveness/', ]; /** - * RESIDUE, declared and self-expiring — not exempted. Test fixtures of the - * two author-time reference walks that still read a list view's `tabs` off - * RAW input (`packages/lint/src/validate-list-view-field-refs.ts#checkTabs`, - * and `computeViewReferenceDiagnostics` in `@objectstack/metadata-protocol`) and - * the CLI's negative i18n pin. Removing those walks is outside this - * retirement's file surface and is reported as its follow-up; each entry here - * is asserted to STILL hold an offender, so the day the follow-up deletes a - * fixture this set goes red and the entry leaves with it. + * RESIDUE, declared and self-expiring — not exempted: the CLI's negative + * i18n pin. Each entry here is asserted to STILL hold an offender, so the day + * its fixture stops authoring the key this set goes red and the entry leaves + * with it. The two author-time reference walks that once read a list view's + * own `tabs` (the lint list-view field-ref rule and + * `computeViewReferenceDiagnostics` in `@objectstack/metadata-protocol`) were + * deleted as unreachable, and their fixtures and entries left with them. */ const RESIDUE = new Set([ 'packages/cli/test/i18n-tab-coverage.test.ts', - 'packages/lint/src/validate-list-view-field-refs.test.ts', - 'packages/objectql/src/metadata-diagnostics.test.ts', ]); /** tsup's own bundle of `tsup.config.ts`, written and deleted mid-build. */ const TSUP_BUNDLED_CONFIG = /\.bundled_[^./]+\.mjs$/;