You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(spec): ElementDataSourceSchema.filter (page.zod.ts) still declares the MongoDB-style FilterConditionSchema — the binding-level sibling of #14406 outside ComponentPropsMap, while objectui's composition seam lowers three shapes and its own pins author the tuple array #15442
Observed by the os-dev executing #14406 (session session_01G4138K1EG7kQ81FNba5Kp4) while measuring that card's read path. Filed unassigned for triage; ⛔ not folded into #14406 — its census, ruling text and dispatch surface are scoped to ComponentPropsMap, and this key is read by every element that carries a binding.
After #14406 lands, element:record_picker carries two filter orthographies at two keys that one renderer read resolves through a single ?? (composed?.filter ?? props.filter, record-picker.tsx:108 at the pin): properties.filter is the ViewFilterRule array, dataSource.filter is the MongoDB-style record. The card the ruling ui#6206-B closes says "one filter orthography platform-wide"; this key is the binding-level one the ComponentPropsMap census could not see.
The consumer at the objectui pin 00d3f09c500c4a45b5f27aca8af80349412faaf1 (identical on objectui mainf7cf7e8)
packages/core/src/data-scope/element-data-source.ts:69-78 types the binding's filter as unknown "because three shapes reach it (a MongoDB-style object, an ObjectQL AST node array, a spec ViewFilterRule[]) and mergeFilterNodes is the single sink that lowers all three"; :247-251 AND-combines it with the saved view's own rule array.
packages/components/src/__tests__/record-picker-element-data-source.test.tsx:80 authors dataSource: { object: 'account', view: 'hot', filter: [['amount', '>', 100]] } — an array the spec REFUSES at dataSource.filter (FilterConditionSchema is a z.record(...).and(z.object(...)); an array is invalid_type there). So the consumer's own pin already contradicts the declaration.
Its own ADR-0087 D3 entry and its own in-repo author sweep (dataSource.filter authors in examples/**, content/docs/**, spec fixtures, check-yaml-examples).
Not decided here: whether ui#6206-B reaches the binding key as written (it reads as if it does — "platform-wide"), or whether a binding-level filter that the view's rule array AND-combines with should simply be the same ViewFilterRule[] for that reason alone. Triage decides; the two shapes are Option A (converge to the array, D3 entry, consumer-first measurement per reader) vs Option B (record a reasoned exception on the binding key).
Refs: #14406 (the last ComponentPropsMap entry) · #12039 (Key 2, PR #14404) · objectui#6206 (the ruling) · objectui#6953 (the binding's consumers) · objectui#6828.
Anchor read, not guessed. Confirmed on origin/mainf1d7872 (2026-09-04T23:50:17Z): packages/spec/src/ui/page.zod.ts:169 — filter: FilterConditionSchema.optional().describe('Additional filter criteria'), imported from ../data/filter.zod at :9. The declaration to change is in packages/spec ⇒ domain:spec. objectui is the consumer here, not the landing site, so this stays an objectstack card and does not get repo:objectui.
Grade — p2
The deciding evidence is the one the filer found and it is unusually clean: the consumer's own pin already contradicts the declaration.
packages/components/src/__tests__/record-picker-element-data-source.test.tsx:80 authors dataSource: { object: 'account', view: 'hot', filter: [['amount','>',100]] } — an array the spec refuses at dataSource.filter (FilterConditionSchema is z.record(...).and(z.object(...)); an array is invalid_type there).
So this is not "a schema that could drift" — it is a schema that is already wrong about what its consumer writes, pinned on both sides. Add the ?? collapse the filer measured (composed?.filter ?? props.filter, record-picker.tsx:108), and after #14406 lands element:record_picker carries two filter orthographies at two keys resolved through one read. Two spellings meeting at a single ?? is how a silent wrong-filter lands — a filter that is dropped or misread does not error, it returns the wrong rows.
Not p1: no measured production incident, and the consumer's mergeFilterNodes sink lowers all three shapes today, so nothing is broken right now — element-data-source.ts:69-78 types the key as unknown precisely because it is absorbing the divergence. That absorption is the workaround, and it is load-bearing.
Boundary test — Option A crosses, Option B does not
Option A (converge to ViewFilterRule[]) narrows a published contract on a key every binding-carrying element reads ⇒ contract change, manual floor, Clause-② review, and an ADR-0087 D3 entry as the filer says.
Option B (record a reasoned exception on the binding key) changes no accept set — it is a documented divergence, and its cost is that "one filter orthography platform-wide" becomes "platform-wide except here".
⇒ ⛔ Triage is not picking. But two inputs the chooser should not have to re-derive, because they cut in opposite directions and the card presents them evenly:
For A: the ruling ui#6206-B says platform-wide, and the filer's reading — that a binding-level filter which the view's rule array is AND-combined with (element-data-source.ts:247-251) should be the same type for that reason alone — is the strongest single argument on this card. Two operands of one AND having different orthographies is hard to defend on its own terms.
For B: element-data-source.ts:69-78 documents three shapes reaching this key (MongoDB-style object, ObjectQL AST node array, spec ViewFilterRule[]), with mergeFilterNodes as the single sink. Converging the declaration does not by itself retire the other two live shapes — it just makes the spec refuse two of the things that arrive.
⚠️ The cost estimate is the part most likely to be underestimated
The filer says the per-renderer measurement #14406 did for one component has to be redone per consumer, and lists them: list-view, object-grid, form, kanban, calendar, gantt, the picker (objectui#6953 / objectstack#7121), plus the spec-side readers — packages/lint's validate-component-props and the console designer's binding inspector. That is not a one-file change under Option A. ⇒ Whoever costs this should produce the consumer census before choosing, the same way #14406 did; choosing A on the strength of the AND-combination argument and then discovering seven renderers is how a card becomes a month.
Its own in-repo author sweep is also part of the deliverable: dataSource.filter authors in examples/**, content/docs/**, spec fixtures, and check-yaml-examples.
Scope fence — endorsed
⛔ Not folded into #14406, correctly: that card's census, ruling text and dispatch surface are scoped to ComponentPropsMap, and this key sits outside it — which is precisely why the ComponentPropsMap census could not see it. Nobody should read this as #14406 being incomplete.
⚠️ Related but distinct, and routed separately this round: #15449 — four object-*filter doors insideComponentPropsMap declaring bare z.unknown(). Same ruling, third location. Whoever takes either should read the other first; if all three converge, they may want one ADR-0087 D3 entry rather than three.
Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T08:46Z. Anchor card of a two-card fold: #15442 (this card) + #15449 — one dev, one branch claude/issue-15442-filter-orthography-consumer-census, mode:subagent, tier claude-fable-5-1.
Clause-②: no
Why folded, and why measurement-first: both cards are the same ruling (objectui#6206-B, one filter orthography platform-wide) at two more locations — the binding-level ElementDataSourceSchema.filter (page.zod.ts:169, still FilterConditionSchema) and the four object-*filter doors in ComponentPropsMap (component.zod.ts:2463 / :2620 / :2664 / :2719, z.unknown()) — and both triage notes (5547778089, 5547799546) decline to choose between converge to ViewFilterRule[] (a published-contract narrowing, manual floor, one ADR-0087 D3 entry for the family) and a reasoned exception, and both say the cost must be produced BEFORE choosing: the consumer census #14406 ran for one renderer, redone per consumer. So this dispatch is the census, not the change: the dev measures every reader of both keys on the pinned objectui tree and the spec-side readers, every in-repo author, and what each renderer's lowering does with each of the three shapes — and reports; the seat turns the report into one decision card (or a sequencing note) for the maintainer. No schema moves under this claim.
Seed datum from the seat's own read (origin/mainfc20f7bda, 2026-09-05T08:45Z): the repo's own showcase app authors the MongoDB-style RECORD form on object-metric today — examples/app-showcase/src/ui/pages/my-work.page.ts:39-41 (filter: { status: { $ne: 'done' } }, filter: { status: 'in_review' }, filter: { health: 'red' }) and index.ts:78 — i.e. the exact form ui#6206-B retired is live in-repo on one of the four z.unknown() doors. That single fact changes the cost of "converge" for #15449 from a declaration edit to an in-repo author migration, and it is why the census comes first.
File face: reads only — packages/spec/src/ui/page.zod.ts, component.zod.ts, component.test.ts (the #14406 census pin), packages/lint/src/validate-component-props.ts + validate-page-field-bindings.ts + page-walk.ts, the objectui pinned tree a472b07 (.objectui-sha; packages/core/src/data-scope/element-data-source.ts, packages/core/src/utils/filter-converter.ts (mergeFilterNodes), the plugin-grid / plugin-kanban / plugin-calendar / metric lowering, record-picker.tsx, the designer's binding inspector), examples/**, content/docs/**, spec fixtures / yaml examples. ⛔ No edit to any of them under this claim; a report is the deliverable. Hot-file check: none (no writes). Dedup: the two cards' own dedup lists (#14406 landed as PR #15456; #12039 Key 2; objectui#6206 / #6953 / #6828) — no other open card names these two keys.
Member card #15449 carries a one-line pointer to this claim; label flips are per card.
Claim (os-dev): branch claude/issue-15442-filter-orthography-consumer-census (pushed empty as the claim marker, zero commits beyond mainfc20f7bda), PM session session_01M59rPZZFzqhfMUPFqqZTkf (mode:subagent, round R2), 2026-09-05T08:55Z.
Scope under this claim: the two-card fold #15442 (anchor) + #15449 (member) — a MEASUREMENT dispatch. Deliverable is the consumer census for both keys (ElementDataSourceSchema.filter in page.zod.ts, and the four object-*filter doors in component.zod.ts): the H1 declaration table, the in-repo author sweep, the objectui reader table at the pin a472b07, the spec-side readers, the ruling's reach, and the per-option cost table. No PR; no edit to any schema, lint rule, renderer, example or doc. Gates: none owed (no diff).
File face: reads only, as listed in the PM claim above. The only writes are this comment, the report comments on both cards, and any out-of-scope finding filed after a dedup read.
{
"issue": 15442,
"status": "done",
"branch": "claude/issue-15442-filter-orthography-consumer-census",
"pr": null,
"premise_still_valid": true,
"premise_per_card": {
"15442": "true — page.zod.ts:169 `filter: FilterConditionSchema.optional().describe('Additional filter criteria')` on origin/main fc20f7bda (read 2026-09-05T09:0xZ); safeParse through the real ElementDataSourceSchema: record ACCEPT, record with $or ACCEPT, tuple array REFUSE (invalid_type expected record/object at filter), rule array REFUSE, AST logical REFUSE; the objectui pin a472b07 test record-picker-element-data-source.test.tsx:80 authors the tuple array at dataSource.filter (sweep row present), so the consumer's own pin still contradicts the declaration.",
"15449": "true — component.zod.ts :2463 object-grid / :2620 object-metric / :2664 object-kanban / :2719 object-calendar all `z.unknown().optional()`; FilterConditionSchema appears in that file only in comments :1840 and :2196; safeParse of each of the four doors accepts record, record-with-$or, tuple array, rule array and AST logical alike (measured). The three array doors (:1121 record:related_list, :1157 its nested add.source picker, :1851 element:number, :2220 element:record_picker) refuse everything but the rule array."
},
"summary": "MEASUREMENT dispatch, two-card fold; no schema/lint/renderer/example/doc moved; the branch is an empty claim marker (zero commits beyond main fc20f7bda). Readings: objectstack origin/main fc20f7bda, objectui pin a472b07 (= .objectui-sha; `git rev-parse HEAD` verified), 2026-09-05T08:55Z–09:40Z UTC. H1 CONFIRMED exactly as the seat read it (lines above); plus an eighth, nested array door at :1157 (record:related_list add.source.filter). The binding's alias spellings (filters/where/criteria) are did-you-mean REFUSALS (unrecognized_keys), not silent acceptance (measured). H2 AUTHOR SWEEP (brace/indent-aware script over every tracked ts/tsx/js/json/yaml/md/mdx file; positive controls my-work.page.ts:39/40/41 and index.ts:78 all fired; control grep over every `dataSource` mention — docs 26, examples 2, spec 55, lint 25 — found nothing the sweep missed): objectstack 11 occurrences. dataSource.filter: exactly 3 authors, ALL spec test fixtures, ALL record form, all on element:number nodes — packages/spec/src/ui/page.test.ts:666, :762, :767; ZERO in examples/**, hand-written content/docs/**, skills/**, yaml fences, lint fixtures. object-* filter: examples/app-showcase/src/ui/pages/my-work.page.ts:39,:40,:41 object-metric RECORD; index.ts:78 object-metric RECORD; my-work.page.ts:59 object-grid AST TUPLE ARRAY ([['owner_id','=','{current_user_id}']]); packages/lint/src/validate-component-props.test.ts:483 object-grid tuple + :488 object-metric record (the clean-corpus control pin, 'stays SILENT'); packages/lint/src/validate-empty-combinators.test.ts:240 object-grid record { $or: [] }. Generated references render both declarations as `any` (content/docs/references/ui/page.mdx:267; component.mdx:314/:384/:459/:510) and regenerate under a converge. PM assumption corrected: `check:yaml-examples` is a packages/spec script reading only os:check-tagged YAML fences — zero YAML authors of either key, so it is unaffected under either option; and validate-component-props is WARNING tier only (validate-component-props.ts:123), the CLI exits 0 on warnings without --strict (cli/src/commands/validate.ts:519) and no CI job validates the showcase with --strict — so under Option A the showcase's own validate WARNS, it does not go red; the mechanical red is the lint's clean-corpus pin. objectui pin sweep: 26 occurrences — dataSource.filter tuple array x15 (all *.elementDataSource / Gate tests), record x2 (plugin-list ListView.elementDataSource.test.tsx:154, ListView.sharedGate.test.tsx:217); object-* filter tuple x4 (grid test :121, kanban :146, calendar :114, metric :110), rule array x2 (plugin-grid README :468/:528), and content/docs/guide/objectos-integration.mdx:280 an `and:`-keyed object no reader lowers (filed objectui#7713). H3 READERS (objectui a472b07; pin→main drift: only an import line moved on ObjectGrid.tsx/ObjectCalendar.tsx via #7660, every other read-path file byte-identical). Composition: composeElementDataSource (core/src/data-scope/element-data-source.ts:235-252) passes a lone filter VERBATIM and AND-combines two sources via mergeFilterNodes; the Gate (react/src/element-data-source/ElementDataSourceGate.tsx:220-226) writes mergeFilterNodes(base.filter, composed.filter) onto schema.filter — MEASURED: even a lone binding filter is lowered there (toFilterNode runs per source). 17 renderers wrap elementDataSourceBlock; 12 map filter:true (calendar, chart, metric, pivot, record:related_list, line-items, gantt, grid, kanban, list-view, map, timeline); object-detail and the three object-form gates map no filter (no collection query); record-picker uses useElementDataSource directly: `composed?.filter ?? props.filter` (record-picker.tsx:108) → `query.$filter = filter` verbatim (:135) → ObjectStackAdapter.convertQueryParams lowers ALL three shapes (data-objectstack/src/index.ts:3832-3851, translateFilterToAST :400). toFilterNode/mergeFilterNodes RUN (verbatim copy of the pin file, blob 2f69f27c matches `git rev-parse a472b07:…`, only the `@objectstack/spec/ui` import re-pointed at the fc20f7bda spec source): record → ['status','!=','done']; record with $or/$and/$not → ['$or','=',[…]] MISREAD (objectui#6948, open); record with an array value → ['status','=',['a','b']] MISREAD; unknown $op → throws FilterOperatorError/INVALID_FILTER; tuple array verbatim; rule array → [['status','equals','in_review']]; unary rule → [['archived_at','is_empty']]; AST logical verbatim; mergeFilterNodes(viewRules, X) → ['and', [[…view…]], X-lowered] and EVERY combined output passes the spec's isFilterAST (measured: raw record false, raw rule array false, raw tuple true; all lowered/combined true). The four doors: object-grid reads schema.filter through toFilterNode (ObjectGrid.tsx:1461-1462 → $filter :1811): record/tuple/rule all lowered (record caveat #6948); object-kanban hands `$filter: schema.filter` verbatim (ObjectKanban.tsx:263) and object-calendar likewise (ObjectCalendar.tsx:454) — on the ObjectStack adapter all three lowered; on a ValueDataSource a rule-object array is refused row-wise by matchesASTFilter ('not an array', ValueDataSource.ts:178-183 → empty result) while the record and AST match; the calendar ALSO reads schema.filter.calendar as its config object (ObjectCalendar.tsx:145, live at :271 — filed objectui#7711, the #4034 class). object-metric: the Gate writes bound.filter (dashboard/src/index.tsx:174-189) → ObjectMetricWidget → resolveFilterPlaceholders (shape-agnostic) → WITH `aggregate` (all five showcase metrics): ds.aggregate({field,function,groupBy:'_all',filter}) → adapter analytics branch (index.ts:4975-5003) `where = Array.isArray(filter) ? translateFilterArray(filter) : filter` → POST /analytics/query → the runtime route asserts AnalyticsQueryRequestSchema (runtime/src/domains/analytics.ts:49-70, called :102) whose `where` is FilterConditionSchema — MEASURED on the spec source: record ACCEPT, AST tuple array REFUSE, single tuple REFUSE, rule array REFUSE (invalid_type expected record). So on the metric's aggregate path ONLY THE RECORD WORKS today; every array shape is a 400 at the route (the objectui#6302 / PR #6828 measurement stopped at the service gate lowerAnalyticsWhere one hop later; no test posts an array where through the route) — filed objectstack#15828. Without `aggregate` the widget's ds.find({$filter}) path lowers all three. 'Read, not run' on the route function itself (the runtime's dependency closure was not built); the schema it calls was run. Adapter (5000-line file) read, not run. H4 SPEC-SIDE READERS: validate-component-props.ts dispatches properties against ComponentPropsMap at WARNING tier and reads dataSource.object only for object inference (:177); a converge on the four doors turns the record/tuple showcase authors into warnings and flips the clean-corpus pin (validate-component-props.test.ts:475-495) red. validate-page-field-bindings.ts:331 reads props.filter only on record:related_list as rule objects (fieldRefsFrom reads .field) — untouched by either key. page-walk.ts:115-117 reads dataSource.object only — untouched. validate-empty-combinators.ts walks EVERY key named filter/filters/runtimeFilter/relatedListFilter (filter-walk.ts:60; surfaces incl. pages :151) and judges record nodes only (isFilterNode excludes arrays) — its fixture :240 authors object-grid { $or: [] } and would need replacing under A (the rule goes silent on arrays: '整个换掉' triage). The #14406 census pin (component.test.ts:1896-1920) defines holdout = 'declares filter AND refuses the rule array'; z.unknown() accepts the array so the four doors are not holdouts by construction; it stays green under both options and cannot catch them — a converge needs the complementary assertion 'every filter door refuses the record'. Designer (console, objectui app-shell/src/views/metadata-admin): PageBlockInspector.tsx curates per-block fields from BLOCK_CONFIG (previews/block-config.ts) — object-grid [objectName, columns, pageSize], object-metric [objectName, label, description, icon, colorVariant, format, prefix, suffix], object-kanban [objectName, groupField, titleField, cardFields], object-calendar NO ENTRY; neither `filter` nor `dataSource` is curated for any of them, so an authored filter surfaces in the shape-agnostic advanced-key InspectorJsonField (JSON.parse of whatever is typed). The designer authors NEITHER form structurally: Option A does not break it, and it offers no guard either. The record-WRITING builder (inspectors/datasetFilterCondition.ts, groupToCondition) belongs to the DashboardWidgetInspector — the dashboard widget.filter family (spec dashboard.zod.ts:387/:720 FilterConditionSchema), a third location filed as objectstack#15829 (finding). objectui parity: registry-inputs-spec-parity.test.ts carries `element:number.filter:array` (deletable when the spec reaches the pin) and `object-grid.data:object`; object-grid/object-metric registry inputs already declare filter `type: 'array'` (plugin-grid/src/index.tsx:218, plugin-dashboard/src/index.tsx:207) so Option A keeps parity trivially; object-kanban/object-calendar registrations declare NO filter input at all while their renderers read it (filed objectui#7712). The html tier's checkType for an `array` arm is Array.isArray (sdui-parser/src/validate.ts:179-180): a record on grid/metric is ALREADY a type-mismatch there while the spec accepts it — declared vs accepted disagree across tiers today. H5 RULING REACH: objectui#6206 comment 5406409590 (2026-08-25), operative sentence verbatim: 'Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): **Option B** — one filter orthography platform-wide: the spec entry `ComponentPropsMap['element:number'].filter` aligns to the platform's `ViewFilterRule` array form, rather than leaving this one block as the record-shaped exception every author and AI would have to remember. **Measurement-first is binding**: the upstream card must verify the adapter/runtime behaviour on array filters for this element's read path before editing — if the runtime genuinely cannot take the array form, that is a fork back to triage, not a silent flip to Option A.' It names ONLY ComponentPropsMap['element:number'].filter; 'platform-wide' is the principle; it names neither element bindings (dataSource) nor the object-* family. The #6206 card body's premise counted object-grid.filter / object-metric.filter as `'array'`-declared on the objectui side with spec entries that 'accept anything (z.any())'. #7751: maintainer ruling 2026-08-12 (comment 5261743573) direction A — the object-* props schemas enter ComponentPropsMap; landed as commit 8f20201f1 (2026-08-13, PR #8524, 'props schemas derived from objectui's renderer read points') — TWELVE DAYS BEFORE ui#6206-B; no sentence in #7751, its ruling or the commit addresses filter's shape. So z.unknown() is a faithful record of read points that accepted three shapes (grid toFilterNode; kanban/calendar verbatim; metric `filter?: any`), written before the orthography ruling existed — an omission relative to a later ruling, not a deliberate exception to it; the .describe() strings ('ObjectQL filter array/AST') record the author's belief. And the wire does NOT 'take whatever': the metric's aggregate wire takes only the record. (The shallow window's oldest commit is 2026-08-15, so `git log -S` misattributed the introduction; REST commits-by-path corrected it.) H6 COST TABLE (numbers from H2–H5). LOCATION 1 dataSource.filter — Option A (z.array(ViewFilterRuleSchema)): 1 declaration line (page.zod.ts:169); 1 ADR-0087 D3 entry (can be the family entry); regenerate authorable-surface/json-schema/api-surface/docs references; spec fixtures to migrate 3 (page.test.ts:666/:762/:767, record→array, '改拼写'); example/doc/skill authors 0; renderers to touch 0 (all 12 gate blocks and the picker lower the rule array today, measured); designer 0; pins flipped: the 3 spec fixtures; objectui follow-up (not spec-parsed, unblocked only at the pin): 15 tuple-array test authors + 2 record test authors of dataSource.filter become off-spec fixtures (a #7663-style card), and ElementDataSourceConfig.filter's 'three shapes' doc. Breaks: nothing at runtime. Option B (recorded exception, keep FilterConditionSchema): 0 schema files; what stays silently wrong: the spec keeps REFUSING the tuple array objectui's own 15 pins author; element:record_picker keeps two orthographies at two keys resolved by one `??`; the record's $and/$or/$not are spec-legal at the binding but misread by every gate block (objectui#6948); two operands of one `and` in two vocabularies. LOCATION 2 the four object-* doors — Option A: 4 declaration lines; the same family D3 entry; regenerate references (component.mdx); in-repo authors to migrate 5 (my-work.page.ts:39/:40/:41 and index.ts:78 records on object-metric; my-work.page.ts:59 tuple on object-grid); lint fixtures 3 (validate-component-props.test.ts:483/:488 → migrate, the pin otherwise reds; validate-empty-combinators.test.ts:240 → replace); spec pins 0 red (census pin stays green; add the 'refuses the record' twin); renderers to touch: object-grid 0, object-kanban 0, object-calendar 0 (adapter lowers rule arrays), object-metric BLOCKED — its aggregate path 400s on every array at the analytics route (#15828), the verbatim ui#6206-B 'fork back to triage' condition; designer 0; objectui registry: grid/metric already `array` (parity holds), kanban/calendar need the declaration (#7712); showcase validate: warnings only (no red), but the 5 authors migrate in the same PR under contract-first. Option B: 0 schema; the exception must rewrite or justify the four .describe() strings that say 'array/AST'; what stays silently wrong: accept-anything doors; the record's logical keys misread on grid/kanban/calendar (#6948); the html tier declares `array` on grid/metric while the spec accepts the record (declared vs enforced split across tiers); on metric the tuple array the objectui pin :110 authors is a 400 in production. MIXED (A for grid/kanban/calendar, metric excepted): 3 declaration lines now, metric later — buys nothing the sequencing does not, and re-creates a per-door exception inside one family.",
"tests": "No gates owed (no diff; the branch has zero commits beyond main fc20f7bda, `git status --porcelain` empty after the spec build). Measurements, all scratch scripts OUTSIDE both trees (scratchpad issue-15442/), run with the worktree's tsx from the spec SOURCE at fc20f7bda after `pnpm install --frozen-lockfile` (exit 0, 6.9s): (1) probe-safeparse.ts exit 0 — ElementDataSourceSchema.filter: record ACCEPT / record $or ACCEPT / tuple REFUSE / rule array REFUSE / AST logical REFUSE; aliases filters/where/criteria REFUSE unrecognized_keys; z.array(ViewFilterRuleSchema): only the rule array ACCEPTs; the seven map doors: three array doors refuse all but the rule array, four object-* doors accept all five shapes; my-work.page.ts:39 record through object-metric ACCEPT; my-work.page.ts:59 tuple through object-grid ACCEPT, the same tuple through array(ViewFilterRule) REFUSE invalid_type@0. (2) probe-converter.ts exit 0 — objectui pin filter-converter.ts copied verbatim (diff shows exactly the one import line; `git hash-object` 2f69f27c131c… equals `git rev-parse a472b07:packages/core/src/utils/filter-converter.ts`), import re-pointed at the fc20f7bda spec source: toFilterNode and mergeFilterNodes outputs per shape as listed in H3 (record with $or → ['$or','=',…]; array value → '='; $like → throws INVALID_FILTER). (3) probe-isfilterast.ts exit 0 — spec isFilterAST on raw shapes (record false, rule array false, tuple/single/logical true), on lowered shapes (all true), on every mergeFilterNodes(viewRules, X) output (all true); control hand-built AND true. (4) probe-analytics.ts exit 0 — AnalyticsQueryRequestSchema (api/analytics.zod.ts:50) where: record ACCEPT; AST tuple array / single tuple / rule array REFUSE invalid_type(expected record). (5) probe-route.mts — NOT RUN: importing runtime/src/domains/analytics.ts needs @objectstack/types and onward dist (the build closure the dispatch forbids); read instead: assertAnalyticsQueryBody (:49-70) calls exactly the schema in (4), no pre-parse normalisation, single HTTP entry dispatcher-plugin.ts:1152. (6) sweep.py over `git ls-files` of both trees: objectstack 11 rows (controls my-work.page.ts:39/40/41, index.ts:78 present), objectui 26 rows (control record-picker-element-data-source.test.tsx:80 present); control grep of every dataSource mention found no miss. (7) The only heavy step: `bash scripts/pm/os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @objectstack/spec build'` with OS_VERIFY_LOCK_SLOT=issue-15442-census — VERDICT line: `command-exit 0 · held the lock 193s (3m13s) · waited 112s (1m52s)`; it ran gen:schema and left the tree clean (0 porcelain lines). Ablation: none (nothing to ablate; no change under this claim). Reverse verification: not applicable.",
"mcp_calls": "10 — all mcp__github__search_issues for dedup: objectui control (hit #7663 first), converter misread (hit objectui#6948 → cited, not filed), metric/aggregate array (hits #6302/#6828 → route hop unmeasured there), analytics route refusal (no hit → #15828), dashboard widget filter (no hit → #15829), guide example (no hit → objectui#7713), kanban/calendar undeclared filter (no hit → objectui#7712), calendar filter.calendar (no hit; only the map twin #4034 → objectui#7711), objectstack control (one 422 on `*`/`()` syntax, re-spelled → hit #15449 first). Every other read went git/payload/REST: issue bodies and comments via the public-repo payload channel (both cards hasNextPage false, 2 comments each) cross-checked by REST GET (200, same 2 comments); commit provenance via REST commits-by-path; all writes (2 claim comments, 5 filings, 3 report comments) via REST POST 201.",
"open_questions": [
{
"question": "Disposition of the two locations under ui#6206-B (one filter orthography platform-wide): Option A — converge both `ElementDataSourceSchema.filter` and the four `object-*` `filter` doors to `z.array(ViewFilterRuleSchema)` with ONE ADR-0087 D3 entry for the family; Option B — a recorded, reasoned exception per location; or a mixed answer per location. Four-axis analysis. 实际业务需求 — measured producers: dataSource.filter has ZERO authors outside spec test fixtures (3 record fixtures; no example, doc, skill or designer writes it; objectui's own pins author the tuple array 15 times and the record twice); the object-* doors have 5 in-repo authors (4 records on object-metric, 1 tuple array on object-grid, all showcase) and no designer producer (the block inspector curates neither filter nor dataSource); the record on object-metric is the ONLY shape its aggregate wire takes today (#15828), while the html tier declares `array` for it. 项目长远合理性 — the ruled direction is one orthography; two operands of one `and` (the Gate's mergeFilterNodes) in one vocabulary is the strongest structural argument; the record's extra expressiveness ($and/$or/$not) is not delivered by the consumer anyway (#6948 misreads it), so B preserves a capability the runtime does not honour; A closes the record-picker's two-orthographies-at-one-`??` hazard. 防 AI 写元数据犯错 — z.unknown() is the accept-anything incubator: today an AI following the manifest writes an array on object-metric and gets a 400, an AI following the showcase writes a record and gets an html-tier type-mismatch; both receipts are wrong somewhere. The fix is contract-first: make the wire take what one declaration says (#15828 first), then declare that one shape loudly at every door; B keeps the silent success receipt for the retired form. 创业阶段不扩散 — no staged window and no dual spelling: the record retires at all doors in one PR, the 5 showcase authors and 6 fixtures migrate in the same change; a per-door exception inside one family is the shape 6206-B refused for element:number.",
"options": [
"A — converge BOTH locations family-wide, one D3 entry, SEQUENCED: (1) #15828 first — the analytics route must take the array form on object-metric's aggregate path (contract question: widen AnalyticsQuerySchema.where to the FilterArray the service already lowers, or have the adapter lower arrays into a record) — the verbatim ui#6206-B 'fork back to triage' condition is live on that door; (2) then one spec PR: page.zod.ts:169 + the four doors + D3 entry + regen + 3 spec fixtures + 3 lint fixtures + 5 showcase authors + the 'every filter door refuses the record' twin of the census pin; objectui follow-ups at the pin (#7712 declarations; a #7663-style card for the 17 dataSource.filter test authors). Cost: ~5 declaration lines, 11 fixture/author edits, 0 renderer edits on 3 doors + the binding, metric gated on #15828.",
"B — recorded exception at both locations: 0 schema edits; must rewrite or justify four .describe() strings that already say 'array/AST'; keeps refusing the tuple array objectui's pins author at dataSource.filter, keeps the metric's html-tier `array` arm contradicting the accepted record, keeps #6948's misread inside the declared accept-set, and makes 'platform-wide' read 'except the two places every data-bound block reads'.",
"Mixed — A for dataSource.filter, object-grid, object-kanban, object-calendar now; object-metric excepted (its wire natively takes the record and refuses arrays): 4 declaration lines now, 1 later; buys nothing over A's sequencing and re-creates a per-door exception inside one family."
],
"recommendation": "A, sequenced behind #15828. It agrees with triage-15442's 'for A' input (the AND-combination argument, now measured: every combined output is wire-legal AST) and with triage-15449's 'measure each read path first' (done: three doors lower the array today, one cannot). It DISAGREES with triage-15449's 'for B' input — z.unknown() was not a deliberate 'whatever the wire takes': it was derived from read points on 2026-08-13, twelve days before the ruling, and the wire does not take whatever — the metric's aggregate wire takes only the record. It also qualifies triage-15442's 'for B' input — three shapes do reach the consumer, but the spec never accepted two of them (tuple and rule arrays are refused at the binding today), so B does not preserve a working contract, it preserves a contradiction between the declaration and the consumer's own pins. If the maintainer rules #15828 in the adapter direction (lower arrays into a record for analytics), A still stands at the declaration; only the sequencing changes."
}
],
"out_of_scope_findings": [
"filed as #15828 (objectstack): runtime `POST /analytics/query` refuses the array `where` the objectui adapter now sends for every array-form filter — AnalyticsQueryRequestSchema.where is FilterConditionSchema (api/analytics.zod.ts:50 / data/analytics.zod.ts:299; runtime/src/domains/analytics.ts:49-70, :102; dispatcher-plugin.ts:1152) while the service gate lowerAnalyticsWhere accepts filter AST; blocks Option A on object-metric and undercuts the element:number D3 entry's runtime claim (measured at the service gate, not the route).",
"filed as #15829 (objectstack, finding): dashboard widget.filter (dashboard.zod.ts:387, :720) still declares FilterConditionSchema — a further ui#6206-B location with a live designer PRODUCER of the record form (objectui app-shell inspectors/datasetFilterCondition.ts).",
"filed as objectui#7711: ObjectCalendar.getCalendarConfig reads schema.filter.calendar as the config container (ObjectCalendar.tsx:145, live at :271) — the #4034 map defect class, still on the calendar.",
"filed as objectui#7712: object-kanban / object-calendar registrations declare no `filter` input (plugin-kanban/src/index.tsx registrations :208/:313/:420/:434; plugin-calendar/src/index.tsx :293/:303) while both renderers read schema.filter (ObjectKanban.tsx:263, ObjectCalendar.tsx:454) and the spec declares it — html-tier unknown-prop on a spec-declared key (#6678 class).",
"filed as objectui#7713: content/docs/guide/objectos-integration.mdx:280 authors object-grid.filter as an `and:`-keyed object of rule entries — a fourth shape no reader lowers (convertFiltersToAST's equality branch, measured on the $or twin).",
"NOT filed (already on file, cited): objectui#6948 (open) — convertFiltersToAST has no $and/$or/$not branch; this census adds the measured datum that an array VALUE ({ status: ['a','b'] }) lowers to '=' rather than 'in' on the same branch, and that both misreads sit inside FilterConditionSchema's accept-set at dataSource.filter today. objectui#7663 (open, pm:blocked) already tracks the record-picker registry description that still says record."
]
}
Zeros, each with its control: dataSource.filter in examples/** 0, hand-written content/docs/** 0, skills/** 0, YAML fences 0 (control: every dataSource mention — docs 26, examples 2, spec 55, lint 25 — re-grepped for a nearby filter; nothing beyond the rows above). check:yaml-examples reads only os:check-tagged YAML fences → unaffected either way. Generated references render both declarations as any (references/ui/page.mdx:267; component.mdx:314/:384/:459/:510).
objectui#6206 comment 5406409590 (2026-08-25): "Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): Option B — one filter orthography platform-wide: the spec entry ComponentPropsMap['element:number'].filter aligns to the platform's ViewFilterRule array form, rather than leaving this one block as the record-shaped exception every author and AI would have to remember. Measurement-first is binding: the upstream card must verify the adapter/runtime behaviour on array filters for this element's read path before editing — if the runtime genuinely cannot take the array form, that is a fork back to triage, not a silent flip to Option A." — names element:number.filter only; neither element bindings nor the object-* family. #7751 (ruling 2026-08-12, direction A; landed 8f20201f1 2026-08-13, PR #8524, "derived from objectui's renderer read points") — twelve days before ui#6206-B; nothing in it addresses filter's shape ⇒ z.unknown() is a read-point record, not a deliberate exception.
grid 0 · kanban 0 · calendar 0 · metric blocked by #15828 (aggregate route refuses every array)
0
pins that flip
the 3 spec fixtures
none
lint clean-corpus pin; empty-combinators fixture; census pin stays green (add the record-refusal twin)
none
designer
0
0
0
0
showcase / docs
nothing red
—
showcase validate WARNS (warning tier, no --strict in CI); references regenerate
—
objectui at the pin
15 tuple + 2 record test authors become off-spec fixtures (a #7663-style card); ElementDataSourceConfig doc
spec keeps refusing the tuple array the pins author
grid/metric registry already array; kanban/calendar need the declaration (#7712)
html tier already type-mismatches a record on grid/metric
stays silently wrong under B
—
two orthographies at one ?? on the picker; $and/$or/$not legal at the binding but misread by every gate block (#6948)
—
accept-anything doors; tuple array on metric+aggregate is a production 400 while pin :110 authors it; declared vs enforced split across tiers
Recommendation (four-axis block applied in the JSON open_questions): A, family-wide, one D3 entry, sequenced behind #15828 — the analytics route must take the array form on object-metric's aggregate path first (the ruling's own "fork back to triage" condition, live on that door); then one spec PR for both locations.
Decision requested — the consumer census is in; the disposition of two more ui#6206-B locations is a contract ruling (domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T11:45Z)
Provenance: the folded measurement dispatch for #15442 (anchor) + #15449 (member) — anchor report 5550899105, readable H1/H2/H3/H6 tables 5550899184, member report 5550899288; all readings on origin/mainfc20f7bda and the objectui pin a472b07 (= .objectui-sha) between 2026-09-05T08:55Z and 09:40Z; nothing edited (the branch claude/issue-15442-filter-orthography-consumer-census is an empty claim marker at fc20f7bda, zero commits). Per the claim (5550679316) the census was the deliverable and the seat turns it into one decision card: this seat moves the anchor into the decision inbox in the same stroke (pm:dispatched → needs-user-decision, assignee cleared) and blocks the member #15449 on it (pm:blocked + Blocked-by: #15442) — one ruling covers both locations. The seat does not rule: the options differ by a published-contract narrowing at five doors (ADR-0087 D3, manual floor), the dev's recommendation disagrees with two of the three triage inputs, and the sequencing gate (#15828) is itself a contract question.
Measured state (from the census; re-check commands are in the anchor report)
Location 1 — ElementDataSourceSchema.filter (packages/spec/src/ui/page.zod.ts:169, still FilterConditionSchema): accepts the MongoDB-style record and refuses every array form (tuple array, rule array, AST logical — invalid_type expected record). Authors in objectstack: exactly 3, all spec test fixtures (page.test.ts:666/:762/:767), all record form; zero in examples/**, hand-written docs, skills, YAML fences, lint fixtures. The consumer's own pins author the tuple array 15 times and the record twice at this key (objectui a472b07) — the declaration and the consumer's pins contradict each other today.
Readers (objectui pin, verbatim-copy run of filter-converter.ts): the Gate lowers every binding filter through mergeFilterNodes; 12 of 17 gate blocks map filter; object-grid lowers all three shapes, object-kanban / object-calendar hand schema.filter verbatim to the adapter which lowers all three; the record's $and/$or/$not keys and array values are MISREAD on every path (objectui#6948, open) — the record's extra expressiveness is not delivered anyway. object-metric with aggregate (all five showcase metrics) reaches POST /analytics/query, whose request schema refuses every array where (measured: record ACCEPT, tuple / rule array REFUSE) — only the record works on that path today. Filed as runtime: POST /analytics/query refuses the array where the objectui adapter now sends for every array-form filter — AnalyticsQueryRequestSchema.where is FilterConditionSchema, while lowerAnalyticsWhere (the gate ui#6302 measured) accepts filter AST #15828; this is the verbatim ui#6206-B "fork back to triage" condition, live on one door.
Spec-side readers: validate-component-props is WARNING tier (no --strict in CI) — under a converge the showcase's own validate warns, it does not go red; the mechanical red is the lint clean-corpus pin (validate-component-props.test.ts:483/:488) and validate-empty-combinators.test.ts:240. The finding(spec): element:record_picker.filter still declares the MongoDB-style FilterConditionSchema — the last record-form filter in ComponentPropsMap after the ui#6206-B convergence #14406 census pin cannot see z.unknown() doors by construction (holdout = refuses the array); a converge needs the complementary "every filter door refuses the record" assertion. The console designer curates neither filter nor dataSource for any of the four blocks — Option A does not break it and it guards nothing either. The html tier already declares array for grid / metric (Array.isArray), so a record there is a type-mismatch at one tier and accepted at the other today.
Ruling reach: ui#6206 comment 5406409590 (2026-08-25, maintainer 「同意」, Option B there) names only ComponentPropsMap['element:number'].filter and states "one filter orthography platform-wide" as the principle; it names neither the binding nor the object-* family.
Options × real cost
option
what
real cost
A — converge both locations, family-wide, sequenced
page.zod.ts:169 + the four doors → z.array(ViewFilterRuleSchema); ONE ADR-0087 D3 entry for the family; regen (authorable-surface, json-schema, api-surface, docs references); migrate 3 spec fixtures + 3 lint fixtures + 5 showcase authors; add the "refuses the record" twin of the census pin
~5 declaration lines, 11 fixture/author edits, 0 renderer edits on the binding and three doors; object-metric gated on #15828 (the analytics route must take the array on the aggregate path first — or the adapter lowers arrays into a record); objectui follow-ups at the pin (#7712 declarations; a #7663-style card for the 17 dataSource.filter test authors)
B — recorded exception at both locations
0 schema edits; rewrite or justify the four .describe() strings that already say "array/AST"
keeps refusing the tuple array the consumer's 15 pins author; keeps the picker's two orthographies resolved by one ??; keeps #6948's misread inside the declared accept-set; keeps the html-tier array arm contradicting the accepted record on grid / metric; "platform-wide" comes to mean "except the two places every data-bound block reads"
Mixed — A everywhere except object-metric
4 declaration lines now, 1 later
buys nothing over A's sequencing and re-creates a per-door exception inside one family — the shape ui#6206-B refused for element:number
Coupled question (sequencing gate) — #15828's direction: (i) widen AnalyticsQueryRequestSchema.where in spec to the filter array the service gate lowerAnalyticsWhere already lowers (a spec-lane card, lands before the converge PR), or (ii) have the objectui adapter lower arrays into a record for the analytics branch (an objectui-lane card; cross-repo Blocked-by + pin lag before the converge). Under either answer A stands at the declaration; only the sequencing changes. The seat recommends ruling #15828's direction in the same stroke so the converge dispatch is not blocked twice.
Released pm:blocked → pm:queue — the pin criterion this card was parked behind is MET
domain:spec seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T13:0xZ. Surfaced by the half-state patrol anchor #9857 (07:50Z run) as an H19 row, "every target it names is closed". H19 is report-only — it reports a candidate, it does not decide it — so the release was run against the state model's two mechanical double-checks, not against the row's sentence.
① The condition carried by the MOST RECENT conversion comment
reading
value
conversion comment
5571612810, 2026-09-07T13:49:44Z, paired with labeled pm:blocked at 13:50:19Z
condition it carries
Blocked-by: #16626, with an executable criterion written out: git merge-base --is-ancestor 53ded82b "$(git show origin/main:.objectui-sha)"
#16626
closed completed 2026-09-08T07:35:37Z
⚠️ The criterion is what was checked, ⛔ not the blocker's closed state — a closed pin-bump card is not by itself proof the pin moved. This lane's own standing rule is that the cross-repo unlock criterion is the consumer can install it, not that upstream merged it, and that rule is exactly why this card was parked in the first place.
leg
reading
git show origin/main:.objectui-sha (at ce7bae8b4)
53ded82bf7a494f54e344e19099dbf00854b8694
objectui compare 53ded82b…53ded82bf7a4
identical, ahead 0 / behind 0
⭐ lit control — the OLD pin a472b07167a3 vs the new one
ahead_by: 34
The pin is now the fix commit itself, so ancestry is trivial. The control is there because a compare API that returned "identical" for everything would produce the same green: the old pin reads 34 commits behind, so the endpoint is answering, and the pin genuinely moved. ⛔ Ancestry was taken through REST, not git merge-base locally — this checkout can be shallow and a shallow clone reports false non-ancestry.
② No merged PR newer than that conversion comment DELIVERS this card
Three merged PRs cross-reference the card after 2026-09-07T13:49Z, so ② has to be asked seriously rather than waved past. It was asked with the repo's own predicate, ⛔ not a hand-rolled reading of "is there a merged PR on the card":
All three are passing mentions; none carries a closing keyword or a Part of for this card. The control fires on a PR known to deliver, so the three nulls are a reading and not a dead predicate. ⇒ ② does not refuse. The card did not move on past its own condition.
Premise re-derived on the tree, because a released block is never a reflexive release
git show origin/main:packages/spec/src/ui/page.zod.ts at ce7bae8b4 — ElementDataSourceSchema opens at :156 and still carries, at :169:
The work is undone and the ruling (5559671308, option A) stands. ⇒ Released.
⛔ Three constraints the re-dispatch inherits — these are measured, not guesses
The flight that parked this card left findings that cost a dev's run to obtain. They are carried here so the next dispatch does not re-buy them:
The ruling's "3 spec fixtures" is 4.page.test.ts:633 calls ElementDataSourceSchema.parse({ … filter: { status: 'pending' } }) directly — a record-form author that a dataSource-keyed sweep cannot see. The other three are :665-666, :762, :767.
A fifth accept-anything filter-shaped door the ruling does not name:object-grid.defaultFilters (component.zod.ts, z.unknown(), "Legacy base-filter fallback, read only when filter is absent"). ⛔ Out of scope for the re-dispatch, and explicitly so rather than silently. The ruling names dataSource.filter plus the four object-*filter doors; defaultFilters is a different key and is not among them. Whether it belongs to the family is the ruler's call, ⛔ not the dev's — see the separate routing note below.
Author-sweep counts otherwise confirmed: 3 lint fixtures and 5 showcase authors, and hand-written docs and skills author neither key.
Housekeeping, flagged rather than done
The card still carries finding alongside a full grading (bug / priority:p2 / domain:spec). finding means awaiting first grading, and leaves the moment grading happens, so it is stale here. ⛔ This seat does not grade and does not strip grading labels — that is the triage seat's write. Flagged for the next triage fire.
Its fold partner #15449 stays pm:blocked: its condition is two targets, and while #16626 has cleared, this card is its other target and is still open.
File surface: packages/spec/src/ui/page.zod.ts (the ElementDataSourceSchema.filter binding-level door) and packages/spec/src/ui/component.zod.ts (the four object-*filter doors), plus their fixtures and the changeset. ⛔ object-grid.defaultFilters is fenced OUT — see below.
Clause-②: yes
⚠️ Declared yesconservatively and provisionally. The converge is two-directional: the MongoDB-style record stops being accepted (narrowing) while the ViewFilterRule tuple array starts being accepted where it was not (widening). A widening in either direction forces yes. ⛔ But do not inherit this — measure it on your finished diff and report. If it measures no, say so and I will correct the claim; the declaration is the seat's to move and I now know the mechanism.
⭐ New standing discipline, and this card is the first to carry it: run node scripts/pm/check-clause2-carriers.mjs --pair <PR> yourself, at your first push, and report the exit code. Five of my defects today came from declaring at claim time and only discovering the truth at landing time. It cannot run before a PR exists, so your first push is the earliest possible moment.
Three constraints inherited from the flight that parked this card — ⛔ do not re-buy them
These cost a dev's round to obtain and are on record in the release comment 5602085198:
The ruling's "3 spec fixtures" is 4.page.test.ts:633 calls ElementDataSourceSchema.parse({ … filter: { status: 'pending' } }) directly — a record-form author a dataSource-keyed sweep cannot see. The other three are :665-666, :762, :767.
The ruling is comment 5559671308 (option A: converge the binding-level dataSource.filter and the four object-*filter doors to ViewFilterRule). Its sequencing gate — the objectui pin — was the reason this card was parked, and it is discharged: .objectui-sha on origin/main is now 53ded82bf7a4…, identical to the fix commit, verified by REST compare with a lit control (the old pin reads 34 behind).
⛔ Re-derive both halves yourself on origin/main before implementing:
ElementDataSourceSchema.filter still declares FilterConditionSchema (it was at page.zod.ts:169, and origin/main has moved many times today);
the four object-*filter doors in ComponentPropsMap still declare what the card says they do.
⚠️ And look at what your controls match, not just their counts. My worst defect today was a correctly-built pattern returning three hits I never read — they were a rename comment and a tombstone, and I reported the field as live.
Standing constraints
⛔ One dedicated worktree; never edit /home/user/objectstack; never git stash; never kill a process by name.
⛔ Never touch content/docs/releases/.
Draft PR, body line 1 Fixes #15442, needs:contract-review on the PR carrier (the card has it). ⛔ Do not flip ready or enqueue.
Merge origin/main through bash scripts/pm/os-regen-merge.sh; regenerate with the repo tooling, ⛔ never hand-edit a generated file.
Derive gates with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (⛔ no hand-fed path list), run every family on the final head, land exit codes to disk before reading, reconcile with --ran. PREREQUISITE NOT MET is NOT MEASURED — never green, never red.
⛔ Run the full package suite, never a hand-picked subset. A round today reported green while CI was red because it tested selected files; two more rounds then had to clean it up.
⛔ Read the PR's own CI before reporting done — newest run per check name, not the raw run list, not the required subset.
Heavy verification under bash scripts/pm/os-verify-lock.sh; ⛔ a queue-timeout is never a pass. Two sibling agents contend for it.
Judge the changeset yourself. The card's own acceptance expects minor with a BREAKING banner; ⛔ re-derive rather than inherit — an inherited grade broke Check Changeset twice on this lane today.
Report in the usual os-dev-report JSON on this card, stating the --pair exit code, the measured clause-② direction, and the four fixtures' disposition.
{
"issue": 15442,
"status": "done",
"branch": "claude/issue-15442-filter-orthography-converge",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17257",
"premise_still_valid": true,
"premise_per_card": {
"15442": "TRUE on origin/main ae19f5edb (branch base, 2026-09-09T22:30Z): page.zod.ts:169 read `filter: FilterConditionSchema.optional().describe('Additional filter criteria')`, import at :9 — the only two occurrences in the file, both read. Sequencing gate discharged: .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694, the objectui#7754 fix commit itself.",
"15449": "TRUE on ae19f5edb: `filter: z.unknown().optional()` at component.zod.ts :2466 object-grid / :2624 object-metric / :2672 object-kanban / :2763 object-calendar (lines moved from the card; describe strings matched). `defaultFilters` at :2468 untouched (out of scope, #11509)."
},
"summary": "Implemented ruling 5559671308 option A family-wide: ElementDataSourceSchema.filter and the four object-* filter doors now declare z.array(ViewFilterRuleSchema); one ADR-0087 D3 semantic entry `element-data-source-and-object-block-filter-rule-array` (protocol major 18), registry/spec-changes/upgrade-guide regenerated by the tooling; FilterConditionSchema import leaves page.zod.ts; ADR-0122 aliases ElementDataSourceParsed / ObjectMetricPropsParsed / ObjectKanbanPropsParsed / ObjectCalendarPropsParsed declared and the four isomorphic pins deleted (815 to 811); docs references, api-surface and export-origins regenerated via check:generated --fix (authorable-surface unchanged). Changeset: @objectstack/spec minor with the literal **BREAKING** banner and the `adr-0087: registered` marker (gate exit 0; PR CI `Check Changeset` success). FOUR spec fixtures disposition, all 改拼写 to the rule array: page.test.ts:633 (direct ElementDataSourceSchema.parse, the one a dataSource-keyed sweep misses), :665-666, :762, :767. Two further spec fixtures at the object-* doors that no count named (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) 改拼写 — found by the full-suite run, not by the sweep. Lint fixtures: validate-component-props.test.ts:483 and :488 改拼写; validate-empty-combinators.test.ts:240 整个换掉 (page row dropped from the covers-case; `pages` stays in EMPTY_COMBINATOR_SURFACES). Showcase authors: my-work.page.ts:39/:40/:41 + index.ts:78 records and :59 AST tuple migrated to rule arrays, plus the showcase pin test/my-work-visibility.test.ts:190 which authored the tuple. New pins: binding accept/normalize/refuse-record-at-filter/refuse-tuple-at-filter.0/two-keys-same-verdict; per-door it.each over the four object-* doors; and the ruled twin of the #14406 census pin — every filter door in ComponentPropsMap refuses the record, asserted over the whole map by shape. Draft PR #17257 body line 1 `Fixes #15442`, line 2 `Fixes #15449` (the ruling folds the member into this PR), a line-start `Clause-②: yes`, needs:contract-review on the PR; not flipped ready, not enqueued, gate label not cleared. CLAUSE-② MEASURED on the finished diff: yes — two-directional at the binding (the record leaves the accept set; the ViewFilterRule OBJECT array enters it, refused before as invalid_type expected record), pure narrowing at the four z.unknown() doors; the AST TUPLE array is newly accepted nowhere — it was refused at the binding before and stays refused one hop deeper (filter.0), and leaves the four doors' accept set. The claim's 'tuple array starts being accepted' reads as the rule array. --pair 17257 exit codes: first run exit 4 (C1: the PR did not yet carry needs:contract-review — that run raced my additive label POST), second run exit 4 (C1 only on the fold member #15449: it declares Clause-②: yes in its os-trump claim and is delivered by this PR via Fixes, but carried no gate label), third run exit 0 for BOTH pairs after I added needs:contract-review to #15449 additively (POST .../issues/15449/labels, read-back shows the union; a write on the fold member the seat may undo — reported, not hidden). DEVIATIONS: (1) the REST PATCH of the PR body (to fill the Verification section after gates) stored my session-URL footer AND a platform-appended bare footer under it — measured, not re-sent (AGENTS.md footer rule); the rest of the body is byte-identical to what was sent. (2) The first spec full-suite run was red 4/13240: two pre-existing object-* fixtures and two defects in my own new pins (issuesUnder matched filter.0; PageComponentSchema does not dispatch properties through ComponentPropsMap) — fixed in commit 6274af365 and the whole suite re-run green. (3) Both long verification calls exceeded the 10-minute foreground cap and were backgrounded by the harness; I waited in the foreground (tail --pid on the runner's own PID) and read exits from disk. mcp_calls 0: every read via public REST GET / git, every write via REST with GITHUB_TOKEN (PR create 201, labels 200, body PATCH 200, this comment).",
"tests": "All exit codes redirected to disk before reading. Head 6274af365 (the PR head, no later commit). Builds under scripts/pm/os-verify-lock.sh slot issue-15442-converge: spec build VERDICT command-exit 0 (held 151s, waited 374s); turbo build of lint^... and example-showcase^... command-exit 0, Tasks 61 successful/61 (7m13s); post-sweep spec rebuild exit 0, tree clean. FULL package suites, never a subset, one lock acquisition, six exit files all 0: @objectstack/spec test `Test Files 469 passed | 1 skipped (470)`, `Tests 13238 passed | 1 skipped (13239)`, 385s; spec typecheck exit 0 (`check:test-typecheck: OK … 54 file(s) / 261 error(s) / 145 pinned signature(s)`, pre-existing ledger); @objectstack/lint test `Test Files 103 passed (103)`, `Tests 3680 passed (3680)`, typecheck exit 0; @objectstack/example-showcase test `Test Files 28 passed (28)`, `Tests 379 passed (379)`, typecheck exit 0. REVERSE VERIFICATION (trap EXIT INT TERM, absolute paths, on the committed state, under the lock) — Leg A runtime cross-package (lint test resolving @objectstack/spec through its exports to dist; dist preflight `marker present in 18 built files`): mutated validate-component-props.test.ts:488 back to the record (on-disk anchors injected 1 / removed 0) → vitest exit 1, `1 failed | 44 passed`, failing case `stays SILENT on the corrected #7750 node and the corpus metric shapes`; restore via `git checkout HEAD -- PATH` → git diff HEAD empty exit 0, hash-object 776b8b641dfc… equals HEAD blob, re-run 45/45. Leg B type cross-package (lint tsc against the rebuilt .d.ts): probe with record at ElementDataSource, record at ObjectMetricProps, tuple at ObjectGridProps, rule array at ElementDataSource → exit 1, exactly 3 errors on the 3 off-spec lines (TS2353 ×2, TS2322), 0 on the rule line; probe removed → exit 0; porcelain 0 lines. Direction observed: red as expected on both legs. GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (stderr line 1 names objectstack-ai/objectstack at 6274af365; --repo holds) → 108 commands, each run to its own log with its exit in a ledger; `--ran` reconcile: `108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED`, exit 0. Readings: 100/108 exit 0 first pass; six spec dist-reading gates (api-surface, dual-source-exports, entry-nameability, exported-any, generated, skill-examples) refused on their own prerequisite (dist/.build-input-hash-dts recorded the pre-test-edit src digest) = NOT MEASURED first pass; after the rebuild all six exit 0 with own verdict lines (`public API surface + factory signatures unchanged ✓`; `no new dual-source exports`; `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-loads` exit 3 PREREQUISITE NOT MET (seven untouched packages have no dist here: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis) = NOT MEASURED. `check:react-declaration-parity` exit 1 `MANIFEST is not set` — the gate's own text calls that a reading; it is the on-demand pin-bump gate needing objectui's sdui.manifest.json from a browser run, not produced here, never a CI job — reported as exit 1 with that reason. Outside the derivation: check-adr-0087-registration exit 0 (registered element-data-source-and-object-block-filter-rule-array), check:nul-bytes exit 0. Ablation not applicable beyond the two legs above (no guard deleted). PR CI on 6274af365, newest run per check name, read at report time: 33 names — success: Build Core, Build Docs, Check Changeset, Check Documentation Links, Dogfood Regression Gate (+1/3, 2/3, 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 2/6 4/6 5/6 6/6, Type Check · consumer gates / debt ledger / source gates / workspace, TypeScript Type Check, filter; skipped: Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke; in_progress at read time: Lint & Repo Gates, Test Core (3/6). Nothing red.",
"gates": {
"derived": 108,
"run": 108,
"reconcile_exit": 0,
"not_measured": [
"pnpm check:dual-build-cjs-loads (exit 3 PREREQUISITE NOT MET, seven untouched packages unbuilt)"
],
"red_by_reading": [
"check:react-declaration-parity exit 1 — MANIFEST is not set (on-demand pin-bump gate, no manifest in this container)"
],
"first_pass_prerequisite_refusals_rerun_green": [
"check:api-surface",
"check:dual-source-exports",
"check:entry-nameability",
"check:exported-any",
"check:generated",
"check:skill-examples"
]
},
"clause_2": {
"declared_by_claim": "yes (provisional)",
"measured": "yes",
"direction": "two-directional at ElementDataSourceSchema.filter (record leaves, ViewFilterRule object array enters); narrowing-only at the four object-* doors (z.unknown accepted everything); the AST tuple array is newly accepted nowhere",
"pair_exit_codes": {
"run1_before_pr_label": 4,
"run2_after_pr_label_member_unlabelled": 4,
"run3_after_member_label": 0
}
},
"fixtures": {
"page.test.ts:633": "改拼写 → rule array (direct parse; the fourth)",
"page.test.ts:665-666": "改拼写",
"page.test.ts:762": "改拼写",
"page.test.ts:767": "改拼写",
"component.test.ts #7751 grid tuple + metric records": "改拼写 (uncounted by every sweep; found by the suite)",
"validate-component-props.test.ts:483/:488": "改拼写",
"validate-empty-combinators.test.ts:240": "整个换掉 — page row dropped, surface kept",
"showcase my-work.page.ts:39/40/41/59 + index.ts:78 + test/my-work-visibility.test.ts:190": "改拼写"
},
"line_budget": "not applicable — no skills/** file touched (gen:skill-docs / gen:skill-refs both reported up to date)",
"mcp_calls": "0 — issue/PR reads via public REST GET and the tool-results files; objectui pin reads via git show 53ded82b in the sibling checkout (never edited); writes via REST with the container token: PR create 201, labels POST 200 (PR, #15449), PR body PATCH 200, this comment POST",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: `pages` in EMPTY_COMBINATOR_SURFACES (packages/lint/src/validate-empty-combinators.ts) now has no spec-legal record filter to judge on a page (dataSource.filter and the four object-* doors are arrays); the walker is key-name based so the retired record form still gets the located error beside the schema refusal — dead-by-spec coverage, not a defect. 承接者:无.",
"noted, not filed: the generated content/docs/references/ui/page.mdx renders the nested rule's `operator` as optional in the PageComponent.dataSource row and required in the ElementDataSource row — how build-docs prints a z.preprocess in a nested position; pre-existing for every other rule-array door (component.mdx shows the same for element:number). 承接者:无.",
"noted, not filed: two object-* fixtures in packages/spec/src/ui/component.test.ts (the #7751 block) mirrored the showcase shapes and were counted by no sweep in the thread (the ruling's '3 spec fixtures' counted dataSource.filter authors only) — migrated here; a future sweep for this family should include properties.filter authors in spec tests. 承接者:the seat's sweep notes.",
"NOT filed (ruling assigns to the seat): the seventeen objectui dataSource.filter test authors at pin 53ded82b (fifteen tuple arrays, two records) and #7712's kanban/calendar registry declarations — objectui cards to be filed by the seat.",
"NOT touched, per dispatch: object-grid.defaultFilters (z.unknown, different key) — the ruler's call on #11509."
]
}
{
"issue": 15442,
"status": "done",
"branch": "claude/issue-15442-filter-orthography-converge",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17257",
"premise_still_valid": true,
"summary": "Patch round on the existing draft PR #17257 for exactly the two findings the seat held it for; no fresh implementation, no new branch, no force-push. Commit 3d7f4eda0 on top of 6274af365, 3 files / 35 insertions / 13 deletions. F1: one at-rest sentence added to the reason of the D3 entry 18.element-data-source-and-object-block-filter-rule-array, matching the register of the precedent entry 17.view-filter-rule-value-shaped-by-operator (its at-rest passage runs :38-50, not :38-47 as the order says — a 3-line offset, the passage itself is exactly where the order points). All three of the review's legs re-verified independently before writing: SemanticMigration converts nothing by its own type (types.ts:30-41, so the order's :29-40 is off by one), this branch adds no D2 conversion (packages/spec/src/conversions/ is absent from the 17-path diff), and applyConversionsToStoredItem (database-loader.ts:831) replays the full chain with includeRetired:true and never validates, by its own contract in conversions/stored.ts. registry.ts follows by gen:migration-registry and that one sentence is its entire delta. F2 and the partial-D2 direction question deliberately untouched: the new sentence says this disposition adds no D2 conversion, and does NOT claim none is possible, so it does not pre-empt the ruler. F3: the pages row restored byte-identical to the base tree (git diff against the merge-base now shows only the added explanatory comment for that file), plus page \"home\" back in the expected list and pages back in the it() title. Nothing weakened. Labels read back after CI: needs:contract-review still on the PR and on both cards, PR still draft.",
"tests": "All exit codes landed to disk before reading (cmd > log 2>&1; EXIT=$?), never through a pipe; heavy runs via scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-15442, verdict read from the wrapper's own VERDICT command-exit line. FULL PACKAGE SUITES (not hand-picked subsets): pnpm --filter @objectstack/lint test -> exit 0, 103 files / 3680 tests passed; pnpm --filter @objectstack/spec test -> exit 0, 469 passed | 1 skipped (470 files), 13238 passed | 1 skipped. TYPECHECK: pnpm --filter @objectstack/spec --filter @objectstack/lint typecheck -> exit 0 (lint's check:test-typecheck also OK, 2 files / 6 errors held in its shrink-only debt ledger, unchanged). F3 EITHER SIDE OF THE RESTORE, same command both times (vitest run src/validate-empty-combinators.test.ts): BEFORE the restore exit 0, 21 passed; AFTER the restore exit 0, 21 passed (the count is unchanged because the row lives inside an existing case; the restored assertion is strictly stronger, 6 expected where-values instead of 5). ABLATION proving the restored row is a real measurement and not a vacuous pass: deleting { key: 'pages', kind: 'page' } from EMPTY_COMBINATOR_SURFACES in validate-empty-combinators.ts turned it RED, exit 1, 1 failed | 20 passed, and the assertion diff names exactly the one lost value, page \"home\" (expected Array(5) to deeply equal Array(6)). Mutation proved on disk before running (anchor grep -c 1 -> 0; git hash-object 8d84ed07 differs from the HEAD blob 7b6eee5a) and the restore leg proved after (hash back to 7b6eee5a, identical to git rev-parse HEAD:path; git diff HEAD for that path empty; git status shows only my two intended files). The script carried trap on a RESTORE function for EXIT INT TERM with an absolute REPO_ROOT path and git checkout HEAD -- path, never a bare git checkout. No dist/build leg was owed: the test imports the rule relatively as ./validate-empty-combinators.js, so vitest resolves it straight from src. MIGRATION AND GENERATED GATES: check:migration-registry exit 0 (self-test ok, registry current at 193 semantic / 165 retired-key / 116 retired-def), check:spec-changes exit 0, check:upgrade-guide exit 0, check:generated exit 0 with all 15 artifacts up to date. First check:generated run was exit 1 on check:api-surface, read correctly as the gate's own documented phantom (a .build-input-hash-dts mismatch because my src edits post-dated the build, NOT a reported export removal) and cured by pnpm --filter @objectstack/spec build, NOT by gen:api-surface. OTHER DERIVED FAMILIES, all exit 0: check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:changeset-gate-self-tests, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check-comment-mask-adoption, check-comment-mask-corpus, check-spec-docblock-symbol-anchors, check-keyed-text-bounds, check:pm-widening-tells, check:doc-anchors, packages/lint check-reference-carrier-shape, check-undeclared-dep-imports. Plus a manual control-byte self-scan over all three touched files (grep -naP over the C0/DEL classes): no hits. GATE DERIVATION: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run twice (before and after git fetch origin main), identical both times, 108 commands over the same 17 paths. It flags STALE TREE, 12 commits behind origin/main, with the staleness confined to scripts/pm/check-half-states.mjs and scripts/pm/check-skill-line-ratchet.mjs. Neither is in the 108 and neither reads anything in my diff (no skills/** path, no half-state path), and the order forbids rebase/merge, so HEAD was deliberately not moved. The balance of the 108 is declared to CI, which has since converged green. check:react-declaration-parity remains UNJUDGED (no sdui.manifest.json at the pin), exactly as the review recorded, and is not a change from my round.",
"mcp_calls": "0 — every GitHub read and write in this round went through repo-scoped REST (probed first: GET /repos/objectstack-ai/objectstack/pulls/17257 -> 200), and everything else through git. No MCP GitHub tool was called at any point, so no channel switch had to be declared.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: hypothesis RAISED AND REFUTED BY ITS OWN CONTROL, recorded so nobody re-derives it. Entry 17 carries an in-file comment saying 'No backticks in surface — build-upgrade-guide.ts renders it inside a code span already, and a nested backtick would close it', and build-upgrade-guide.ts:92 does render surface inside a code span, while entry 18's surface is full of backticks. That reads like a latent break at major 18. It is not: the control says backticks in surface are the norm, not the exception — 94 of the 193 semantic entries have them, including major-16 and major-17 entries that ARE rendered in the guide today and have never been flagged. So entry 17's comment is a local note about that one entry, not a repo-wide invariant entry 18 violates. Nothing to fix and nothing to file. 承接者: none needed — this line IS the deliverable, so the next reader stops where I stopped.",
"noted, not filed: two line-number references in the dispatch order are each off by a line or three against the tree at 6274af365 — migrations/types.ts SemanticMigration is :30-41 not :29-40, and entry 17's at-rest passage runs :38-50 not :38-47. Both point at the right construct; recorded only so the next reader is not hunting a boundary. 承接者: none.",
"noted, not filed: the contract review says the major-17 control entry view-filter-rule-value-shaped-by-operator is 'present ×2' in both projections. Measured: ×2 in packages/spec/spec-changes.json but ×1 in docs/protocol-upgrade-guide.md. The review's CONCLUSION is unaffected and independently confirmed by me — the control is lit in both files while both major-18 entries are absent from both — only the count for one of the two files is off. 承接者: the domain:spec seat re-checking this verdict against the new head."
]
}
The reason field previously ENDED here (unchanged text, for the seam):
… and three lint fixtures — every one rewritten to the rule array in the same change, and zero outside those files; this entry carries the prescription for authors outside the repo.
Appended after it, verbatim as it now reads:
⚠️ Metadata AT REST is deliberately NOT rewritten, and this disposition adds no D2 conversion — a SemanticMigration converts nothing by its own type, and no author runs os migrate meta over sys_metadata. The read path does not re-validate stored rows (applyConversionsToStoredItem replays the full chain without validating, by its own contract), so a stored page or block carrying the record form keeps loading unchanged and is still rendered by objectui at the pinned .objectui-sha; what changes is that RE-SAVING it is refused at the filter door, on its next save and not before.
Precedent matched:18.…filter-rule-array.ts was written against 17.view-filter-rule-value-shaped-by-operator.ts, whose at-rest passage runs :38-50 (the order says :38-47). Register borrowed deliberately, phrase for phrase: the ⚠️ Metadata AT REST is deliberately NOT rewritten opener, the explicit "no D2 conversion", the parenthetical that applyConversionsToStoredItem does not validate by its own contract, and the closing RE-SAVING contrast. No new phrasing family invented.
One deliberate divergence from the precedent, and why. Entry 17 goes on to argue at length why a D2 conversion would be dishonest for its family. I did not carry that half over, because whether the losslessly mappable subset of THIS family should get a real D2 is on the ACCEPT list as the ruler's question. The sentence therefore states the fact ("this disposition adds no D2 conversion") and stops; it does not assert that none is possible. F2 likewise untouched.
All three legs re-verified before writing, not adopted:
leg
how I read it
reading
a SemanticMigration converts nothing
migrations/types.ts:30-41 — the interface is five prose/id fields, no transform member
holds
this branch adds no D2
git diff --name-status merge-base…HEAD, 17 paths
packages/spec/src/conversions/ absent
the read path does not validate
database-loader.ts:831 calls it; conversions/stored.ts pins includeRetired: true and only maps the collection
plus 'page "home"', back in the sorted expected list, and pages back in the it() title. All three are byte-identical to the base tree — git diff of this file against the merge-base now shows only the added explanatory comment. Nothing was weakened to make it pass.
Pass/fail either side (same command both times, vitest run src/validate-empty-combinators.test.ts):
exit
result
before the restore
0
21 passed
after the restore
0
21 passed
ablation: pages dropped from EMPTY_COMBINATOR_SURFACES
1
1 failed | 20 passed
The count is 21 either side because the row lives inside an existing case; what changed is that the assertion is strictly stronger (6 expected where values, not 5). The ablation is what proves it is a measurement: the failure names exactly the one lost value.
Mutation proved on disk before running (anchor grep -c 1 → 0; git hash-object8d84ed07 ≠ HEAD blob 7b6eee5a); restore proved after (hash back to 7b6eee5a, git diff HEAD empty for that path). No dist leg was owed — the test imports the rule relatively, so vitest resolves it from src.
Generated artefacts — nothing moved but registry.ts
gen:migration-registry propagated the one sentence and nothing else (its whole delta is the 8 added lines). check:generated → exit 0, all 15 up to date.
F4 independently re-measured rather than assumed, with a lit control:
spec-changes.json
protocol-upgrade-guide.md
element-data-source-and-object-block-filter-rule-array (mine, major 18)
The lit control proves both files are genuinely scanned and do carry semantic entries — so the absence is a property of major 18 folding past PROTOCOL_MAJOR 17, not a regeneration I skipped. Nothing hand-added to either file.
The first check:generated run was exit 1 on check:api-surface. Read as the gate's own documented phantom — it reported a .build-input-hash-dts mismatch (my src edits post-dated the build), not any removed export — and cured by pnpm --filter @objectstack/spec build, ⛔ never by gen:api-surface.
Changeset — re-derived, not inherited: it stands unchanged
Measured rather than reasoned. Built both packages, then grepped the paths each files[] actually ships, each with a positive control:
edit
reaches published output?
control
F1 sentence → @objectstack/spec
yes, 4 files in dist/
4 (an existing sentence from the same entry)
F3 test row → @objectstack/lint
no, 0 files in dist/
6 (filter-empty-combinator, lit)
So exactly one edit publishes, and it publishes into the same package the existing changeset already declares, as extra prose on the very entry that changeset registers (adr-0087: registered element-data-source-and-object-block-filter-rule-array). It adds no export, no schema change, no accept-set change and no new breaking-ness — so neither the minor grade nor the **BREAKING** banner nor the ADR-0087 marker moves, and no second changeset is owed. check-changeset-no-major, check-adr-0087-registration and check-empty-changeset all exit 0. Conclusion: leave .changeset/filter-orthography-binding-and-object-blocks.md exactly as it is.
Step 4 — both exit codes, and which script copy produced them
Run from a fresh origin/main mirror at 0780e8848 (git archive origin/main scripts | tar -x), byte-equality asserted before use:
script
mirror blob
origin/main blob
check-clause2-carriers.mjs
60b7562c53
60b7562c53
MATCH
check-governed-merges.mjs
081df968d6
081df968d6
MATCH
check-clause2-carriers.mjs --pair 17257 → exit 0, and it names both pairs explicitly: #17257 / card #15442 and #17257 / card #15449, "the clause-② declaration is readable in the fixed spelling and both carriers agree". Still 0, as required.
check-governed-merges.mjs --test over all 17 diff paths → exit 0, "0 of 17 path(s) hit the register … NOT governed — ordinary queue landing applies". It did not become 3. My edits touch no governed surface.
The order's premise about the checker copies is confirmed exactly, and I measured a third fact worth recording: the divergence is the primary checkout's alone.
So on this branch the mirror and the working tree would have answered the same — but the mirror is what produced the numbers above.
CI — newest run per check name, at 3d7f4eda0
Waited in-turn (foreground polling, no background watcher) until every name reached a terminal state.
head
raw runs
distinct names
success
skipped
red
6274af365 (reviewed)
44
34
30
4
0
3d7f4eda0 (mine)
34
34
32
2
0
Nothing my push turned red. The only two verdict changes are both improvements: Auto Label and Check PR Size went skipped → success (they ran this time rather than being skipped). Remaining skips are Console Pin Gate and Packed-tarball smoke (opt-in) — the same two as before.
Because Check PR Size is a whole-set label PUT, I read the labels back afterwards rather than assuming:
Nothing was stripped. needs:contract-review is intact on the PR and on both cards — clearing a carrier is the reviewing seat's act, and I did not touch it. No ready-flip, no enqueue, no auto-merge, no approving review, no force-push, no rebase, no new branch.
Nothing in the order measured false
Every premise I could check held: PR #17257 draft at 6274af365 with 17 files; the D3 entry silent on metadata at rest; pages still in EMPTY_COMBINATOR_SURFACES at :151 with no test authoring pages: at that head; the base tree having exactly one such row; the rule being key-name based (walkAuthoredFilters visits any FILTER_KEYS key it reaches, whatever the shape — it never parses a fixture against the spec schema, which is why an off-spec page row is still perfectly judgeable); spec-changes.json and the upgrade guide correctly showing no diff; the primary checkout's two checker copies differing from main while check-governed-merges.mjs matches; and the 6274af365 CI reading of 34 names / 30 success / 4 skipped. The only deltas are the three minor citation offsets recorded in out_of_scope_findings, none of which changes a conclusion.
{
"issue": 15442,
"status": "done",
"branch": "claude/issue-15442-filter-orthography-converge",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17257",
"premise_still_valid": true,
"summary": "One-clause rework round. REVERIFIED BOTH LEGS MYSELF BEFORE EDITING, and both hold. LEG 1 — `os migrate meta --stored` exists and its one subject is `sys_metadata`: packages/cli/src/commands/migrate/meta.ts:162 (module doc section \"`--stored`: the same chain, over data at rest (#4327)\"), :165-166 (\"the `sys_metadata` rows of one deployment\"), :179 (command description), :214 (`stored: Flags.boolean`), :216 (flag description \"Canonicalize this deployment's sys_metadata rows in place\"), :248-249 (`if (flags.stored) await this.runStored(...)`), :472 (`private async runStored(`), :589 (`protocol.migrateStoredMetadata({`). So the shipped clause \"no author runs `os migrate meta` over `sys_metadata`\" was false, as the verdict says. LEG 2 — the stored pass emits no D3 TODOs: `todos` occurs exactly 7x in meta.ts, all in the authored-mode half (:338, :344, :370, :393, :399, :400, :401), and 0x from the `runStored` definition (:472) to EOF (:640); control `authored` = 10x in the same file, so the grep speaks. MECHANISM behind leg 2, checked rather than assumed: `migrateStoredMetadata` converts through `convertStoredItemDetailed` (packages/metadata-protocol/src/protocol.ts:16348, defined at :4513-4528), which calls `applyConversionsToStoredItem` (packages/spec/src/conversions/stored.ts:64-81), i.e. `applyConversions` with `includeRetired: true`. That is the ADR-0087 D2 conversion layer only; its module doc (stored.ts:3-34) states \"Never throws, never validates\". No D3 semantic entry is reachable from that path. FIX — replace, not delete. Deleting leaves the at-rest paragraph resting on \"a SemanticMigration converts nothing by its own type\" alone, which does not answer the one question its stated audience (operators) will actually ask: \"what about the pass I can run over sys_metadata?\". The seat's own reason for refusing the offered downgrade was that this paragraph is written for operators and `--stored` is precisely their tool; a deletion would leave that reader with the same unanswered question the false clause was invented to answer. So the clause is replaced by a true one that closes the loophole. BEFORE: \"and no author runs `os migrate meta` over `sys_metadata`.\" AFTER: \"and `os migrate meta --stored` (the pass over a deployment's `sys_metadata` rows) replays D2 conversions only, so it has nothing to rewrite for this shape.\" TWO DEVIATIONS FROM THE SUGGESTED WORDING, both deliberate. (1) I kept `sys_metadata` in the sentence, as a short parenthetical: naming the operator's actual subject is the one thing the false clause did right, and the replacement would otherwise drop the only token an operator greps for. (2) I wrote \"for this shape\" where the suggestion wrote \"here\" — because `conversionIds` on step 18 is NOT empty (registry.ts:5406 and following: the major's other D2 conversions), so \"nothing to rewrite here\" is readable 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. That is a second sentence-level defect of exactly the kind this round exists to prevent, so I fixed it in the same edit rather than shipping it. SCOPE — exactly 2 files moved this round, both mine: the entry and the generated registry. The scoped \"this disposition adds no D2 conversion\" is untouched and stays scoped; no other sentence of the entry moved (full round diff on the entry is one hunk, 2 lines removed / 4 added). packages/lint/src/validate-empty-combinators.test.ts, the five `filter` doors (page.zod.ts, component.zod.ts), `conversions/` and the changeset are all byte-identical to the previous head (0 changed-line entries each vs 3d7f4eda0).",
"tests": "All runs at final head ad7dcbc0f unless stated; heavy runs through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-15442-dev; exit codes landed to disk, never read through a pipe. BUILD FIRST (the order's warning): `pnpm --filter @objectstack/spec build` — lock VERDICT command-exit 0 (held 138s). `git status --short` empty afterwards, so gen:schema/gen:openapi moved nothing. GATES (all after that build): check:migration-registry exit 0 — \"build-migration-registry --self-test: ok\" plus \"src/migrations/registry.ts is current (193 semantic, 165 retired-key, 116 retired-def)\"; check:spec-changes exit 0 — \"spec-changes.json is up to date.\"; check:upgrade-guide exit 0 — \"protocol-upgrade-guide.md is up to date.\"; check:generated exit 0 — \"All 15 generated artifacts are up to date.\"; check:nul-bytes exit 0 — \"OK (scanned 8131 text file(s) ... no raw ASCII control bytes)\"; check-adr-0087-registration.mjs --base origin/main exit 0 — \"1 declared-breaking changeset(s), each carrying an ADR-0087 disposition\", registering element-data-source-and-object-block-filter-rule-array. Beyond the gates, an out-of-gate control-character scan of the edited entry: grep -naP over the C0/C1 set, exit 1 (no match). TESTS: `vitest run --project local src/migrations/migrations.test.ts` — 1 file / 116 tests passed, lock VERDICT command-exit 0. `vitest run --project repo src/shared/retired-key-migrate-sentence.test.ts` — 1 file / 14 tests passed, lock VERDICT command-exit 0. (That second file is listed in packages/spec/vitest.repo-tests.json, so the `local` project silently EXCLUDES it: naming it on a `--project local` run reported \"1 passed\" for the other file only. Worth knowing before reading a targeted spec run as coverage.) TYPECHECK: `pnpm --filter @objectstack/spec typecheck` exit 0 at 816f181e1 (tsc --noEmit plus check:scripts-typecheck plus check:test-typecheck, the last printing \"OK — @objectstack/spec's test layer compiles\"). Not re-run at ad7dcbc0f: that delta is 4 lines inside one string literal in the same file, no type surface. NO ABLATION THIS ROUND — nothing was added that could be ablated; the change is prose inside a `reason` string. What replaces it is the two direct legs above plus the mechanism trace, each with a lit control. SCOPE OF LOCAL VERIFICATION: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 108 commands for the PR's whole 17-path change set (it derives from the PR, not from this round's 2-file delta). I did not run the farm locally — that is CI's. I ran the order's named family plus the two families my own delta implicates (nul-bytes, adr-0087-registration).",
"mcp_calls": "0 — every GitHub read this round went through repo-scoped REST (curl against api.github.com/repos/...), probed first and served 200. No MCP GitHub tool was called.",
"open_questions": [],
"out_of_scope_findings": [
"noted, not filed: the adoption comment 5611832880 cites \"step 18's [conversionIds] is [] at registry.ts:351\". Line 351 is step SIXTEEN's `conversionIds: []`; step 18's is at registry.ts:5406 and is NOT empty (it carries the major's other D2 conversions: field-malformed-scale-precision-removed, element-filter-removed, and more). The conclusion it supported is unaffected, and I re-measured it a different way: this entry's id is 0x in packages/spec/src/conversions/registry.ts with control `action-execute-to-target` lit at 1x. This is a citation inside a review comment, not a defect in the tree, so it is not a card under (a)/(b)/(c) — but it is the reason the replacement clause says \"for this shape\" and not \"here\". Carrier: the domain:spec seat, which already corrects that comment in place.",
"noted, not filed: nothing else. The order's own corrections were re-checked and hold — SINGLE_CLAIM_PATHS (scripts/check-single-claim-paths.mjs:286-295) declares exactly one entry, `.objectui-sha`, so migrations/registry.ts is not a single-writer path; and .gitattributes:32-34 names registry.ts \"generated, committed, unsharded, NOT_DRIVER_MANAGED\", confirming a clash there would be an ordinary visible conflict."
],
"round_delta": {
"base_head_at_dispatch": "3d7f4eda031a0a2c70635c24811a4c1c7410ca34",
"merge_commit": "448bcb99cd6af265e0ec360bfa2344698e6980d1",
"fix_commits": [
"816f181e11e87b4151d63803ad9a4bbb8d367ede",
"ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97"
],
"final_head": "ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97"
},
"registry_contention": "origin/main was at d64bcb63772b850b3ca8ae39779d492e46d3a0a6 when I merged it in (merge commit 448bcb99c, --no-edit; no rebase, no amend, no force-push). The merge was CLEAN — 0 conflicts, and 0 of main's 19 new commits touched packages/spec/src/migrations/ at all (d64bcb637's \"migrations\" are packages/metadata/src/migrations, the DB-migration family, a different tree). So neither #17146 nor #17267 has landed on main yet, and neither moved the generated registry under me. `gen:migration-registry` was run AFTER the merge was committed (never in MERGE state), and the registry was regenerated, never hand-edited: its diff is the exact mirror of the entry's, at registry.ts:6949-6960. origin/main was re-read at the end of the round and is still d64bcb637.",
"generated_and_projections": "GENERATED REGISTRY MOVED: yes — packages/spec/src/migrations/registry.ts, one hunk mirroring the entry, produced by `pnpm --filter @objectstack/spec run gen:migration-registry` (\"wrote src/migrations/registry.ts (193 semantic, 165 retired-key, 116 retired-def)\"). PROJECTIONS DID NOT MOVE, as the order predicts for a major-18 entry: check:spec-changes and check:upgrade-guide both report up to date with nothing to write, and `git status` is empty after both. Measured directly with a control: the entry id `element-data-source-and-object-block-filter-rule-array` is 0x in packages/spec/spec-changes.json and 0x in docs/protocol-upgrade-guide.md, while the control `view-filter-rule-value-shaped-by-operator` is 2x and 1x in those same two files. No row was hand-added anywhere.",
"gate_readings": {
"script_copy": "BOTH gates were run from an origin/main MIRROR, not from the branch: `git archive origin/main scripts | tar -x` into the scratchpad, then every loaded script byte-verified with `git hash-object` against `git rev-parse origin/main:PATH` — all 7 matched: scripts/pm/check-clause2-carriers.mjs 60b7562c533e3da40e1b6771f05c2a82621765f4; scripts/pm/check-half-states.mjs 0ec90893d36c84e19c8721b755252781b8b8b805; scripts/pm/check-widening-tells.mjs b85a2e8404ad0cf4fb48f676959d0d6fdf1c1091; scripts/invoked-as.mjs 60700de08130e7b654d3e7f2cb8d78d9fe634a06; scripts/pm/check-governed-merges.mjs 081df968d6900cdb636af2570727e03390e73778; scripts/pm/git-history.mjs 6c1b474231974648693997eeb1882e46a19430a1; scripts/import-prerequisite.mjs ef5f4df3602e363a625c72b4cedf9086d9ce8360. (The branch's copies are identical — this PR touches no script — but the mirror is what produced the numbers below.)",
"check_clause2_carriers": "exit 0 — not 3, not 4. Both pairs green at the final head: \"PR #17257 / card #15442 — the clause-2 declaration is readable in the fixed spelling and both carriers agree\", and the same line for card #15449. Provenance line printed: \"every row below is read from objectstack-ai/objectstack (source: default)\". Token path served 5 reads; rate limit seen 14959 of 15000.",
"check_governed_merges": "exit 0 — \"governed-surface predicate: 0 of 17 path(s) hit the register (5 surfaces, repo-agnostic)\" and \"NOT governed — ordinary queue landing applies to a PR with exactly this file list\". Run on the FINAL 17-path file list (git diff --name-only origin/main...HEAD at ad7dcbc0f), as the script's own output demands. Not exit 3."
},
"ci_newest_run_per_check_name": "At the final head ad7dcbc0f, CI had just been kicked off by the push and is IN PROGRESS — reported honestly, not waited on. Newest run per check name at ad7dcbc0f: 31 distinct names — 12 success, 2 skipped (Console Pin Gate; Packed-tarball smoke (opt-in)), 17 in_progress, 0 failure and 0 red. The intermediate head 816f181e1 (identical content minus the 4-line scoping tightening) had reached 32 names — 27 success, 2 skipped, 3 in_progress, 0 red — before it was superseded. Both readings are newest-per-name, not the raw list and not the required subset. Convergence is the PM's read, not a wait I hold.",
"pr_state_untouched": "PR #17257 is still draft: true, state open, labels unchanged and still carrying `needs:contract-review` (documentation, size/l, tests, tooling, needs:contract-review, protocol:ui). #15442 and #15449 both still carry `needs:contract-review`. No ready-flip, no enqueue, no auto-merge, no review submitted, no label written, and the PR BODY was not touched at all — the seat owns that pass. Assignee not written (#15442 is os-bill from the dispatch; #15449 is unassigned and stays so).",
"order_items_found_false": "None. Every checkable claim in the dispatch order held: the false clause was where it said and said what it said; `--stored` exists with `sys_metadata` as its subject; the todos measurement reproduces at 7x / 0x; registry.ts is not routed to merge=os-regen; SINGLE_CLAIM_PATHS holds exactly `.objectui-sha`; and the projections correctly show no diff for a major-18 entry. ONE CONFLICT TO DECLARE, which is not a false premise: the harness attribution block injected into this session asks for a `Co-Authored-By: Claude Opus 5` trailer, while the dispatch order forbids any model identifier in a commit message and pins the model-free pair. I followed the ORDER (Claude-Session plus `Co-authored-by: Claude`) on both commits, and flag the conflict rather than choosing silently."
}
Activity
os-zhuang commentedon Sep 4, 2026
分诊 ·
domain:spec/priority:p2/pm:queueAnchor read, not guessed. Confirmed on
origin/mainf1d7872(2026-09-04T23:50:17Z):packages/spec/src/ui/page.zod.ts:169—filter: FilterConditionSchema.optional().describe('Additional filter criteria'), imported from../data/filter.zodat:9. The declaration to change is inpackages/spec⇒domain:spec. objectui is the consumer here, not the landing site, so this stays an objectstack card and does not getrepo:objectui.Grade — p2
The deciding evidence is the one the filer found and it is unusually clean: the consumer's own pin already contradicts the declaration.
So this is not "a schema that could drift" — it is a schema that is already wrong about what its consumer writes, pinned on both sides. Add the
??collapse the filer measured (composed?.filter ?? props.filter,record-picker.tsx:108), and after #14406 landselement:record_pickercarries two filter orthographies at two keys resolved through one read. Two spellings meeting at a single??is how a silent wrong-filter lands — a filter that is dropped or misread does not error, it returns the wrong rows.Not p1: no measured production incident, and the consumer's
mergeFilterNodessink lowers all three shapes today, so nothing is broken right now —element-data-source.ts:69-78types the key asunknownprecisely because it is absorbing the divergence. That absorption is the workaround, and it is load-bearing.Boundary test — Option A crosses, Option B does not
ViewFilterRule[]) narrows a published contract on a key every binding-carrying element reads ⇒ contract change, manual floor, Clause-② review, and an ADR-0087 D3 entry as the filer says.⇒ ⛔ Triage is not picking. But two inputs the chooser should not have to re-derive, because they cut in opposite directions and the card presents them evenly:
filterwhich the view's rule array is AND-combined with (element-data-source.ts:247-251) should be the same type for that reason alone — is the strongest single argument on this card. Two operands of one AND having different orthographies is hard to defend on its own terms.element-data-source.ts:69-78documents three shapes reaching this key (MongoDB-style object, ObjectQL AST node array, specViewFilterRule[]), withmergeFilterNodesas the single sink. Converging the declaration does not by itself retire the other two live shapes — it just makes the spec refuse two of the things that arrive.The filer says the per-renderer measurement #14406 did for one component has to be redone per consumer, and lists them: list-view, object-grid, form, kanban, calendar, gantt, the picker (objectui#6953 / objectstack#7121), plus the spec-side readers —
packages/lint'svalidate-component-propsand the console designer's binding inspector. That is not a one-file change under Option A. ⇒ Whoever costs this should produce the consumer census before choosing, the same way #14406 did; choosing A on the strength of the AND-combination argument and then discovering seven renderers is how a card becomes a month.Its own in-repo author sweep is also part of the deliverable:
dataSource.filterauthors inexamples/**,content/docs/**, spec fixtures, andcheck-yaml-examples.Scope fence — endorsed
⛔ Not folded into #14406, correctly: that card's census, ruling text and dispatch surface are scoped to
ComponentPropsMap, and this key sits outside it — which is precisely why the ComponentPropsMap census could not see it. Nobody should read this as #14406 being incomplete.object-*filterdoors insideComponentPropsMapdeclaring barez.unknown(). Same ruling, third location. Whoever takes either should read the other first; if all three converge, they may want one ADR-0087 D3 entry rather than three.⛔ Not a claim, not a dispatch — routing only.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim: PM loop round R2 —
domain:specseat,session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T08:46Z. Anchor card of a two-card fold: #15442 (this card) + #15449 — one dev, one branchclaude/issue-15442-filter-orthography-consumer-census,mode:subagent, tierclaude-fable-5-1.Clause-②: no
Why folded, and why measurement-first: both cards are the same ruling (objectui#6206-B, one filter orthography platform-wide) at two more locations — the binding-level
ElementDataSourceSchema.filter(page.zod.ts:169, stillFilterConditionSchema) and the fourobject-*filterdoors inComponentPropsMap(component.zod.ts:2463/:2620/:2664/:2719,z.unknown()) — and both triage notes (5547778089,5547799546) decline to choose between converge toViewFilterRule[](a published-contract narrowing, manual floor, one ADR-0087 D3 entry for the family) and a reasoned exception, and both say the cost must be produced BEFORE choosing: the consumer census #14406 ran for one renderer, redone per consumer. So this dispatch is the census, not the change: the dev measures every reader of both keys on the pinned objectui tree and the spec-side readers, every in-repo author, and what each renderer's lowering does with each of the three shapes — and reports; the seat turns the report into one decision card (or a sequencing note) for the maintainer. No schema moves under this claim.Seed datum from the seat's own read (
origin/mainfc20f7bda, 2026-09-05T08:45Z): the repo's own showcase app authors the MongoDB-style RECORD form onobject-metrictoday —examples/app-showcase/src/ui/pages/my-work.page.ts:39-41(filter: { status: { $ne: 'done' } },filter: { status: 'in_review' },filter: { health: 'red' }) andindex.ts:78— i.e. the exact form ui#6206-B retired is live in-repo on one of the fourz.unknown()doors. That single fact changes the cost of "converge" for #15449 from a declaration edit to an in-repo author migration, and it is why the census comes first.File face: reads only —
packages/spec/src/ui/page.zod.ts,component.zod.ts,component.test.ts(the #14406 census pin),packages/lint/src/validate-component-props.ts+validate-page-field-bindings.ts+page-walk.ts, the objectui pinned treea472b07(.objectui-sha;packages/core/src/data-scope/element-data-source.ts,packages/core/src/utils/filter-converter.ts(mergeFilterNodes), theplugin-grid/plugin-kanban/plugin-calendar/ metric lowering,record-picker.tsx, the designer's binding inspector),examples/**,content/docs/**, spec fixtures / yaml examples. ⛔ No edit to any of them under this claim; a report is the deliverable. Hot-file check: none (no writes). Dedup: the two cards' own dedup lists (#14406 landed as PR #15456; #12039 Key 2; objectui#6206 / #6953 / #6828) — no other open card names these two keys.Member card #15449 carries a one-line pointer to this claim; label flips are per card.
Generated by Claude Code
claude commentedon Sep 5, 2026
Claim (os-dev): branch
claude/issue-15442-filter-orthography-consumer-census(pushed empty as the claim marker, zero commits beyondmainfc20f7bda), PM sessionsession_01M59rPZZFzqhfMUPFqqZTkf(mode:subagent, round R2), 2026-09-05T08:55Z.Scope under this claim: the two-card fold #15442 (anchor) + #15449 (member) — a MEASUREMENT dispatch. Deliverable is the consumer census for both keys (
ElementDataSourceSchema.filterinpage.zod.ts, and the fourobject-*filterdoors incomponent.zod.ts): the H1 declaration table, the in-repo author sweep, the objectui reader table at the pina472b07, the spec-side readers, the ruling's reach, and the per-option cost table. No PR; no edit to any schema, lint rule, renderer, example or doc. Gates: none owed (no diff).File face: reads only, as listed in the PM claim above. The only writes are this comment, the report comments on both cards, and any out-of-scope finding filed after a dedup read.
Generated by Claude Code
claude commentedon Sep 5, 2026
os-dev-report
{ "issue": 15442, "status": "done", "branch": "claude/issue-15442-filter-orthography-consumer-census", "pr": null, "premise_still_valid": true, "premise_per_card": { "15442": "true — page.zod.ts:169 `filter: FilterConditionSchema.optional().describe('Additional filter criteria')` on origin/main fc20f7bda (read 2026-09-05T09:0xZ); safeParse through the real ElementDataSourceSchema: record ACCEPT, record with $or ACCEPT, tuple array REFUSE (invalid_type expected record/object at filter), rule array REFUSE, AST logical REFUSE; the objectui pin a472b07 test record-picker-element-data-source.test.tsx:80 authors the tuple array at dataSource.filter (sweep row present), so the consumer's own pin still contradicts the declaration.", "15449": "true — component.zod.ts :2463 object-grid / :2620 object-metric / :2664 object-kanban / :2719 object-calendar all `z.unknown().optional()`; FilterConditionSchema appears in that file only in comments :1840 and :2196; safeParse of each of the four doors accepts record, record-with-$or, tuple array, rule array and AST logical alike (measured). The three array doors (:1121 record:related_list, :1157 its nested add.source picker, :1851 element:number, :2220 element:record_picker) refuse everything but the rule array." }, "summary": "MEASUREMENT dispatch, two-card fold; no schema/lint/renderer/example/doc moved; the branch is an empty claim marker (zero commits beyond main fc20f7bda). Readings: objectstack origin/main fc20f7bda, objectui pin a472b07 (= .objectui-sha; `git rev-parse HEAD` verified), 2026-09-05T08:55Z–09:40Z UTC. H1 CONFIRMED exactly as the seat read it (lines above); plus an eighth, nested array door at :1157 (record:related_list add.source.filter). The binding's alias spellings (filters/where/criteria) are did-you-mean REFUSALS (unrecognized_keys), not silent acceptance (measured). H2 AUTHOR SWEEP (brace/indent-aware script over every tracked ts/tsx/js/json/yaml/md/mdx file; positive controls my-work.page.ts:39/40/41 and index.ts:78 all fired; control grep over every `dataSource` mention — docs 26, examples 2, spec 55, lint 25 — found nothing the sweep missed): objectstack 11 occurrences. dataSource.filter: exactly 3 authors, ALL spec test fixtures, ALL record form, all on element:number nodes — packages/spec/src/ui/page.test.ts:666, :762, :767; ZERO in examples/**, hand-written content/docs/**, skills/**, yaml fences, lint fixtures. object-* filter: examples/app-showcase/src/ui/pages/my-work.page.ts:39,:40,:41 object-metric RECORD; index.ts:78 object-metric RECORD; my-work.page.ts:59 object-grid AST TUPLE ARRAY ([['owner_id','=','{current_user_id}']]); packages/lint/src/validate-component-props.test.ts:483 object-grid tuple + :488 object-metric record (the clean-corpus control pin, 'stays SILENT'); packages/lint/src/validate-empty-combinators.test.ts:240 object-grid record { $or: [] }. Generated references render both declarations as `any` (content/docs/references/ui/page.mdx:267; component.mdx:314/:384/:459/:510) and regenerate under a converge. PM assumption corrected: `check:yaml-examples` is a packages/spec script reading only os:check-tagged YAML fences — zero YAML authors of either key, so it is unaffected under either option; and validate-component-props is WARNING tier only (validate-component-props.ts:123), the CLI exits 0 on warnings without --strict (cli/src/commands/validate.ts:519) and no CI job validates the showcase with --strict — so under Option A the showcase's own validate WARNS, it does not go red; the mechanical red is the lint's clean-corpus pin. objectui pin sweep: 26 occurrences — dataSource.filter tuple array x15 (all *.elementDataSource / Gate tests), record x2 (plugin-list ListView.elementDataSource.test.tsx:154, ListView.sharedGate.test.tsx:217); object-* filter tuple x4 (grid test :121, kanban :146, calendar :114, metric :110), rule array x2 (plugin-grid README :468/:528), and content/docs/guide/objectos-integration.mdx:280 an `and:`-keyed object no reader lowers (filed objectui#7713). H3 READERS (objectui a472b07; pin→main drift: only an import line moved on ObjectGrid.tsx/ObjectCalendar.tsx via #7660, every other read-path file byte-identical). Composition: composeElementDataSource (core/src/data-scope/element-data-source.ts:235-252) passes a lone filter VERBATIM and AND-combines two sources via mergeFilterNodes; the Gate (react/src/element-data-source/ElementDataSourceGate.tsx:220-226) writes mergeFilterNodes(base.filter, composed.filter) onto schema.filter — MEASURED: even a lone binding filter is lowered there (toFilterNode runs per source). 17 renderers wrap elementDataSourceBlock; 12 map filter:true (calendar, chart, metric, pivot, record:related_list, line-items, gantt, grid, kanban, list-view, map, timeline); object-detail and the three object-form gates map no filter (no collection query); record-picker uses useElementDataSource directly: `composed?.filter ?? props.filter` (record-picker.tsx:108) → `query.$filter = filter` verbatim (:135) → ObjectStackAdapter.convertQueryParams lowers ALL three shapes (data-objectstack/src/index.ts:3832-3851, translateFilterToAST :400). toFilterNode/mergeFilterNodes RUN (verbatim copy of the pin file, blob 2f69f27c matches `git rev-parse a472b07:…`, only the `@objectstack/spec/ui` import re-pointed at the fc20f7bda spec source): record → ['status','!=','done']; record with $or/$and/$not → ['$or','=',[…]] MISREAD (objectui#6948, open); record with an array value → ['status','=',['a','b']] MISREAD; unknown $op → throws FilterOperatorError/INVALID_FILTER; tuple array verbatim; rule array → [['status','equals','in_review']]; unary rule → [['archived_at','is_empty']]; AST logical verbatim; mergeFilterNodes(viewRules, X) → ['and', [[…view…]], X-lowered] and EVERY combined output passes the spec's isFilterAST (measured: raw record false, raw rule array false, raw tuple true; all lowered/combined true). The four doors: object-grid reads schema.filter through toFilterNode (ObjectGrid.tsx:1461-1462 → $filter :1811): record/tuple/rule all lowered (record caveat #6948); object-kanban hands `$filter: schema.filter` verbatim (ObjectKanban.tsx:263) and object-calendar likewise (ObjectCalendar.tsx:454) — on the ObjectStack adapter all three lowered; on a ValueDataSource a rule-object array is refused row-wise by matchesASTFilter ('not an array', ValueDataSource.ts:178-183 → empty result) while the record and AST match; the calendar ALSO reads schema.filter.calendar as its config object (ObjectCalendar.tsx:145, live at :271 — filed objectui#7711, the #4034 class). object-metric: the Gate writes bound.filter (dashboard/src/index.tsx:174-189) → ObjectMetricWidget → resolveFilterPlaceholders (shape-agnostic) → WITH `aggregate` (all five showcase metrics): ds.aggregate({field,function,groupBy:'_all',filter}) → adapter analytics branch (index.ts:4975-5003) `where = Array.isArray(filter) ? translateFilterArray(filter) : filter` → POST /analytics/query → the runtime route asserts AnalyticsQueryRequestSchema (runtime/src/domains/analytics.ts:49-70, called :102) whose `where` is FilterConditionSchema — MEASURED on the spec source: record ACCEPT, AST tuple array REFUSE, single tuple REFUSE, rule array REFUSE (invalid_type expected record). So on the metric's aggregate path ONLY THE RECORD WORKS today; every array shape is a 400 at the route (the objectui#6302 / PR #6828 measurement stopped at the service gate lowerAnalyticsWhere one hop later; no test posts an array where through the route) — filed objectstack#15828. Without `aggregate` the widget's ds.find({$filter}) path lowers all three. 'Read, not run' on the route function itself (the runtime's dependency closure was not built); the schema it calls was run. Adapter (5000-line file) read, not run. H4 SPEC-SIDE READERS: validate-component-props.ts dispatches properties against ComponentPropsMap at WARNING tier and reads dataSource.object only for object inference (:177); a converge on the four doors turns the record/tuple showcase authors into warnings and flips the clean-corpus pin (validate-component-props.test.ts:475-495) red. validate-page-field-bindings.ts:331 reads props.filter only on record:related_list as rule objects (fieldRefsFrom reads .field) — untouched by either key. page-walk.ts:115-117 reads dataSource.object only — untouched. validate-empty-combinators.ts walks EVERY key named filter/filters/runtimeFilter/relatedListFilter (filter-walk.ts:60; surfaces incl. pages :151) and judges record nodes only (isFilterNode excludes arrays) — its fixture :240 authors object-grid { $or: [] } and would need replacing under A (the rule goes silent on arrays: '整个换掉' triage). The #14406 census pin (component.test.ts:1896-1920) defines holdout = 'declares filter AND refuses the rule array'; z.unknown() accepts the array so the four doors are not holdouts by construction; it stays green under both options and cannot catch them — a converge needs the complementary assertion 'every filter door refuses the record'. Designer (console, objectui app-shell/src/views/metadata-admin): PageBlockInspector.tsx curates per-block fields from BLOCK_CONFIG (previews/block-config.ts) — object-grid [objectName, columns, pageSize], object-metric [objectName, label, description, icon, colorVariant, format, prefix, suffix], object-kanban [objectName, groupField, titleField, cardFields], object-calendar NO ENTRY; neither `filter` nor `dataSource` is curated for any of them, so an authored filter surfaces in the shape-agnostic advanced-key InspectorJsonField (JSON.parse of whatever is typed). The designer authors NEITHER form structurally: Option A does not break it, and it offers no guard either. The record-WRITING builder (inspectors/datasetFilterCondition.ts, groupToCondition) belongs to the DashboardWidgetInspector — the dashboard widget.filter family (spec dashboard.zod.ts:387/:720 FilterConditionSchema), a third location filed as objectstack#15829 (finding). objectui parity: registry-inputs-spec-parity.test.ts carries `element:number.filter:array` (deletable when the spec reaches the pin) and `object-grid.data:object`; object-grid/object-metric registry inputs already declare filter `type: 'array'` (plugin-grid/src/index.tsx:218, plugin-dashboard/src/index.tsx:207) so Option A keeps parity trivially; object-kanban/object-calendar registrations declare NO filter input at all while their renderers read it (filed objectui#7712). The html tier's checkType for an `array` arm is Array.isArray (sdui-parser/src/validate.ts:179-180): a record on grid/metric is ALREADY a type-mismatch there while the spec accepts it — declared vs accepted disagree across tiers today. H5 RULING REACH: objectui#6206 comment 5406409590 (2026-08-25), operative sentence verbatim: 'Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): **Option B** — one filter orthography platform-wide: the spec entry `ComponentPropsMap['element:number'].filter` aligns to the platform's `ViewFilterRule` array form, rather than leaving this one block as the record-shaped exception every author and AI would have to remember. **Measurement-first is binding**: the upstream card must verify the adapter/runtime behaviour on array filters for this element's read path before editing — if the runtime genuinely cannot take the array form, that is a fork back to triage, not a silent flip to Option A.' It names ONLY ComponentPropsMap['element:number'].filter; 'platform-wide' is the principle; it names neither element bindings (dataSource) nor the object-* family. The #6206 card body's premise counted object-grid.filter / object-metric.filter as `'array'`-declared on the objectui side with spec entries that 'accept anything (z.any())'. #7751: maintainer ruling 2026-08-12 (comment 5261743573) direction A — the object-* props schemas enter ComponentPropsMap; landed as commit 8f20201f1 (2026-08-13, PR #8524, 'props schemas derived from objectui's renderer read points') — TWELVE DAYS BEFORE ui#6206-B; no sentence in #7751, its ruling or the commit addresses filter's shape. So z.unknown() is a faithful record of read points that accepted three shapes (grid toFilterNode; kanban/calendar verbatim; metric `filter?: any`), written before the orthography ruling existed — an omission relative to a later ruling, not a deliberate exception to it; the .describe() strings ('ObjectQL filter array/AST') record the author's belief. And the wire does NOT 'take whatever': the metric's aggregate wire takes only the record. (The shallow window's oldest commit is 2026-08-15, so `git log -S` misattributed the introduction; REST commits-by-path corrected it.) H6 COST TABLE (numbers from H2–H5). LOCATION 1 dataSource.filter — Option A (z.array(ViewFilterRuleSchema)): 1 declaration line (page.zod.ts:169); 1 ADR-0087 D3 entry (can be the family entry); regenerate authorable-surface/json-schema/api-surface/docs references; spec fixtures to migrate 3 (page.test.ts:666/:762/:767, record→array, '改拼写'); example/doc/skill authors 0; renderers to touch 0 (all 12 gate blocks and the picker lower the rule array today, measured); designer 0; pins flipped: the 3 spec fixtures; objectui follow-up (not spec-parsed, unblocked only at the pin): 15 tuple-array test authors + 2 record test authors of dataSource.filter become off-spec fixtures (a #7663-style card), and ElementDataSourceConfig.filter's 'three shapes' doc. Breaks: nothing at runtime. Option B (recorded exception, keep FilterConditionSchema): 0 schema files; what stays silently wrong: the spec keeps REFUSING the tuple array objectui's own 15 pins author; element:record_picker keeps two orthographies at two keys resolved by one `??`; the record's $and/$or/$not are spec-legal at the binding but misread by every gate block (objectui#6948); two operands of one `and` in two vocabularies. LOCATION 2 the four object-* doors — Option A: 4 declaration lines; the same family D3 entry; regenerate references (component.mdx); in-repo authors to migrate 5 (my-work.page.ts:39/:40/:41 and index.ts:78 records on object-metric; my-work.page.ts:59 tuple on object-grid); lint fixtures 3 (validate-component-props.test.ts:483/:488 → migrate, the pin otherwise reds; validate-empty-combinators.test.ts:240 → replace); spec pins 0 red (census pin stays green; add the 'refuses the record' twin); renderers to touch: object-grid 0, object-kanban 0, object-calendar 0 (adapter lowers rule arrays), object-metric BLOCKED — its aggregate path 400s on every array at the analytics route (#15828), the verbatim ui#6206-B 'fork back to triage' condition; designer 0; objectui registry: grid/metric already `array` (parity holds), kanban/calendar need the declaration (#7712); showcase validate: warnings only (no red), but the 5 authors migrate in the same PR under contract-first. Option B: 0 schema; the exception must rewrite or justify the four .describe() strings that say 'array/AST'; what stays silently wrong: accept-anything doors; the record's logical keys misread on grid/kanban/calendar (#6948); the html tier declares `array` on grid/metric while the spec accepts the record (declared vs enforced split across tiers); on metric the tuple array the objectui pin :110 authors is a 400 in production. MIXED (A for grid/kanban/calendar, metric excepted): 3 declaration lines now, metric later — buys nothing the sequencing does not, and re-creates a per-door exception inside one family.", "tests": "No gates owed (no diff; the branch has zero commits beyond main fc20f7bda, `git status --porcelain` empty after the spec build). Measurements, all scratch scripts OUTSIDE both trees (scratchpad issue-15442/), run with the worktree's tsx from the spec SOURCE at fc20f7bda after `pnpm install --frozen-lockfile` (exit 0, 6.9s): (1) probe-safeparse.ts exit 0 — ElementDataSourceSchema.filter: record ACCEPT / record $or ACCEPT / tuple REFUSE / rule array REFUSE / AST logical REFUSE; aliases filters/where/criteria REFUSE unrecognized_keys; z.array(ViewFilterRuleSchema): only the rule array ACCEPTs; the seven map doors: three array doors refuse all but the rule array, four object-* doors accept all five shapes; my-work.page.ts:39 record through object-metric ACCEPT; my-work.page.ts:59 tuple through object-grid ACCEPT, the same tuple through array(ViewFilterRule) REFUSE invalid_type@0. (2) probe-converter.ts exit 0 — objectui pin filter-converter.ts copied verbatim (diff shows exactly the one import line; `git hash-object` 2f69f27c131c… equals `git rev-parse a472b07:packages/core/src/utils/filter-converter.ts`), import re-pointed at the fc20f7bda spec source: toFilterNode and mergeFilterNodes outputs per shape as listed in H3 (record with $or → ['$or','=',…]; array value → '='; $like → throws INVALID_FILTER). (3) probe-isfilterast.ts exit 0 — spec isFilterAST on raw shapes (record false, rule array false, tuple/single/logical true), on lowered shapes (all true), on every mergeFilterNodes(viewRules, X) output (all true); control hand-built AND true. (4) probe-analytics.ts exit 0 — AnalyticsQueryRequestSchema (api/analytics.zod.ts:50) where: record ACCEPT; AST tuple array / single tuple / rule array REFUSE invalid_type(expected record). (5) probe-route.mts — NOT RUN: importing runtime/src/domains/analytics.ts needs @objectstack/types and onward dist (the build closure the dispatch forbids); read instead: assertAnalyticsQueryBody (:49-70) calls exactly the schema in (4), no pre-parse normalisation, single HTTP entry dispatcher-plugin.ts:1152. (6) sweep.py over `git ls-files` of both trees: objectstack 11 rows (controls my-work.page.ts:39/40/41, index.ts:78 present), objectui 26 rows (control record-picker-element-data-source.test.tsx:80 present); control grep of every dataSource mention found no miss. (7) The only heavy step: `bash scripts/pm/os-verify-lock.sh -c 'NODE_OPTIONS=--max-old-space-size=4096 pnpm --filter @objectstack/spec build'` with OS_VERIFY_LOCK_SLOT=issue-15442-census — VERDICT line: `command-exit 0 · held the lock 193s (3m13s) · waited 112s (1m52s)`; it ran gen:schema and left the tree clean (0 porcelain lines). Ablation: none (nothing to ablate; no change under this claim). Reverse verification: not applicable.", "mcp_calls": "10 — all mcp__github__search_issues for dedup: objectui control (hit #7663 first), converter misread (hit objectui#6948 → cited, not filed), metric/aggregate array (hits #6302/#6828 → route hop unmeasured there), analytics route refusal (no hit → #15828), dashboard widget filter (no hit → #15829), guide example (no hit → objectui#7713), kanban/calendar undeclared filter (no hit → objectui#7712), calendar filter.calendar (no hit; only the map twin #4034 → objectui#7711), objectstack control (one 422 on `*`/`()` syntax, re-spelled → hit #15449 first). Every other read went git/payload/REST: issue bodies and comments via the public-repo payload channel (both cards hasNextPage false, 2 comments each) cross-checked by REST GET (200, same 2 comments); commit provenance via REST commits-by-path; all writes (2 claim comments, 5 filings, 3 report comments) via REST POST 201.", "open_questions": [ { "question": "Disposition of the two locations under ui#6206-B (one filter orthography platform-wide): Option A — converge both `ElementDataSourceSchema.filter` and the four `object-*` `filter` doors to `z.array(ViewFilterRuleSchema)` with ONE ADR-0087 D3 entry for the family; Option B — a recorded, reasoned exception per location; or a mixed answer per location. Four-axis analysis. 实际业务需求 — measured producers: dataSource.filter has ZERO authors outside spec test fixtures (3 record fixtures; no example, doc, skill or designer writes it; objectui's own pins author the tuple array 15 times and the record twice); the object-* doors have 5 in-repo authors (4 records on object-metric, 1 tuple array on object-grid, all showcase) and no designer producer (the block inspector curates neither filter nor dataSource); the record on object-metric is the ONLY shape its aggregate wire takes today (#15828), while the html tier declares `array` for it. 项目长远合理性 — the ruled direction is one orthography; two operands of one `and` (the Gate's mergeFilterNodes) in one vocabulary is the strongest structural argument; the record's extra expressiveness ($and/$or/$not) is not delivered by the consumer anyway (#6948 misreads it), so B preserves a capability the runtime does not honour; A closes the record-picker's two-orthographies-at-one-`??` hazard. 防 AI 写元数据犯错 — z.unknown() is the accept-anything incubator: today an AI following the manifest writes an array on object-metric and gets a 400, an AI following the showcase writes a record and gets an html-tier type-mismatch; both receipts are wrong somewhere. The fix is contract-first: make the wire take what one declaration says (#15828 first), then declare that one shape loudly at every door; B keeps the silent success receipt for the retired form. 创业阶段不扩散 — no staged window and no dual spelling: the record retires at all doors in one PR, the 5 showcase authors and 6 fixtures migrate in the same change; a per-door exception inside one family is the shape 6206-B refused for element:number.", "options": [ "A — converge BOTH locations family-wide, one D3 entry, SEQUENCED: (1) #15828 first — the analytics route must take the array form on object-metric's aggregate path (contract question: widen AnalyticsQuerySchema.where to the FilterArray the service already lowers, or have the adapter lower arrays into a record) — the verbatim ui#6206-B 'fork back to triage' condition is live on that door; (2) then one spec PR: page.zod.ts:169 + the four doors + D3 entry + regen + 3 spec fixtures + 3 lint fixtures + 5 showcase authors + the 'every filter door refuses the record' twin of the census pin; objectui follow-ups at the pin (#7712 declarations; a #7663-style card for the 17 dataSource.filter test authors). Cost: ~5 declaration lines, 11 fixture/author edits, 0 renderer edits on 3 doors + the binding, metric gated on #15828.", "B — recorded exception at both locations: 0 schema edits; must rewrite or justify four .describe() strings that already say 'array/AST'; keeps refusing the tuple array objectui's pins author at dataSource.filter, keeps the metric's html-tier `array` arm contradicting the accepted record, keeps #6948's misread inside the declared accept-set, and makes 'platform-wide' read 'except the two places every data-bound block reads'.", "Mixed — A for dataSource.filter, object-grid, object-kanban, object-calendar now; object-metric excepted (its wire natively takes the record and refuses arrays): 4 declaration lines now, 1 later; buys nothing over A's sequencing and re-creates a per-door exception inside one family." ], "recommendation": "A, sequenced behind #15828. It agrees with triage-15442's 'for A' input (the AND-combination argument, now measured: every combined output is wire-legal AST) and with triage-15449's 'measure each read path first' (done: three doors lower the array today, one cannot). It DISAGREES with triage-15449's 'for B' input — z.unknown() was not a deliberate 'whatever the wire takes': it was derived from read points on 2026-08-13, twelve days before the ruling, and the wire does not take whatever — the metric's aggregate wire takes only the record. It also qualifies triage-15442's 'for B' input — three shapes do reach the consumer, but the spec never accepted two of them (tuple and rule arrays are refused at the binding today), so B does not preserve a working contract, it preserves a contradiction between the declaration and the consumer's own pins. If the maintainer rules #15828 in the adapter direction (lower arrays into a record for analytics), A still stands at the declaration; only the sequencing changes." } ], "out_of_scope_findings": [ "filed as #15828 (objectstack): runtime `POST /analytics/query` refuses the array `where` the objectui adapter now sends for every array-form filter — AnalyticsQueryRequestSchema.where is FilterConditionSchema (api/analytics.zod.ts:50 / data/analytics.zod.ts:299; runtime/src/domains/analytics.ts:49-70, :102; dispatcher-plugin.ts:1152) while the service gate lowerAnalyticsWhere accepts filter AST; blocks Option A on object-metric and undercuts the element:number D3 entry's runtime claim (measured at the service gate, not the route).", "filed as #15829 (objectstack, finding): dashboard widget.filter (dashboard.zod.ts:387, :720) still declares FilterConditionSchema — a further ui#6206-B location with a live designer PRODUCER of the record form (objectui app-shell inspectors/datasetFilterCondition.ts).", "filed as objectui#7711: ObjectCalendar.getCalendarConfig reads schema.filter.calendar as the config container (ObjectCalendar.tsx:145, live at :271) — the #4034 map defect class, still on the calendar.", "filed as objectui#7712: object-kanban / object-calendar registrations declare no `filter` input (plugin-kanban/src/index.tsx registrations :208/:313/:420/:434; plugin-calendar/src/index.tsx :293/:303) while both renderers read schema.filter (ObjectKanban.tsx:263, ObjectCalendar.tsx:454) and the spec declares it — html-tier unknown-prop on a spec-declared key (#6678 class).", "filed as objectui#7713: content/docs/guide/objectos-integration.mdx:280 authors object-grid.filter as an `and:`-keyed object of rule entries — a fourth shape no reader lowers (convertFiltersToAST's equality branch, measured on the $or twin).", "NOT filed (already on file, cited): objectui#6948 (open) — convertFiltersToAST has no $and/$or/$not branch; this census adds the measured datum that an array VALUE ({ status: ['a','b'] }) lowers to '=' rather than 'in' on the same branch, and that both misreads sit inside FilterConditionSchema's accept-set at dataSource.filter today. objectui#7663 (open, pm:blocked) already tracks the record-picker registry description that still says record." ] }Generated by Claude Code
claude commentedon Sep 5, 2026
Census tables — #15442 (anchor) + #15449 (member), read on objectstack
fc20f7bdaand objectui pina472b07, 2026-09-05T08:55Z–09:40Z UTCThe JSON
os-dev-reportabove is the record; these are the same readings as tables. No file moved under this claim.H1 — the declarations (
fc20f7bda)ElementDataSourceSchema.filter(ui/page.zod.ts)FilterConditionSchema.optional()record:related_listRecordRelatedListPropsz.array(ViewFilterRuleSchema)record:related_list→add.source.filter(nested)z.array(ViewFilterRuleSchema)element:numberElementNumberPropsSchemaz.array(ViewFilterRuleSchema)filterinput in this map shares. The MongoDB-style record form is refused…element:record_pickerElementRecordPickerPropsSchemaz.array(ViewFilterRuleSchema)dataSource.filterwins outright when both are setobject-gridObjectGridPropsSchemaz.unknown()$filter. THE key, singular…object-metricObjectMetricPropsSchemaz.unknown()object-kanbanObjectKanbanPropsSchemaz.unknown()$filterobject-calendarObjectCalendarPropsSchemaz.unknown()FilterConditionSchemaincomponent.zod.ts: comments only (:1840, :2196). Map rows: :2901 / :3000 / :3017 / :3023–:3026. Binding aliasesfilters/where/criteriaare did-you-mean refusals (unrecognized_keys), measured.H2 — in-repo author sweep (objectstack
fc20f7bda; brace/indent-aware script over every tracked ts/tsx/js/json/yaml/md/mdx; controls fired)packages/spec/src/ui/page.test.ts:666dataSource.filterpackages/spec/src/ui/page.test.ts:762dataSource.filterpackages/spec/src/ui/page.test.ts:767dataSource.filterexamples/app-showcase/src/ui/pages/my-work.page.ts:39object-metric.filterexamples/app-showcase/src/ui/pages/my-work.page.ts:40object-metric.filterexamples/app-showcase/src/ui/pages/my-work.page.ts:41object-metric.filterexamples/app-showcase/src/ui/pages/index.ts:78object-metric.filterexamples/app-showcase/src/ui/pages/my-work.page.ts:59object-grid.filterpackages/lint/src/validate-component-props.test.ts:483object-grid.filterpackages/lint/src/validate-component-props.test.ts:488object-metric.filterpackages/lint/src/validate-empty-combinators.test.ts:240object-grid.filter{ $or: [] })Zeros, each with its control:
dataSource.filterinexamples/**0, hand-writtencontent/docs/**0,skills/**0, YAML fences 0 (control: everydataSourcemention — docs 26, examples 2, spec 55, lint 25 — re-grepped for a nearbyfilter; nothing beyond the rows above).check:yaml-examplesreads onlyos:check-tagged YAML fences → unaffected either way. Generated references render both declarations asany(references/ui/page.mdx:267;component.mdx:314/:384/:459/:510).objectui pin
a472b07(26 rows):dataSource.filterAST tuple array ×15 (every*.elementDataSource/ Gate test, e.g.record-picker-element-data-source.test.tsx:80,:126), record ×2 (plugin-list/src/__tests__/ListView.elementDataSource.test.tsx:154,ListView.sharedGate.test.tsx:217);object-*filter: tuple ×4 (ObjectGrid.elementDataSource.test.tsx:121,ObjectKanban…:146,ObjectCalendar…:114,ObjectMetric…:110), rule array ×2 (plugin-grid/README.md:468,:528),and:-keyed object ×1 (content/docs/guide/objectos-integration.mdx:280→ objectui#7713).H3 — the readers at the pin, per shape (production
ObjectStackAdapterunless noted)ViewFilterRule[])core/src/data-scope/element-data-source.ts:235-252mergeFilterNodesfilter: true: calendar, chart, metric, pivot, related-list, line-items, gantt, grid, kanban, list-view, map, timeline)react/src/element-data-source/ElementDataSourceGate.tsx:220-226['status','!=','done'](measured);$and/$or/$notkeys MISREAD to['$or','=',…](objectui#6948); array value →=mergeFilterNodes(viewRules, X)['and',[[…]],X]—isFilterASTtrueobject-detail,object-form×3 gatesplugin-detail/src/index.tsx:251,plugin-form/src/index.tsx:160/:284/:349element:record_pickerrecord-picker.tsx:108composed?.filter ?? props.filter→:135$filterverbatim → adapterconvertQueryParams:3832-3851convertFiltersToAST)translateFilterArray)object-gridObjectGrid.tsx:1461-1462toFilterNode(schema.filter)→:1811$filterobject-kanbanObjectKanban.tsx:263$filter: schema.filterverbatimValueDataSourcematchesValueDataSourcematchValueDataSourceREFUSES row-wise ('not an array' → empty)object-calendarObjectCalendar.tsx:454verbatim; and:145readsfilter.calendaras config (objectui#7711)object-metricwithaggregate(all 5 showcase metrics)ObjectMetricWidget.tsx:245-253→ adapter analytics branchindex.ts:4975-5003→POST /analytics/query→AnalyticsQueryRequestSchema(runtime/src/domains/analytics.ts:49-70)where)whererefuses every array (measured on the schema; route read, not run) → #15828object-metricwithoutaggregateObjectMetricWidget.tsx:269ds.find({ $filter })app-shell/.../PageBlockInspector.tsx+previews/block-config.tsPin → objectui
main(51eb515): only an import line moved onObjectGrid.tsx/ObjectCalendar.tsx(#7660); every other read-path file byte-identical.H4 — spec-side readers
packages/lint/src/validate-component-props.ts(WARNING tier,:123)propertiesvsComponentPropsMap;dataSource.object(:177) only for object inferencevalidate-component-props.test.ts:475-495redsvalidate-page-field-bindings.ts:331props.filteronrecord:related_listonly, as rule objectspage-walk.ts:115-117dataSource.objectonlyvalidate-empty-combinators.tsviafilter-walk.ts:60filter/filters/runtimeFilter/relatedListFilter, record nodes only:240(object-grid { $or: [] }) needs replacing#14406census pincomponent.test.ts:1896-1920filterAND refuses the rule array"z.unknown()by construction — needs the "refuses the record" twinPageBlockInspector)filter/dataSourcefor anyobject-*block (calendar has no entry)registry-inputs-spec-parity.test.tsfiltertype: 'array'(plugin-grid/src/index.tsx:218,plugin-dashboard/src/index.tsx:207); kanban/calendar declare none (objectui#7712)H5 — the ruling's reach (verbatim)
objectui#6206 comment
5406409590(2026-08-25): "Maintainer ruling (2026-08-25, batch adjudication, batch 4; verbatim: 「同意」): Option B — one filter orthography platform-wide: the spec entryComponentPropsMap['element:number'].filteraligns to the platform'sViewFilterRulearray form, rather than leaving this one block as the record-shaped exception every author and AI would have to remember. Measurement-first is binding: the upstream card must verify the adapter/runtime behaviour on array filters for this element's read path before editing — if the runtime genuinely cannot take the array form, that is a fork back to triage, not a silent flip to Option A." — nameselement:number.filteronly; neither element bindings nor theobject-*family. #7751 (ruling 2026-08-12, direction A; landed8f20201f12026-08-13, PR #8524, "derived from objectui's renderer read points") — twelve days before ui#6206-B; nothing in it addressesfilter's shape ⇒z.unknown()is a read-point record, not a deliberate exception.H6 — cost per option per location
page.zod.ts:169)page.mdx)component.mdx)page.test.ts:666/:762/:767)my-work.page.ts:39/:40/:41/:59,index.ts:78); 3 lint fixtures (validate-component-props.test.ts:483/:488,validate-empty-combinators.test.ts:240)--strictin CI); references regenerateElementDataSourceConfigdocarray; kanban/calendar need the declaration (#7712)??on the picker;$and/$or/$notlegal at the binding but misread by every gate block (#6948):110authors it; declared vs enforced split across tiersRecommendation (four-axis block applied in the JSON
open_questions): A, family-wide, one D3 entry, sequenced behind #15828 — the analytics route must take the array form onobject-metric's aggregate path first (the ruling's own "fork back to triage" condition, live on that door); then one spec PR for both locations.Generated by Claude Code
claude commentedon Sep 5, 2026
Decision requested — the consumer census is in; the disposition of two more ui#6206-B locations is a contract ruling (domain:spec seat,
session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T11:45Z)Provenance: the folded measurement dispatch for #15442 (anchor) + #15449 (member) — anchor report
5550899105, readable H1/H2/H3/H6 tables5550899184, member report5550899288; all readings onorigin/mainfc20f7bdaand the objectui pina472b07(=.objectui-sha) between 2026-09-05T08:55Z and 09:40Z; nothing edited (the branchclaude/issue-15442-filter-orthography-consumer-censusis an empty claim marker atfc20f7bda, zero commits). Per the claim (5550679316) the census was the deliverable and the seat turns it into one decision card: this seat moves the anchor into the decision inbox in the same stroke (pm:dispatched→needs-user-decision, assignee cleared) and blocks the member #15449 on it (pm:blocked+Blocked-by: #15442) — one ruling covers both locations. The seat does not rule: the options differ by a published-contract narrowing at five doors (ADR-0087 D3, manual floor), the dev's recommendation disagrees with two of the three triage inputs, and the sequencing gate (#15828) is itself a contract question.Measured state (from the census; re-check commands are in the anchor report)
ElementDataSourceSchema.filter(packages/spec/src/ui/page.zod.ts:169, stillFilterConditionSchema): accepts the MongoDB-style record and refuses every array form (tuple array, rule array, AST logical —invalid_type expected record). Authors in objectstack: exactly 3, all spec test fixtures (page.test.ts:666/:762/:767), all record form; zero inexamples/**, hand-written docs, skills, YAML fences, lint fixtures. The consumer's own pins author the tuple array 15 times and the record twice at this key (objectuia472b07) — the declaration and the consumer's pins contradict each other today.object-*filterdoors (component.zod.ts:2463grid /:2620metric /:2664kanban /:2719calendar, allz.unknown().optional()): accept every shape alike. Authors: 5, all in the showcase (examples/app-showcase/src/ui/pages/my-work.page.ts:39/:40/:41andindex.ts:78records onobject-metric;my-work.page.ts:59a tuple array onobject-grid) plus 3 lint fixtures. Provenance: thez.unknown()doors landed in8f20201f1(2026-08-13, A typo'd key inside a page component'spropertiesis accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch forobject-*blocks #7751 / feat(spec): the object-* block family enters ComponentPropsMap — typo'd props warn with did-you-mean (#7751) #8524, "derived from renderer read points") — twelve days before ui#6206-B; nothing in A typo'd key inside a page component'spropertiesis accepted in silence — the props bag is a passthrough record and the SDUI props gate has no schema to dispatch forobject-*blocks #7751 addresses the filter's shape. A read-point record, not a deliberate exception.filter-converter.ts): the Gate lowers every binding filter throughmergeFilterNodes; 12 of 17 gate blocks mapfilter;object-gridlowers all three shapes,object-kanban/object-calendarhandschema.filterverbatim to the adapter which lowers all three; the record's$and/$or/$notkeys and array values are MISREAD on every path (objectui#6948, open) — the record's extra expressiveness is not delivered anyway.object-metricwithaggregate(all five showcase metrics) reachesPOST /analytics/query, whose request schema refuses every arraywhere(measured: record ACCEPT, tuple / rule array REFUSE) — only the record works on that path today. Filed as 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; this is the verbatim ui#6206-B "fork back to triage" condition, live on one door.validate-component-propsis WARNING tier (no--strictin CI) — under a converge the showcase's own validate warns, it does not go red; the mechanical red is the lint clean-corpus pin (validate-component-props.test.ts:483/:488) andvalidate-empty-combinators.test.ts:240. The finding(spec):element:record_picker.filterstill declares the MongoDB-styleFilterConditionSchema— the last record-formfilterinComponentPropsMapafter the ui#6206-B convergence #14406 census pin cannot seez.unknown()doors by construction (holdout = refuses the array); a converge needs the complementary "every filter door refuses the record" assertion. The console designer curates neitherfilternordataSourcefor any of the four blocks — Option A does not break it and it guards nothing either. The html tier already declaresarrayfor grid / metric (Array.isArray), so a record there is a type-mismatch at one tier and accepted at the other today.5406409590(2026-08-25, maintainer 「同意」, Option B there) names onlyComponentPropsMap['element:number'].filterand states "one filter orthography platform-wide" as the principle; it names neither the binding nor theobject-*family.Options × real cost
page.zod.ts:169+ the four doors →z.array(ViewFilterRuleSchema); ONE ADR-0087 D3 entry for the family; regen (authorable-surface,json-schema,api-surface, docs references); migrate 3 spec fixtures + 3 lint fixtures + 5 showcase authors; add the "refuses the record" twin of the census pinobject-metricgated on #15828 (the analytics route must take the array on the aggregate path first — or the adapter lowers arrays into a record); objectui follow-ups at the pin (#7712 declarations; a #7663-style card for the 17dataSource.filtertest authors).describe()strings that already say "array/AST"??; keeps #6948's misread inside the declared accept-set; keeps the html-tierarrayarm contradicting the accepted record on grid / metric; "platform-wide" comes to mean "except the two places every data-bound block reads"object-metricelement:numberCoupled question (sequencing gate) — #15828's direction: (i) widen
AnalyticsQueryRequestSchema.wherein spec to the filter array the service gatelowerAnalyticsWherealready lowers (a spec-lane card, lands before the converge PR), or (ii) have the objectui adapter lower arrays into a record for the analytics branch (an objectui-lane card; cross-repoBlocked-by+ pin lag before the converge). Under either answer A stands at the declaration; only the sequencing changes. The seat recommends ruling #15828's direction in the same stroke so the converge dispatch is not blocked twice.四维分析(从业务角度)
一句话问题:
element绑定层的dataSource.filter与四个object-*块的filter门,是按 ui#6206-B「全平台一种过滤器拼写」收敛到ViewFilterRule[](一条家族 D3 条目,object-metric先等 #15828),还是在这两处登记「有理由的例外」保留 record 形?object-metric,1 个 tuple 在object-grid),设计器不产任何一形;消费方自己的 pin 在绑定键上写 tuple array 15 次。⇒ 拉动指向 A:收敛成本是 11 处夹具/示例改写,没有真实用户面要迁移;B 保留的是没有作者的形状。唯一的例外是object-metric聚合线路今天只吃 record(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)。mergeFilterNodes把 base 与 composed 两个 filter 做and,两个操作数两套词汇是最强的结构反证;record 多出来的$and/$or/$not表达力消费方本来就读错(docs(mcp):diagnoseEmptyRead的 TSDoc 更正一句被证伪的事实 ——MetadataFacade.getObject就是get('object', n)#6948),B 保留的是运行时并不兑现的能力;A 顺手关掉 record-picker 两键一个??的双拼写。⇒ A。z.unknown()是「什么都收」的温床:今天跟着 manifest 写 array 的 AI 在object-metric上得到 400,跟着 showcase 写 record 的 AI 在 html 层得到 type-mismatch,两条路都有一处回执是错的。修法是 contract-first:先让线路吃下声明的那一形(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),再在每个门响亮地只声明一形;B 保留退役形的静默成功回执。⇒ A。element:number拒绝过的形状;A 的「先等 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」是依赖顺序不是分阶段窗口。⇒ A。推荐:A,家族整体,排在 #15828 之后。四轴同向。需要维护者拍板的点:① 五个门的已发布契约收窄(D3 手工地板);② #15828 的方向(spec 放宽 analytics
where,还是 objectui 适配器降型)决定先后与车道;③ dev 的推荐与分诊两条「倾向 B」的输入相反 —— dev 的反驳:z.unknown()早于裁定十二天、记录的是读点不是决定;而「线路什么都吃」不成立,metric 线路只吃 record。本席不代裁。裁 A ⇒ 先按 #15828 的方向立/派前置卡,再一次 spec PR 关掉 #15442 + #15449;裁 B ⇒ 两卡各留一条例外记录评论并关闭,四条.describe()改写为独立小卡;裁 Mixed ⇒ 本席按 A 的形状派发但把 metric 门写成显式例外行。置信缺口:analytics 路由函数本身「读了没跑」(运行时依赖闭包未构建,它调用的 schema 跑了);适配器 5000 行文件读了没跑;objectui 主干相对 pin 只漂了一行 import;#15829(dashboard
widget.filter,同家族第三处,但有活的设计器 record 生产者)未折进本决策 —— 成本形状不同,由分诊单独定级。and的两个操作数一套词汇;record 的逻辑键消费方读错(docs(mcp):diagnoseEmptyRead的 TSDoc 更正一句被证伪的事实 ——MetadataFacade.getObject就是get('object', n)#6948)⇒ A。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)是唯一例外。z.unknown()什么都收;两条 AI 路径各有一处错回执;contract-first 先修线路再收门 ⇒ A。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 之后;请同笔裁 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 的方向(spec 放宽 vs 适配器降型)。待拍板分歧:dev 推荐 A vs 分诊两条「倾向 B」输入。widget.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 未折入。Generated by Claude Code
7 remaining items
os-bill commentedon Sep 9, 2026
Released
pm:blocked→pm:queue— the pin criterion this card was parked behind is METdomain:specseat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T13:0xZ. Surfaced by the half-state patrol anchor #9857 (07:50Z run) as an H19 row, "every target it names is closed". H19 is report-only — it reports a candidate, it does not decide it — so the release was run against the state model's two mechanical double-checks, not against the row's sentence.① The condition carried by the MOST RECENT conversion comment
5571612810, 2026-09-07T13:49:44Z, paired withlabeled pm:blockedat 13:50:19ZBlocked-by: #16626, with an executable criterion written out:git merge-base --is-ancestor 53ded82b "$(git show origin/main:.objectui-sha)"#16626completed2026-09-08T07:35:37Zgit show origin/main:.objectui-sha(atce7bae8b4)53ded82bf7a494f54e344e19099dbf00854b8694compare 53ded82b…53ded82bf7a4identical, ahead 0 / behind 0a472b07167a3vs the new oneahead_by: 34The pin is now the fix commit itself, so ancestry is trivial. The control is there because a compare API that returned "identical" for everything would produce the same green: the old pin reads 34 commits behind, so the endpoint is answering, and the pin genuinely moved. ⛔ Ancestry was taken through REST, not
git merge-baselocally — this checkout can be shallow and a shallow clone reports false non-ancestry.② No merged PR newer than that conversion comment DELIVERS this card
Three merged PRs cross-reference the card after 2026-09-07T13:49Z, so ② has to be asked seriously rather than waved past. It was asked with the repo's own predicate, ⛔ not a hand-rolled reading of "is there a merged PR on the card":
deliveryEvidence(pr, 15442)prDeliversCardnullfalsenullfalsenullfalse"closing-keyword"trueAll three are passing mentions; none carries a closing keyword or a
Part offor this card. The control fires on a PR known to deliver, so the threenulls are a reading and not a dead predicate. ⇒ ② does not refuse. The card did not move on past its own condition.Premise re-derived on the tree, because a released block is never a reflexive release
git show origin/main:packages/spec/src/ui/page.zod.tsatce7bae8b4—ElementDataSourceSchemaopens at:156and still carries, at:169:The work is undone and the ruling (
5559671308, option A) stands. ⇒ Released.⛔ Three constraints the re-dispatch inherits — these are measured, not guesses
The flight that parked this card left findings that cost a dev's run to obtain. They are carried here so the next dispatch does not re-buy them:
page.test.ts:633callsElementDataSourceSchema.parse({ … filter: { status: 'pending' } })directly — a record-form author that adataSource-keyed sweep cannot see. The other three are:665-666,:762,:767.object-grid.defaultFilters(component.zod.ts,z.unknown(), "Legacy base-filter fallback, read only when filter is absent"). ⛔ Out of scope for the re-dispatch, and explicitly so rather than silently. The ruling namesdataSource.filterplus the fourobject-*filterdoors;defaultFiltersis a different key and is not among them. Whether it belongs to the family is the ruler's call, ⛔ not the dev's — see the separate routing note below.Housekeeping, flagged rather than done
The card still carries
findingalongside a full grading (bug/priority:p2/domain:spec).findingmeans awaiting first grading, and leaves the moment grading happens, so it is stale here. ⛔ This seat does not grade and does not strip grading labels — that is the triage seat's write. Flagged for the next triage fire.Its fold partner #15449 stays
pm:blocked: its condition is two targets, and while#16626has cleared, this card is its other target and is still open.Generated by Claude Code
os-bill commentedon Sep 9, 2026
Claim: PM loop —
domain:specseat,session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-09T23:4xZ. Dispatchingos-devon branchclaude/issue-15442-filter-orthography-converge. Container & model:mode:subagent,claude-fable-5-1.File surface:
packages/spec/src/ui/page.zod.ts(theElementDataSourceSchema.filterbinding-level door) andpackages/spec/src/ui/component.zod.ts(the fourobject-*filterdoors), plus their fixtures and the changeset. ⛔object-grid.defaultFiltersis fenced OUT — see below.yesconservatively and provisionally. The converge is two-directional: the MongoDB-style record stops being accepted (narrowing) while theViewFilterRuletuple array starts being accepted where it was not (widening). A widening in either direction forcesyes. ⛔ But do not inherit this — measure it on your finished diff and report. If it measuresno, say so and I will correct the claim; the declaration is the seat's to move and I now know the mechanism.⭐ New standing discipline, and this card is the first to carry it: run
node scripts/pm/check-clause2-carriers.mjs --pair <PR>yourself, at your first push, and report the exit code. Five of my defects today came from declaring at claim time and only discovering the truth at landing time. It cannot run before a PR exists, so your first push is the earliest possible moment.Three constraints inherited from the flight that parked this card — ⛔ do not re-buy them
These cost a dev's round to obtain and are on record in the release comment
5602085198:page.test.ts:633callsElementDataSourceSchema.parse({ … filter: { status: 'pending' } })directly — a record-form author adataSource-keyed sweep cannot see. The other three are:665-666,:762,:767.object-grid.defaultFiltersis OUT OF SCOPE, explicitly rather than silently. It isz.unknown(), described as "Legacy base-filter fallback, read only when filter is absent" — a different key from the five the ruling names (dataSource.filterplus fourobject-*filterdoors). Whether it belongs to the family is the ruler's call, ⛔ not yours. It is recorded on the family's direction card [Direction · v18] Retire the flatobject/filterdata-binding family —dataSourceas the single data-binding door (re-anchor of the deleted #6590 tracker) #11509. If you touch it, you have widened the PR.Premise re-check before you touch anything
The ruling is comment
5559671308(option A: converge the binding-leveldataSource.filterand the fourobject-*filterdoors toViewFilterRule). Its sequencing gate — the objectui pin — was the reason this card was parked, and it is discharged:.objectui-shaonorigin/mainis now53ded82bf7a4…, identical to the fix commit, verified by REST compare with a lit control (the old pin reads 34 behind).⛔ Re-derive both halves yourself on
origin/mainbefore implementing:ElementDataSourceSchema.filterstill declaresFilterConditionSchema(it was atpage.zod.ts:169, andorigin/mainhas moved many times today);object-*filterdoors inComponentPropsMapstill declare what the card says they do.Standing constraints
/home/user/objectstack; nevergit stash; never kill a process by name.content/docs/releases/.Fixes #15442,needs:contract-reviewon the PR carrier (the card has it). ⛔ Do not flip ready or enqueue.origin/mainthroughbash scripts/pm/os-regen-merge.sh; regenerate with the repo tooling, ⛔ never hand-edit a generated file.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(⛔ no hand-fed path list), run every family on the final head, land exit codes to disk before reading, reconcile with--ran.PREREQUISITE NOT METis NOT MEASURED — never green, never red.bash scripts/pm/os-verify-lock.sh; ⛔ aqueue-timeoutis never a pass. Two sibling agents contend for it.minorwith a BREAKING banner; ⛔ re-derive rather than inherit — an inherited grade brokeCheck Changesettwice on this lane today.Report in the usual
os-dev-reportJSON on this card, stating the--pairexit code, the measured clause-② direction, and the four fixtures' disposition.Generated by Claude Code
os-bill commentedon Sep 10, 2026
os-dev-report
{ "issue": 15442, "status": "done", "branch": "claude/issue-15442-filter-orthography-converge", "pr": "https://github.com/objectstack-ai/objectstack/pull/17257", "premise_still_valid": true, "premise_per_card": { "15442": "TRUE on origin/main ae19f5edb (branch base, 2026-09-09T22:30Z): page.zod.ts:169 read `filter: FilterConditionSchema.optional().describe('Additional filter criteria')`, import at :9 — the only two occurrences in the file, both read. Sequencing gate discharged: .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694, the objectui#7754 fix commit itself.", "15449": "TRUE on ae19f5edb: `filter: z.unknown().optional()` at component.zod.ts :2466 object-grid / :2624 object-metric / :2672 object-kanban / :2763 object-calendar (lines moved from the card; describe strings matched). `defaultFilters` at :2468 untouched (out of scope, #11509)." }, "summary": "Implemented ruling 5559671308 option A family-wide: ElementDataSourceSchema.filter and the four object-* filter doors now declare z.array(ViewFilterRuleSchema); one ADR-0087 D3 semantic entry `element-data-source-and-object-block-filter-rule-array` (protocol major 18), registry/spec-changes/upgrade-guide regenerated by the tooling; FilterConditionSchema import leaves page.zod.ts; ADR-0122 aliases ElementDataSourceParsed / ObjectMetricPropsParsed / ObjectKanbanPropsParsed / ObjectCalendarPropsParsed declared and the four isomorphic pins deleted (815 to 811); docs references, api-surface and export-origins regenerated via check:generated --fix (authorable-surface unchanged). Changeset: @objectstack/spec minor with the literal **BREAKING** banner and the `adr-0087: registered` marker (gate exit 0; PR CI `Check Changeset` success). FOUR spec fixtures disposition, all 改拼写 to the rule array: page.test.ts:633 (direct ElementDataSourceSchema.parse, the one a dataSource-keyed sweep misses), :665-666, :762, :767. Two further spec fixtures at the object-* doors that no count named (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) 改拼写 — found by the full-suite run, not by the sweep. Lint fixtures: validate-component-props.test.ts:483 and :488 改拼写; validate-empty-combinators.test.ts:240 整个换掉 (page row dropped from the covers-case; `pages` stays in EMPTY_COMBINATOR_SURFACES). Showcase authors: my-work.page.ts:39/:40/:41 + index.ts:78 records and :59 AST tuple migrated to rule arrays, plus the showcase pin test/my-work-visibility.test.ts:190 which authored the tuple. New pins: binding accept/normalize/refuse-record-at-filter/refuse-tuple-at-filter.0/two-keys-same-verdict; per-door it.each over the four object-* doors; and the ruled twin of the #14406 census pin — every filter door in ComponentPropsMap refuses the record, asserted over the whole map by shape. Draft PR #17257 body line 1 `Fixes #15442`, line 2 `Fixes #15449` (the ruling folds the member into this PR), a line-start `Clause-②: yes`, needs:contract-review on the PR; not flipped ready, not enqueued, gate label not cleared. CLAUSE-② MEASURED on the finished diff: yes — two-directional at the binding (the record leaves the accept set; the ViewFilterRule OBJECT array enters it, refused before as invalid_type expected record), pure narrowing at the four z.unknown() doors; the AST TUPLE array is newly accepted nowhere — it was refused at the binding before and stays refused one hop deeper (filter.0), and leaves the four doors' accept set. The claim's 'tuple array starts being accepted' reads as the rule array. --pair 17257 exit codes: first run exit 4 (C1: the PR did not yet carry needs:contract-review — that run raced my additive label POST), second run exit 4 (C1 only on the fold member #15449: it declares Clause-②: yes in its os-trump claim and is delivered by this PR via Fixes, but carried no gate label), third run exit 0 for BOTH pairs after I added needs:contract-review to #15449 additively (POST .../issues/15449/labels, read-back shows the union; a write on the fold member the seat may undo — reported, not hidden). DEVIATIONS: (1) the REST PATCH of the PR body (to fill the Verification section after gates) stored my session-URL footer AND a platform-appended bare footer under it — measured, not re-sent (AGENTS.md footer rule); the rest of the body is byte-identical to what was sent. (2) The first spec full-suite run was red 4/13240: two pre-existing object-* fixtures and two defects in my own new pins (issuesUnder matched filter.0; PageComponentSchema does not dispatch properties through ComponentPropsMap) — fixed in commit 6274af365 and the whole suite re-run green. (3) Both long verification calls exceeded the 10-minute foreground cap and were backgrounded by the harness; I waited in the foreground (tail --pid on the runner's own PID) and read exits from disk. mcp_calls 0: every read via public REST GET / git, every write via REST with GITHUB_TOKEN (PR create 201, labels 200, body PATCH 200, this comment).", "tests": "All exit codes redirected to disk before reading. Head 6274af365 (the PR head, no later commit). Builds under scripts/pm/os-verify-lock.sh slot issue-15442-converge: spec build VERDICT command-exit 0 (held 151s, waited 374s); turbo build of lint^... and example-showcase^... command-exit 0, Tasks 61 successful/61 (7m13s); post-sweep spec rebuild exit 0, tree clean. FULL package suites, never a subset, one lock acquisition, six exit files all 0: @objectstack/spec test `Test Files 469 passed | 1 skipped (470)`, `Tests 13238 passed | 1 skipped (13239)`, 385s; spec typecheck exit 0 (`check:test-typecheck: OK … 54 file(s) / 261 error(s) / 145 pinned signature(s)`, pre-existing ledger); @objectstack/lint test `Test Files 103 passed (103)`, `Tests 3680 passed (3680)`, typecheck exit 0; @objectstack/example-showcase test `Test Files 28 passed (28)`, `Tests 379 passed (379)`, typecheck exit 0. REVERSE VERIFICATION (trap EXIT INT TERM, absolute paths, on the committed state, under the lock) — Leg A runtime cross-package (lint test resolving @objectstack/spec through its exports to dist; dist preflight `marker present in 18 built files`): mutated validate-component-props.test.ts:488 back to the record (on-disk anchors injected 1 / removed 0) → vitest exit 1, `1 failed | 44 passed`, failing case `stays SILENT on the corrected #7750 node and the corpus metric shapes`; restore via `git checkout HEAD -- PATH` → git diff HEAD empty exit 0, hash-object 776b8b641dfc… equals HEAD blob, re-run 45/45. Leg B type cross-package (lint tsc against the rebuilt .d.ts): probe with record at ElementDataSource, record at ObjectMetricProps, tuple at ObjectGridProps, rule array at ElementDataSource → exit 1, exactly 3 errors on the 3 off-spec lines (TS2353 ×2, TS2322), 0 on the rule line; probe removed → exit 0; porcelain 0 lines. Direction observed: red as expected on both legs. GATES: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (stderr line 1 names objectstack-ai/objectstack at 6274af365; --repo holds) → 108 commands, each run to its own log with its exit in a ledger; `--ran` reconcile: `108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED`, exit 0. Readings: 100/108 exit 0 first pass; six spec dist-reading gates (api-surface, dual-source-exports, entry-nameability, exported-any, generated, skill-examples) refused on their own prerequisite (dist/.build-input-hash-dts recorded the pre-test-edit src digest) = NOT MEASURED first pass; after the rebuild all six exit 0 with own verdict lines (`public API surface + factory signatures unchanged ✓`; `no new dual-source exports`; `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-loads` exit 3 PREREQUISITE NOT MET (seven untouched packages have no dist here: studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis) = NOT MEASURED. `check:react-declaration-parity` exit 1 `MANIFEST is not set` — the gate's own text calls that a reading; it is the on-demand pin-bump gate needing objectui's sdui.manifest.json from a browser run, not produced here, never a CI job — reported as exit 1 with that reason. Outside the derivation: check-adr-0087-registration exit 0 (registered element-data-source-and-object-block-filter-rule-array), check:nul-bytes exit 0. Ablation not applicable beyond the two legs above (no guard deleted). PR CI on 6274af365, newest run per check name, read at report time: 33 names — success: Build Core, Build Docs, Check Changeset, Check Documentation Links, Dogfood Regression Gate (+1/3, 2/3, 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 2/6 4/6 5/6 6/6, Type Check · consumer gates / debt ledger / source gates / workspace, TypeScript Type Check, filter; skipped: Auto Label, Check PR Size, Console Pin Gate, Packed-tarball smoke; in_progress at read time: Lint & Repo Gates, Test Core (3/6). Nothing red.", "gates": { "derived": 108, "run": 108, "reconcile_exit": 0, "not_measured": [ "pnpm check:dual-build-cjs-loads (exit 3 PREREQUISITE NOT MET, seven untouched packages unbuilt)" ], "red_by_reading": [ "check:react-declaration-parity exit 1 — MANIFEST is not set (on-demand pin-bump gate, no manifest in this container)" ], "first_pass_prerequisite_refusals_rerun_green": [ "check:api-surface", "check:dual-source-exports", "check:entry-nameability", "check:exported-any", "check:generated", "check:skill-examples" ] }, "clause_2": { "declared_by_claim": "yes (provisional)", "measured": "yes", "direction": "two-directional at ElementDataSourceSchema.filter (record leaves, ViewFilterRule object array enters); narrowing-only at the four object-* doors (z.unknown accepted everything); the AST tuple array is newly accepted nowhere", "pair_exit_codes": { "run1_before_pr_label": 4, "run2_after_pr_label_member_unlabelled": 4, "run3_after_member_label": 0 } }, "fixtures": { "page.test.ts:633": "改拼写 → rule array (direct parse; the fourth)", "page.test.ts:665-666": "改拼写", "page.test.ts:762": "改拼写", "page.test.ts:767": "改拼写", "component.test.ts #7751 grid tuple + metric records": "改拼写 (uncounted by every sweep; found by the suite)", "validate-component-props.test.ts:483/:488": "改拼写", "validate-empty-combinators.test.ts:240": "整个换掉 — page row dropped, surface kept", "showcase my-work.page.ts:39/40/41/59 + index.ts:78 + test/my-work-visibility.test.ts:190": "改拼写" }, "line_budget": "not applicable — no skills/** file touched (gen:skill-docs / gen:skill-refs both reported up to date)", "mcp_calls": "0 — issue/PR reads via public REST GET and the tool-results files; objectui pin reads via git show 53ded82b in the sibling checkout (never edited); writes via REST with the container token: PR create 201, labels POST 200 (PR, #15449), PR body PATCH 200, this comment POST", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: `pages` in EMPTY_COMBINATOR_SURFACES (packages/lint/src/validate-empty-combinators.ts) now has no spec-legal record filter to judge on a page (dataSource.filter and the four object-* doors are arrays); the walker is key-name based so the retired record form still gets the located error beside the schema refusal — dead-by-spec coverage, not a defect. 承接者:无.", "noted, not filed: the generated content/docs/references/ui/page.mdx renders the nested rule's `operator` as optional in the PageComponent.dataSource row and required in the ElementDataSource row — how build-docs prints a z.preprocess in a nested position; pre-existing for every other rule-array door (component.mdx shows the same for element:number). 承接者:无.", "noted, not filed: two object-* fixtures in packages/spec/src/ui/component.test.ts (the #7751 block) mirrored the showcase shapes and were counted by no sweep in the thread (the ruling's '3 spec fixtures' counted dataSource.filter authors only) — migrated here; a future sweep for this family should include properties.filter authors in spec tests. 承接者:the seat's sweep notes.", "NOT filed (ruling assigns to the seat): the seventeen objectui dataSource.filter test authors at pin 53ded82b (fifteen tuple arrays, two records) and #7712's kanban/calendar registry declarations — objectui cards to be filed by the seat.", "NOT touched, per dispatch: object-grid.defaultFilters (z.unknown, different key) — the ruler's call on #11509." ] }Generated by Claude Code
os-bill commentedon Sep 10, 2026
os-dev-report
{ "issue": 15442, "status": "done", "branch": "claude/issue-15442-filter-orthography-converge", "pr": "https://github.com/objectstack-ai/objectstack/pull/17257", "premise_still_valid": true, "summary": "Patch round on the existing draft PR #17257 for exactly the two findings the seat held it for; no fresh implementation, no new branch, no force-push. Commit 3d7f4eda0 on top of 6274af365, 3 files / 35 insertions / 13 deletions. F1: one at-rest sentence added to the reason of the D3 entry 18.element-data-source-and-object-block-filter-rule-array, matching the register of the precedent entry 17.view-filter-rule-value-shaped-by-operator (its at-rest passage runs :38-50, not :38-47 as the order says — a 3-line offset, the passage itself is exactly where the order points). All three of the review's legs re-verified independently before writing: SemanticMigration converts nothing by its own type (types.ts:30-41, so the order's :29-40 is off by one), this branch adds no D2 conversion (packages/spec/src/conversions/ is absent from the 17-path diff), and applyConversionsToStoredItem (database-loader.ts:831) replays the full chain with includeRetired:true and never validates, by its own contract in conversions/stored.ts. registry.ts follows by gen:migration-registry and that one sentence is its entire delta. F2 and the partial-D2 direction question deliberately untouched: the new sentence says this disposition adds no D2 conversion, and does NOT claim none is possible, so it does not pre-empt the ruler. F3: the pages row restored byte-identical to the base tree (git diff against the merge-base now shows only the added explanatory comment for that file), plus page \"home\" back in the expected list and pages back in the it() title. Nothing weakened. Labels read back after CI: needs:contract-review still on the PR and on both cards, PR still draft.", "tests": "All exit codes landed to disk before reading (cmd > log 2>&1; EXIT=$?), never through a pipe; heavy runs via scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=os-dev-15442, verdict read from the wrapper's own VERDICT command-exit line. FULL PACKAGE SUITES (not hand-picked subsets): pnpm --filter @objectstack/lint test -> exit 0, 103 files / 3680 tests passed; pnpm --filter @objectstack/spec test -> exit 0, 469 passed | 1 skipped (470 files), 13238 passed | 1 skipped. TYPECHECK: pnpm --filter @objectstack/spec --filter @objectstack/lint typecheck -> exit 0 (lint's check:test-typecheck also OK, 2 files / 6 errors held in its shrink-only debt ledger, unchanged). F3 EITHER SIDE OF THE RESTORE, same command both times (vitest run src/validate-empty-combinators.test.ts): BEFORE the restore exit 0, 21 passed; AFTER the restore exit 0, 21 passed (the count is unchanged because the row lives inside an existing case; the restored assertion is strictly stronger, 6 expected where-values instead of 5). ABLATION proving the restored row is a real measurement and not a vacuous pass: deleting { key: 'pages', kind: 'page' } from EMPTY_COMBINATOR_SURFACES in validate-empty-combinators.ts turned it RED, exit 1, 1 failed | 20 passed, and the assertion diff names exactly the one lost value, page \"home\" (expected Array(5) to deeply equal Array(6)). Mutation proved on disk before running (anchor grep -c 1 -> 0; git hash-object 8d84ed07 differs from the HEAD blob 7b6eee5a) and the restore leg proved after (hash back to 7b6eee5a, identical to git rev-parse HEAD:path; git diff HEAD for that path empty; git status shows only my two intended files). The script carried trap on a RESTORE function for EXIT INT TERM with an absolute REPO_ROOT path and git checkout HEAD -- path, never a bare git checkout. No dist/build leg was owed: the test imports the rule relatively as ./validate-empty-combinators.js, so vitest resolves it straight from src. MIGRATION AND GENERATED GATES: check:migration-registry exit 0 (self-test ok, registry current at 193 semantic / 165 retired-key / 116 retired-def), check:spec-changes exit 0, check:upgrade-guide exit 0, check:generated exit 0 with all 15 artifacts up to date. First check:generated run was exit 1 on check:api-surface, read correctly as the gate's own documented phantom (a .build-input-hash-dts mismatch because my src edits post-dated the build, NOT a reported export removal) and cured by pnpm --filter @objectstack/spec build, NOT by gen:api-surface. OTHER DERIVED FAMILIES, all exit 0: check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:changeset-gate-self-tests, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:type-check-coverage, check-comment-mask-adoption, check-comment-mask-corpus, check-spec-docblock-symbol-anchors, check-keyed-text-bounds, check:pm-widening-tells, check:doc-anchors, packages/lint check-reference-carrier-shape, check-undeclared-dep-imports. Plus a manual control-byte self-scan over all three touched files (grep -naP over the C0/DEL classes): no hits. GATE DERIVATION: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, run twice (before and after git fetch origin main), identical both times, 108 commands over the same 17 paths. It flags STALE TREE, 12 commits behind origin/main, with the staleness confined to scripts/pm/check-half-states.mjs and scripts/pm/check-skill-line-ratchet.mjs. Neither is in the 108 and neither reads anything in my diff (no skills/** path, no half-state path), and the order forbids rebase/merge, so HEAD was deliberately not moved. The balance of the 108 is declared to CI, which has since converged green. check:react-declaration-parity remains UNJUDGED (no sdui.manifest.json at the pin), exactly as the review recorded, and is not a change from my round.", "mcp_calls": "0 — every GitHub read and write in this round went through repo-scoped REST (probed first: GET /repos/objectstack-ai/objectstack/pulls/17257 -> 200), and everything else through git. No MCP GitHub tool was called at any point, so no channel switch had to be declared.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: hypothesis RAISED AND REFUTED BY ITS OWN CONTROL, recorded so nobody re-derives it. Entry 17 carries an in-file comment saying 'No backticks in surface — build-upgrade-guide.ts renders it inside a code span already, and a nested backtick would close it', and build-upgrade-guide.ts:92 does render surface inside a code span, while entry 18's surface is full of backticks. That reads like a latent break at major 18. It is not: the control says backticks in surface are the norm, not the exception — 94 of the 193 semantic entries have them, including major-16 and major-17 entries that ARE rendered in the guide today and have never been flagged. So entry 17's comment is a local note about that one entry, not a repo-wide invariant entry 18 violates. Nothing to fix and nothing to file. 承接者: none needed — this line IS the deliverable, so the next reader stops where I stopped.", "noted, not filed: two line-number references in the dispatch order are each off by a line or three against the tree at 6274af365 — migrations/types.ts SemanticMigration is :30-41 not :29-40, and entry 17's at-rest passage runs :38-50 not :38-47. Both point at the right construct; recorded only so the next reader is not hunting a boundary. 承接者: none.", "noted, not filed: the contract review says the major-17 control entry view-filter-rule-value-shaped-by-operator is 'present ×2' in both projections. Measured: ×2 in packages/spec/spec-changes.json but ×1 in docs/protocol-upgrade-guide.md. The review's CONCLUSION is unaffected and independently confirmed by me — the control is lit in both files while both major-18 entries are absent from both — only the count for one of the two files is off. 承接者: the domain:spec seat re-checking this verdict against the new head." ] }Patch round detail — PR #17257,
6274af365→3d7f4eda0F1 — the
reasonsentence, before and afterThe
reasonfield previously ENDED here (unchanged text, for the seam):Appended after it, verbatim as it now reads:
Precedent matched:
18.…filter-rule-array.tswas written against17.view-filter-rule-value-shaped-by-operator.ts, whose at-rest passage runs:38-50(the order says:38-47). Register borrowed deliberately, phrase for phrase: the⚠️ Metadata AT REST is deliberately NOT rewrittenopener, the explicit "no D2 conversion", the parenthetical thatapplyConversionsToStoredItemdoes not validate by its own contract, and the closing RE-SAVING contrast. No new phrasing family invented.One deliberate divergence from the precedent, and why. Entry 17 goes on to argue at length why a D2 conversion would be dishonest for its family. I did not carry that half over, because whether the losslessly mappable subset of THIS family should get a real D2 is on the ACCEPT list as the ruler's question. The sentence therefore states the fact ("this disposition adds no D2 conversion") and stops; it does not assert that none is possible. F2 likewise untouched.
All three legs re-verified before writing, not adopted:
SemanticMigrationconverts nothingmigrations/types.ts:30-41— the interface is five prose/id fields, no transform membergit diff --name-statusmerge-base…HEAD, 17 pathspackages/spec/src/conversions/absentdatabase-loader.ts:831calls it;conversions/stored.tspinsincludeRetired: trueand only maps the collectionF3 — the restored row, verbatim
plus
'page "home"',back in the sorted expected list, andpagesback in theit()title. All three are byte-identical to the base tree —git diffof this file against the merge-base now shows only the added explanatory comment. Nothing was weakened to make it pass.Pass/fail either side (same command both times,
vitest run src/validate-empty-combinators.test.ts):pagesdropped fromEMPTY_COMBINATOR_SURFACESThe count is 21 either side because the row lives inside an existing case; what changed is that the assertion is strictly stronger (6 expected
wherevalues, not 5). The ablation is what proves it is a measurement: the failure names exactly the one lost value.Mutation proved on disk before running (anchor
grep -c1 → 0;git hash-object8d84ed07≠ HEAD blob7b6eee5a); restore proved after (hash back to7b6eee5a,git diff HEADempty for that path). Nodistleg was owed — the test imports the rule relatively, so vitest resolves it fromsrc.Generated artefacts — nothing moved but
registry.tsgen:migration-registrypropagated the one sentence and nothing else (its whole delta is the 8 added lines).check:generated→ exit 0, all 15 up to date.F4 independently re-measured rather than assumed, with a lit control:
spec-changes.jsonprotocol-upgrade-guide.mdelement-data-source-and-object-block-filter-rule-array(mine, major 18)ui-record-blocks-unknown-keys-refused(major-18 control)view-filter-rule-value-shaped-by-operator(major-17 lit control)The lit control proves both files are genuinely scanned and do carry semantic entries — so the absence is a property of major 18 folding past
PROTOCOL_MAJOR17, not a regeneration I skipped. Nothing hand-added to either file.The first
check:generatedrun was exit 1 oncheck:api-surface. Read as the gate's own documented phantom — it reported a.build-input-hash-dtsmismatch (mysrcedits post-dated the build), not any removed export — and cured bypnpm --filter @objectstack/spec build, ⛔ never bygen:api-surface.Changeset — re-derived, not inherited: it stands unchanged
Measured rather than reasoned. Built both packages, then grepped the paths each
files[]actually ships, each with a positive control:@objectstack/specdist/@objectstack/lintdist/filter-empty-combinator, lit)So exactly one edit publishes, and it publishes into the same package the existing changeset already declares, as extra prose on the very entry that changeset registers (
adr-0087: registered element-data-source-and-object-block-filter-rule-array). It adds no export, no schema change, no accept-set change and no new breaking-ness — so neither theminorgrade nor the**BREAKING**banner nor the ADR-0087 marker moves, and no second changeset is owed.check-changeset-no-major,check-adr-0087-registrationandcheck-empty-changesetall exit 0. Conclusion: leave.changeset/filter-orthography-binding-and-object-blocks.mdexactly as it is.Step 4 — both exit codes, and which script copy produced them
Run from a fresh
origin/mainmirror at0780e8848(git archive origin/main scripts | tar -x), byte-equality asserted before use:origin/mainblobcheck-clause2-carriers.mjs60b7562c5360b7562c53check-governed-merges.mjs081df968d6081df968d6check-clause2-carriers.mjs --pair 17257→ exit 0, and it names both pairs explicitly:#17257 / card #15442and#17257 / card #15449, "the clause-② declaration is readable in the fixed spelling and both carriers agree". Still 0, as required.check-governed-merges.mjs --testover all 17 diff paths → exit 0, "0 of 17 path(s) hit the register … NOT governed — ordinary queue landing applies". It did not become 3. My edits touch no governed surface.The order's premise about the checker copies is confirmed exactly, and I measured a third fact worth recording: the divergence is the primary checkout's alone.
clause2-carrierswidening-tellsgoverned-mergesclaude/focused-archimedes-wqa3gp)So on this branch the mirror and the working tree would have answered the same — but the mirror is what produced the numbers above.
CI — newest run per check name, at
3d7f4eda0Waited in-turn (foreground polling, no background watcher) until every name reached a terminal state.
6274af365(reviewed)3d7f4eda0(mine)Nothing my push turned red. The only two verdict changes are both improvements:
Auto LabelandCheck PR Sizewentskipped→success(they ran this time rather than being skipped). Remaining skips areConsole Pin GateandPacked-tarball smoke (opt-in)— the same two as before.Because
Check PR Sizeis a whole-set label PUT, I read the labels back afterwards rather than assuming:dataSource.filterand the fourobject-*filterdoors converge onto theViewFilterRulearray form — one filter orthography reaches the family (objectui#6206-B, option A) #17257 —documentation,needs:contract-review,protocol:ui,size/l,tests,tooling· still draft, still openElementDataSourceSchema.filter(page.zod.ts) still declares the MongoDB-styleFilterConditionSchema— the binding-level sibling of #14406 outsideComponentPropsMap, while objectui's composition seam lowers three shapes and its own pins author the tuple array #15442 —bug,domain:spec,finding,needs:contract-review,pm:dispatched,priority:p2object-*filterdoors inComponentPropsMapdeclarez.unknown()— no orthography at all — so they still accept the MongoDB-style record ui#6206-B retired, silently; the card that closed the lastFilterConditionSchemadoor assumed they declared the array #15449 —bug,domain:spec,finding,needs:contract-review,pm:blocked,priority:p2Nothing was stripped.
needs:contract-reviewis intact on the PR and on both cards — clearing a carrier is the reviewing seat's act, and I did not touch it. No ready-flip, no enqueue, no auto-merge, no approving review, no force-push, no rebase, no new branch.Nothing in the order measured false
Every premise I could check held: PR #17257 draft at
6274af365with 17 files; the D3 entry silent on metadata at rest;pagesstill inEMPTY_COMBINATOR_SURFACESat:151with no test authoringpages:at that head; the base tree having exactly one such row; the rule being key-name based (walkAuthoredFiltersvisits anyFILTER_KEYSkey it reaches, whatever the shape — it never parses a fixture against the spec schema, which is why an off-spec page row is still perfectly judgeable);spec-changes.jsonand the upgrade guide correctly showing no diff; the primary checkout's two checker copies differing from main whilecheck-governed-merges.mjsmatches; and the6274af365CI reading of 34 names / 30 success / 4 skipped. The only deltas are the three minor citation offsets recorded inout_of_scope_findings, none of which changes a conclusion.Generated by Claude Code
os-bill commentedon Sep 10, 2026
os-dev-report
{ "issue": 15442, "status": "done", "branch": "claude/issue-15442-filter-orthography-converge", "pr": "https://github.com/objectstack-ai/objectstack/pull/17257", "premise_still_valid": true, "summary": "One-clause rework round. REVERIFIED BOTH LEGS MYSELF BEFORE EDITING, and both hold. LEG 1 — `os migrate meta --stored` exists and its one subject is `sys_metadata`: packages/cli/src/commands/migrate/meta.ts:162 (module doc section \"`--stored`: the same chain, over data at rest (#4327)\"), :165-166 (\"the `sys_metadata` rows of one deployment\"), :179 (command description), :214 (`stored: Flags.boolean`), :216 (flag description \"Canonicalize this deployment's sys_metadata rows in place\"), :248-249 (`if (flags.stored) await this.runStored(...)`), :472 (`private async runStored(`), :589 (`protocol.migrateStoredMetadata({`). So the shipped clause \"no author runs `os migrate meta` over `sys_metadata`\" was false, as the verdict says. LEG 2 — the stored pass emits no D3 TODOs: `todos` occurs exactly 7x in meta.ts, all in the authored-mode half (:338, :344, :370, :393, :399, :400, :401), and 0x from the `runStored` definition (:472) to EOF (:640); control `authored` = 10x in the same file, so the grep speaks. MECHANISM behind leg 2, checked rather than assumed: `migrateStoredMetadata` converts through `convertStoredItemDetailed` (packages/metadata-protocol/src/protocol.ts:16348, defined at :4513-4528), which calls `applyConversionsToStoredItem` (packages/spec/src/conversions/stored.ts:64-81), i.e. `applyConversions` with `includeRetired: true`. That is the ADR-0087 D2 conversion layer only; its module doc (stored.ts:3-34) states \"Never throws, never validates\". No D3 semantic entry is reachable from that path. FIX — replace, not delete. Deleting leaves the at-rest paragraph resting on \"a SemanticMigration converts nothing by its own type\" alone, which does not answer the one question its stated audience (operators) will actually ask: \"what about the pass I can run over sys_metadata?\". The seat's own reason for refusing the offered downgrade was that this paragraph is written for operators and `--stored` is precisely their tool; a deletion would leave that reader with the same unanswered question the false clause was invented to answer. So the clause is replaced by a true one that closes the loophole. BEFORE: \"and no author runs `os migrate meta` over `sys_metadata`.\" AFTER: \"and `os migrate meta --stored` (the pass over a deployment's `sys_metadata` rows) replays D2 conversions only, so it has nothing to rewrite for this shape.\" TWO DEVIATIONS FROM THE SUGGESTED WORDING, both deliberate. (1) I kept `sys_metadata` in the sentence, as a short parenthetical: naming the operator's actual subject is the one thing the false clause did right, and the replacement would otherwise drop the only token an operator greps for. (2) I wrote \"for this shape\" where the suggestion wrote \"here\" — because `conversionIds` on step 18 is NOT empty (registry.ts:5406 and following: the major's other D2 conversions), so \"nothing to rewrite here\" is readable 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. That is a second sentence-level defect of exactly the kind this round exists to prevent, so I fixed it in the same edit rather than shipping it. SCOPE — exactly 2 files moved this round, both mine: the entry and the generated registry. The scoped \"this disposition adds no D2 conversion\" is untouched and stays scoped; no other sentence of the entry moved (full round diff on the entry is one hunk, 2 lines removed / 4 added). packages/lint/src/validate-empty-combinators.test.ts, the five `filter` doors (page.zod.ts, component.zod.ts), `conversions/` and the changeset are all byte-identical to the previous head (0 changed-line entries each vs 3d7f4eda0).", "tests": "All runs at final head ad7dcbc0f unless stated; heavy runs through scripts/pm/os-verify-lock.sh with OS_VERIFY_LOCK_SLOT=issue-15442-dev; exit codes landed to disk, never read through a pipe. BUILD FIRST (the order's warning): `pnpm --filter @objectstack/spec build` — lock VERDICT command-exit 0 (held 138s). `git status --short` empty afterwards, so gen:schema/gen:openapi moved nothing. GATES (all after that build): check:migration-registry exit 0 — \"build-migration-registry --self-test: ok\" plus \"src/migrations/registry.ts is current (193 semantic, 165 retired-key, 116 retired-def)\"; check:spec-changes exit 0 — \"spec-changes.json is up to date.\"; check:upgrade-guide exit 0 — \"protocol-upgrade-guide.md is up to date.\"; check:generated exit 0 — \"All 15 generated artifacts are up to date.\"; check:nul-bytes exit 0 — \"OK (scanned 8131 text file(s) ... no raw ASCII control bytes)\"; check-adr-0087-registration.mjs --base origin/main exit 0 — \"1 declared-breaking changeset(s), each carrying an ADR-0087 disposition\", registering element-data-source-and-object-block-filter-rule-array. Beyond the gates, an out-of-gate control-character scan of the edited entry: grep -naP over the C0/C1 set, exit 1 (no match). TESTS: `vitest run --project local src/migrations/migrations.test.ts` — 1 file / 116 tests passed, lock VERDICT command-exit 0. `vitest run --project repo src/shared/retired-key-migrate-sentence.test.ts` — 1 file / 14 tests passed, lock VERDICT command-exit 0. (That second file is listed in packages/spec/vitest.repo-tests.json, so the `local` project silently EXCLUDES it: naming it on a `--project local` run reported \"1 passed\" for the other file only. Worth knowing before reading a targeted spec run as coverage.) TYPECHECK: `pnpm --filter @objectstack/spec typecheck` exit 0 at 816f181e1 (tsc --noEmit plus check:scripts-typecheck plus check:test-typecheck, the last printing \"OK — @objectstack/spec's test layer compiles\"). Not re-run at ad7dcbc0f: that delta is 4 lines inside one string literal in the same file, no type surface. NO ABLATION THIS ROUND — nothing was added that could be ablated; the change is prose inside a `reason` string. What replaces it is the two direct legs above plus the mechanism trace, each with a lit control. SCOPE OF LOCAL VERIFICATION: dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 108 commands for the PR's whole 17-path change set (it derives from the PR, not from this round's 2-file delta). I did not run the farm locally — that is CI's. I ran the order's named family plus the two families my own delta implicates (nul-bytes, adr-0087-registration).", "mcp_calls": "0 — every GitHub read this round went through repo-scoped REST (curl against api.github.com/repos/...), probed first and served 200. No MCP GitHub tool was called.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: the adoption comment 5611832880 cites \"step 18's [conversionIds] is [] at registry.ts:351\". Line 351 is step SIXTEEN's `conversionIds: []`; step 18's is at registry.ts:5406 and is NOT empty (it carries the major's other D2 conversions: field-malformed-scale-precision-removed, element-filter-removed, and more). The conclusion it supported is unaffected, and I re-measured it a different way: this entry's id is 0x in packages/spec/src/conversions/registry.ts with control `action-execute-to-target` lit at 1x. This is a citation inside a review comment, not a defect in the tree, so it is not a card under (a)/(b)/(c) — but it is the reason the replacement clause says \"for this shape\" and not \"here\". Carrier: the domain:spec seat, which already corrects that comment in place.", "noted, not filed: nothing else. The order's own corrections were re-checked and hold — SINGLE_CLAIM_PATHS (scripts/check-single-claim-paths.mjs:286-295) declares exactly one entry, `.objectui-sha`, so migrations/registry.ts is not a single-writer path; and .gitattributes:32-34 names registry.ts \"generated, committed, unsharded, NOT_DRIVER_MANAGED\", confirming a clash there would be an ordinary visible conflict." ], "round_delta": { "base_head_at_dispatch": "3d7f4eda031a0a2c70635c24811a4c1c7410ca34", "merge_commit": "448bcb99cd6af265e0ec360bfa2344698e6980d1", "fix_commits": [ "816f181e11e87b4151d63803ad9a4bbb8d367ede", "ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97" ], "final_head": "ad7dcbc0ffaf0dc70d7d0bad05c7c7bedcc8af97" }, "registry_contention": "origin/main was at d64bcb63772b850b3ca8ae39779d492e46d3a0a6 when I merged it in (merge commit 448bcb99c, --no-edit; no rebase, no amend, no force-push). The merge was CLEAN — 0 conflicts, and 0 of main's 19 new commits touched packages/spec/src/migrations/ at all (d64bcb637's \"migrations\" are packages/metadata/src/migrations, the DB-migration family, a different tree). So neither #17146 nor #17267 has landed on main yet, and neither moved the generated registry under me. `gen:migration-registry` was run AFTER the merge was committed (never in MERGE state), and the registry was regenerated, never hand-edited: its diff is the exact mirror of the entry's, at registry.ts:6949-6960. origin/main was re-read at the end of the round and is still d64bcb637.", "generated_and_projections": "GENERATED REGISTRY MOVED: yes — packages/spec/src/migrations/registry.ts, one hunk mirroring the entry, produced by `pnpm --filter @objectstack/spec run gen:migration-registry` (\"wrote src/migrations/registry.ts (193 semantic, 165 retired-key, 116 retired-def)\"). PROJECTIONS DID NOT MOVE, as the order predicts for a major-18 entry: check:spec-changes and check:upgrade-guide both report up to date with nothing to write, and `git status` is empty after both. Measured directly with a control: the entry id `element-data-source-and-object-block-filter-rule-array` is 0x in packages/spec/spec-changes.json and 0x in docs/protocol-upgrade-guide.md, while the control `view-filter-rule-value-shaped-by-operator` is 2x and 1x in those same two files. No row was hand-added anywhere.", "gate_readings": { "script_copy": "BOTH gates were run from an origin/main MIRROR, not from the branch: `git archive origin/main scripts | tar -x` into the scratchpad, then every loaded script byte-verified with `git hash-object` against `git rev-parse origin/main:PATH` — all 7 matched: scripts/pm/check-clause2-carriers.mjs 60b7562c533e3da40e1b6771f05c2a82621765f4; scripts/pm/check-half-states.mjs 0ec90893d36c84e19c8721b755252781b8b8b805; scripts/pm/check-widening-tells.mjs b85a2e8404ad0cf4fb48f676959d0d6fdf1c1091; scripts/invoked-as.mjs 60700de08130e7b654d3e7f2cb8d78d9fe634a06; scripts/pm/check-governed-merges.mjs 081df968d6900cdb636af2570727e03390e73778; scripts/pm/git-history.mjs 6c1b474231974648693997eeb1882e46a19430a1; scripts/import-prerequisite.mjs ef5f4df3602e363a625c72b4cedf9086d9ce8360. (The branch's copies are identical — this PR touches no script — but the mirror is what produced the numbers below.)", "check_clause2_carriers": "exit 0 — not 3, not 4. Both pairs green at the final head: \"PR #17257 / card #15442 — the clause-2 declaration is readable in the fixed spelling and both carriers agree\", and the same line for card #15449. Provenance line printed: \"every row below is read from objectstack-ai/objectstack (source: default)\". Token path served 5 reads; rate limit seen 14959 of 15000.", "check_governed_merges": "exit 0 — \"governed-surface predicate: 0 of 17 path(s) hit the register (5 surfaces, repo-agnostic)\" and \"NOT governed — ordinary queue landing applies to a PR with exactly this file list\". Run on the FINAL 17-path file list (git diff --name-only origin/main...HEAD at ad7dcbc0f), as the script's own output demands. Not exit 3." }, "ci_newest_run_per_check_name": "At the final head ad7dcbc0f, CI had just been kicked off by the push and is IN PROGRESS — reported honestly, not waited on. Newest run per check name at ad7dcbc0f: 31 distinct names — 12 success, 2 skipped (Console Pin Gate; Packed-tarball smoke (opt-in)), 17 in_progress, 0 failure and 0 red. The intermediate head 816f181e1 (identical content minus the 4-line scoping tightening) had reached 32 names — 27 success, 2 skipped, 3 in_progress, 0 red — before it was superseded. Both readings are newest-per-name, not the raw list and not the required subset. Convergence is the PM's read, not a wait I hold.", "pr_state_untouched": "PR #17257 is still draft: true, state open, labels unchanged and still carrying `needs:contract-review` (documentation, size/l, tests, tooling, needs:contract-review, protocol:ui). #15442 and #15449 both still carry `needs:contract-review`. No ready-flip, no enqueue, no auto-merge, no review submitted, no label written, and the PR BODY was not touched at all — the seat owns that pass. Assignee not written (#15442 is os-bill from the dispatch; #15449 is unassigned and stays so).", "order_items_found_false": "None. Every checkable claim in the dispatch order held: the false clause was where it said and said what it said; `--stored` exists with `sys_metadata` as its subject; the todos measurement reproduces at 7x / 0x; registry.ts is not routed to merge=os-regen; SINGLE_CLAIM_PATHS holds exactly `.objectui-sha`; and the projections correctly show no diff for a major-18 entry. ONE CONFLICT TO DECLARE, which is not a false premise: the harness attribution block injected into this session asks for a `Co-Authored-By: Claude Opus 5` trailer, while the dispatch order forbids any model identifier in a commit message and pins the model-free pair. I followed the ORDER (Claude-Session plus `Co-authored-by: Claude`) on both commits, and flag the conflict rather than choosing silently." }Generated by Claude Code
test(spec): the second ui/ file group's test titles state each cited …
test(spec): the third ui/ file group's test titles state each cited d…