Skip to content

feat(spec)!: element:record_picker.filter converges onto the ViewFilterRule array form — the last record-form filter in ComponentPropsMap (objectui#6206 Option B) - #15456

Merged
os-sales merged 6 commits into
mainfrom
claude/issue-14406-record-picker-filter-orthography
Sep 4, 2026
Merged

feat(spec)!: element:record_picker.filter converges onto the ViewFilterRule array form — the last record-form filter in ComponentPropsMap (objectui#6206 Option B)#15456
os-sales merged 6 commits into
mainfrom
claude/issue-14406-record-picker-filter-orthography

Conversation

@os-sales

@os-sales os-sales commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14406

Draft, CONTRACT_REVIEW_TIER, Clause ②: this PR changes what the published contract ACCEPTS at element:record_picker.filter. needs:contract-review is hung on this PR and on the card together with this reviewable increment; the seat reviews in-seat first — not flipped ready, not enqueued, not auto-merged.

R3 verdict: the read path takes the array form end to end — converge

Maintainer ruling objectui#6206-B (2026-08-25, verbatim 「同意」): one filter orthography platform-wide. After #12039 Key 2 (PR #14404) converged element:number, ComponentPropsMap['element:record_picker'].filter was the last filter input in the map still declared as the MongoDB-style FilterConditionSchema record. Triage fixed the sequencing: measure the consumer BEFORE moving the declaration, because the element:number runtime refused raw rule arrays until objectui#6828. So step 1 was measured, at the pin and at objectui main, by symbol not by line — and the array form is taken end to end, so this PR executes the last entry.

Step 1 readings (H4) at the pin 00d3f09c500c4a45b5f27aca8af80349412faaf1 (= .objectui-sha on origin/main)

Read-only over the sibling checkout with git show PIN:path / git grep PIN; objectui was not edited.

  • packages/components/src/renderers/basic/record-picker.tsx:108const filter = composed?.filter ?? props.filter;:135 if (filter) query.$filter = filter;:138 await adapter.find(object, query). The door is find(), not aggregate().
  • packages/data-objectstack/src/index.ts:3846-3851 (ObjectStackAdapter.convertQueryParams, the find() route): an ARRAY $filter is lowered through translateFilterArray; an object goes through convertFiltersToAST. translateFilterArray (:348) recognises [{ field, operator, value }, ...] via isObjectFilterEntryForm and lowers each rule to an AST tuple (objectFilterEntriesToAST, ['and', ...] for several). Contrast aggregate() (:5002), which only learned this on objectui#6828 — the reason the element:number convergence had to be consumer-first; find() has had it since objectui#6302.
  • packages/core/src/data-scope/element-data-source.ts:69-78, :247-251 — the composition seam types filter as unknown because three shapes reach it (record object, AST array, ViewFilterRule[]) and mergeFilterNodes lowers all three; packages/components/src/__tests__/record-picker-element-data-source.test.tsx:35,72,80 already ship array-shaped filters through the picker's $filter.
  • Nothing on that path parses properties against the installed @objectstack/spec: readProps (renderers/basic/readProps.ts) is a spread; non-test source names ElementRecordPickerPropsSchema / ElementRecordPickerProps only inside comments (git grep -w, exit 1 on non-comment lines). So the pin's npm spec (^17.0.0, which refuses arrays at this key) never judges the authored value at render time.
  • objectstack side: packages/objectql/src/engine-filter-array-lowering.test.ts pins that Door 2 lowers the tuple array to a FilterCondition before the driver — the same door every list view's stored rule array already takes.
  • Authoring-time residue in objectui that is NOT a runtime refusal: the registry declares inputs.filter as type: 'object' (record-picker.tsx:344-370), so a JSX page writing the array gets a type-mismatch diagnostic — severity warning (sdui-parser/src/validate.ts:246), and renderers/layout/page.tsx:553 acts on error only, so the page still renders and filters. That, plus the two parity pins (record-picker-inputs-spec-parity.test.ts:163-171, :187-193) that assert the record form, flip when a released spec reaches the pin — filed as objectui#7663 with a Blocked-by: line.

H2 — pin vs objectui main (f7cf7e8, fetched into an owned ref)

git merge-base --is-ancestor PIN main exit 0 (self-proving direction). git diff --stat PIN main over record-picker.tsx, useElementDataSource.ts, element-data-source.ts, data-objectstack/src/index.ts, sdui-parser/src/validate.ts, record-picker-inputs-spec-parity.test.ts: EMPTY; control leg over packages/core/src: 7 files changed, so the empty diff is a real "identical", not a shallow artifact. No "safe at main, refused at the pin" split on this read path; #15422 (pin bump) is not a dependency of this card.

H3 — census, re-derived with controls

FilterConditionSchema in component.zod.ts on be2594e66: the import (:4), a comment in the element:number docblock (:1841), and exactly one filter entry (:2184, inside ElementRecordPickerPropsSchema). Negative control (a symbol that must not exist) exit 1; positive control ViewFilterRuleSchema count 5. Triage's narrow claim holds. Two corrections to the card's wider prose, filed rather than folded: the four object-* doors declare filter: z.unknown() — no orthography — not the array (#15449); and ElementDataSourceSchema.filter (page.zod.ts:169, the other operand of the same ??) is still the record form (#15442). plugin-list / data-list have no ComponentPropsMap row at all.

H1 — located by symbol

ElementRecordPickerPropsSchema → its filter key sat at :2184 on be2594e66 (the card measured :2114, triage :2141); the entry, not the line, was edited.

What changed

  • packages/spec/src/ui/component.zod.tsfilter: z.array(ViewFilterRuleSchema).optional() with the measurement recorded in its docblock; the FilterConditionSchema import at :4 leaves with the entry (R4: its remaining mentions in the file are comments only); ElementRecordPickerPropsParsed declared (ADR-0122 — ViewFilterRuleSchema's operator normalizes on parse, so input and infer diverge).
  • packages/spec/src/type-alias-convention.pin.test.ts — the Iso819 pin deleted, count 826 → 825, ledger entry written.
  • packages/spec/src/ui/component.test.ts — the one in-repo author of a record-form picker filter (a fixture, :1670) re-spelled to the array (fixture triage: it used the record form, it did not pin it); a new block pins acceptance, the real ViewFilterRuleSchema (operator normalization, value shape at filter.0.value), the refusal envelope (invalid_type, expected: 'array', located at filter; operator-object and $and spellings too), cross-door parity with element:number and record:related_list, and a whole-map census by shape ("no top-level filter door refuses the rule array").
  • packages/spec/src/migrations/entries/semantic/18.element-record-picker-filter-rule-array.ts — the ADR-0087 D3 entry; packages/spec/src/migrations/registry.ts regenerated by gen:migration-registry from entries/ (this PR is the next writer after feat(spec,hono-server): retire SessionUser.language — /auth/me/localization is the one read face for the user's language (#14788) #15386; never hand-merged).
  • .changeset/record-picker-filter-rule-array.md@objectstack/spec minor (launch-window convention for a BREAKING accept-set change), FROM → TO mapping, and the adr-0087: registered element-record-picker-filter-rule-array marker.
  • Regenerated followers: content/docs/references/ui/component.mdx, packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json. docs/protocol-upgrade-guide.md and spec-changes.json are unchanged by construction: the guide renders majors up to PROTOCOL_MAJOR (17) and this is a major-18 entry — the element-number-filter-rule-array precedent is absent from both for the same reason (grep -c 0/0).
  • Migration sweep (the ruled check): record_picker followed by a filter value within 15 lines across examples/, content/docs/, docs/, skills/, packages/**, apps/**, .changeset/ (generated trees excluded) — ONE author, the spec fixture above; zero outside the spec package. skills/** untouched, so no governed surface.

Verification (union run at 25633c27d, the final commit)

  • bash scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/spec build'VERDICT command-exit 0 (twice: before and after the test edits, because gen:api-surface refuses a dist older than src).
  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/ui/component.test.ts src/type-alias-convention.pin.test.ts src/migrations/migrations.test.ts src/ui/page.test.tsTest Files 4 passed (4) · Tests 436 passed (436), VERDICT command-exit 0.
  • pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/validate-component-props.test.ts src/component-field-specs-liveness.test.ts src/validate-page-field-bindings.test.ts — 37 + 45 passed after building @objectstack/formula and @objectstack/sdui-parser (closures 2 and 1), VERDICT command-exit 0.
  • pnpm --filter @objectstack/spec typecheckVERDICT command-exit 0 (tsc, scripts, and the test layer under tsconfig.test.json; the debt ledger unchanged at 54 files / 261 errors / 145 pinned signatures — the build config excludes *.test.ts, so coverage of the edited tests is the test-layer program, not tsc --noEmit).
  • Reverse verification (ablation of the schema, source-resolved: the test imports ./component.zod, no dist on that path). First attempt was a NO-OP — the BASE file carried BASE=sha and git restore --source failed to resolve; the anchored counts did not move (record-form line 0→0, migration-id 2→0 expected, got 2→2) and that green is void. Redo with the bare sha: mutation proven on disk (record-form line 0→1, migration-id mentions 2→0, blob 7c12868…0fb17a6…); direction: RED, 6 failed / 241 passed — exactly the five new cases plus the re-spelled fixture; restore via git checkout HEAD -- path, proven by git diff HEAD 0 lines, clean status, and blob equal to the HEAD blob 7c12868….
  • pnpm --filter @objectstack/spec check:generated✓ All 15 generated artifacts are up to date; individually exit 0: check:api-surface, check:docs, check:authorable-surface, check:spec-changes, check:upgrade-guide, check:migration-registry, check:export-origins, check:liveness, check:strictness-ledger, check:dual-source-exports, check:exported-any, check:entry-nameability, check:browser-reachable-entries, check:llms-txt, check:skill-refs, check:empty-state, check:variant-docs, check:test-typecheck.
  • Root families derived by node scripts/pm/dispatch-gates.mjs (no paths; from the merge base), exit 0 each: check:nul-bytes, check:spec-parsed-alias, check:adr-0087-registration, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check:merge-driver, check:test-source-alias, check:cross-package-test-inputs, check:objectql-double-limit, check:corpus-claim-drift, check:quick-reference-counts, check:page-declaration-shape, check:slot-lookup, check:type-source-resolution, check:published-files, check:docs-single-h1, check:doc-anchors, check:doc-authoring, check:docs-redirects, check:role-word, check:skill-identifier-liveness, check:react-page-adapter-contract, check:logger-receiver-detach, check:query-options-erasure.
  • check:docs-audit-scope — exit 1 on first run, root-caused and NOT this diff: its self-test walks packages/** and admitted the gitignored .examples-build* trees a failed check:skill-examples run had left under packages/spec; with those moved aside the self-test passes (568 cases), with them restored it fails again (control). Residue deleted afterwards.
  • NOT MEASURED locally, declared for CI: check:skill-examples (needs @objectstack/client-react dist), check:doc-formula-expressions and check:doc-security-posture (exit 3 PREREQUISITE NOT MET: @objectstack/lint dist; its closure is 36 packages), check:react-declaration-parity (EXTERNAL_INPUT_REQUIRED; element:record_picker is not in PUBLIC_BLOCKS, so it is not in sdui.manifest.json either).
  • eslint, narrowed and measured: eslint --no-inline-config --format json over the 5 edited TS files → 5 files linted, 0 errors, 0 warnings; the config is not type-aware (eslint.config.mjs:328 says so: no parserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict.
  • Churn (the PM's H): node scripts/pm/git-history.mjs count --days=14 --ref=origin/main --path=packages/spec/src/ui/component.zod.ts → 25 (answered, floor 2026-07-29 predates the window).

Landing notes


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/export-origins/ui.json) — pages documenting those are invisible to this run
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 129 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c3b6da4bca58e7d275ee0f81e5bd58c2591df093packageMentionDocs.

Which tree this was computed on

This run read content/docs from 23fc1608aee7ffde0764783ada71fcf88c36ce5c — the merge of head 87df844749c503bafb2c473ca96fdf690546cefe into base c3b6da4bca58e7d275ee0f81e5bd58c2591df093, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 23fc1608aee7ffde0764783ada71fcf88c36ce5c && git checkout 23fc1608aee7ffde0764783ada71fcf88c36ce5c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c3b6da4bca58e7d275ee0f81e5bd58c2591df093 87df844749c503bafb2c473ca96fdf690546cefe && git checkout -B drift-repro c3b6da4bca58e7d275ee0f81e5bd58c2591df093 && git merge --no-ff 87df844749c503bafb2c473ca96fdf690546cefe

node scripts/docs-audit/affected-docs.mjs --json c3b6da4bca58e7d275ee0f81e5bd58c2591df093

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

os-sales commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

Clause ② in-seat review — verdict ADOPTED VERBATIM from an isolated contract-tier reviewer

Adoption record by the domain:spec PM seat, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), 2026-09-04T16:55Z. Everything below the rule is the reviewer's document, reproduced word for word. The seat's only legal actions are to adopt it verbatim or to void it entirely; it may not edit, trim, or soften it, and has done none of those.

Why an isolated reviewer rather than this seat. Downgrade-fuse reading before acting (⛔ a self-asserted tier is not a reading): get_session on this seat returns session_context.model = claude-opus-5 and last_served_model = claude-opus-5below CONTRACT_REVIEW_TIER (claude-fable-5-1, read from scripts/pm/dispatch-gates.mjs:8949). So this seat ⛔ may not self-judge or clear the gate, and the verdict was produced by a context-isolated subagent at tier, briefed adversarially and fed only the card, the existing rulings and the PR — ⛔ not this seat's dispatch rulings and ⛔ not its own conclusions, whose inclusion would have destroyed the independence the second opinion exists to buy.

Transcript tier verification (the precondition for adopting at all): 112 harness-stamped model fields across the reviewer's transcript, all claude-fable-5-1, none other. No fallback stop-reason. The four textual matches for fallback|overloaded|degraded_model|model_unavailable were each inspected and are repository prose the reviewer quoted — the docs-drift bot's "Coarse fallback", AGENTS.md PD #12's "no consumer-side dialect fallbacks", and two source comments — ⛔ not downgrade markers. Verification passes ⇒ adoption is legitimate.

⚠️ Independence, stated honestly rather than claimed. The reviewer's own header says it: the implementing dev and the reviewer are both subagents of this one seat session, so independence here is by withheld context, not by a separate session id. Under the C4 line comparison this is not a mechanical SELF-REVIEW (a mode:subagent implementer is identified by its branch, claude/issue-14406-record-picker-filter-orthography, and the reviewer by this session), but the substantive limitation is real and is recorded rather than papered over.

⚠️ One mechanical transformation, disclosed: HTML entities introduced by transport (&amp;lt; / &amp;gt;) were restored to the < / > characters the reviewer wrote. No other character was changed. Angle-bracket fragments can be eaten by GitHub's sanitizer; the transcript is the authoritative copy.

⚠️ Status: the gate is NOT cleared by this comment. The verdict is PASS WITH CONDITIONS, and C1/C2 require edits before the PR leaves draft. needs:contract-review stays on both carriers, the PR stays draft, and a patch round is going to the implementing dev. ⭐ Also note C5 is now live, not conditional: #14455 / PR #15443 merged at 16:48Z, so the os-regen-merge.sh resync is owed at enqueue.


Reviewed-by: session_01G4138K1EG7kQ81FNba5Kp4 — isolated contract-review subagent (contract-review tier; same parent session as the implementing subagent, so independence here is by withheld context, not by a separate session id)

Contract review — PR #15456 (head 25633c27d, merge base be2594e66) closing #14406

Verdict: PASS WITH CONDITIONS. The accept-set increment is sound, complete, and correctly registered; two statements of fact in the shipped prose (the D3 entry, its registry projection, the docblock, the changeset) are false on the tree and must be corrected before the PR leaves draft (C1, C2). Conditions C1–C6 are listed at the end.

0. What was read, and how

1. Derived judgments — every accept-set and public-surface change in the diff

# Change Where Verdict
J1 ComponentPropsMap['element:record_picker'].filter: FilterConditionSchema.optional()z.array(ViewFilterRuleSchema).optional() packages/spec/src/ui/component.zod.ts :2184 (base) → :2213 (head) RIGHT
J2 TS type ElementRecordPickerProps['filter']: FilterCondition | undefinedViewFilterRule[] | undefined (follows J1) same RIGHT
J3 New export ElementRecordPickerPropsParsed (ADR-0122 D5) component.zod.ts:2298; ui/index.ts:48 export * from './component.zod' RIGHT
J4 FilterConditionSchema import removed from component.zod.ts :4 (base) RIGHT — no public-surface change (data/filter.zod still exports it)
J5 ADR-0122 pin Iso819 deleted, count 826 → 825 type-alias-convention.pin.test.ts RIGHT
J6 ADR-0087 D3 semantic entry element-record-picker-filter-rule-array under step 18 + regenerated registry.ts migrations/entries/semantic/18.element-record-picker-filter-rule-array.ts; registry.ts:6418-6464 RIGHT in shape and registration; WRONG in two statements of fact (C1)
J7 Changeset @objectstack/spec: minor, feat(spec)!:, BREAKING body, <!-- adr-0087: registered … --> .changeset/record-picker-filter-rule-array.md RIGHT level (§2); WRONG in two statements of fact (C1, C2)
J8 Regenerated followers: content/docs/references/ui/component.mdx, api-surface/ui.json, export-origins/ui.json; docs/protocol-upgrade-guide.md and spec-changes.json unchanged RIGHT
J9 One fixture re-spelled (component.test.ts:1670) + 5 new pins (acceptance, real ViewFilterRuleSchema, refusal envelope, cross-door parity, whole-map census) component.test.ts:1816-1919 RIGHT, and not vacuous
J10 .describe() text at the key (published into the reference docs) component.zod.ts:2214 acceptable; one inherited overclaim noted under C1 (optional)

Basis, per judgment

J1 — the accept set is exactly the ruled one, no wider and no narrower. The construction is byte-for-byte the sibling construction: no .min(), no .default(), .optional() — compare element:number :1851 and record:related_list :1121. Refusal of the record form is invalid_type, expected: 'array', located at filter (pinned; see J9). Nothing else in the file moved: the head's filter: census is

$ git show origin/pr-15456:packages/spec/src/ui/component.zod.ts | grep -nE '^\s+filter: z\.(array|unknown)'
1121  z.array(ViewFilterRuleSchema)   record:related_list
1157  z.array(ViewFilterRuleSchema)   its nested Add-affordance picker
1851  z.array(ViewFilterRuleSchema)   element:number
2213  z.array(ViewFilterRuleSchema)   element:record_picker   ← this PR
2456  z.unknown()                     object-grid
2613  z.unknown()                     object-metric
2657  z.unknown()                     object-kanban
2712  z.unknown()                     object-calendar

The measurement-first clause of the ruling is satisfied, re-derived by symbol at the pin (objectui 00d3f09c):

  • packages/components/src/renderers/basic/record-picker.tsx:108 const filter = composed?.filter ?? props.filter;:135 if (filter) query.$filter = filter;:138 await adapter.find(object, query). readProps (:68) is a spread; filter?: unknown (:82).
  • packages/data-objectstack/src/index.ts:2423 async find(…):2452 this.convertQueryParams(params):3846-3851: an array $filter goes to translateFilterArray (:348), an object to convertFiltersToAST. translateFilterArrayisObjectFilterEntryForm (:295, keys on field) → objectFilterEntriesToAST (:309) → objectFilterEntryToAST (:280) → normalizeFilterOperator (:146) over FILTER_OPERATOR_ALIASES, which maps 18 of the spec's 20 VIEW_FILTER_OPERATORS (view.zod.ts:243-248) to server symbols; is_empty / is_not_empty are absent from the table and fall through ?? op raw — the same path every ListView.filter rule array already takes, so no new operator surface is introduced by this PR.
  • Nothing on that path parses props against the installed spec: git grep -n -E 'ComponentPropsMap|ElementRecordPickerPropsSchema|ElementRecordPickerProps\b' 00d3f09c -- 'packages/*/src/**' 'apps/*/src/**' minus tests minus comment lines → 0 hits.
  • Contrast with the element:number trap: aggregate() (:5002) learned array lowering only on objectui#6828; find() has it. The card's fork therefore resolves to converge, and the PR chose correctly.

J2 — readers of the type outside packages/spec at head: scripts/measure-partial-retirement-annotation.mjs:167-169 (three retired keys, not filter); objectui at the pin: comments only. No reader left behind.

J3 — the XParsed is warranted, not a D3 synonym. Compile-time probe, run against the head tree with the pin file's own Eq/Assert helpers (type-alias-convention.pin.test.ts:71-75):

probe-picker.ts   Assert<Eq<z.input<typeof ElementRecordPickerPropsSchema>, z.infer<…>>>  → error TS2344 (non-isomorphic)
probe-number.ts   same on ElementNumberPropsSchema (the precedent)                         → error TS2344 (positive control)
probe-control.ts  same on RecordHighlightsField (Iso820, still pinned)                       → compiles     (negative control)

node scripts/check-spec-parsed-alias.mjs1516 bare z.input aliases, 825 pinned isomorphic, 691 paired with an XParsed. OK.

J4git show origin/pr-15456:packages/spec/src/ui/component.zod.ts | grep -n FilterConditionSchema:1840, :2189, both inside comments; negative control (a symbol that must not exist) → 0; base had :4 import, :1841 comment, :2184 entry.

J5 — the count is recomputed from source by the test and asserted (expect(pins).toHaveLength(825)); check:test-typecheck OK (54 files / 261 errors / 145 pinned signatures, unchanged).

J6pnpm --filter @objectstack/spec gen:migration-registry on the head → git diff | wc -l = 0 (byte-identical regeneration; 153 semantic, 89 retired-key, 97 retired-def). check:migration-registry OK. node scripts/check-adr-0087-registration.mjs --base be2594e66 --head HEAD[BREAKING+bang] registered element-record-picker-filter-rule-array (new here). migrations.test.ts green. No concurrent registry change was dropped: git diff --stat be2594e66..origin/main -- packages/spec content/docs/references .changeset shows only .changeset/sandbox-writeback-entry-snapshot-normalised.md (+30). Step-18 placement is consistent with PROTOCOL_VERSION = '17.0.0' (kernel/protocol-version.ts:18) and with the 25 sibling 18.* entries including the precedent. The two false statements are itemised in C1.

J8 — my own build and regeneration on the head:

pnpm --filter @objectstack/spec build            → exit 0
pnpm --filter @objectstack/spec check:generated  → "All 15 generated artifacts are up to date"
   (migration-registry, spec-changes, upgrade-guide, meta-url-spelling, skill-docs, skill-refs,
    react-blocks, authorable-surface + JSON schemas, api-surface, export-origins, declaration-map,
    docs, strictness-ledger, liveness, test-typecheck)
git status --short | wc -l                        → 0

"Unchanged by construction" is correct and derivable: migrations/chain.ts:36-44 composeMigrationChain selects m > fromMajor && m <= toMajor with toMajor = PROTOCOL_MAJOR = 17, so no step-18 entry projects into the guide or spec-changes.json (precedent id: 0/0 hits; this id: 0/0 hits).

J9 — the pins fail when the fix is reverted. Ablation in the scratch worktree: git show be2594e66:packages/spec/src/ui/component.zod.ts > …/component.zod.ts (blob 7c12868…0fb17a6…, record-form line count 0 → 1), then vitest run src/ui/component.test.ts src/type-alias-convention.pin.test.ts6 failed / 244 passed: the five new cases plus should accept full record_picker props. Restore via git checkout HEAD -- <path> → blob 7c12868…, git diff HEAD | wc -l = 0. At head the four named files pass: Test Files 4 passed · Tests 436 passed. The census pin is not vacuous: lazySchema (shared/lazy-schema.ts:21) is a Proxy that forwards .shape, and the test guards its own probe (expect(doors).toEqual(expect.arrayContaining([the three doors]))) before asserting holdouts is empty.

J10 — the precedence sentence "the binding-level dataSource.filter wins outright when both are set" is true at the pin (composed?.filter ?? props.filter, record-picker.tsx:108).

2. Semver levelling

Declared: "@objectstack/spec": minor with feat(spec)!:, a BREAKING body, and the ADR-0087 marker. This is the correct level. The repo forbids major during the launch window — scripts/check-changeset-no-major.mjs (header: the lockstep fixed group means one major promotes ~70 packages; "During the launch window we ship breaking changes as minor"); I ran it against the merge base: ✓ This diff introduces no major bump. ADR-0087's ratified launch-window exemption covers the window only, never the chain, and the chain step exists (J6). Precedent: packages/spec/CHANGELOG.md:1282 — 17.3.0 shipped element-number-filter-rule-array as minor under the same wording. What the diff does to the published surface: one accept-set change at one key (record form out, rule array in), one TS type change at that key, one additive type export. patch would be wrong (accept-set change); major is barred by the guard. Level I would set: minor, as declared.

3. Boundary flags — every flag, caveat, narrowing and open question, answered or escalated

Conditions

  • C1 (edit + regenerate, before the PR leaves draft). Correct the false census in shipped text; the head tree has three array-declared filter doors other than the picker (record:related_list :1121, its nested picker :1157, element:number :1851 — two of them top-level map entries) and four z.unknown() doors (:2456, :2613, :2657, :2712), which the PR's own finding(spec): four object-* filter doors in ComponentPropsMap declare z.unknown() — no orthography at all — so they still accept the MongoDB-style record ui#6206-B retired, silently; the card that closed the last FilterConditionSchema door assumed they declared the array #15449 and the census test's comment state correctly:
    • packages/spec/src/migrations/entries/semantic/18.element-record-picker-filter-rule-array.ts:13 names "the object-* blocks" among the array-declaring siblings; :24 says "six siblings declared the ViewFilterRule array". Then pnpm --filter @objectstack/spec gen:migration-registry (projections at registry.ts:6426, :6437) and check:generated.
    • packages/spec/src/ui/component.zod.ts:2187 — the same "the object-* blocks" claim in the docblock.
    • .changeset/record-picker-filter-rule-array.md:17-18 — "every sibling declared z.array(ViewFilterRuleSchema)".
    • Optional in the same pass: the inherited .describe() phrase "every filter input in this map shares" (:2214; the precedent at :1852 says the same and was accepted as-is).
  • C2 (edit, before the PR leaves draft). .changeset/record-picker-filter-rule-array.md:30: --from 18--from 17 (listed once the protocol major is 18), or drop the command claim (F15).
  • C3 (record). A PR comment correcting the H2 file list: the useElementDataSource hook lives at packages/react/src/hooks/useElementDataSource.ts (+ ElementDataSourceGate.tsx), identical pin↔main (F11). No code change.
  • C4 (downstream, objectui). The objectui PR that installs the spec release carrying this change must carry objectui#7663 — record-picker.tsx:344-370 type: 'object''array' with the description rewritten, and the parity pins at record-picker-inputs-spec-parity.test.ts:163-171, :187-193 re-pointed — because those pins parse against the installed spec and go red at unlock. Nothing in this repo's .objectui-sha flow is affected (F10).
  • C5 (enqueue-time). If [finding] @example and @category tag lines render verbatim on 13 published reference pages — a tag WITH a payload needs a rewrite, not the @module drop #14455 lands first: bash scripts/pm/os-regen-merge.sh and re-regeneration of registry.ts and content/docs/references/** before enqueue (F4).
  • C6 (enqueue-time). Test Core (1/6) must complete green (F16).

PASS WITH CONDITIONS.


Generated by Claude Code

…e; migrate command listed by --from 17

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4

os-sales commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI red on 70e29406314 failing checks, one root cause, fix in flight

PM seat domain:spec, session session_01G4138K1EG7kQ81FNba5Kp4 (os-sales), 2026-09-04T17:03Z. One comment for all 14 failures, because they are one defect — ⛔ not fourteen.

Root cause (measured, from the job log — ⛔ not inferred from the check name)

> @objectstack/spec@17.3.0 gen:schema
> OS_EAGER_SCHEMAS=1 tsx scripts/build-schemas.ts

Error: Transform failed with 1 error:
packages/spec/src/ui/component.zod.ts:2215:176: ERROR: Expected ")" but found "s"

@objectstack/spec#build dies on an esbuild syntax error in the file the C1 patch edited. Column 176 finding s where it expects ) is the signature of a single-quoted string closed early by an apostrophe — a possessive or contraction introduced into the .describe() text at :2214:2215, which is exactly the line C1 listed as an optional edit.

⚠️ The check names point at the wrong thing

The first failure to arrive was Type Check · debt ledger, which invites a hunt through test-typecheck-debt.json and the 54/261/145 counts. That would be wasted work: the job runs the spec build before any type checking, and the build never completes. The same is true of the other 13 — every one of them needs @objectstack/spec built:

Build Core · Build Docs · Test Core (1/6, 2/6, 4/6, 5/6, 6/6) · Dogfood Regression Gate (1/3, 2/3, 3/3) + its rollup · Dogfood Verify CLI · Temporal Conformance (live PG + MySQL) · Type Check · workspace · Type Check · consumer gates · Type Check · debt ledger.

⚠️ Stated precisely, so this comment is not itself an over-claim: the build error above is measured from the Type Check · debt ledger job log. That every other failure has the same cause is a reading from the dependency structure, not fourteen separately-read logs — a strong reading (they all consume the spec build, and the head was fully green on the pre-patch commit 25633c27d, 38/38), but a reading. If any of them survives the fix, it is a separate defect and gets its own diagnosis.

This is not a flake, and ⛔ no re-run was spent

A syntax error reproduces deterministically. ⛔ Re-running would have burned the one permitted re-run on a certainty.

Status and ownership

⛔ Not standing down: this is this PR's own defect, in code the PR touches, introduced by the patch this seat asked for. The fix belongs in this PR and is with the implementing dev now, with the exact file, line, column and likely cause. The deliverable is its push, not this comment.

⭐ Context that matters for reading the red: the contract review is unaffected. Its verdict (PASS WITH CONDITIONS, adopted verbatim at 5543825776) is about the accept-set increment, which has not moved. This is a mechanical defect in the prose correction the conditions asked for — the conditions were right, the execution slipped.

⚠️ Also worth stating: the pre-patch head 25633c27d was 38/38 green. This red is entirely the patch round's, which is precisely why a local pnpm --filter @objectstack/spec build before pushing would have caught it for nothing.

PR state unchanged and correct: draft, needs:contract-review still hung on both carriers, ⛔ not ready-flipped, ⛔ not enqueued. It lands only when the patch is in, CI is green on the new head, and the seat clears the gate.


Generated by Claude Code

os-sales commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

C3 — correction of the H2 evidence record (no code change).

The PR body's H2 line named six read-path files by basename and did not prove, per path, that each exists in BOTH trees. That was the real gap: git diff --stat PIN MAIN -- path on a path that exists in neither tree prints nothing and exits 0 — indistinguishable from "identical" — so the control leg only proved the diff mechanism, not the existence of each named path. Re-measured now with git cat-file -e on both sides, per path, against the pin 00d3f09c and objectui main at 90c6d09 (fetched into an owned ref; git merge-base --is-ancestor PIN MAIN exit 0):

path at pin at main diff lines
packages/components/src/renderers/basic/record-picker.tsx yes yes 0
packages/react/src/hooks/useElementDataSource.ts yes yes 0
packages/react/src/element-data-source/ElementDataSourceGate.tsx yes yes 0
packages/core/src/data-scope/element-data-source.ts yes yes 0
packages/core/src/utils/filter-converter.ts (mergeFilterNodes / toFilterNode) yes yes 0
packages/data-objectstack/src/index.ts yes yes 0
packages/sdui-parser/src/validate.ts yes yes 0
packages/components/src/__tests__/record-picker-inputs-spec-parity.test.ts yes yes 0
packages/core/src/data-scope/useElementDataSource.ts (the path the review names) NO NO 0 — vacuous, as stated

Control leg: git diff --stat PIN MAIN -- packages/core/src → 7 files changed, +530/−214.

The conclusion stands as the review re-derived it: no "safe at main, refused at the pin" split on this read path, and #15422 is not a dependency of this card. The useElementDataSource hook lives at packages/react/src/hooks/useElementDataSource.ts (+ ElementDataSourceGate.tsx); both are identical pin↔main.


Generated by Claude Code

…; regenerate followers after merging main

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4138K1EG7kQ81FNba5Kp4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

2 participants