feat(spec)!: dataSource.filter and the four object-* filter doors converge onto the ViewFilterRule array form — one filter orthography reaches the family (objectui#6206-B, option A) - #17257
Conversation
…ors onto the ViewFilterRule array Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
… for the filter converge Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…k each key at its own door in the binding pins Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
📓 Docs Drift Check10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 134 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 bbc4e2be0d6c47ffc1c42e184fffb7a23acdbc46 && git checkout bbc4e2be0d6c47ffc1c42e184fffb7a23acdbc46
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d64bcb63772b850b3ca8ae39779d492e46d3a0a6 ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97 && git checkout -B drift-repro d64bcb63772b850b3ca8ae39779d492e46d3a0a6 && git merge --no-ff ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97
node scripts/docs-audit/affected-docs.mjs --json d64bcb63772b850b3ca8ae39779d492e46d3a0a6 |
Contract review at
|
| # | check | reading |
|---|---|---|
| ① | at-tier PASS on record | PASS WITH FINDINGS, tier-fused above |
| ② | check-clause2-carriers.mjs --pair 17257 |
exit 0 on BOTH pairs (#15442 and #15449) |
| ③ | every check green — newest run per check name, ⛔ not the raw list, ⛔ not the required subset | 44 raw runs → 34 distinct names: 30 success, 4 skipped, 0 red (skipped: Auto Label, Check PR Size, Packed-tarball smoke, Console Pin Gate) |
origin/main mirror of the checker, not the working tree: the primary checkout sits on another branch and its copies of check-clause2-carriers.mjs and check-widening-tells.mjs differ from origin/main by git hash-object (check-governed-merges.mjs is identical). check-governed-merges.mjs --test over the full 17-path list → exit 0, NOT governed ⇒ ordinary queue landing applies to this PR.
⛔ Seat decision: HOLD before landing, for F1 and F3 — patch round dispatched
All three pre-checks pass, so this PR could be landed now. It is not being landed now, for two reasons, and both are measurements rather than preferences:
- ⭐ F3 is a coverage regression this PR itself introduced. It deleted the only
pagesrow fromvalidate-empty-combinators.test.tswhilepagesremains inEMPTY_COMBINATOR_SURFACES— the sole measurement of that rule's page walk. Deleting a pin is deleting a measurement, and a deleted measurement carded as a follow-up is a measurement nobody restores. It is ~5 lines in a file this PR already edits. F1 is one sentence with a precedent already in the tree. Both are cheaper now than ever again. - Sequencing, measured: this PR's file list and PR refactor(spec)!: delete the seven cron-typed positions outright — no tombstone, no D2, no D3 (#16320, ADR-0049, the 直接删 ruling) #17146's branch diff intersect in exactly one path —
packages/spec/src/migrations/registry.ts— and refactor(spec)!: delete the seven cron-typed positions outright — no tombstone, no D2, no D3 (#16320, ADR-0049, the 直接删 ruling) #17146'sos-regen-mergeround is in flight right now. Enqueuing this PR mid-round would movemainunder a round whose entire purpose is mergingmain. So the queue slot is not free of consequences even if the PR is.
⛔ F2 and the partial-D2 question are not in that round — see the ACCEPT list.
Findings
F1 — non-blocking (in the held round). migrations/entries/semantic/18.element-data-source-and-object-block-filter-rule-array.ts ~:28-60 states the sweep and the objectui route but is silent on metadata at rest — the one population its TODO cannot reach — os migrate meta --stored replays D2 conversions only and emits no D3 TODOs, so this entry’s prescription never reaches a stored row. (migrate meta over sys_metadata”, which is false; see the correction at the end.) Measured: the disposition is a SemanticMigration, which by its type (migrations/types.ts:30-41) converts nothing; no D2 conversion is added; applyConversionsToStoredItem (database-loader.ts ~:831) replays the chain without validating, so a stored row carrying the old record keeps loading, is still rendered by objectui at the pin, and is refused only on its next save. Fix: one sentence in reason, on the model of 17.view-filter-rule-value-shaped-by-operator.ts:38-50.
F2 — non-blocking, family-wide ⇒ card, ⛔ not a rider here. At all five doors an author submitting the old shape gets a bare zod invalid_type (filter, expected array; tuple → filter.0); the prescription lives only in .describe() and the D3 entry. The repo has a precedent for putting the prescription on the refusal itself (zod-v4 { error } at expression.zod.ts:158, view.zod.ts:4328, strict-object.ts:328), and the reviewer probed that z.array(ViewFilterRuleSchema, { error }) keeps code: invalid_type / expected: array, so the new pins stay green. The same gap exists at the two precedent doors (element:number, element:record_picker) — which is exactly why it is a family card and not a rider.
F3 — non-blocking (in the held round). packages/lint/src/validate-empty-combinators.test.ts ~:231-246, as above. Restore a page row and put page "home" back in the expected list. ⛔ Not by weakening the case.
F4 — informational, recorded so nobody hunts a missing diff. spec-changes.json and the upgrade guide show no diff correctly: both projections fold only to the current PROTOCOL_MAJOR (17), and this is a major-18 entry. Control: the neighbouring 18.ui-record-blocks-unknown-keys-refused is likewise absent, while 17.view-filter-rule-value-shaped-by-operator is present ×2 in spec-changes.json and ×1 in the upgrade guide. check:spec-changes / check:upgrade-guide exit 0.
F5 — non-blocking, pre-existing, generated. content/docs/references/ui/page.mdx:32 vs :277 render the nested rule's operator as required in one table and optional in the other (the generator's handling of a z.preprocess in a nested position). ⛔ Not this PR's.
What the review confirmed, so it is on the record
- Ruling
5559671308executed exactly — five doors onz.array(ViewFilterRuleSchema), one D3 entry folded intoregistry.tsstep 18, regenerated artefacts, the census twin pin over the whole map by shape, finding(spec): dashboardwidget.filter(dashboard.zod.ts:387,:720) still declaresFilterConditionSchema— a further location under ui#6206-B outsideComponentPropsMap, and the console's DashboardWidgetInspector authors exactly that record form #15829 anddefaultFiltersuntouched. Nothing required is missing; everything present beyond the letter is consequential (the four ADR-0122 aliases are forced by that ADR's gate), ⛔ not widening. - Clause ②
yesverified independently on both trees, not adopted from the dev: two-directional at the binding (record leaves, rule array enters), pure narrowing at the four doors, and the AST tuple newly accepted nowhere. minor+**BREAKING**is coherent here, and is the repo's ruled convention, not a sleight —pr-automation.yml:717-719: during the launch windowmajorstays refused and breaking-ness rides the banner plus the ADR-0087 disposition.check-changeset-no-majorexit 0,check-adr-0087-registrationexit 0, FROM → TO plus the one-line fix present in the changeset's Migration paragraph.- Prime Directive 13 grep done over 139 ADRs for
FilterCondition,ViewFilterRule,dataSource.filter, the four object-block names,defaultFilters,ComponentPropsMap,z.unknown():ViewFilterRule= zero, withFilterConditionlit in 5 ADRs as the control and every hit read. No accepted ADR records a decision this diff reverses ⇒ no new ADR and no amended status line owed. - The four deleted isomorphic pins are not lost measurements: each asserted
z.input === z.inferfor a schema where that is now false by construction (ViewFilterRuleSchema.operatoris az.preprocess), so the assertion would be a compile error; each is replaced by its ADR-0122 alias, and the runtime companion recomputes 811. - The fix commit strengthened assertions rather than erasing them —
issuesUnder(a prefix match that would have matchedfilter.0) replaced by exactissuesAt, and the two-keys case moved off a schema that treatspropertiesas a bag onto the realComponentPropsMapdoor.
File at ACCEPT (⛔ not folded into this PR)
- F2 family-wide: a prescription-carrying refusal at the rule-array
filterdoors — the five here pluselement:numberandelement:record_picker. 承接者:domain:specseat. - Partial D2 conversion for the filter-orthography family — whether the losslessly mappable subset (flat record,
{k:{$op}}, single-level tuples) should get a real conversion with only the groups left as the D3 TODO, so stored rows andmigrate metarewrite what can be rewritten. ⭐ This is a direction question, not a defect of this PR: the ruling prescribed D3 only, and both family precedents did the same. 承接者: director seat / maintainer. - objectui follow-ups already assigned by ruling
5559671308: the 17dataSource.filtertest authors at pin53ded82b;record-picker.tsx:369, which documentsdataSource.filteras aFilterConditionand says the spec rejects the array — now inverted; theelement-data-source.ts:11"three shapes" note; and docs(qa): refresh cli dev-boot DB clause to unified objectstack.db; narrow build exit clause to the #4873 leak #7712's kanban/calendar registry declarations. 承接者:domain:specseat, filed in objectui withBlocked-by:.
Not measured — UNJUDGED, ⛔ never clean
check:react-declaration-parity could not run (no sdui.manifest.json in the sibling at the pin — it is an objectui build output). Reasoned unaffected, since by its own header it compares declared prop names and filter is declared on both sides before and after — but reasoned-unaffected is not measured, and it is recorded as UNJUDGED rather than green. check:dual-build-cjs-loads is not a gap: it is hosted by the build-core job, which is green on this PR.
⛔ needs:contract-review stays on the PR and on both cards until the held round lands and this verdict is re-checked against the new head.
⛔ Correction (2026-09-10T01:55Z) — the independence claim above was mine, not a measurement
The line "Distinct sessions ⇒ ⛔ not a self-review" was wrong as written, and I am correcting it rather than leaving it to be relied on. It is fixed in place above; here is what happened and why it matters.
I asserted distinct sessions. I never measured one. The delta reviewer dispatched for the sibling PR #17249 — same mechanism, same seat — reported its own session id, and it is this seat's own: session_01MkQhmuuJAVDjmeWNixwDDH. It flagged that itself, unprompted. A subagent inherits the dispatching session's id, so this PR's reviewer and this PR's implementer share one session id, exactly as that one did.
What is actually true, stated at the strength the evidence supports:
- Reviewer and implementer were separate isolated subagents with no shared context, at different tiers, the reviewer measuring in its own detached worktrees and writing nothing. That is the protocol's own definition of an at-tier reviewer — 「达档者 = 显式传
model的契约复审档子代理(逐份过转录核验)」 — and this one was transcript-fused (113 harness"model":"claude-fable-5-1", zero residue, lit control). - ⛔ It is not the cross-seat independence the phrase "distinct sessions" claims. The stronger form is what the previous review on the sibling PR feat(spec): HookContext admits a row-invariant-in-effect rewrite by per-row previous on a predicate write, kept safe by the key-divergence refusal #17249 had: the director seat,
session_017Js5kTpTtxieBjPyScgxJ3— a different seat, a different session, not dispatched by the seat whose work it judged.
Does the verdict still stand? Yes — the protocol names an at-tier subagent as a qualifying reviewer, and the tier fuse is on record. ⛔ What does not stand is the claim of independence at the level I wrote it, and a landing decision should be read knowing which of the two kinds of review this was. Nothing else in this comment changes.
⭐ Recorded as a seat defect rather than a wording slip: an unmeasured assertion about who checked whose work is exactly the sentence a later reader has no way to falsify, and it is the same failure shape this seat has been penalising in others all shift.
⛔ Correction (2026-09-10T03:05Z) — two wording errors of mine in this comment, both found downstream
Both are fixed in place above. Recorded rather than silently edited, because a dev inherited one of them and shipped it into a migration entry.
1. F1's "since no author runs migrate meta over sys_metadata" — false, and it was mine. os migrate meta has a --stored mode whose one subject is exactly sys_metadata (packages/cli/src/commands/migrate/meta.ts, module doc "--stored: the same chain, over data at rest (#4327)", verified directly, control: authored appears 10× in the same file). What is true is narrower and is what the sentence was reaching for: the stored pass replays D2 conversions only and emits no D3 TODOs, so this entry's prescription never reaches a stored row, and with no D2 conversion the stored pass rewrites nothing for this shape.
reason, so 18.element-data-source-and-object-block-filter-rule-array.ts:61-62 (mirrored at registry.ts:6952-6953) now carries the false clause — which is why the delta review returned REWORK on a 13-word clause. ⭐ A defensible narrow reading exists ("an author cannot point --from N at a database"), and ⛔ this seat does not take it: the at-rest paragraph's audience is operators, for whom --stored is the tool, so the clause misleads precisely the reader it was written for.
2. F4's control count. 17.view-filter-rule-value-shaped-by-operator is present ×2 in packages/spec/spec-changes.json (:1006, :2085, both migrationId fields) but ×1 in docs/protocol-upgrade-guide.md (:561). I wrote "×2" of both. ⭐ The conclusion that control supported is unaffected and was re-measured on the new head: both major-18 ids are 0× in both files with the control lit in both. The dev caught this and the delta review confirmed it.
Not corrected, because they were right: two line-number offsets the dev also reported are real and are now fixed above — migrations/types.ts SemanticMigration is :30-41, not :29-40; entry 17's at-rest passage runs :38-50, not :38-47 (the shorter span cuts mid-sentence).
⭐ The pattern worth keeping, not the apology: both errors were found by people downstream of me re-measuring what I handed them instead of building on it. That is the behaviour the dispatch orders ask for, and it is the only reason a false sentence in a shipped migration entry is being caught before it lands rather than after.
Generated by Claude Code
…e the deleted `pages` pin
Contract review findings F1 and F3 on this PR.
F1 — the D3 entry stated the sweep and the objectui route but was silent on
metadata AT REST, the one population its TODO cannot reach: no author runs
`os migrate meta` over `sys_metadata`. Measured, all three legs: a
`SemanticMigration` converts nothing by its own type (`migrations/types.ts`),
this disposition adds no D2 conversion (`packages/spec/src/conversions/` is
untouched by this branch), and `applyConversionsToStoredItem`
(`database-loader.ts:831`) replays the full chain without validating, by its
own contract. So a stored row carrying the record form keeps loading, is still
rendered by objectui at the pin, and is refused only on its next save. One
sentence added to `reason`, on the register of
`17.view-filter-rule-value-shaped-by-operator.ts:38-47`, which states the same
disposition for its own family. `registry.ts` follows by `gen:migration-registry`
— that one sentence is its whole delta.
F2 and the partial-D2 direction question are deliberately NOT in this commit:
F2 is family-wide (it also reaches `element:number` and `element:record_picker`)
and the D2 direction is the ruler's, not a dev's. The new sentence therefore
states that this disposition adds no D2 conversion, and does not claim none is
possible.
F3 — this branch had dropped the only `pages` row from the "covers ..." case
while `pages` remains in `EMPTY_COMBINATOR_SURFACES`, leaving that surface
declared and unmeasured. Restored byte-identical to the base tree, plus a
comment recording that the row authors the retired record form on purpose:
this rule never parses a fixture against the spec schema — `walkAuthoredFilters`
visits any `FILTER_KEYS` key it reaches, whatever its shape — so an author still
writing the record form is judged here and gets a located error beside the
schema refusal. Ablation: dropping `{ key: 'pages', kind: 'page' }` from
`EMPTY_COMBINATOR_SURFACES` turns the case red losing exactly `page "home"`
(21 passed -> 1 failed | 20 passed); the rule file was restored to its HEAD blob
and verified by `git hash-object`.
No generated artefact moved beyond `registry.ts`: `check:generated` reports all
15 up to date, and `spec-changes.json` / the upgrade guide correctly show no
diff because both projections fold only to the current PROTOCOL_MAJOR (17) and
this is a major-18 entry — measured against a lit control (the major-17 entry
`view-filter-rule-value-shaped-by-operator` is present in both; the neighbouring
major-18 `ui-record-blocks-unknown-keys-refused` is absent from both).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
Delta review at
|
| check | reading | |
|---|---|---|
| ① | at-tier verdict on record for this head | ⛔ not satisfied — this document is REWORK for the clause above |
| ② | --pair 17257 |
exit 0 on both pairs (#15442, #15449) |
| ③ | every check green, newest run per check name | 32 success, 2 skipped, 0 red (34 names, each once) |
② and ③ are green now; ① turns on that 13-word deletion plus gen:migration-registry, which one short round lands.
packages/spec/src/migrations/registry.ts.
⛔ needs:contract-review stays on the PR and both cards.
At ACCEPT — unchanged, plus one seat-side item
Nothing new. F2 (family-wide prescription-carrying refusal) → domain:spec seat. The partial-D2 direction question → director seat / maintainer, and ⭐ once the clause above is fixed it is left fully open rather than half-answered by a stray sentence.
⛔ Correction (2026-09-10T03:22Z) — a citation in this comment is wrong, and it is mine
The line "the replay vector is MigrationStep.conversionIds, and step 18's is [] at registry.ts:351" is false. Measured on origin/main:
registry.ts:351is theconversionIds: []of a much earlier step — its ownreasonsays the shape it catches "was already removed at protocol 9".- Step 18's
conversionIdsis at ~:5406and is NOT empty — it carries the major's other D2 conversions (field-malformed-scale-precision-removed,element-filter-removed,record-chatter-position-vocabulary, and more), and its neighbouringreasonnamesRETIRED_KEYS_BY_MAJOR[18].
⭐ The conclusion it was supporting still holds, and it was re-established a different way rather than patched over: this entry's id is 0× in packages/spec/src/conversions/registry.ts, with the control action-execute-to-target lit at 1× — so this disposition adds no D2 conversion, which is what the sentence in the entry claims.
conversionIds is not empty, "here" reads as "the stored pass rewrites nothing at protocol 18" — false. The dev caught it and wrote "…nothing to rewrite for this shape" instead, correctly scoping it to this entry's own disposition. ⛔ That is a second sentence-level defect of exactly the kind this round existed to prevent, and it was avoided only because the dev re-derived the citation instead of trusting it.
The citation came into this comment from the verdict I adopted; I published it, so it is mine to correct.
Generated by Claude Code
…lter-orthography-converge
…red` does exist The entry said "no author runs `os migrate meta` over `sys_metadata`". That is false: `os migrate meta --stored` has exactly that subject (packages/cli/src/commands/migrate/ meta.ts — module doc "`--stored`: the same chain, over data at rest", the flag, `runStored`, `protocol.migrateStoredMetadata(...)`). The true and narrower fact the clause was reaching for: the stored pass replays ADR-0087 D2 conversions only — `migrateStoredMetadata` converts through `applyConversionsToStoredItem`, i.e. `applyConversions`, and emits no D3 TODOs (`todos` is 7x in the authored-mode half of meta.ts and 0x from `runStored` to EOF) — so with no D2 conversion added by this disposition it has nothing to rewrite for this shape. The scoped "this disposition adds no D2 conversion" is unchanged and stays scoped: the partial-D2 direction question is the ruler's, not this entry's. `registry.ts` follows by `gen:migration-registry`; no other sentence of the entry moved. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
… protocol 18 `conversionIds` on step 18 is NOT empty — it carries the major's other D2 conversions — so "nothing to rewrite here" could be read as "the stored pass rewrites nothing at protocol 18", which is false. The true scope is this entry's disposition, which adds no D2 conversion of its own (its id is 0x in `conversions/registry.ts`, control `action-execute-to-target` lit at 1x). `registry.ts` follows by `gen:migration-registry`. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
Contract review at
|
| # | check | reading |
|---|---|---|
| ① | at-tier verdict for this head | PASS (seat-side at-tier) |
| ② | --pair 17257 |
exit 0 on both pairs, from an origin/main mirror |
| ③ | every check green, newest run per name | 32 success / 2 skipped / 0 red, 34 names, all terminal |
check-governed-merges --test: the 17-path list is unchanged from the round that read NOT governed ⇒ ordinary queue landing.
⛔ Why it is still not enqueued, and how that ends
One reason only: PR #17146 is a governed PR sitting with the maintainer for a human merge, and it shares packages/spec/src/migrations/registry.ts with this branch. That file is generated but ⛔ not routed to merge=os-regen, so a clash there is an ordinary visible conflict — not a silent drop. If this PR lands first, #17146 needs another os-regen-merge.sh round before it can be merged by hand, which re-creates the state it was just repaired out of.
⇒ This seat committed to holding and to saying so before releasing. This is that notice: #17257 is PASSed, green and landable; the hold is now the only thing between it and the queue, and it is spent the moment #17146 merges. ⛔ The seat will not enqueue unilaterally — one word releases it, and the cost of releasing is one mechanical regen round on #17146, ⛔ not a re-review.
#17121 is an open p1 whose fix PR #17282 is open and unmerged. Test Core (4/6) is green on this head, and this diff touches packages/drivers/ 0× — but that shard is where the signature fires, so it remains a queue-ejection risk on the merge-queue full-suite run. ⛔ Not a finding against this PR.
At ACCEPT — unchanged
F2 of the standing verdict (prescription-carrying refusal at the seven rule-array filter doors, 承接者 domain:spec seat); the partial-D2 direction question (承接者 director seat / maintainer); the objectui follow-ups from ruling 5559671308 (承接者 domain:spec seat, filed in objectui with Blocked-by:).
Generated by Claude Code
…ropped `packages/spec/api-surface/ui.json` and `packages/spec/export-origins/ui.json` are routed to `merge=os-regen`, and both sides of the merge had edited them, so the driver merged them with exit 0 while keeping one side. The dropped side was main's: #17257 added four `*Parsed` type exports on the `ui` entry point (`ElementDataSourceParsed`, `ObjectCalendarPropsParsed`, `ObjectKanbanPropsParsed`, `ObjectMetricPropsParsed`), and the merge commit's copy of both artifacts carried none of them. Regenerated from the committed merge — `pnpm --filter @objectstack/spec build` then the gen chain — so the content is re-derived from the merged source rather than hand-reconciled. Both files gain exactly those four rows and nothing else; every other generated artifact this branch touches (migration registry, authorable surface, spec-changes, upgrade guide, liveness counts, react-blocks, the reference docs) regenerated byte-identical to the merged tree. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
Fixes #15442
Fixes #15449
Clause-②: yes
What moves
One filter orthography platform-wide (objectui#6206-B; decision batch #55, ruling
5559671308, option A) reaches the two locations theComponentPropsMapcensus could not see — as ONE family, one ADR-0087 D3 entry:ElementDataSourceSchema.filter(page.zod.ts, thedataSourcebinding every data-bound element carries)FilterConditionSchema— the MongoDB-style record; every array form refusedz.array(ViewFilterRuleSchema)ComponentPropsMap['object-grid'].filterz.unknown()— record, AST tuple array and rule array all accepted silentlyz.array(ViewFilterRuleSchema)ComponentPropsMap['object-metric'].filterz.unknown()z.array(ViewFilterRuleSchema)ComponentPropsMap['object-kanban'].filterz.unknown()z.array(ViewFilterRuleSchema)ComponentPropsMap['object-calendar'].filterz.unknown()z.array(ViewFilterRuleSchema)element-data-source-and-object-block-filter-rule-array(packages/spec/src/migrations/entries/semantic/18.…), registry /spec-changes.json/ upgrade guide regenerated with the repo tooling.FilterConditionSchemaleavespage.zod.tswith its one site. ADR-0122 aliases declared:ElementDataSourceParsed,ObjectMetricPropsParsed,ObjectKanbanPropsParsed,ObjectCalendarPropsParsed(operatornormalizes on parse, so input ≠ infer now); the four isomorphic pins deleted (815 → 811).content/docs/references/ui/{component,page}.mdx,api-surface/ui.json,export-origins/ui.json(viacheck:generated --fix;authorable-surfaceunchanged).@objectstack/specminor with the**BREAKING**banner and theadr-0087: registered …disposition (check-adr-0087-registrationexit 0).⛔ Not touched, on purpose:
object-grid.defaultFilters(a different key,z.unknown(), not named by the ruling — recorded on #11509 for the ruler); the dashboardwidget.filterfamily (#15829); the four already-array doors (record:related_list, its nested picker,element:number,element:record_picker).Premise re-derived on
origin/mainae19f5edb(the branch base)page.zod.ts:169readfilter: FilterConditionSchema.optional().describe('Additional filter criteria')— import at:9, the only two occurrences in the file (read, not counted).filter: z.unknown().optional()atcomponent.zod.ts:2466(grid) /:2624(metric) /:2672(kanban) /:2763(calendar) — lines moved from the card; the describe strings matched the card..objectui-sha=53ded82bf7a494f54e344e19099dbf00854b8694— the fix commit of objectui#7754 itself, so the sequencing gate the flight before this one measured false (chore(console): bump the objectui pin past 53ded82b — the shipped console refuses every array analyticswhere, which blocks the ruled #15442/#15449 filter converge #16626) is discharged.Measured at the objectui pin
53ded82bbefore the declarations moved (read fromgit show 53ded82b:…in the sibling checkout; read, not run)core/src/data-scope/element-data-source.tstypesfilterasunknownand AND-combines it with the named view's rules viamergeFilterNodes;core/src/utils/filter-converter.tstoFilterNodemaps eachViewFilterRuleto an AST node (isViewFilterRulearm). No non-test file at the pin imports the spec'sElementDataSourcetype (git grep, zero hits; control: the same grep with the test dirs included hits the*.elementDataSourcesuites).object-grid:ObjectGrid.tsx:1455-1456lowersschema.filterthroughtoFilterNode.object-kanban/object-calendar:ObjectKanban.tsx:263andObjectCalendar.tsx:430handschema.filterverbatim to$filter;data-objectstack/src/index.ts:3971runstranslateFilterArray, whose first armisObjectFilterEntryForm(:350-357) takes[{ field, … }].object-metricwithaggregate(all five showcase tiles):ObjectMetricWidget.tsx:230resolves placeholders (resolveFilterPlaceholderswalks arrays and objects alike,filter-tokens.ts:209),:247callsds.aggregate, and the adapter'slowerAnalyticsFilterForWire(index.ts:510-540) lowers an authored array throughtranslateFilterArray+isFilterAST+parseFilterASTbeforePOST /analytics/query— the door runtime:POST /analytics/queryrefuses the arraywherethe objectui adapter now sends for every array-form filter —AnalyticsQueryRequestSchema.whereisFilterConditionSchema, whilelowerAnalyticsWhere(the gate ui#6302 measured) accepts filter AST #15828 was about.shiftFilterByCompareTo(compare-to.ts) substitutes tokens through arrays too.The four spec fixtures (ruling said three; the parked flight found the fourth) — all 改拼写
page.test.ts:633(directElementDataSourceSchema.parse)filter: { status: 'pending' }[{ field: 'status', operator: 'equals', value: 'pending' }]page.test.ts:665-666{ status: 'completed' }page.test.ts:762{ status: 'pending' }page.test.ts:767{ status: 'completed' }Two more spec fixtures the sweep counted under no heading, at the object-* doors (
component.test.ts"#7751" block: the corrected #7750 grid node with the AST tuple, and the three showcase metric records in the clean-corpus control) — migrated the same way; they mirror the showcase authors.Lint fixtures (3):
validate-component-props.test.ts:483(grid tuple → rule) and:488(metric record → rule) 改拼写;validate-empty-combinators.test.ts:240整个换掉 — the page row is dropped from the "covers …" case (no page-level key carries the record form any more, and the rule judges record nodes only),pagesstays inEMPTY_COMBINATOR_SURFACES.Showcase authors (5):
my-work.page.ts:39/:40/:41andindex.ts:78(metric records → rule arrays),my-work.page.ts:59(grid AST tuple → rule) — plus the showcase's own pintest/my-work-visibility.test.ts:190, which authored the tuple.New pins
page.test.ts: the binding accepts the rule array (operator normalization and value-shape check prove the realViewFilterRuleSchema), refuses the record atfilter(invalid_type, expected array — the envelope, not a bare throw), refuses the AST tuple array atfilter.0, andelement:record_picker's two keys now give the same verdict each at its own door.component.test.ts: per door (it.eachover the four) accept / normalize / refuse-record-at-filter/ refuse-tuple-at-filter.0; plus the ruled twin of the finding(spec):element:record_picker.filterstill declares the MongoDB-styleFilterConditionSchema— the last record-formfilterinComponentPropsMapafter the ui#6206-B convergence #14406 census pin — everyfilterdoor inComponentPropsMaprefuses the record, asserted over the whole map by shape.Clause ② — measured on the finished diff
yes, and two-directional at the binding: the record leaves the accept set and the rule array enters it (before: rule array refusedinvalid_type expected record; after: accepted). At the four doors it is a pure narrowing of a published accept-anything door. The AST tuple array is not "newly accepted" anywhere — it was refused at the binding before and stays refused (one hop deeper,filter.0), and it leaves the four doors' accept set.check-clause2-carriers.mjs --pairexit code: see the report comment on #15442.Verification
All exit codes were redirected to disk before being read (⚠️ the head at the time of this round; two later rounds have moved it — see “Head of record” at the end).
cmd > log 2>&1; echo $? > file), never through a pipe. Head verified:6274af365(Builds (under
scripts/pm/os-verify-lock.sh, slotissue-15442-converge):pnpm --filter @objectstack/spec build—VERDICT command-exit 0 · held the lock 151s · waited 374s.pnpm turbo run build --filter='@objectstack/lint^...' --filter='@objectstack/example-showcase^...' --concurrency=2—command-exit 0,Tasks: 61 successful, 61 total(7m13s held).srcafter the test-file edits:pnpm --filter @objectstack/spec buildexit 0 (stamp now matchessrc), tree clean after it.Package suites, full, never a subset (one lock acquisition, six commands, six exit files, all
0on6274af365):@objectstack/specpnpm test:Test Files 469 passed | 1 skipped (470),Tests 13238 passed | 1 skipped (13239), 385s.pnpm typecheck(tsc + scripts + test-typecheck): exit 0,check:test-typecheck: OK … 54 file(s) / 261 error(s) / 145 pinned signature(s)(the pre-existing debt ledger, unchanged).@objectstack/lintpnpm test:Test Files 103 passed (103),Tests 3680 passed (3680).pnpm typecheck: exit 0.@objectstack/example-showcasepnpm test:Test Files 28 passed (28),Tests 379 passed (379).pnpm typecheck: exit 0.component.test.tsand the two corrections to my own new pins) was red 4/13240 — the failures are what drove commit6274af365; the targeted re-run of the two files was 349/349 and the full suite above is the measurement.Reverse verification (script with
trap … EXIT INT TERM, absolute paths, run on the committed state under the lock):@objectstack/specdist/): mutatedvalidate-component-props.test.ts:488back tofilter: { status: { $ne: 'done' } }(on-disk anchors: injected 1 / removed 0) →vitest run src/validate-component-props.test.tsexit 1,1 failed | 44 passed, the failing case named:stays SILENT on the corrected #7750 node and the corpus metric shapes (clean-corpus control). Restoregit checkout HEAD -- PATH:git diff HEADempty (exit 0),git hash-object= HEAD blob776b8b641dfc…(equal); re-run 45/45.tsc --noEmit→ the rebuilt.d.ts): a probe file assigning the record form toElementDataSourceandObjectMetricProps, the AST tuple toObjectGridProps, and the rule array toElementDataSource→ exit 1 with exactly three errors, one per off-spec line (TS2353 'status' does not exist in type '{ field: string; operator: …'×2,TS2322 Type '[string, string, string]' is not assignable), zero on the rule-array line; probe deleted → exit 0;git status --porcelain0 lines.node scripts/ablation-dist-preflight.mjs @objectstack/spec 'element-data-source-and-object-block-filter-rule-array'→marker present in 18 built files, tree clean.Gate families — derived, not hand-fed:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(stderr line 1:derived from the tree of 'objectstack-ai/objectstack' at commit 6274af365,--repo … holds), 108 commands, each run to its own log with its exit in a ledger, then--ranreconciled:✓ dispatch-gates --ran: 108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED(the reconciler counts commands run, not exit codes).check:api-surface,check:dual-source-exports,check:entry-nameability,check:exported-any,check:generated,check:skill-examples) refused on their own prerequisite —dist/.build-input-hash-dtsrecorded the pre-test-editsrcdigest — so their first-pass exit 1 is NOT MEASURED; after the rebuild all six exit 0 with their own verdict lines:public API surface + factory signatures unchanged ✓,no new dual-source exports,entry-nameability … 0 new unnameable,no exported type resolves to any,All 15 generated artifacts are up to date,258 prose examples type-check.pnpm check:dual-build-cjs-loadsexit 3PREREQUISITE NOT MET— seven packages my diff never touches have nodist/in this container (@objectstack/studio,client-react,embedder-openai,knowledge-memory,knowledge-ragflow,organizations,service-cluster-redis): NOT MEASURED locally, CI builds them.pnpm --filter @objectstack/spec run check:react-declaration-parityexit 1:MANIFEST is not set — there is no registry side to compare against; the gate's own text says a non-zero exit is a reading, not an unavailable input. It is the on-demand pin-bump gate (AGENTS.md: never a CI job) and needs objectui'ssdui.manifest.jsonfrom a browser run; not produced here. Reported as exit 1 with that reason.check-adr-0087-registrationexit 0 (registered element-data-source-and-object-block-filter-rule-array),check:nul-bytesexit 0.PR CI (newest run per check name, read at report time): head
6274af365, 33 distinct check names — skipped: Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke (opt-in); success: Build Core, Build Docs, Check Changeset, Check Documentation Links, Dogfood Regression Gate, Dogfood Regression Gate (1/3), Dogfood Regression Gate (2/3), Dogfood Regression Gate (3/3), Dogfood Verify CLI, Flag docs affected by code changes, Governed Surface Queue Guard, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Temporal Conformance (live PG + MySQL), Test Core (1/6), Test Core (2/6), Test Core (4/6), Test Core (5/6), Test Core (6/6), Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter; in_progress: Lint & Repo Gates, Test Core (3/6). Nothing red at read time;in_progressis recorded as such, not as green.验收备注
pagesinEMPTY_COMBINATOR_SURFACES(validate-empty-combinators.ts) now has no spec-legal record filter to judge on a page — the walker is key-name based, so an author writing the retired form still gets the located error beside the schema refusal; 承接者:无.page.mdxrenders the nested rule'soperatoras optional in thePageComponent.dataSourcerow and as required in theElementDataSourcerow — how the docs generator prints az.preprocessin a nested position, pre-existing for every other rule-array door; 承接者:无.dataSource.filtertest authors at the pin; docs(qa): refresh cli dev-boot DB clause to unified objectstack.db; narrow build exit clause to the #4873 leak #7712) are not filed here.维护者速读(草稿)
改了什么:
dataSource.filter(每个数据绑定元素都带的绑定键)和object-grid/object-metric/object-kanban/object-calendar四个块的filter,五个门统一收敛到平台的ViewFilterRule数组形([{ field, operator, value }]);MongoDB 风格的 record 形和 ObjectQL 元组数组在这五处不再被接受。一条 ADR-0087 D3 迁移条目覆盖整个家族;仓内 4 条 spec 夹具、2 条 object-* 夹具、3 条 lint 夹具、5 个 showcase 作者同 PR 迁移。为什么改:裁决
5559671308(批次 #55,「同意」,方案 A)——「全平台一种过滤器拼写」的原则触达 ComponentPropsMap 普查看不到的两处。绑定键原本只收 record,拒绝消费方自己的 pin 写的数组;四个块门是z.unknown(),什么都收:跟着 showcase 写 record 和跟着 manifest 写数组的作者各得到一张错的静默回执。风险与代价(含回滚):已发布契约收窄(spec
minor+ BREAKING 横幅,D3 条目带 FROM → TO 处方)。运行时按 objectui pin53ded82b实测读点:四个块和绑定都把 rule array 降型到同一条 AST 路径,object-metric聚合线路经parseFilterAST落到 analyticswhere(#15828 的门已在该 pin 修好)。回滚 = revert 本 PR(生成物随check:generated --fix回退);对外已发布版本的作者靠 D3 条目迁移。席位意见:(留空)
你要做的:确认
needs:contract-review的合同审阅;确认object-grid.defaultFilters是否并入家族(#11509 上的方向问题,本 PR 未动它)。Head of record (added 2026-09-10T03:30Z by the
domain:specseat)ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97. Everything above is the round-1 record at6274af365and is accurate as history — ⛔ but the clause "the PR head; no commit after it" is no longer true of it, and is corrected here rather than rewritten above.Two rounds landed after that record, each for a finding of the at-tier contract review:
3d7f4eda0— the D3 entry gained its metadata-at-rest sentence, and the deletedpagesrow was restored topackages/lint/src/validate-empty-combinators.test.ts(with an ablation proving the restored pin can fail).816f181e1+ad7dcbc0f— one false clause in that at-rest sentence replaced: it had said "no author runsos migrate metaoversys_metadata", which is false becauseos migrate meta --storedexists and its subject is exactlysys_metadata. It now says the stored pass replays D2 conversions only, so it has nothing to rewrite for this shape.CI at the current head, newest run per check name: 34 distinct names — 32 success, 2 skipped (
Console Pin Gate,Packed-tarball smoke (opt-in)), 0 red, all terminal.check-clause2-carriers --pair 17257→ exit 0 on both pairs;check-governed-merges --testover the unchanged 17-path list → exit 0, NOT governed.Generated by Claude Code