fix(spec): the stored-filter conversion's TODO for a null-valued key is true on every block, and no longer says to drop the key - #20709
Conversation
…s true on every block `page-component-filter-record-to-rule-array` declines a record-form filter with a null-valued key. Its reason said the renderer skips that key, so it constrains nothing, and to drop it. At the objectui pin that holds only on a block that queries an object; on a block whose rows are inline, `ValueDataSource.find` matches the key and selects the rows whose value is null, so dropping it widens the block. The reason, its docblocks and the protocol-18 D3 entry now state both behaviours, name the `is_null` rule for the rows with no value, and leave which rows to select to the author. The verdict does not move. Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d66495a5d4dbaa11130925990f31259b5c6602de && git checkout d66495a5d4dbaa11130925990f31259b5c6602de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b291fcdae9ac6dd4152367082905cfe81ddf5fb0 eaf2d6e6e8d55e3d164ed72a786ee7d260997a6b && git checkout -B drift-repro b291fcdae9ac6dd4152367082905cfe81ddf5fb0 && git merge --no-ff eaf2d6e6e8d55e3d164ed72a786ee7d260997a6b
node scripts/docs-audit/affected-docs.mjs --json b291fcdae9ac6dd4152367082905cfe81ddf5fb0
|
…he key, not that it constrains nothing
`page-component-filter-record-to-rule-array` declines a record-form filter
whose key is an empty operator object (`{ amount: {} }`). Its reason said the
object constrains nothing. At the objectui pin the renderer refuses it:
`convertFiltersToAST` throws INVALID_FILTER (400) through
`refuseEmptyOperatorMap` where a block queries an object, and
`ValueDataSource.find` answers no rows through `zeroKeyConditionRefusal`
where a block's rows are inline. The reason and its docblock now say so and
keep the renderer's own remedy, dropping the key. The verdict does not move.
Also corrects a test comment that named `owner_id: null` for a row that is
`deleted_at: { $null: true }`.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Inputs: card #20662 (body, triage direction 5893989151, claim 5896419836, round-1 report 5897718691, seat note 5897754447, round-2 report 5898670276), PR #20709 (body, comments, five-file list, net diff against ① Derived judgments1. The null-key declined reason (
2. The 3. The conversion entry docblock's parenthetical ("skipped where a block queries an object, matched where its rows are inline — no one rule keeps both"). RIGHT. 4. The D3 entry 5. The empty-operator-object reason and its docblock sentence. "names the field and no operator, so no rule spells it" — RIGHT. "where the block queries an object, it refuses the filter ( 6. Verdict unchanged. The 7. Test comments ( Gate coverage (42 check-runs on
② Semver level
Clause-②: the PR body reads ③ Boundary flagsDev flags, round 1 (5897718691):
Dev flags, round 2 (5898670276):
ESCALATED to the seat:
FAIL reason (one):
Implemented-by: VERDICT: FAIL Generated by Claude Code |
… objectui pin
The null-valued-key TODO reason, the changeset and the protocol-18 D3 entry
listed the inline row sources as `data: { provider: 'value' }`, a `data`
array or `staticData`. At the objectui pin a bare `data` array reaches no
`ValueDataSource.find`: `object-calendar` draws it as pre-fetched rows with
no filter applied, and `object-map` / `object-gantt` do not take it as a
record source. The list now names `data: { provider: 'value' }` and
`staticData` only; `migrations/registry.ts` is regenerated. Nothing else
moves.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <[email protected]>
…r reaches at the objectui pin
The rationale under the conversion's `## Reach` verdict said the in-memory
renderers match the inline rows of every listed source. At the objectui pin
they take those rows from `data: { provider: 'value' }` or `staticData`; a
bare `data` array reaches none of them (`object-calendar` draws it unfiltered,
`object-map` / `object-gantt` do not take it as a record source). Comment text
only; the verdict sentence is unchanged.
Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: Re-judging after the FAIL at ① Derived judgments1. The previous FAIL reason is cleared. "a 2. The remaining list is true at the pin. 3. The calendar precision boundary the dev flags (round 3) — judged: the un-qualified list does not over-claim. On 4. The 5. The D3 entry 6. Round 4's 7. The empty-operator-object reason and its docblock sentence. Unchanged since the previous review; re-read at the pin: 8. Verdict unchanged; pins on named subjects. The 9. The merges lost nothing. Five merges of 10. Not this PR's text. Gate coverage (41 check-runs on
② Semver level
Clause-②: the PR body reads ③ Boundary flagsDev flags, round 3 (5900465814):
Dev flags, round 4 (5900584237):
Everything the previous review judged right is still right at this head: the null-key and empty-operator reasons, the ESCALATED: none. FAIL reasons: none. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20662
Clause-②: no (author-shown wording only; no accept or reject moves)
What changes
The ADR-0087 D2 conversion
page-component-filter-record-to-rule-arrayleaves a record-form filter with anull-valued key as stored and reports it as a TODO, whichos migrate meta --storedprints. Its reason said the renderer skips that key, so it "constrains nothing today", and told the operator to "Drop the key". At the.objectui-shapindd3f7e1be356that is true only on a block that queries an object. On a block whose rows are inline, the key selects the rows whose value is null, so dropping it widens the block.Following triage's direction (comment 5893989151: one wording true on both kinds of block, no "drop the key" advice), the reason now:
data: { provider: 'value' }orstaticData), so it selects the rows whose value is null;{"field":"owner_id","operator":"is_null"}(built from the key), and says that a filter which leaves the key unconstrained has no rule for it. It advises neither rewrite. The choice is the author's.The verdict does not move. The filter is still declined, left byte-identical and reported as one TODO, on any block. The reason text does not branch on where the rows come from; the conversion never reads that.
Round 2 (seat note 5897754447, a claim amendment): the empty-operator-object reason beside it (
{ amount: {} }) said the object "constrains nothing". At the pin the renderer refuses it instead. Where the block queries an object,convertFiltersToASTthrows throughrefuseEmptyOperatorMap(INVALID_FILTER, 400). Where the block's rows are inline,ValueDataSource.findanswers no rows throughzeroKeyConditionRefusal. The reason and its docblock sentence now say that, say that no rule spells an operator object with no operator, and keep the renderer's own remedy, dropping the key. The verdict does not move.Files:
packages/spec/src/conversions/registry.ts: the reason string inrecordFilterToRules, the sentence in its docblock, and the conversion entry docblock's parenthetical in "What is left exactly as stored" ("the renderer skips that key today"). That parenthetical is a fourth copy of the same claim in the same file. It is text only and fixed in place: same defect, same file under this claim, same gates. Round 2 rewrites the empty-operator-object declined reason and its docblock sentence, text only. Round 3 (review 5898951990) drops "adataarray" from the null-key reason's inline list: at the pin a baredataarray reaches noValueDataSource.find. Round 4 makes the rationale in the conversion entry docblock's## Reachparagraph name the inline sources the filter reaches at the pin (data: { provider: 'value' }orstaticData), and adds that a baredataarray reaches none of them (object-calendardraws it unfiltered;object-map/object-ganttdo not take it as a record source). Comment text only; the verdict sentence is unchanged.packages/spec/src/migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts: the same claim in the D3 entry'sreason.packages/spec/src/migrations/registry.tsis regenerated bygen:migration-registryand not edited by hand. Round 3 narrows the inline list in its older sentence ("None of this depends on where a block's rows come from …") todata: { provider: 'value' }orstaticData.packages/spec/src/conversions/page-component-filter-record-to-rule-array.test.ts: theDECLINED_ROWScomment that restated the null-key claim; the all-or-nothing test's comment, which namedowner_id: nullfor a row that isdeleted_at: { $null: true }; and two new pins. A null-valued key gets the same reason on an inline-rowobject-mapand an object-bound one. That reason names theis_nullrule for the key, and the block's door takes that rule; the control is that the door refuses the stored record. An empty operator object gets the same reason on both blocks, and that reason namesINVALID_FILTER, a code inStandardErrorCode..changeset/20662-null-key-todo-reason.md:@objectstack/specpatch, covering both reasons.Verification record
Pin reading (
dd3f7e1be356, raw source):packages/core/src/utils/filter-converter.tsconvertFiltersToASTskips a key whose value isnull/undefined(skippedNullKeys).packages/core/src/adapters/ValueDataSource.tsfindsends an object$filtertomatchesFilter, and its simple-equality arm compares withcomparandEquals, which isvalue === target. Itsis_nullarm isvalue === null || value === undefined. Server-side,parseFilterASTlowersis_nullto{ $null: true }.ObjectMap/ObjectCalendarpassuseResolvedFilter(schema.filter)tonew ValueDataSource(...).find.filter-tokens.tsresolveContextTokensreturns anullvalue unchanged.page-component-filter-record-to-rule-arrayonce the objectui pin carries objectui#10767 #20305 dev's live probe (report 5893209491) measured thatfindwith{ owner_id: null }selects only the null row, and not the'u1'row or the row that has no key.Lit, at base
31ed067639(the stored-migration pass andformatStoredMigrationReport, the functionos migrate meta --storedprints through, over a one-page stubsys_metadataholding anobject-gridthat queriesdealand anobject-mapwithdata: { provider: 'value' }, bothfilter: { owner_id: null }):The
object-gridline carried the same sentence.Dark, at
a51c02fe83(same probe, spec rebuilt):In both runs the row is still
skippedwith two TODOs. The probe file was temporary and is not in the diff.Round 2, empty operator object (same printer probe, with
filter: { amount: {} }on the same two blocks). Lit atc5eed1b4d3: "... this filter has the keyamountset to an empty operator object, which constrains nothing — and no rule says "nothing". Drop the key. Left as stored, ...". Dark at3fcedfb564: "... set to an empty operator object, which names the field and no operator, so no rule spells it. The renderer does not ignore it today: where the block queries an object, it refuses the filter (INVALID_FILTER, 400); where its rows are inline, it answers no rows. Drop the key. Left as stored, ...". Both runs: rowskipped, two TODOs. Pin reading atdd3f7e1be356:filter-converter.ts:818callsrefuseEmptyOperatorMap, whoseFilterOperatorErrorhascode = 'INVALID_FILTER'andhttpStatus = 400;ValueDataSource.findanswers[]whenzeroKeyConditionRefusalreturns a refusal.Round 3, the inline list (the null-key printer probe, rows
null/u1/ missing). Lit at3fcedfb564: "... where its rows are inline (data: { provider: 'value' }, adataarray orstaticData), it selects the rows whoseowner_idis null. ...". Dark ata6e54de377: "... where its rows are inline (data: { provider: 'value' }orstaticData), it selects the rows whoseowner_idis null. ...". Pin reading atdd3f7e1be356:record-source.ts:303-307foldsstaticDatato{ provider: 'value', items }, and the value branches hand the resolved filter toValueDataSource.find(ObjectMap.tsx:950-952,ObjectCalendar.tsx:708-710,ObjectTree.tsx:911-913,ObjectGantt.tsx:1009-1010). A baredataarray reaches none of them:ObjectCalendar.tsx:387-389, 599-600, 647draws it with no fetch and no filter, and theview-dataarm refuses an array (record-source.ts:179). The round-1 Dark quote above is thea51c02fe83reading, before this narrowing.Tests and gates, at
6f1396efa2(this branch merged withorigin/main671d4c164fthroughscripts/pm/os-regen-merge.sh; the delta against main is exactly the five files above). Round 4's one-comment commiteaf2d6e6e8re-ran the conversions and migrations set (1034 passed), spec typecheck,check:generated(15 up to date),check:doc-authoringandcheck:issue-citations, all green; the derived gate list is unchanged:@objectstack/speclocalproject: 575 files, 16972 passed, 1 todo.typecheck, including the test layer, passed.repoproject, narrowed to the three files that read the conversion and migration registries (conversions-major18-merge,step18-rationale-merge,retired-key-migrate-sentence): 35 passed. The fullrepoproject did not finish inside the foreground cap and is NOT MEASURED locally; CI runs it.check:generated: all 15 artifacts are up to date against a spec rebuilt after the merge.dispatch-gates --commandsderived 87 commands. 84 exited 0, includingcheck:migration-registry,check:spec-changes,check:upgrade-guide,check:docs,check:api-surface,check:authorable-surface,check:objectui-pin-citations,check:doc-authoring,check:nul-bytesandcheck:adr-0087-registration. Three exited 3 with PREREQUISITE NOT MET, because they need a whole-workspace build:check:dual-build-cjs-loads,check:lean-entry-closureandcheck:type-check-debt. They are NOT MEASURED locally.--ranreconciles 87 derived: 84 run, 3 NOT-MEASURED, 0 unrun.scripts/ablation-replace.mjs, from the committed state): the anchoroperator: isNull })}was replaced so the reason named anequals/nullrule instead. The new test went red: expected the reason to contain{"field":"owner_id","operator":"is_null"}. The other five null-related tests stayed green. The file was restored: its blob matches HEAD andgit diff HEADis empty.bcda701b88: the anchor "block queries an object, it refuses the filter (INVALID_FILTER, 400); where its rows " was replaced with "block queries an object, it constrains nothing; where its rows ". The pin went red: expected the reason to containINVALID_FILTER. The file was restored: its blob matches HEADf1f29e6f4802andgit diff HEADis empty.Acceptance notes
owner_idset to null", so there was nothing to re-pin. The new pin checks named subjects: the same reason on both blocks, theis_nullrule, and that the door takes it. It does not pin prose. The empty-operator reason was the same: only its prefix was asserted.packages/spec/CHANGELOG.md's 17.5.0 entry carries the old null-key sentence. It is a released record and is not edited; the corrected text ships in this PR's changeset.Generated by Claude Code