diff --git a/.changeset/plugin-detail-8937-parent-scope-residue.md b/.changeset/plugin-detail-8937-parent-scope-residue.md new file mode 100644 index 0000000000..2ab4898fb1 --- /dev/null +++ b/.changeset/plugin-detail-8937-parent-scope-residue.md @@ -0,0 +1,28 @@ +--- +'@object-ui/plugin-detail': patch +'@object-ui/core': patch +--- + +docs(parent-scope): state the driver's real arity rule, and the arity-dependent parent scope in the shipped README (objectui#8937) + +Two published texts that objectui#8886 left behind, both measured false on `origin/main`: + +- **`packages/plugin-detail/README.md` (it is in `files[]`, so it ships).** It said the + node's `filter` is AND-combined with `{ [relationshipField]: parentId }`, full stop. + Since objectui#7299 the parent condition is compiled to match the relationship field's + arity, so a multi-valued relationship gets + `{ [relationshipField]: { $contains: parentId } }` instead. The paragraph now states + both spellings and names the arbiter (`@objectstack/spec/data`'s `isMultiValueField`). +- **The claim that the SQL driver decides arity on that same predicate.** It does not: + `driver-sql` gates the equality family on its own storage question, which reads + `multiple` as truthy on ANY type. The two rules therefore disagree for a type outside + `MULTI_CAPABLE_TYPES` carrying `multiple: true`. objectui#9184 moved the arity compiler + into `@object-ui/core`'s `parent-scope` seam and carried the claim with it, so the + correction is recorded there — the seam now states the driver's measured rule, records + the divergence as a divergence, and points at the upstream card that owns which of the + two rules is right (objectstack#17469). `RelatedList.tsx`'s pointer comment and the + objectui#7299 test header carried the same sentence and are corrected to match. + +No predicate moved and no wire changed — this release carries corrected published text +only. All three claims are pinned by re-derivation rather than transcription, in +`relatedListParentScopeResidue-8937.test.ts`. diff --git a/packages/core/src/utils/parent-scope.ts b/packages/core/src/utils/parent-scope.ts index 5ecc618c42..ce8bb586d4 100644 --- a/packages/core/src/utils/parent-scope.ts +++ b/packages/core/src/utils/parent-scope.ts @@ -23,11 +23,11 @@ * * ## The rule itself is not invented here * - * The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`, the - * same predicate the driver that executes the query decides on. That matters - * more here than anywhere: this function chooses `$contains` vs `=`, the driver - * chooses whether to accept it, and two readers of one question disagreeing is - * the entire defect class. The spec's rule is BROADER than an eyeballed + * The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`. That + * matters more here than anywhere: this function chooses `$contains` vs `=` and + * every surface that asks the question now reads that one choice, so two + * readers of one question disagreeing — the entire defect class — cannot + * recur INSIDE this repository. The spec's rule is BROADER than an eyeballed * `multiple === true` in both directions — `multiselect` / `checkboxes` / * `tags` persist an array with no flag at all, and `multiple: true` is INERT on * a type outside the spec's multi-capable set (`master_detail`, say) — so a @@ -36,6 +36,36 @@ * ⛔ Do not add a local arity rule at any call site, however small, and ⛔ do * not widen this function to accept an arity the caller computed: the parameter * it takes is METADATA, and the verdict is drawn from it here. + * + * ## ⚠️ The STORAGE side does not read this predicate — the two rules DIVERGE + * + * This header used to say `isMultiValueField` is "the same predicate the driver + * that executes the query decides on". It is not, and the difference is + * observable (objectui#8937). Measured on objectstack `origin/main`, + * `driver-sql` gates the equality family on its own STORAGE question, + * `isJsonField`: a column is JSON when the field's type is in that driver's + * `JSON_COLUMN_TYPES` — the spec's `STRUCTURED_JSON_TYPES` and + * `MULTI_OPTION_TYPES`, plus the driver-internal `object` / `array` aliases — + * OR when `multiple` is merely TRUTHY, on ANY type; a single-value media type + * answers from the ADR-0104 dual-encoding window instead. The spec's predicate + * is `MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) && + * multiple === true)`. + * + * ⇒ They diverge for a type OUTSIDE `MULTI_CAPABLE_TYPES` carrying + * `multiple: true` (`master_detail` / `tree` / `text`): the spec says + * single-valued, so this seam compiles `=`, while the driver stores a JSON + * column and refuses `=` with the same `400 INVALID_FILTER` objectui#7299 was + * filed for. That is a KNOWN divergence and ⛔ not a regression — both + * surfaces sent `=` for that shape before objectui#7299 and objectui#8882 too. + * + * ⛔ Do NOT close it by widening the predicate here: which of the two rules is + * right is an upstream question, filed as objectstack#17469 (enforce-or-align), + * and this seam is deliberately not blocked on it. Widening on this side would + * move the disagreement across the wire rather than end it, and would make the + * ONE compiler this module exists to be disagree with the spec it delegates to. + * The divergence is re-derived from the installed spec on every run by + * `relatedListParentScopeResidue-8937.test.ts`, so this section reddens when the + * spec side moves. */ import { isMultiValueField, type ValueShapeFieldDef } from '@objectstack/spec/data'; diff --git a/packages/plugin-detail/README.md b/packages/plugin-detail/README.md index c0f4bc64c9..a5971eecc2 100644 --- a/packages/plugin-detail/README.md +++ b/packages/plugin-detail/README.md @@ -329,9 +329,16 @@ filter can express). The node's `filter` (spec `RecordRelatedListProps.filter`, "additional filter criteria") narrows the list beyond the parent relationship: it is -**AND-combined** with `{ [relationshipField]: parentId }`, never substituted for +**AND-combined** with the parent-relationship condition, never substituted for it, so a related list stays scoped to the record it appears on and an additional -criterion can only ever narrow that set. Authors write it in the spec's own +criterion can only ever narrow that set. That condition is **not one fixed +spelling** — it is compiled to match the relationship field's ARITY on the +child object (`objectui#7299`). A single-valued relationship gets the equality +form `{ [relationshipField]: parentId }`; a multi-valued one gets the membership +form `{ [relationshipField]: { $contains: parentId } }`, because the stored +value is an array of ids and equality would ask whether the whole array *is* one +id. The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`, +never a rule this package keeps locally. Authors write `filter` in the spec's own vocabulary (`[{ field, operator, value }]`); a `dataSource` binding's composed filter (component AND saved view AND binding) lands on the same key. Both are lowered to ObjectQL through the repo's single filter sink, so no second dialect diff --git a/packages/plugin-detail/src/RelatedList.tsx b/packages/plugin-detail/src/RelatedList.tsx index 497c46e56d..66905f605b 100644 --- a/packages/plugin-detail/src/RelatedList.tsx +++ b/packages/plugin-detail/src/RelatedList.tsx @@ -367,11 +367,19 @@ export const RelatedToolbarButton: React.FC<{ * * ⛔ Do not reintroduce a local arity rule here, however small — the warning * that stood at this spot still stands, and now names one more reader. This - * component decides `$contains` vs `=`, the driver that refuses the query - * decides on `@objectstack/spec/data`'s `isMultiValueField`, and the badge - * decides too; readers of one question disagreeing is the whole defect class. - * Moving the decision to a shared seam is NOT "putting a local rule one layer - * up" — the rule is still the spec's, and there is now exactly one caller of it. + * component decides `$contains` vs `=` and the badge decides too; readers of + * one question disagreeing is the whole defect class. Moving the decision to a + * shared seam is NOT "putting a local rule one layer up" — the rule is still + * the spec's, and there is now exactly one caller of it. + * + * ⚠️ What the seam does NOT buy is agreement with STORAGE. This spot used to + * add that the driver refusing the query decides on that same + * `isMultiValueField`. It does not (objectui#8937): `driver-sql` gates the + * equality family on its own storage question, which reads `multiple` as truthy + * on ANY type, so the two rules diverge for `master_detail` / `tree` / `text` + * carrying `multiple: true`. The measured rule, the divergence and the upstream + * card that owns which of them is right (objectstack#17469) are recorded on the + * seam itself — `@object-ui/core`'s `parent-scope` — so one place answers it. */ export const RelatedList: React.FC = ({ diff --git a/packages/plugin-detail/src/__tests__/RelatedList.multiValueParentScope-7299.test.tsx b/packages/plugin-detail/src/__tests__/RelatedList.multiValueParentScope-7299.test.tsx index c9d48f29ff..eb46b8765f 100644 --- a/packages/plugin-detail/src/__tests__/RelatedList.multiValueParentScope-7299.test.tsx +++ b/packages/plugin-detail/src/__tests__/RelatedList.multiValueParentScope-7299.test.tsx @@ -54,12 +54,21 @@ * * The arity VERDICT is `@objectstack/spec/data`'s exported `isMultiValueField`, * imported rather than re-implemented — the component only finds the field def - * to hand it. That is not hygiene: this component picks `$contains` vs `=` on - * the answer, and the driver that refuses the query picks on the spec's, so a - * local copy would be two readers of one question disagreeing, which is the - * defect this card is about. The three `SPEC PREDICATE` cases below pin the - * places where the spec's rule and an eyeballed `multiple === true` DIFFER, - * measured on `@objectstack/spec` 17.4.0, in both directions. + * to hand it. That is not hygiene: a second copy of the rule inside this + * package would be two readers of one question disagreeing, which is the defect + * this card is about. The three `SPEC PREDICATE` cases below pin the places + * where the spec's rule and an eyeballed `multiple === true` DIFFER, measured + * on `@objectstack/spec` 17.4.0, in both directions. + * + * ⚠️ This header used to add that "the driver that refuses the query picks on + * the spec's". It does NOT (objectui#8937). `driver-sql` gates the equality + * family on its own STORAGE question — a JSON column when the type is one it + * stores as JSON OR when `multiple` is merely TRUTHY, on ANY type — so the two + * rules diverge for a type outside `MULTI_CAPABLE_TYPES` carrying + * `multiple: true`. See the `INERT` case below and `parentRelationshipFieldDef`'s + * docblock in `RelatedList.tsx`; the divergence itself is re-derived each run by + * `relatedListParentScopeResidue-8937.test.ts` and owned upstream by + * objectstack#17469. * * ## The control * @@ -308,12 +317,22 @@ describe('RelatedList — a related list on a MULTI-VALUE relationship returns d }); it('SPEC PREDICATE — `multiple: true` is INERT on a type outside MULTI_CAPABLE_TYPES', async () => { - // The same delegation read the other way. `master_detail` is not a - // multi-capable type, so the flag does not make its stored value an array - // and equality is the correct predicate. An eyeballed `multiple === true` - // would send `$contains` against a scalar column — wrong in the opposite - // direction, and invisible, because it fails as "no rows" rather than as a - // refusal. + // The same delegation read the other way: `master_detail` is not a + // multi-capable type, so the SPEC's rule says the flag does not make the + // stored value an array, and this component sends `=`. + // + // ⚠️ What this case pins is the component's DELEGATION, ⛔ not that `=` is + // the predicate the storage will answer (objectui#8937). `driver-sql` reads + // `multiple` as truthy on ANY type, stores a JSON column, and refuses `=` + // here with the same `400 INVALID_FILTER` this card was filed for. That + // divergence is KNOWN and owned upstream (objectstack#17469); it is ⛔ not a + // regression of this card — the pre-fix renderer sent `=` for this shape + // too — and ⛔ not repairable by widening the predicate on this side, which + // would only move the disagreement rather than end it. + // + // The opposite error is still real and still guarded: an eyeballed + // `multiple === true` would send `$contains` against a genuinely scalar + // column, which fails as "no rows" rather than as a refusal. const ds = makeDS( recordSchema('contact', { account: { type: 'master_detail', reference: 'account', multiple: true } }), SINGLE_ROWS, diff --git a/packages/plugin-detail/src/__tests__/relatedListParentScopeResidue-8937.test.ts b/packages/plugin-detail/src/__tests__/relatedListParentScopeResidue-8937.test.ts new file mode 100644 index 0000000000..af29f8fc7c --- /dev/null +++ b/packages/plugin-detail/src/__tests__/relatedListParentScopeResidue-8937.test.ts @@ -0,0 +1,283 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#8937 — the published texts objectui#8886 left behind, each pinned to + * the thing it describes rather than to a transcription of it. + * + * Every pin RE-DERIVES on every run. A pin that hard-coded the sentence would + * only assert that nobody edited the sentence, which is the failure mode these + * defects already are: text that stayed put while the thing it described moved. + * + * ⚠️ Two of these pins read `@object-ui/core`'s `parent-scope` seam from a + * plugin-detail test file, which is deliberate and is why it is said out loud: + * objectui#8882 / PR objectui#9184 moved the arity compiler OUT of + * `RelatedList.tsx` and into that seam while this card was in flight, taking + * the false sentence with it. The pins follow the claim, not the package — and + * the README half of the subject is still plugin-detail's own published text, + * so this is the one file that can see both ends. + * + * ## Pin one — the shipped README's parent-scope claim, against the compiler + * + * `packages/plugin-detail/README.md` is in that package's `files[]`, so it + * SHIPS. It used to say the node's `filter` is AND-combined with + * `{ [relationshipField]: parentId }` full stop, which stopped being true when + * objectui#7299 made the parent condition arity-dependent. The pin reads the + * spellings out of `composeParentScopeFilter`'s own returned literal and + * requires the README paragraph to name EXACTLY that set — both directions, so + * the README cannot over-claim an operator the seam never composes either. + * + * ## Pin two — the divergence the seam records, against the installed spec + * + * The seam's header used to say `isMultiValueField` is "the same predicate the + * driver that executes the query decides on", and `RelatedList.tsx` used to say + * the equivalent. Neither is true: `driver-sql` gates the equality family on + * its own storage question, which treats `multiple` as truthy on ANY type, so + * the two rules disagree for a type OUTSIDE `MULTI_CAPABLE_TYPES` carrying + * `multiple: true`. + * + * The DRIVER half cannot be imported here — it lives in objectstack's + * `driver-sql`. What can be re-derived is the SPEC half, and it is the half + * that moves: the likely settlement of objectstack#17469 is the spec admitting + * these types, at which point the seam's list stops being a divergence and this + * file reddens. The driver half is a constant for the cases listed — its rule + * contains the disjunct "`multiple` is truthy", `true` for every + * `multiple: true` def regardless of the type set beside it — so no import is + * needed to know the driver answers "JSON column" for all of them. + * + * ## Pin three — a THIRD file may not relate the two sides without the citation + * + * The rework this file is part of exists because the claim was COPIED into a + * new file rather than edited in place. Pins one and two judge the text where + * it lives today and would both stay green through exactly that move, so the + * sweep below is the one that would have caught it: every file in this tree + * that names both the spec predicate and the driver must cite the card that + * owns their disagreement. + */ + +import { execFileSync } from 'node:child_process'; +import { readFileSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +import { + FieldType, + MULTI_CAPABLE_TYPES, + MULTI_OPTION_TYPES, + isMultiValueField, +} from '@objectstack/spec/data'; +import { describe, it, expect } from 'vitest'; + +/** + * Rooted on THIS FILE, never on `process.cwd()` (objectui#7799): the package + * and repo-root invocations have different cwds and would read different trees. + * Bare `import.meta.url` taken apart by hand — `new URL(rel, import.meta.url)` + * is rewritten by Vite into a dev-server URL that `fileURLToPath` rejects. + */ +const SELF_DEPTH_BELOW_REPO_ROOT = 5; // packages / plugin-detail / src / __tests__ / this file +const REPO_ROOT = decodeURIComponent(new URL(import.meta.url).pathname) + .split('/') + .slice(0, -SELF_DEPTH_BELOW_REPO_ROOT) + .join('/'); + +/** Read a tracked file, and prove the read landed or every assertion on it is vacuous. */ +function sourceOf(rel: string): string { + const path = join(REPO_ROOT, rel); + expect(existsSync(path), `source not found at ${path}`).toBe(true); + const text = readFileSync(path, 'utf8'); + expect(text.length, `${rel} read back empty`).toBeGreaterThan(0); + return text; +} + +/** The one compiler of the parent-relationship condition (objectui#8882). */ +const SEAM = 'packages/core/src/utils/parent-scope.ts'; +/** The shipped text that describes what that compiler puts on the wire. */ +const README = 'packages/plugin-detail/README.md'; + +// ── pin one: README ↔ the compiler it describes ────────────────────────────── + +/** The object literal `composeParentScopeFilter` returns, as written. */ +function composedLiteralBody(source: string): string { + const fn = /export function composeParentScopeFilter\([\s\S]*?\n\}/.exec(source); + expect(fn, `could not find composeParentScopeFilter in ${SEAM}`).not.toBeNull(); + const lit = /return \{([\s\S]*?)\};/.exec((fn as RegExpExecArray)[0]); + expect(lit, 'could not find the returned condition literal').not.toBeNull(); + return (lit as RegExpExecArray)[1]; +} + +/** Query-DSL operators the parent condition composes, e.g. `$contains`. */ +function composedOperators(body: string): string[] { + return [...new Set(body.match(/\$[A-Za-z][A-Za-z0-9_]*/g) ?? [])].sort(); +} + +/** + * Does the parent condition still have a BARE equality arm? + * + * Operator objects are stripped first, so `{ $contains: parentId }` cannot + * masquerade as one — the question is whether `parentId` is ever placed on the + * field key directly. + */ +function hasBareEqualityArm(body: string): boolean { + return /:\s*parentId\s*(?:[,}]|$)/.test(body.replace(/\{[^{}]*\}/g, '').trim()); +} + +/** The blank-line-delimited README paragraph carrying the AND-combination claim. */ +function parentScopeParagraph(readme: string): string { + const hits = readme.split(/\n\s*\n/).filter((p) => p.includes('**AND-combined**')); + expect(hits, 'expected exactly one README paragraph claiming the AND-combination').toHaveLength(1); + return hits[0]; +} + +describe('objectui#8937 pin one — the shipped README states the condition the seam compiles', () => { + it('names every operator the seam composes, and no operator it does not', () => { + const body = composedLiteralBody(sourceOf(SEAM)); + const paragraph = parentScopeParagraph(sourceOf(README)); + + const composed = composedOperators(body); + // Landing proof: a body this extraction failed to read would make every + // assertion below pass by describing nothing. + expect(composed.length, 'the condition composes no operator at all — extraction likely missed').toBeGreaterThan(0); + + const claimed = [...new Set(paragraph.match(/\$[A-Za-z][A-Za-z0-9_]*/g) ?? [])].sort(); + expect(claimed, 'the README paragraph must name exactly the operators the seam composes').toEqual(composed); + }); + + it('names the equality spelling exactly when the seam still has a bare equality arm', () => { + const body = composedLiteralBody(sourceOf(SEAM)); + const paragraph = parentScopeParagraph(sourceOf(README)); + const EQUALITY = '{ [relationshipField]: parentId }'; + + expect(paragraph.includes(EQUALITY)).toBe(hasBareEqualityArm(body)); + }); + + it('spells the membership form in full whenever the seam composes `$contains`', () => { + const body = composedLiteralBody(sourceOf(SEAM)); + const paragraph = parentScopeParagraph(sourceOf(README)); + const MEMBERSHIP = '{ [relationshipField]: { $contains: parentId } }'; + + expect(paragraph.includes(MEMBERSHIP)).toBe(composedOperators(body).includes('$contains')); + }); +}); + +// ── pin two: the seam's divergence list ↔ the installed spec ───────────────── + +/** Comment prose, un-commented: leading ` * ` stripped, whitespace collapsed. */ +function docblockProse(source: string): string { + return source + .split('\n') + .map((line) => line.replace(/^\s*\*\s?/, '')) + .join(' ') + .replace(/\s+/g, ' '); +} + +/** The field types the seam names as diverging, read back out of its header. */ +function divergentTypesFrom(source: string): string[] { + const match = /They diverge for[^(]*\(([^)]*)\)/.exec(docblockProse(source)); + expect(match, 'the seam no longer records a divergence set — pin two has nothing to judge').not.toBeNull(); + return [...((match as RegExpExecArray)[1].match(/`([a-z_]+)`/g) ?? [])].map((t) => t.replace(/`/g, '')); +} + +describe('objectui#8937 pin two — the seam divergence is re-derived from the installed spec', () => { + it('names real spec field types that the spec predicate still calls single-valued', () => { + const types = divergentTypesFrom(sourceOf(SEAM)); + expect(types.length, 'expected the seam to name the diverging types').toBeGreaterThanOrEqual(3); + + for (const type of types) { + expect(FieldType.safeParse(type).success, `${type} is not a spec field type`).toBe(true); + expect(MULTI_CAPABLE_TYPES.has(type), `${type} joined MULTI_CAPABLE_TYPES`).toBe(false); + expect(MULTI_OPTION_TYPES.has(type), `${type} joined MULTI_OPTION_TYPES`).toBe(false); + // The spec half of the divergence. The driver half is its `multiple` + // disjunct, which is `true` for each of these by construction. + expect(isMultiValueField({ type, multiple: true }), `the spec now calls ${type} multi-valued`).toBe(false); + } + }); + + it('CONTROL — the same predicate answers `true` where it should, so the cases above are not vacuous', () => { + // Without this, a predicate stuck at `false` would satisfy every assertion + // in the case above while asserting nothing about any divergence. + expect(isMultiValueField({ type: 'lookup', multiple: true })).toBe(true); + expect(isMultiValueField({ type: 'user', multiple: true })).toBe(true); + expect(isMultiValueField({ type: 'multiselect' })).toBe(true); + }); + + it('points at the upstream card that owns which of the two rules is right', () => { + expect(sourceOf(SEAM)).toContain('objectstack#17469'); + }); +}); + +// ── pin three: relating the spec predicate to the driver obliges a citation ── + +/** + * The population: every tracked file under `packages` that mentions BOTH the + * spec predicate and the driver, i.e. every place in this tree that relates the + * two sides of the divergence. Each one must cite the card that owns which side + * is right, so a fresh comment asserting they agree cannot land silently. + * + * ⚠️ Why the sweep is not for the retired SENTENCE. The obvious pin would grep + * for the wording objectui#8886 and objectui#9184 each used. It was built that + * way first and measured degenerate: the two corrections rephrase the claim + * across a line break, a line-oriented grep therefore misses them, and the only + * remaining match in the tree was the pattern string in this file matching + * itself. A sweep whose sole carrier is its own pattern asserts nothing. This + * one keys on the two SUBJECTS instead, which no rewording moves. + * + * Boundary, stated rather than implied: a file that discusses the divergence + * without naming the driver in those words is outside the population, and + * generated release history (`CHANGELOG.md`) is excluded because it records + * what was once written rather than what the tree now claims. + * + * Enumeration and reading come from the SAME tree — the working tree — so the + * population cannot be drawn from one source and the content from another + * (AGENTS.md's enumeration-vs-read rule). + */ +function filesRelatingPredicateToDriver(): { carriers: string[]; predicateOnly: string[] } { + const out = execFileSync('git', ['grep', '-l', '--', 'isMultiValueField', 'packages'], { + cwd: REPO_ROOT, + encoding: 'utf8', + }); + const mentionsPredicate = out + .split('\n') + .filter(Boolean) + .filter((rel) => !rel.endsWith('CHANGELOG.md')); + const carriers: string[] = []; + const predicateOnly: string[] = []; + for (const rel of mentionsPredicate) { + (/driver/i.test(sourceOf(rel)) ? carriers : predicateOnly).push(rel); + } + return { carriers, predicateOnly }; +} + +describe('objectui#8937 pin three — relating the predicate to the driver obliges the divergence citation', () => { + it('cites objectstack#17469 in every file that relates the two sides', () => { + const { carriers } = filesRelatingPredicateToDriver(); + + // Landing proof: a sweep that matched nothing would pass while checking + // nothing. Three is under the four carriers this tree has today, so the + // number is an alarm rather than a census needing an edit per new file. + expect(carriers.length, 'the sweep found almost no carrier — the enumeration has gone stale').toBeGreaterThanOrEqual(3); + + const uncited = carriers.filter((rel) => !sourceOf(rel).includes('objectstack#17469')); + expect( + uncited, + 'a file relates the spec predicate to the driver without citing the card that owns their divergence', + ).toEqual([]); + }); + + it('DISCRIMINATOR — the driver filter narrows, so the citation demand is not made of every mention', () => { + // Without this, a filter that matched every file would turn the case above + // into "every file mentioning the predicate must cite objectstack#17469", + // which is a different and wrong demand that would happen to be green. + const { carriers, predicateOnly } = filesRelatingPredicateToDriver(); + expect(predicateOnly.length, 'the driver filter excluded nothing — it is a no-op').toBeGreaterThan(0); + // `plugin-grid`'s wrapper names the spec predicate and the server-side + // WRITE pipeline that shares it, and says nothing about the driver. It is + // the nearest true neighbour of the retired claim, so it is the right file + // to require on the excluded side. + expect(predicateOnly).toContain('packages/plugin-grid/src/hooks/multiValueFields.ts'); + expect(carriers).not.toContain('packages/plugin-grid/src/hooks/multiValueFields.ts'); + }); +}); diff --git a/scripts/markdown-test-inputs.mjs b/scripts/markdown-test-inputs.mjs index e32eb12152..4457d08898 100644 --- a/scripts/markdown-test-inputs.mjs +++ b/scripts/markdown-test-inputs.mjs @@ -260,6 +260,23 @@ export const ADJUDICATED = new Map([ 'packages/plugin-detail/src/__tests__/guideCrudAppRenders.test.tsx', { reads: ['content/docs/guide/building-crud-app.md'] }, ], + // The `reads` below is a MEASURED subset of a DERIVED population, which is + // why it is one document rather than a tree. Two of this test's three pins + // name `packages/plugin-detail/README.md` by constant; the third enumerates + // `git grep -l isMultiValueField packages` and opens every hit, so any + // markdown that starts mentioning that symbol joins the set. Measured on this + // card: the enumeration offers 18 paths, ten of them CHANGELOGs the test + // drops before opening anything, and exactly one markdown document survives + // into the read set -- the same README the other two pins name. The root + // `CHANGELOG.md` the scanner resolves is never in the population at all: the + // pathspec is `packages`, and the bare literal comes from the drop filter. + [ + 'packages/plugin-detail/src/__tests__/relatedListParentScopeResidue-8937.test.ts', + { + reads: ['packages/plugin-detail/README.md'], + notRead: ['CHANGELOG.md', 'packages/plugin-detail/CHANGELOG.md'], + }, + ], [ 'packages/plugin-detail/src/renderers/__tests__/docsActivityMapTranscription-7876.test.ts', { reads: ['content/docs/plugins/plugin-detail.mdx'] },