Skip to content

Commit 56bd756

Browse files
committed
fix(spec,lint,metadata-protocol): correct the view container name ledger note; delete the unreachable list-view tabs walks
The liveness row for the view container's body `name` stays `dead`, but its note claimed "a copy nobody reads". Measured, the metadata door stamps the save name into every saved view body (`normalizeViewMetadata`) and its overlay paths key on that copy (`hydrateOverlayIntoRegistry`, `mergePackageAwareOverlay`). The note and the ledger README now say so, and record why the key is kept rather than retired. The list view's own `tabs` is a tombstone on every list-view shape, so the two author-time walks that still read it (the lint list-view field-ref rule, `computeViewReferenceDiagnostics`) could never see it: the lint judges the parsed stack, and every view door refuses or strips the key. Both reads, and their fixtures, are deleted; each keeps its `userFilters.tabs` walk. The stale i18n-resolver comment that still called the list carrier live is corrected. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6d67ad5 commit 56bd756

8 files changed

Lines changed: 53 additions & 59 deletions

File tree

‎packages/lint/src/validate-list-view-field-refs.test.ts‎

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ const FULL_LIST_VIEW: AnyRec = {
8383
fields: [{ field: 'status' }],
8484
tabs: [{ name: 'open', filter: [{ field: 'status', operator: 'equals', value: 'open' }] }],
8585
},
86-
tabs: [{ name: 'mine', filter: [{ field: 'business_unit', operator: 'equals', value: 'x' }] }],
8786
kanban: { groupByField: 'status', summarizeField: 'estimate', columns: ['title'], titleField: 'title' },
8887
calendar: {
8988
startDateField: 'due_at',
@@ -241,23 +240,22 @@ function positionAsserted(path: string, walked: ReadonlySet<string>): string | u
241240
}
242241

243242
/**
244-
* [#18836] The rule's three hard-coded filter walks, spelled as
243+
* [#18836] The rule's two hard-coded filter walks, spelled as
245244
* {@link casePath} normalises the paths they report at.
246245
*
247246
* They are open code, not table rows — `checkListView` calls `checkFilter` on
248-
* `listView.filter`, on `tabs[]` and on `userFilters.tabs[]` — so the position
247+
* `listView.filter` and on `userFilters.tabs[]` — so the position
249248
* set derived from the rule cannot reach them, and the completeness assertion
250249
* below would let their rows be deleted in silence. That is precisely the hole
251250
* the floor this card replaced DID cover, by counting rows.
252251
*
253252
* So they are declared here and asserted EXACTLY: delete one of their rows and
254-
* the list comes up short; give a fourth hard-coded walk a row without adding
253+
* the list comes up short; give a third hard-coded walk a row without adding
255254
* it here and the list comes up long. Together with the derived assertion, every
256255
* row in both tables is then accounted for by one criterion or the other.
257256
*/
258257
const HARD_CODED_FILTER_WALKS = [
259258
'filter.field',
260-
'tabs.filter.field',
261259
'userFilters.tabs.filter.field',
262260
];
263261

@@ -292,11 +290,6 @@ describe('#14107 — every other walked position', () => {
292290
'views[0].list.userFilters.tabs[0].filter[0].field',
293291
'error',
294292
],
295-
[
296-
{ tabs: [{ name: 'a', filter: [{ field: BAD, operator: 'equals', value: 1 }] }] },
297-
'views[0].list.tabs[0].filter[0].field',
298-
'error',
299-
],
300293
[{ kanban: { summarizeField: BAD } }, 'views[0].list.kanban.summarizeField', 'warning'],
301294
[{ kanban: { columns: [BAD] } }, 'views[0].list.kanban.columns[0]', 'warning'],
302295
// [#18565] Calendar's level, not the required siblings' — see the row's
@@ -426,15 +419,15 @@ describe('#14107 — every other walked position', () => {
426419
// filter walks — nothing else. A set, compared in both directions, which
427420
// holds three things the completeness assertion does not:
428421
//
429-
// - SHORT ⇒ RED. Delete a filter-walk row and the set loses a member. All
430-
// three rows measured, one by one. That is exactly the coverage the
422+
// - SHORT ⇒ RED. Delete a filter-walk row and the set loses a member. Every
423+
// row measured, one by one. That is exactly the coverage the
431424
// row-counting floor had and the derived assertion cannot reach.
432-
// - LONG ⇒ RED, measured two ways. Remove one of the three declarations
425+
// - LONG ⇒ RED, measured two ways. Remove one of the declarations
433426
// below and the rows outnumber them. Leave a row behind for a position the
434427
// rule has DROPPED and it matches neither side, so it arrives here as an
435428
// extra — red here as well as in that row's own per-case assertion, which
436429
// is how this assertion ends up carrying the direction its sibling above
437-
// cannot. A fourth hard-coded walk given a row without being declared
430+
// cannot. A third hard-coded walk given a row without being declared
438431
// below lands in the same place by the same comparison.
439432
it('accounts for every asserted path, as a walked position or a declared filter walk', () => {
440433
const walkedSet = new Set(listViewWalkedPositions());
@@ -707,11 +700,10 @@ describe('#14282 — a dotted key the FILTER door refuses, and the ones it serve
707700
expect(validateListViewFieldRefs(stackWith(mutate(filterOn('id.x'))))).toEqual([]);
708701
});
709702

710-
it('the tab and user-filter tab presets are judged on the same axis', () => {
703+
it('the user-filter tab presets are judged on the same axis', () => {
711704
const findings = validateListViewFieldRefs(
712705
stackWith(
713706
mutate({
714-
tabs: [{ name: 'mine', filter: [{ field: 'owner.name', operator: 'equals', value: 'x' }] }],
715707
userFilters: {
716708
fields: [{ field: 'status' }],
717709
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
720712
),
721713
);
722714
expect(idsOf(findings).sort()).toEqual([
723-
'views[0].list.tabs[0].filter[0].field',
724715
'views[0].list.userFilters.tabs[0].filter[0].field',
725716
]);
726717
expect(findings.every((f) => f.rule === LIST_VIEW_FIELD_DOTTED)).toBe(true);

‎packages/lint/src/validate-list-view-field-refs.ts‎

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@
134134
* `INVALID_FIELD` / 400 (`packages/objectql/src/engine.ts`, #7589), and
135135
* `assertProjectionFieldsExist` answers the same at the REST ingress
136136
* (#7532). So every dotted column is reported, whatever its head's type.
137-
* - **Filter** — the view's own `filter`, its `tabs[].filter`, its
138-
* `userFilters.tabs[].filter`, and the two positions that DECLARE which
139-
* names the end user may filter on (`filterableFields`, spelled by the
137+
* - **Filter** — the view's own `filter`, its `userFilters.tabs[].filter`,
138+
* and the two positions that DECLARE which names the end user may
139+
* filter on (`filterableFields`, spelled by the
140140
* spec as "bare field names enabled for end-user filtering", and
141141
* `userFilters.fields`; objectui folds the resulting conditions into the
142142
* fetched query through `buildEffectiveFilter`). Here the door does NOT
@@ -201,9 +201,10 @@
201201
* owned by `validateActionNameRefs`.
202202
* - **`conditionalFormatting[].condition`** — a CEL predicate, owned by the
203203
* expression rules.
204-
* - **`tabs[].view` / `addRecord.formView`** — view names, owned by
205-
* `lintViewRefs`. (`pageName` was here too until #17063 retired the
206-
* `type: 'page'` view mount; a list view carries no page reference now.)
204+
* - **`addRecord.formView`** — a view name, owned by `lintViewRefs`.
205+
* (`pageName` was here too until #17063 retired the `type: 'page'` view
206+
* mount, and `tabs[].view` until the list view's own `tabs` was retired; a
207+
* list view carries neither reference now.)
207208
* - **The `data.object` binding itself** — `validateObjectReferences` owns
208209
* object-name reference sites, with the curated cross-package severity
209210
* 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
520521
* It names positions only — no severity, no shape — so reading it can never
521522
* stand in for reading the tables.
522523
*
523-
* The three filter walks further down (`listView.filter`, `tabs[].filter`,
524+
* The two filter walks further down (`listView.filter`,
524525
* `userFilters.tabs[].filter`) are deliberately absent: they are open code, not
525526
* table rows, so there is nothing HERE to derive them from. ⛔ Absent from this
526527
* list is not absent from the test's account of itself — the test declares those
527-
* three as an explicit list and asserts it exactly, because a row of theirs
528+
* two as an explicit list and asserts it exactly, because a row of theirs
528529
* deleted in silence is the one thing the row-counting floor this replaced did
529530
* cover.
530531
*/
@@ -768,12 +769,16 @@ export function validateListViewFieldRefs(stack: AnyRec): ListViewFieldRefFindin
768769
}
769770
}
770771

771-
// ── Filter KEYS: the view's own filter, its tabs' filters, and the tab
772-
// presets inside `userFilters`. `walkFilterFieldKeys` handles all three
773-
// authored filter shapes (Mongo condition object, `{ field, operator,
774-
// value }` rules, `[field, op, value]` triples) so a filter authored one
775-
// way is not judged while another is silently skipped (#3574's own
776-
// failure mode).
772+
// ── Filter KEYS: the view's own filter and the tab presets inside
773+
// `userFilters`. `walkFilterFieldKeys` handles all three authored filter
774+
// shapes (Mongo condition object, `{ field, operator, value }` rules,
775+
// `[field, op, value]` triples) so a filter authored one way is not judged
776+
// while another is silently skipped (#3574's own failure mode).
777+
//
778+
// The list view's OWN `tabs` is not walked: it is a `retiredKey` tombstone
779+
// on every list-view shape, and this rule judges the PARSED stack
780+
// (`input: 'parsed'` in the authoring-rule registry), where the key can
781+
// never arrive — the parse refuses it with its prescription first.
777782
const checkFilter = (filter: unknown, filterWhere: string, filterPath: string): void => {
778783
if (filter === undefined || filter === null) return;
779784
walkFilterFieldKeys(filter, filterPath, ({ field, path: at }) => {
@@ -792,7 +797,6 @@ export function validateListViewFieldRefs(stack: AnyRec): ListViewFieldRefFindin
792797
});
793798
};
794799

795-
checkTabs(listView.tabs, `${where} › tabs`, `${path}.tabs`);
796800
if (isRec(listView.userFilters)) {
797801
checkTabs(
798802
listView.userFilters.tabs,

‎packages/metadata-protocol/src/metadata-diagnostics.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,11 @@ export function computeViewReferenceDiagnostics(
228228
userFilters?.tabs?.forEach((t, i) =>
229229
t?.filter?.forEach((r, j) => requireField(r?.field, `userFilters.tabs.${i}.filter.${j}.field`)));
230230

231-
(view?.tabs as Array<{ filter?: Array<{ field?: string }> }> | undefined)?.forEach((t, i) =>
232-
t?.filter?.forEach((r, j) => requireField(r?.field, `tabs.${i}.filter.${j}.field`)));
231+
// A list view's OWN `tabs` is not walked: it is a `retiredKey` tombstone on
232+
// every list-view shape. The write door refuses it, and a stored or
233+
// artifact-shipped body has it stripped by the conversion replay before it
234+
// is served. A body that still carries it is already badged by the spec
235+
// diagnostics, with the tombstone's prescription.
233236

234237
(view?.filterableFields as string[] | undefined)?.forEach((f, i) =>
235238
requireField(f, `filterableFields.${i}`));

‎packages/objectql/src/metadata-diagnostics.test.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ describe('computeViewReferenceDiagnostics (ADR-0047)', () => {
3030
fields: [{ field: 'industry' }, { field: 'is_active' }],
3131
tabs: [{ name: 't', filter: [{ field: 'status', operator: 'equals', value: 'x' }] }],
3232
},
33-
tabs: [{ name: 'a', filter: [{ field: 'industry', operator: 'equals', value: 'technology' }] }],
3433
filterableFields: ['status'],
3534
kanban: { groupByField: 'status', columns: ['name'] },
3635
}, objectDef);
@@ -48,12 +47,12 @@ describe('computeViewReferenceDiagnostics (ADR-0047)', () => {
4847
});
4948
});
5049

51-
it('flags tab filter rules pointing at unknown fields', () => {
50+
it('flags user-filter tab preset rules pointing at unknown fields', () => {
5251
const result = computeViewReferenceDiagnostics({
53-
tabs: [{ name: 'bad', filter: [{ field: 'ghost', operator: 'equals', value: 1 }] }],
52+
userFilters: { element: 'tabs', tabs: [{ name: 'bad', filter: [{ field: 'ghost', operator: 'equals', value: 1 }] }] },
5453
}, objectDef);
5554
expect(result.valid).toBe(false);
56-
expect(result.errors?.[0].path).toBe('tabs.0.filter.0.field');
55+
expect(result.errors?.[0].path).toBe('userFilters.tabs.0.filter.0.field');
5756
});
5857

5958
it('flags kanban groupBy on a non-select-like field', () => {

0 commit comments

Comments
 (0)