docs(plugin-detail): state the driver's real arity rule and the arity-dependent parent scope (objectui#8937) - #9195
Conversation
…-dependent parent scope
Two published texts objectui#8886 left behind, both measured false on origin/main:
- packages/plugin-detail/README.md ships (it is in files[]) and 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 one gets `{ $contains: parentId }` instead.
The paragraph now states both spellings and names the arbiter.
- `parentRelationshipFieldDef`'s docblock, and the same sentence in the
objectui#7299 test header, said the SQL driver decides arity on the spec's
`isMultiValueField`. 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`; that divergence is now recorded as one and pointed at the
upstream card that owns it (objectstack#17469).
No predicate moved and no wire changed. Both claims are pinned by re-derivation
rather than transcription, in relatedListParentScopeResidue-8937.test.ts.
The manifest floor finding on the same card is NOT actioned here: measurement
falsified its premise. See the pull request body.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectui#9184 (card objectui#8882) moved the parent-scope compiler out of RelatedList.tsx into @object-ui/core's parent-scope seam while this branch was in flight, conflicting with this branch's docblock correction at the same spot. Conflict resolution, packages/plugin-detail/src/RelatedList.tsx: origin/main's pointer comment is taken as the base, because the function it described has moved. Its one false clause is corrected in place -- it asserted that the driver refusing the query decides on @objectstack/spec/data's isMultiValueField, which measurement on objectstack origin/main falsifies. The pointer now says where the measured rule, the divergence and the upstream card are recorded, rather than restating a rule that no longer lives in this file. Merge, never rebase (AGENTS.md). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…rage arity objectui#9184 moved the arity compiler into @object-ui/core's parent-scope seam and newly wrote the false claim into its header: that isMultiValueField is "the same predicate the driver that executes the query decides on". Measured on objectstack origin/main, 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 a type outside MULTI_CAPABLE_TYPES carrying `multiple: true` (master_detail / tree / text). The seam now states the driver's measured rule, records that divergence as a divergence rather than as agreement, and points at objectstack#17469, which owns which of the two rules is right. No predicate and no assertion moved: the arity rules are upstream business. The pin re-anchors onto the seam, since the composition and the docblock it judged both left RelatedList.tsx: - pin one re-derives the operator set from composeParentScopeFilter's returned literal and holds the shipped README to exactly that set, both directions; - pin two re-derives the divergence list from the INSTALLED spec, so it reddens when the spec side moves -- the likely settlement of the upstream card; - pin three is new and is the one that would have caught this rework's cause: every file relating the spec predicate to the driver must cite the card that owns their disagreement, so the claim cannot be copied into a fresh file silently. A discriminator case proves the driver filter narrows rather than demanding the citation of every mention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
PM review — content ACCEPTED; ⛔ CI is RED and the failure is this PR's ownRead against Accepted: the sync is a real merge ( ⭐ The most valuable line in the report is the one admitting a failure: pin three was first built degenerate — a line-oriented grep for the retired sentence whose only surviving match was its own pattern string — and that was reported rather than shipped. It is independently corroborated: The failure —
|
`Test (shard 1/4)` was red on this branch's own omission: the new pin reads
`packages/plugin-detail/README.md`, and a test that reads markdown without a
ledger entry can be SKIPPED by a markdown-only pull request -- ci.yml's
"needs a full run" step excludes markdown paths, so someone could edit that
README and the pin would never execute (objectui#8861).
That is the same failure mode the pin's own third case exists to prevent, one
level up, and this gate caught it before it shipped.
The declared set is MEASURED, not inferred from the constants. Two of the three
pins name the README by constant; the third enumerates
`git grep -l isMultiValueField packages` and opens every hit, so the population
is derived. Measured: the enumeration offers 18 paths, ten of them CHANGELOGs
dropped before anything is opened, and exactly one markdown document survives
into the read set -- the same README. The root `CHANGELOG.md` the scanner
resolves is never in the population: the pathspec is `packages`, and the bare
literal it resolved comes from the drop filter. Both CHANGELOG candidates are
therefore recorded as `notRead` rather than dropped, and no `walker` is
declared because the scanner sees no directory walk here.
Readings: `--audit` exit 1 before ("1 finding(s)", unadjudicated-test naming
all three paths) and exit 0 after ("47 candidate test files, all adjudicated").
The gate's own suite goes 1 failed / 11 passed -> 12 passed. `--changed` now
answers in both directions: it prints the README (a markdown-only edit forces
the run) and prints nothing for the CHANGELOG.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Second rework ACCEPTED — both claims re-measured hereHead The ledger entry ( reads: ['packages/plugin-detail/README.md'],
notRead: ['CHANGELOG.md', 'packages/plugin-detail/CHANGELOG.md'],18 paths offered → 10 CHANGELOGs dropped before opening → 8 opened → 1 markdown in the read set. Recording the two CHANGELOG candidates as The i18n annotation is root-caused, not waved off, and I verified the load-bearing half myself:
⇒ the exclusion is an established convention in this tree that exactly one of the two whole-tree sweeps is missing. That is a defect with a name, not a flake, and the card's own argument for why is the right one: "the interleaving is what varies; the defect does not." Filed as #9201 with both halves measured, a lit convention control, and an honest scope limit (it does not claim to have surveyed other sweeps). ⭐ Nothing was skipped, disabled or quarantined to get green — which was the instruction and is the part that matters. The self-report that the first suite invocation named a nonexistent file and the vitest guard refused rather than passing on a zero-match path is worth keeping too: that guard (objectui#3288) is the reason a mistyped path did not read as a green run. ⛔ Arm still held: 36 checks, 7 in progress, 0 failing. Not-red-yet is not green. I arm on Generated by Claude Code |
…arity Brings in objectui#8937 (PR #9195), whose residue sweep pin now requires every file naming both the spec arity predicate and the driver to cite objectstack#17469. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
…e 8883 rail pin objectui#8937's residue sweep (PR #9195, pin three) requires every tracked file under `packages` that names both the spec arity predicate and the driver to cite the card that owns their disagreement. This test file entered that population when it landed and did not cite it, so the sweep reds on the combined tree even though it is green on this branch's own base — which is what ejected the PR from the merge queue. The docblock now states the relationship it was implying: the spec gates on the field TYPE, `driver-sql` decides storage on `!!field.multiple` whatever the type, the two part company for a `multiple: true` def on a type outside the spec's multi-capable set, and objectstack#17469 owns which is right. It also records why these cases sit clear of that disagreement — they relate through a `lookup` field, which both rules call multi-valued. Prose only: the diff adds docblock lines and removes none. No predicate, no assertion and no executable line moved. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzHd6hDYatoDn17BuwKxnZ
Part of objectui#8937 — two of the card's three findings are actioned; the third is not, because measurement falsified its premise and the lane accepted that. Details and readings below.
What landed (F2, F3)
F3 — the shipped README sentence.
packages/plugin-detail/README.mdis listed in that package'sfiles[], so it reaches npm. It said the node'sfilteris 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.F2 — the false driver-arity claim, corrected where it now lives. Three sites carried it; all three are corrected and none of them changes a predicate or an assertion.
packages/core/src/utils/parent-scope.ts(the seam objectui#9184 created)isMultiValueFieldis "the same predicate the driver that executes the query decides on"packages/plugin-detail/src/RelatedList.tsx(the pointer comment objectui#9184 left behind)@objectstack/spec/data'sisMultiValueField"RelatedList.multiValueParentScope-7299.test.tsx(header, and the comment on itsINERTcase, which asserted="is the correct predicate" for a shape the live driver refuses)objectstack#17469 already exists and is open — ⛔ nothing re-filed.
The driver's real rule, measured on objectstack
origin/mainJSON_COLUMN_TYPESis the spec'sSTRUCTURED_JSON_TYPESandMULTI_OPTION_TYPESplus the driver-internalobject/arrayaliases;!!field.multipleis truthy on any type. The spec's predicate isMULTI_OPTION_TYPES.has(type) OR (MULTI_CAPABLE_TYPES.has(type) AND multiple === true), andMULTI_CAPABLE_TYPESisselect/radio/lookup/user/file/image.⇒ they disagree for a type outside that set carrying
multiple: true—master_detail/tree/text: objectui sends=, the driver stores a JSON column and refuses=with a400 INVALID_FILTER. Not a regression: both surfaces sent=for that shape before objectui#7299 and objectui#8882 too.What did NOT land, and why — F1's premise is false
The card's F1 says
@objectstack/spec@^17.1.0predatesisMultiValueField(first shipped 17.3.0) and that consumers resolving 17.1.x/17.2.x take a runtime import failure. Measured against the published artifacts and against this repo's own floor gate, that is not so. The floor is already correct andpackages/plugin-detail/package.jsonis untouched. The lane re-measured this independently with a third instrument and accepted the finding.The reading was taken in the failing configuration, never off this repo's lockfile.
Reading 1 — isolated installs of each published 17.x, exact import form of the component's spec import.
npm install @objectstack/spec@Vinto three empty scratch packages; resolution printed per run and confirmed to land inside each probe dir (no hoisting above them).import { isMultiValueField } from '@objectstack/spec/data'require().d.mts/.d.tsdeclare ittsc --noEmiton the two spec import lines verbatimtypeof === 'function'functiontypeof === 'function'functiontypeof === 'function'functionControls, both lit. Negative: importing a name that cannot exist fails on every version with
SyntaxError: The requested module '@objectstack/spec/data' does not provide an export named '__definitelyNotAnExport_8937'— exactly the failure shape F1 predicts, so the probe can produce it. Positive:AddressSchema, genuinely present in 17.2.x, imports cleanly. Thetscprobe has its own negative control (error TS2305 ... has no exported member, exit 2).Reading 2 — the export-set diff.
@objectstack/spec/dataexports 442 names on 17.1.0 and 17.2.0, 457 on 17.3.0. The 15 names 17.3.0 added are all credential / value-roundtrip symbols:BOUNDED_STRING_FIELD_TYPES,CANONICAL_CREDENTIAL_KEYS,CREDENTIAL_KEY_SPELLINGS,FIELD_GROUP_KEY_PATTERN,FORMER_CREDENTIAL_ALIASES,PASSTHROUGH_NESTED_CREDENTIAL_REFUSED,SYS_ACTIVITY_BUILTIN_TYPES,VALUE_DOMAIN_FIELD_TYPES,VALUE_ROUNDTRIP_CASES,VALUE_ROUNDTRIP_COLLISION_PAIRS,VALUE_ROUNDTRIP_FIELDS,VALUE_ROUNDTRIP_ROWS,refusedCredentialPaths,refusedCredentialPathsOfSchema,valueRoundTripDivergence. Nothing removed, andisMultiValueFieldis not among the additions.Reading 3 — this repository's own gate says the same thing, re-run after the merge.
pnpm check:spec-floors(scripts/check-spec-range-floors.mjs) exists for exactly this defect class (objectui#5793). It never resolves through the installed tree: it fetches the declared-minimum tarball from the registry and walks that manifest's ownexportsmap under theimportcondition, judging the package's built artifact. Scoped to the two packages this branch touches, after building their closure:The judged pairs include
isMultiValueFieldfrom./data, so the symbol F1 is about is in the judged set, not skipped. The control lights, so the gate can fail on this artifact. Earlier in the round the same control named its reason:dist/renderers/record-reference-rail.d.tsreferencesReferenceRailEntry, which@objectstack/spec@17.0.0does not export.⇒
^17.1.0is not merely passing, it is the lowest floor that passes — the honest one, set deliberately by objectui#5793 ("floors track reality"). Raising it to^17.3.0would pass the gate too, because the gate only detects floor-too-low; it would narrow a published contract with nothing measured behind it and break consumers pinned to 17.1/17.2 for no reason. Under AGENTS.md #0.1 that is the wrong direction.Also checked: declared
@objectstack/specfloors across the tree are^17.0.0(22 sites),^17.1.0(plugin-detail,react),^17.2.0(data-objectstack),^17.3.0(core),^17.4.0(types) — nothing here would have been disturbed either way, and the question is moot now that the manifest is untouched.Pins, and the ablation for each
All three pins re-derive on every run, never transcribe the sentence — a transcription pin only asserts that nobody edited the sentence, which is the failure mode these defects already are. They live in
relatedListParentScopeResidue-8937.test.ts.composeParentScopeFilterand requires the shipped README paragraph to name exactly the operator set the seam composes, in both directions, plus the equality spelling exactly when a bare equality arm still exists. Re-anchored this round: the literal used to live inRelatedList.tsx.FieldType, must be in neitherMULTI_CAPABLE_TYPESnorMULTI_OPTION_TYPES, andisMultiValueField({ type, multiple: true })must still answerfalse. ACONTROLcase asserts the same predicate answerstrueforlookup/user/multiselect, so the divergence cases cannot pass vacuously. Re-anchored this round for the same reason.packagesthat names both the spec predicate and the driver must cite objectstack#17469, so the claim cannot be copied into a fresh file silently. ADISCRIMINATORcase proves the driver filter narrows rather than demanding the citation of every mention, and names the file that must stay on the excluded side (plugin-grid'smultiValueFields.ts, which relates the predicate to the server-side write pipeline — a true claim about a different consumer).Ablation, six legs, re-anchored. Each leg: mutate, prove the bytes reached disk by blob-hash change, run, restore with
git checkout HEAD -- PATH, prove restoration by state (git diff HEADempty and hash back to the HEAD blob). Exit codes are evidence for neither direction, and the harness carries a trap on EXIT/INT/TERM calling a restore function with absolute paths. Run from the committed state, so each restore leg has a real restore point.plugin-detail/README.mda5971ee→9ee32ce$containscore/src/utils/parent-scope.tsce8bb58→2fbfc88core/src/utils/parent-scope.tsce8bb58→0e54a6dlookup, a type the spec already calls multi-valuedcore/src/utils/parent-scope.tsce8bb58→3156412plugin-detail/src/RelatedList.tsx66905f6→d501975plugin-grid/src/hooks/multiValueFields.ts6ce74fc→aa0090dLegs 2–4 pointed at
RelatedList.tsxbefore the merge and would have stopped discriminating there; they were re-anchored onto the seam rather than dropped. All six restored;git status --porcelainempty afterwards.Second rework round — the gate this PR tripped, and the one it filed
Test (shard 1/4)was red on this branch's own omission, and it is the same failure mode as pin three one level up.scripts/markdown-test-inputs.mjsis the ledger behindci.yml's "does this change need a full run" step: that step excludes markdown paths, so a test that READS markdown and is not in the ledger can be skipped by a markdown-only pull request. The new pin readspackages/plugin-detail/README.mdand was not registered — someone could have edited that README and the pin would never have run. The gate caught it before it shipped.The declared set is measured, not inferred from the constants. Two of the three pins name the README by constant; pin three enumerates
git grep -l isMultiValueField packagesand opens every hit, so the population is derived and could be wider than the constants suggest. Measured on this branch:packages/plugin-detail/README.md, the same README the other two pins nameCHANGELOG.mdpackages; the scanner resolved it only from the bare literal in the drop filterSo the entry declares
reads: ['packages/plugin-detail/README.md']and records both CHANGELOG candidates asnotReadrather than dropping them. Nowalkeris declared:auditTreeraisesstale-walkerwhen one is declared and the scanner sees no directory walk, and it sees none here.Red, then green.
node scripts/markdown-test-inputs.mjs --auditunadjudicated-test: …relatedListParentScopeResidue-8937.test.ts reads markdown the ledger does not describe (CHANGELOG.md, packages/plugin-detail/CHANGELOG.md, packages/plugin-detail/README.md)47 candidate test files, all adjudicated; 41 declared entries, all presentvitest run scripts/__tests__/markdown-test-inputs.test.tsTests 1 failed | 11 passed (12)Tests 12 passed (12)--changedonpackages/plugin-detail/README.md--changedCONTROL onpackages/plugin-detail/CHANGELOG.mdThe second annotation on that shard: root-caused, ⛔ not called a flake
Consumer:
check-i18n-dead-keys.mjsruns a whole-tree filesystemgrep -rFn(lines 606-616) whoseTEXT_SWEEP_SKIP_DIRS(lines 428-431) isnode_modules,.git,dist,build,coverage,.next,.turbo,.changeset—.objectui-tmpis not in it (grep -creturns 0). Producer:packages/cli/src/__tests__/app-generator.test.ts'swithGeneratedApp(lines 1386-1396)mkdtempSyncsREPO_ROOT/.objectui-tmp/tsc-gate-3853-…, writes a generated app including atsconfig.json, and removes it in afinally— so it lives for one test inside the same shard. Convention control, lit: the sibling whole-tree sweepcheck-comment-mask-corpus.mjsline 246 already lists.objectui-tmpin its skip set.⇒ a missing exclusion, not a flake: only whether the two overlap varies; given overlap the outcome is determined. This branch does not contribute — the diff touches neither script, neither the CLI test nor
.gitignore; the new test shells out togit grep, which reads tracked files only, and.objectui-tmpis.gitignored (.gitignore:50) hence untracked, so it is never opened by it, and a read cannot create a directory. The error also names a different grep in a different process. Filed as objectui#9201 after a dedup search (see below); ⛔ nothing here skipped, disabled or quarantined.Dedup channel, declared. The container's REST
/search/issuesanswers403withsessions are bound to their configured repositories— the proxy's path whitelist, ⛔ not a statement about GitHub scope — so the dedup went through one targeted MCPsearch_issuesinstead. Nine hits, all about how that script classifies keys, none about the sweep racing a temp directory. The control query in the same session returned this card, objectui#8937, as its second hit, so the channel was alive when the dedup ran.Verification
Run from the repo root with paths relative to it, per AGENTS.md; every exit code captured by redirecting to a file first, never through a pipe. All of this is post-merge; the ledger row is the second rework round.
turbo run build --filter=@object-ui/plugin-detail --filter=@object-ui/core --concurrency=2, under the shared verify lockTasks: 12 successful, 12 total; lock printedVERDICT command-exit 0pnpm exec vitest run packages/plugin-detail/ packages/core/ packages/i18n/src/__tests__/residue-namespaces-3546.test.tsx, under the lockTest Files 323 passed (323)·Tests 4848 passed (4848); lock printedVERDICT command-exit 0pnpm --filter @object-ui/plugin-detail type-check·... @object-ui/core ...TS6305/TS2307) — a prerequisite, not a red; they pass once ① has runpnpm --filter @object-ui/plugin-detail lint·... @object-ui/core ...997 problems (0 errors)and542 problems (0 errors), all pre-existing; the new test file contributes nonecheck-changeset-presence·check-changeset-no-major·check-changeset-claimsnode scripts/check-control-bytes.mjs, plus agrep -naPsweep over the touched filesnode scripts/check-new-cross-file-line-citations.mjsnode scripts/check-comment-mask-corpus.mjsapps/console/src/pages/DocsIndex.tsxresidue objectui#7882 holds open, not this diffcheck-doc-fence-languages.mjs·check-doc-links.mjsanalyze()over both packages, after ①findings: []; the^17.0.0control is REDcheck-governed-queue-guard.mjs --testover the six changed pathspnpm check:readme-exportspackagesRead: found 13, floor is 25,24 unbuilt. It owns a whole-workspace build step inside.github/workflows/readme-exports.yml, and objectui#4846 rejected a per-PR full-repo build.pnpm lint(turbo, every package) ·pnpm test(full) ·check:spec-floorson its own workflow and the release pathscripts/pm/dispatch-gates.mjs— that script is objectstack's, and pointing it at this tree would return a confident answer about the wrong repository. The list above was derived by hand from this repo's rootpackage.jsoncheck:*scripts and.github/workflows/.Merge, not rebase
git merge origin/mainat759606e1d, one conflict (packages/plugin-detail/src/RelatedList.tsx, the docblock both sides edited), resolved by taking origin/main's pointer comment as the base — the function it described has moved — and correcting its one false clause in place. Landing of objectui#9184 was confirmed by content, not by sha:git cat-file -e origin/main:packages/core/src/utils/parent-scope.tsexits 0, withRelatedList.tsxas a lit positive control. ⛔ No rebase, ⛔ no force-push, at any point.Status
⛔ Draft, ⛔ not enqueued, ⛔ no auto-merge — the lane arms it. Clause-② stays
yes;needs:contract-reviewis attached and stays until the lane clears it.packages/plugin-detail/package.jsonis untouched.packages/plugin-detail/src/renderers/record-reference-rail.tsxis untouched (objectui#8883 is live on it).Out of scope, noted
⛔ No new card is opened for the arity divergence — objectstack#17469 already owns it and is open.
Generated by Claude Code; this round's session is
session_01UzHd6hDYatoDn17BuwKxnZ(recorded as prose, because a link-form footer does not survive a body edit).Generated by Claude Code