diff --git a/packages/data-objectstack/src/filter-operator-ast-parity.test.ts b/packages/data-objectstack/src/filter-operator-ast-parity.test.ts index e0c8441853..805769021f 100644 --- a/packages/data-objectstack/src/filter-operator-ast-parity.test.ts +++ b/packages/data-objectstack/src/filter-operator-ast-parity.test.ts @@ -30,6 +30,10 @@ import { FILTER_OPERATOR_ALIASES } from './index'; /** * View operators this adapter is not the bridge for — the value-shape ones the * view layer resolves to a null comparison before an operator is ever emitted. + * + * Every token here must still be a member of `VIEW_FILTER_OPERATORS` — the + * ratchet below enforces it. Subtracting a name the spec has retired excuses + * nothing and must be deleted rather than left as a dead subtraction (#3628). */ const NOT_THIS_ADAPTERS_JOB = new Set(['is_empty', 'is_not_empty']); @@ -39,6 +43,33 @@ describe('FILTER_OPERATOR_ALIASES lands inside the spec AST vocabulary', () => { expect(VALID_AST_OPERATORS.size).toBeGreaterThan(0); }); + // The exclusion ratchet (#3628). The coverage sweep further down subtracts a + // hand-written set from a spec-derived vocabulary, and that subtraction only + // excuses something while the spec still lists the subtracted tokens. Once + // upstream retires or renames one, the sweep stays green (it is still total + // over what remains) but the row becomes dead weight, and its comment goes on + // telling the next reader that the view layer resolves this one to a null + // comparison — about an operator no author can declare any more. That is the + // shape that rotted 37 of 82 deny-list entries in #3601 with nothing to report + // it: a hand-written list beside a spec-derived vocabulary and no assertion + // that its members still exist in that vocabulary. + // + // Collected rather than asserted per entry on purpose (same call as PR #3623): + // vocabulary retirements land as whole families, and failing on the first entry + // would hide the rest. + it('every NOT_THIS_ADAPTERS_JOB token is still in the spec view vocabulary', () => { + const vocabulary = new Set(VIEW_FILTER_OPERATORS); + const retired = [...NOT_THIS_ADAPTERS_JOB].filter((op) => !vocabulary.has(op)); + expect( + retired, + `VIEW_FILTER_OPERATORS no longer lists these NOT_THIS_ADAPTERS_JOB tokens: ` + + `${retired.join(', ')}. The spec has retired them, so subtracting them from ` + + 'the coverage sweep below excuses nothing — delete each from the set (with ' + + 'the comment claiming the view layer resolves it) rather than leaving a dead ' + + 'subtraction', + ).toEqual([]); + }); + it('every alias target is an operator the AST gate accepts', () => { const bad = Object.entries(FILTER_OPERATOR_ALIASES) .filter(([, target]) => !VALID_AST_OPERATORS.has(String(target).toLowerCase())) diff --git a/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts b/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts index 366f30510b..be4d4d982d 100644 --- a/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts +++ b/packages/plugin-list/src/__tests__/filter-operator-ast-parity.test.ts @@ -31,7 +31,13 @@ import { VALID_AST_OPERATORS, isFilterAST } from '@objectstack/spec/data'; import { VIEW_FILTER_OPERATORS, VIEW_FILTER_OPERATOR_ALIASES } from '@objectstack/spec/ui'; import { mapOperator, normalizeFilterCondition } from '../ListView'; -/** Operators this bridge deliberately resolves without reaching the AST gate. */ +/** + * Operators this bridge deliberately resolves without reaching the AST gate. + * + * Every token here must still be a member of `VIEW_FILTER_OPERATORS` — the + * ratchet below enforces it. Subtracting a name the spec has retired excuses + * nothing and must be deleted rather than left as a dead subtraction (#3628). + */ const HANDLED_BEFORE_MAPPING = new Set([ // convertFilterGroupToAST rewrites these to `[field, '=' | '!=', null]` // before mapOperator is consulted, so they never need an AST spelling. @@ -45,6 +51,31 @@ describe('mapOperator bridges the spec view vocabulary onto the AST vocabulary', expect(VALID_AST_OPERATORS.size).toBeGreaterThan(0); }); + // The exclusion ratchet (#3628). The sweep below subtracts a hand-written set + // from a spec-derived vocabulary, and that subtraction only excuses something + // while the spec still lists the subtracted tokens. Once upstream retires or + // renames one, the sweep stays green (it is still total over what remains) but + // the row becomes dead weight, and its comment goes on telling the next reader + // that "the view layer rewrites this first" about an operator no author can + // declare any more. That is the shape that rotted 37 of 82 deny-list entries in + // #3601 with nothing to report it — a hand-written list beside a spec-derived + // vocabulary and no assertion that its members still exist in that vocabulary. + // + // Collected rather than asserted per entry on purpose (same call as PR #3623): + // vocabulary retirements land as whole families, and failing on the first entry + // would hide the rest. + it('every HANDLED_BEFORE_MAPPING token is still in the spec view vocabulary', () => { + const vocabulary = new Set(VIEW_FILTER_OPERATORS); + const retired = [...HANDLED_BEFORE_MAPPING].filter((op) => !vocabulary.has(op)); + expect( + retired, + `VIEW_FILTER_OPERATORS no longer lists these HANDLED_BEFORE_MAPPING tokens: ` + + `${retired.join(', ')}. The spec has retired them, so subtracting them from ` + + 'the sweep below excuses nothing — delete each from the set (with the comment ' + + 'claiming the view layer rewrites it) rather than leaving a dead subtraction', + ).toEqual([]); + }); + const bridged = VIEW_FILTER_OPERATORS.filter((op) => !HANDLED_BEFORE_MAPPING.has(op)); it.each(bridged)('%s maps to an AST-valid operator', (viewOp) => {