feat(spec): the seven converged rule-array filter doors name the array form when they refuse the record form - #17846
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…uses the record form Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…lter-rule-array-guidance
…lter-rule-array-guidance
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 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 a1c25d57bb30baba5d068b6a926da93664d0499e && git checkout a1c25d57bb30baba5d068b6a926da93664d0499e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6059b29c03e470dbaa513cc3f0992a8e610855be cdfd8d142add46f581e020ca621f27a8cf025f1f && git checkout -B drift-repro 6059b29c03e470dbaa513cc3f0992a8e610855be && git merge --no-ff cdfd8d142add46f581e020ca621f27a8cf025f1f
node scripts/docs-audit/affected-docs.mjs --json 6059b29c03e470dbaa513cc3f0992a8e610855be
|
Seat review of head
|
| # | check | reading |
|---|---|---|
| ① | at-tier verdict | not owed — Clause-②: no |
| ③ | check-governed-merges --test over the five final paths |
exit 0 · ⭐ control with .claude/skills/pm-dispatch/SKILL.md appended → exit 3 GOVERNED |
| ④ | every check, newest run per name | 35 names, 0 pending, 0 failing |
| — | carrier | needs:contract-review absent on the PR and on card #17320, checked before any flip |
What the round measured, accepted as read
Seven doors, seven separate readings before and after against the built artifact — not one generalised — each still producing exactly one issue at filter, now naming the array form, the author's own key lifted into field, the canonical operator and the migration id. Three door-shaped negative controls confirm the ordinary messages are untouched: a bad element still reports at filter.0.operator in zod's own words, a string still gets expected array, received string, and a valid rule array is accepted at all seven.
⭐ Two things in the ablation are worth keeping. It has two legs, and they differ by exactly one case — unwiring the doors reds 12, neutralising the helper reds the same 12 plus the helper's own unit pin, which leg A cannot reach. And leg B's first attempt did not happen: a quoting error wrote nothing, the on-disk marker count came back 0, and the guard refused the run rather than reporting a green ablation over an unmutated tree. ⛔ That is the vacuous pass caught by its own control, and it was reported rather than silently retried into existence.
⭐ The dispatch order's load-bearing claims were all re-derived and held — the first round today where nothing in my order turned out to be wrong. The one nuance it added rather than corrected: the pins resolve src/, so no rebuild leg was needed, and it said so instead of assuming it.
Landing
Flipping ready, re-reading ④ after the flip, arming, then confirming the queue ref.
Generated by Claude Code
Fixes #17320
Clause-②: no — the change is to what a refusal says. No accept set moves in either direction, no key is added to any published payload, and the generated
json-schema/+authorable-surfaceartifacts are byte-identical after the change (git statusclean across two full@objectstack/specbuilds).check:api-surfaceis green with no regeneration: the helper is module-internal and is not exported from any barrel, exactly as its modelshared/strict-object.tsis not.What was wrong
Seven
filterdoors converged onz.array(ViewFilterRuleSchema)in the objectui#6206 family. On the record form an author used to write, each produced exactly one zod issue and nothing else — measured on the built artifact before the change, at all seven:The prescription was already written down twice, in two places a parse never reaches: every one of the seven
.describe()strings, and in full in the three18.*-filter-rule-arraysemantic migration entries. Nothing bridges.describe()into a zod issue, and this package installs no global error map. Re-verified on this branch's own head with the card's own lit control:setErrorMap/z.configunderpackages/spec/src→ 0 hits;strictObjectError(which does exactly this bridging for the unknown-key case) → 14 hits, so the probe reaches.The population at these doors is the authors — human and AI — whose previously-legal metadata the convergence broke, which is when a refusal most needs to name the new spelling.
The seven doors, located by declaring symbol
Each was re-derived from the tree rather than trusted from the card; the card's list is correct and complete. The marker that separates a converged door from a door that was always an array is the migration pointer in its own
.describe()—The MongoDB-style record form is refused — see migration ...— which occurs exactly 7 times in non-test sources, in exactly these two files:ElementDataSourceSchema.filterpackages/spec/src/ui/page.zod.tsobject-gridObjectGridPropsSchema.filterpackages/spec/src/ui/component.zod.tsobject-metricObjectMetricPropsSchema.filterpackages/spec/src/ui/component.zod.tsobject-kanbanObjectKanbanPropsSchema.filterpackages/spec/src/ui/component.zod.tsobject-calendarObjectCalendarPropsSchema.filterpackages/spec/src/ui/component.zod.tselement:numberElementNumberPropsSchema.filterpackages/spec/src/ui/component.zod.tselement:record_pickerElementRecordPickerPropsSchema.filterpackages/spec/src/ui/component.zod.tsThe six
ComponentPropsMaprows name those schemas, so the card'sComponentPropsMap['x'].filterspelling and the symbol spelling are the same door. Four otherz.array(ViewFilterRuleSchema)keys exist inui/(RecordRelatedListProps.filterand its Add-affordance picker,ViewTabSchema.filter,ListViewShapeSchema.filter,FormFieldPublicPickerSchema.filter,ListPageSchema.filterBy) — none carries the migration pointer, because none of them ever took the record form. They are out of this card's population and are untouched.The shape
shared/strict-object.tsis the model, for the reason the card gives: guidance derived from the schema rather than transcribed beside it, so it cannot drift. A hand-copied sentence at seven sites is what that argues against — and this card's own subject is a prescription that fell out of step with a refusal.New module
packages/spec/src/ui/filter-rule-array.tsexports one helper, wired through the zod-v4{ error }param at all seven doors. Everything it can derive, it derives:[{ field, operator, value }, ...]is read fromViewFilterRuleSchema's own shape (_zod.def.shape), on first refusal — never at module load, which would force thelazySchemawhileview.zodis still initialising underOS_EAGER_SCHEMAS=1, the import-cycle footgunstrictObjectErroralready defers around;normalizeFilterOperator('eq'), the same fold the door itself runs;What stays per-call is what carries judgement rather than transcription — the same split
strictObjectdraws:surfaceand themigrationid. Both are pinned: the test holds everymigrationequal to a real entry inMIGRATIONS_BY_MAJOR, and holds every wired door'ssurfaceequal to the one its ownstrictObjectdeclaration registered (walked out ofstrictObjectDeclarations(), with a lit control that the walk reached all seven).Fall-through is deliberate and pinned: the map answers only a plain record and returns
undefinedfor everything else, asflattenedViewOverlayFields()does. A blanket message would overwrite the element-level issues an array author needs, which is the diagnosis this change exists to protect.Before / after, per door, on the BUILT artifact
Seven separate readings, taken by parsing against
packages/spec/dist/ui/index.mjs—pnpm --filter @objectstack/spec buildrun to completion (both tsup passes;check-dts-emitted: 34/34) before each side.Before — every door, exactly one issue:
[invalid_type] path=["filter"] expected=array, "Invalid input: expected array, received object".After — every door, still exactly one issue, still
invalid_typeatfilter, now saying (this isobject-grid; the other six differ only in the surface and, forelement:number/element:record_picker, the migration id):The message names the new spelling: the array form, the author's own field lifted into
field, the canonicalequals, and the entry id.Per-door surfaces after the change:
this element data source·this `object-grid`·this `object-metric`·this `object-kanban`·this `object-calendar`·this `element:number`·this `element:record_picker`. Per-door migration ids: the fiveelement-data-source-and-object-block-filter-rule-arraydoors, pluselement-number-filter-rule-arrayandelement-record-picker-filter-rule-array.Door-shaped negative controls, all seven, after the change — unchanged from before:
filter.0.operator, "Invalid option: expected one of "equals"|"not_equals"|…" — zod's own words, no guidance text, and the array door itself says nothing atfilter;Ablation
The pins resolve
src/, notdist/:filter-rule-array-guidance.test.tsimports./component.zod,./page.zod,./view.zodrelatively, andpackages/spec's vitest config declares no alias that would route them elsewhere. So no rebuild leg is needed, and both legs below changed the verdict from a source-only mutation, which is itself the proof.Leg A — restore the two door files to their pre-change bytes (
git checkout BASE -- THE_TWO_DOOR_PATHS), i.e. the helper exists but nothing is wired:ruleArrayFilterErroroccurrencespage.zod.ts2 → 0,component.zod.ts7 → 0;Leg B — neutralise the helper itself (one injected early
return undefined):Both legs restored and proven restored by
git hash-objectagainst the HEAD blob, not assumed from an exit code: leg A3c4942e3cbfd8071eb372b9dec91dced7682c1d1/65eb6d491b12e9879238bafa03c7c127e0e9ae47, leg B9b2f92cf38f5d3fc6534f87e6b637825319d100a, each equal togit rev-parse HEAD:PATH, withgit diff HEADempty for those paths afterwards. Both scripts carriedtrap RESTORE EXIT INT TERMwith absolute paths resolved fromgit rev-parse --show-toplevel, and both treated an empty or mismatched hash as a loud failure. Leg B's first attempt is worth recording: aperl -0piquoting error wrote nothing, the marker count came back 0, and the guard refused the run rather than reporting a green ablation over an unmutated tree.Changeset
Measured, not assumed. Both tsup passes confirmed finished before the reading (
check-dts-emitted: @objectstack/spec - 34/34), thennpm pack --dry-run --json— 2012 packed files:dist/*/index.js|.mjs, browser builds included);src/ui/page.zod.tsandsrc/ui/component.zod.tsare additionally shipped as source byfiles[]'ssrc/**/*.zod.ts. The new helper and the new test are not packed. ⇒ published text moves ⇒.changeset/17320-filter-rule-array-guidance.md,patch.Verification
pnpm --filter @objectstack/spec test— 472 files / 13426 tests passed.pnpm --filter @objectstack/spec typecheck— green (tsc --noEmit,check:scripts-typecheck,check:test-typecheck: the test layer compiles, 54 files / 259 pinned errors held).pnpm lint(eslint . --no-inline-config, repo-wide, no narrowing) — green.node scripts/pm/dispatch-gates.mjs --commands→ 82; all 82 run with the exit code captured before any pipe;--ranreconciliation: 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero — every family recorded a code and none is 3). Five needed a second, correct invocation and are reported at their real reading, not their first:check:doc-formula-expressions,check:lean-entry-closure,check:dual-build-cjs-loadsandcheck:type-check-debteach exited 3 = PREREQUISITE NOT MET = NOT MEASURED and were re-run after building the closure they named (the last two after a fullturbo run buildover every workspace package);check:react-declaration-parityexited 1 only becauseMANIFESTwas unset, and is green run as CI runs it, with the baseline ratchet clean.[\x00-\x08\x0b\x0c\x0e-\x1f\x7f], with the lit control (same engine, same file list, class widened by one printable byte) hitting 149 / 209 / 840 / 3133 / 48.pnpm check:nul-bytesgreen.#+ digits 0, model identifiers 0 (the only matches for a deliberately over-broad model pattern are the two requiredClaude-Session:trailers).Gate sweep derived and run at
cea666718f;origin/mainwas merged once more afterwards (cdfd8d142a, disjoint files) and the pin file re-run green on that head. Mergedorigin/mainbefore opening, as asked — PR #17835 is parked onui/page.zod.tsand its hunks are untouched.验收备注
Out of scope, noted, not filed — no PR or person is queued to touch these files for these reasons:
see migration ...ids that seven.describe()strings already carry; a renamed or deleted entry would strand all seven silently. This change's ownmigrationids are pinned against the registry, so the coupling is checked on the new channel but not on the old one. Carrier: none today.invalid_typeatfilter.0, raised byViewFilterRuleSchemaitself rather than by the array door. Out of this card's population (the array door is the subject), and deliberately left alone so the element-level diagnosis stays zod's.objectStackErrorMap(shared/error-map.zod.ts) does exist and does handleinvalid_type— it is opt-in per parse (safeParsePretty), never installed globally. The card's "no global error map" reading is exact as written; this is a note that the package is not entirely without one, in case a future round looks for a home for cross-cutting guidance.Generated by Claude Code