Skip to content

feat(spec)!: type ViewFilterRule.operator's input as the canonical ViewFilterOperator (#20450) - #20503

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20450-view-filter-operator-input-typed
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-20450-view-filter-operator-input-typed

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20450
Clause-②: no (narrowing)

What

ViewFilterRuleSchema.operator is a z.preprocess over the alias fold, and zod 4.6.1 types a preprocess's INPUT from its function's parameter (preprocess(fn: (arg: B, ctx) => A, schema): ZodPreprocess(U, B), with B = unknown by default). The function was normalizeFilterOperator(op: unknown), so ViewFilterRule['operator'] was unknown, and { field: 'status', operator: 42 } compiled as a rule on every carrier.

This PR lands triage's direction A: the typed input of operator is the canonical ViewFilterOperator. The runtime fold is untouched, so stored sys_metadata rows and plain-JS producers that carry an alias still parse and fold.

  • packages/spec/src/ui/view.zod.ts: the preprocess now runs through a module-private wrapper, foldAuthoredViewFilterOperator(op: ViewFilterOperator): string, which returns normalizeFilterOperator(op). The wrapper is not exported, so no export is added and api-surface/ is byte-identical.
  • packages/spec/src/ui/view-filter-operator-input-typed.test.ts: the two-half pin (see the verification record below).
  • packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts and the regenerated registry region: the ADR-0087 registration (see Acceptance notes, item 1).
  • .changeset/20450-view-filter-operator-input-typed.md: @objectstack/spec minor, a BREAKING banner, a FROM → TO table and the one-line fix, Clause-②: no (narrowing) (corrected in round 2, the seat's answer 5877491493), and the registered disposition.

Which site, and why not normalizeFilterOperator's own parameter

Triage's execution line named normalizeFilterOperator's parameter. I measured that site first. It is exported (api-surface/ui.json), and its callers pass raw strings or unknown by design:

  • packages/lint/src/validate-preset-comparands.ts:634 passes a stored rule's rule.operator.
  • packages/rest/src/view-filter-rule-lowering.ts:72 passes a stored rule's operator.
  • packages/spec/src/conversions/registry.ts passes op.slice(1) (:10984), op (:11167) and the literal 'eq' (:11060, :11187).
  • packages/spec/src/ui/filter-rule-array.ts:152 passes the literal 'eq'.
  • objectui at its pin calls it in ObjectView.tsx, ListView.tsx, UserFilters.tsx, filter-converter.ts, filter-builder.tsx and viewFilterFold.ts, each with a string or unknown.

Narrowing that parameter would break every one of these callers. A canonical-in, canonical-out fold would also have nothing left to fold. The smallest correct site is the preprocess's own input at :923. The wrapper reaches the same result (the typed input is canonical) and leaves the exported fold's signature as it was.

Verification record

Final gate union on HEAD bed8cabb44 (after merging origin/main at fc0db22bcf through scripts/pm/os-regen-merge.sh). My delta against the merged main commit is the five files above: +288 / -5.

Premise, measured at base 8e02859185 against the built dist/*.d.ts. I ran a downstream probe that resolves @objectstack/spec/ui through the package exports. tsc gave EXIT 0 on all of these: operator: 42, operator: 'eq' and operator: Symbol('x'), on ViewFilterRule and on three carriers (ListView['filter'], ViewTab['filter'], InterfacePageConfig['filterBy']).

Reverse verification. I rebuilt spec with the change and ran the same probe. tsc gave EXIT 2 with 9 errors, one on each of the 9 non-canonical lines. The canonical line still compiles. The runtime is byte-identical to base: a number is refused with code: 'invalid_value', path: ['operator'] and the canonical values; 'eq' folds to equals; 'NotIn' folds to not_in.

Compile-half ablation (at 60102b7b42, through scripts/ablation-replace.mjs). I widened the wrapper's parameter back to unknown. The anchor went 1 → 0 and the blob went 4403a5bb → d8324f31. check:test-typecheck then reported one problem: src/ui/view-filter-operator-input-typed.test.ts: 6 type error(s), which are the six @ts-expect-error directives (TS2578). Restored: the blob equals HEAD 4403a5bb and git diff HEAD is empty.

Runtime-half ablation (at bed8cabb44). I replaced the wrapper body return normalizeFilterOperator(op); with return op;, which still compiles. The pin went 3 failed | 2 passed: the three fold tests failed; the canonical and refusal tests held. Restored: the blob equals HEAD 07956823 and git diff HEAD is empty.

Spec package (post-merge, bed8cabb44):

  • pnpm --filter @objectstack/spec typecheck exit 0 (src, scripts and the test layer). check:test-typecheck: OK — 53 file(s) / 251 error(s) / 138 pinned signature(s) held, so no test file moved.
  • vitest run --project local: Test Files 573 passed (573), Tests 16794 passed | 1 todo.
  • check:generated: all 15 generated artifacts up to date. Nothing records the input type: api-surface/, export-origins/, declaration-map/, authorable-surface/ and the JSON schemas are unchanged. spec-changes.json and the upgrade guide hold because in-progress major-18 entries reach them at release (control: the sibling view-filter-rule-scalar-operator-array-refused is in neither).

Consumer typechecks. These ran against spec dist built from 60102b7b42; the narrowing is identical after the merge. They are a selection of @objectstack/spec's downstream consumers (the ...@objectstack/spec direction), not the whole 73-package closure. Every one exited 0, and each test-layer ledger held exactly:

  • @objectstack/lint (2 files / 6 errors)
  • @objectstack/metadata-protocol (plain tsc, tests included)
  • @objectstack/rest (0 / 0)
  • @objectstack/objectql (40 / 234)
  • @objectstack/platform-objects (1 / 3)
  • @objectstack/plugin-sharing (2 / 3)
  • @objectstack/plugin-security (0 / 0)
  • @objectstack/plugin-audit (0 / 0)
  • @objectstack/plugin-approvals (8 / 324)
  • all five examples: app-showcase, app-todo, app-crm, app-multi-package, embed-objectql

The rest of the closure is CI's.

Census of typed producers. Non-test .ts/.tsx under packages/, examples/ and apps/:

  • 3 operator: literals carry an alias spelling. None is on a view-filter carrier: an app-showcase lookupFilters entry, and two doc comments in other dialects. As a control, the same grep for canonical spellings hits 113 times.
  • 2 non-literal operator: writes. Neither is a view-filter rule: a cross-field refusal record in formula, and a text-operator door verdict in objectql.
  • 0 non-test typed producers write an alias through ViewFilterRule.

No test fixture needed an escape: every alias-carrying fixture already reaches the fold through .parse / .safeParse, which take unknown.

objectui at its pin dd3f7e1be3 (read-only). I exported its sources with git archive into a scratch directory, so nothing was written to the objectui repo. I compiled packages/*/src and apps/console/src against this branch's spec dist, with its root compiler options, @object-ui/* mapped to source, and React 19.2.8 / @types/react 19.2.18. There were 0 errors naming the operator union or ViewFilterOperator.

  • Positive control: a scratch-only file wrote a string, an alias, and a string through objectui's own RecordRelatedListComponentProps.filter. Exactly those 3 lines errored, and every other error was identical to the run without the control.
  • The typed producers read the same way. viewFilterFold.ts:211 casts to ViewFilterRule['operator']. ObjectDataPage.tsx builds a record and pushes the parse OUTPUT.
  • objectui resolves published @objectstack/spec (^17.x), so it meets this at its next spec bump, not through the Console Pin Gate.

Gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 88 families on bed8cabb44. I ran 87 and each exited 0. --ran reconciliation: 88 derived famil(ies) accounted for — 87 run, 1 NOT-MEASURED.

  • check-adr-0087-registration: [BREAKING+bang+clause-②-narrowing] registered view-filter-rule-operator-input-canonical (new here: …).
  • check-changeset-no-major: no major. Its level axis is PR-scoped and reads NOT APPLICABLE locally, so that half is CI's.

NOT MEASURED: check:type-check-debt. Reason: its --re-measure runs a full-repo turbo run build, which does not fit the foreground cap. I took targeted readings instead:

  • The root program (tsc -p tsconfig.json) has 26 errors, equal to its DEBT ledger's 26, and 0 of them name the operator union.
  • The other three DEBT packages (cloud-connection, hono, observability) contain no operator or ViewFilter text.

Acceptance notes

  1. Surface extension: one hand-written file beyond the claim's file surface. packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts sits outside the claimed surface; its generated registry region is inside it. Why:
    • The changeset carries the FROM → TO the dispatch asked for, and check-adr-0087-registration refuses no-migration-prescription on a body with a prescription.
    • type-surface-only is closed to any diff that touches packages/spec or moves a *.zod.ts (ADR-0087 D8, predicates 2 and 3). The ADR's reason is that for such a diff the ledger is a channel that can carry the change.
    • So the honest disposition is registered. The precedent is spec-type-alias-input-suffix-retired, a type-only spec change registered the same way.
    • The entry records that nothing at rest is rewritten (no D2 conversion). The family predecessor, fix(spec): type ApiError.code and the list overlay options bag #20448, took no-migration-prescription without a concrete rewrite. The reviewer may prefer that route; it means dropping the FROM → TO table.
  2. Clause-② value. Round 1 carried the claim's yes. By scripts/pm/clause2-line.mjs's own definitions this diff reads no (narrowing), "NOT a widening, but breaking": it adds no export and widens no accept set, as the family precedents (finding(spec): ViewFilterRuleSchema accepts two shapes every consumer refuses, and ObjectGridProps.defaultFilters is z.unknown() so nothing gates it at all — the protocol half of objectui#9050's ruling C′ #19514, fix(spec): type ApiError.code and the list overlay options bag #20448) declared. The seat answered 5877491493 (Q1: A), and round 2 corrected the changeset line and this body's declaration line to Clause-②: no (narrowing). The level (minor), the BREAKING banner and the ADR-0087 marker are unchanged.
  3. Prose that now names the old spelling (noted, not filed). These still describe the operator as z.preprocess(normalizeFilterOperator, …): packages/metadata-protocol/src/protocol.ts:1187, protocol.graft-normalized-operators.test.ts:6, packages/spec/src/ui/view-filter-rule-wire-id.test.ts:185, and objectui's packages/types/src/zod/complex.zod.ts:283. The preprocess now runs a wrapper that delegates to that fold, so each statement is still true about behaviour. Only the literal spelling drifted.
  4. Observation (not filed). The fold's case-folded branch reaches only aliases whose lower-cased form is itself a table key. GREATERTHANOREQUAL does not fold: the table holds greaterorequal and greaterThanOrEqual, not greaterthanorequal. This is deliberate per the table's docblock ("the folds exist per measured legacy spelling"). The pin tests the case-fold branch on GT and NotIn only, for that reason.
  5. The objectui measurement has a stated blind spot. Third-party modules (radix, lucide and others) were not installed, so the types that flow through them collapse to any and cannot error either way. The positive control shows the channel is live for every spec-typed position.

Generated by Claude Code

…wFilterOperator

The preprocess on ViewFilterRuleSchema.operator now runs through a
module-private wrapper whose parameter is ViewFilterOperator, so zod types
the rule's input (and every carrier's) as the canonical vocabulary instead of
unknown. The wrapper delegates to the unchanged exported fold, so stored
rows and plain-JS producers that carry an alias still parse and fold.

Adds the two-half pin beside it: @ts-expect-error on an alias and a number
through ViewFilterRule and three carriers, and the runtime fold/refusal.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
…d its changeset

Adds the semantic migration entry view-filter-rule-operator-input-canonical
(a type-surface narrowing carried to the upgrade guide, nothing at rest
rewritten), regenerates the migration registry regions, and adds the
BREAKING-narrowing minor changeset with its FROM -> TO table and ADR-0087
registered disposition.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 137 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 2b24b8b82304e925110800efb0e092845a931d16 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2f380506f32fb069f93f9b20b3b1189cd852133c — the merge of head 00a3e0d22e9ce3349dc0cbbacbe78df5c9af22ce into base 2b24b8b82304e925110800efb0e092845a931d16, 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 2f380506f32fb069f93f9b20b3b1189cd852133c && git checkout 2f380506f32fb069f93f9b20b3b1189cd852133c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2b24b8b82304e925110800efb0e092845a931d16 00a3e0d22e9ce3349dc0cbbacbe78df5c9af22ce && git checkout -B drift-repro 2b24b8b82304e925110800efb0e092845a931d16 && git merge --no-ff 00a3e0d22e9ce3349dc0cbbacbe78df5c9af22ce

node scripts/docs-audit/affected-docs.mjs --json 2b24b8b82304e925110800efb0e092845a931d16

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

… Clause-② no (narrowing)

The diff narrows a published type and widens no accept set or public
surface, which scripts/pm/clause2-line.mjs spells `no (narrowing)`. The
minor level, the BREAKING banner, the FROM -> TO table and the ADR-0087
registered disposition are unchanged.

Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 00a3e0d22e9ce3349dc0cbbacbe78df5c9af22ce
Local-runs: none

Inputs: card #20450 (body; triage 5871538633; claim 5874596249; round-1 report 5877444573; the seat's answers 5877491493; round-2 report 5877521420), PR #20503 (body, comments, file list), the net diff origin/main...00a3e0d22e (5 files, +288 / -5, merge base fc0db22bcf), the check-runs on the head read once at 2026-09-28T20:05Z, and the code at the head and at the objectui pin dd3f7e1be3 (unchanged by this PR). Judged from the code, not from the report's prose.

① Derived judgments

  1. The narrowing mechanism — right. zod 4.6.1 (packages/spec pins ^4.6.1) types preprocess(fn, schema) as a pipe whose in-side is a transform typed by fn's parameter, so the input type of operator is the parameter of foldAuthoredViewFilterOperator, the canonical ViewFilterOperator. lazySchema is identity-typed (T in, T out) and ViewFilterRule is z.input of the schema, so ViewFilterRule['operator'] moves from unknown to the canonical union. Before the diff the parameter was normalizeFilterOperator's unknown, which is the class (b) gap the card names.
  2. Every carrier narrows in step — right, and the set is wider than the four the card named. All carriers embed the same ViewFilterRuleSchema object through z.array(...): ListView.filter (view.zod.ts:2572), a tab's filter (:1567), the :3143 door, page.zod.ts:226 and filterBy (:578), and the component.zod.ts doors — related-list :1282, record-picker :1318, the object-* block filter doors and defaultFilters (:3441). No carrier declares its own copy of the rule, so no carrier is left at unknown. The changeset's carrier list names these.
  3. The runtime fold and refusal are byte-identical — right. The wrapper's body is return normalizeFilterOperator(op); the fold returns a non-string verbatim and the unchanged z.enum(VIEW_FILTER_OPERATORS) refuses it with invalid_value at operator; every own key of VIEW_FILTER_OPERATOR_ALIASES still folds; the case-folded branch is reached as before. No .describe() moved ('Filter operator' is unchanged; only the property's TSDoc did), so the JSON schema, the generated docs and the authorable surface do not move. The accept set moves nowhere.
  4. The site — the private wrapper, not the exported fold's parameter — right. The export's callers pass string or unknown by design: packages/rest/src/view-filter-rule-lowering.ts:72 (a string after a typeof guard), packages/lint/src/validate-preset-comparands.ts:634, packages/spec/src/conversions/registry.ts (op.slice(1) and the literal 'eq'), packages/spec/src/ui/filter-rule-array.ts:152, and at the objectui pin ObjectView.tsx:731 (String(operator)), ObjectDataPage.tsx:136, viewFilterFold.ts:211 (an unknown), filter-builder.tsx:347. Typing the export's parameter as the canonical union would break each of them to change a type only this schema publishes, and a canonical-in fold would have nothing to fold. The wrapper is module-private, so no export is added: api-surface/, export-origins/ and declaration-map/ record no input type and stay byte-identical.
  5. The two-half pin is a real check — right. tsconfig.test.json includes src/**/* under bundler resolution (the ./view.zod spelling matches 25 sibling tests), and test-typecheck-debt.json carries no entry for the new file, so each of the six @ts-expect-error directives that stops applying is a TS2578 red under check:test-typecheck, which spec's typecheck script runs. The compile half covers the rule, ListView.filter, a tab filter and Page.filterBy, with a canonical positive control on each so a directive cannot fire on some other key. The runtime half folds every table alias, reaches the case-folded branch on keys the table holds (gt, notin), and pins the non-string refusal to the enum's own issue.
  6. In-repo blast radius — nothing fails, measured here. My own census at the head over non-test .ts/.tsx under packages/, examples/, apps/: zero alias-spelled operator: literals on a view-filter carrier. The hits are examples/app-showcase/src/data/objects/invoice.object.ts:85 (lookupFilters, a separate closed dialect), a comment in plugin-auth/src/objectql-adapter.ts:214, a TSDoc example in spec/src/ai/skill.zod.ts:461 (the skill trigger dialect), and the new entry's own prose. The typed in-repo producers write canonical ids (examples/app-showcase/src/ui/pages/my-work.page.ts); rest-server.ts:10770 builds operator: 'contains' and lowers through unknown.
  7. objectui at its pin dd3f7e1be3 — nothing fails, judged from the sibling's code. Both typed producers survive by construction: ObjectDataPage.tsx:131-156 builds a string-keyed record of unknown, runs ViewFilterRuleSchema.safeParse, and pushes parsed.data (the parsed operator is the enum, assignable to the narrowed input); viewFilterFold.ts:211 asserts the fold's string to ViewFilterRule['operator'], a string-to-literal-union assertion TypeScript accepts. packages/types/src/zod/complex.zod.ts:318 derives FilterOperatorSchema from ViewFilterRuleSchema.shape.operator, so its input narrows in step — and objectui's own typed twin FilterBuilderOperator (complex.ts:495) is already the spec's ViewFilterOperator. Nothing the pinned sibling imports is removed or renamed, so no sibling fix and no pin bump is owed (Post-Task Checklist 4). Note that no check-run here compiles objectui with tsc against this spec: the Console Pin Gate is filtered to console paths (skipped on this head) and bundles through vite without a type pass; objectui resolves published spec ^17.x and meets this at its next spec bump.
  8. The ADR-0087 entry view-filter-rule-operator-input-canonical — right in content. File 18.view-filter-rule-operator-input-canonical.ts is shardNameFor(18, id) for the in-progress major. The author-shown fields (surface, replacement, reason, acceptanceCriteria) carry no tracker number — the only citations are ADR-0087 and ADR-0122. The FROM → TO is true against the alias table at the head: eq → equals, ne/neq → not_equals, gt → greater_than, gte → greater_than_or_equal, nin/notIn → not_in, isNull → is_null. It adds no D2 conversion, which is correct: nothing at rest moves. The precedent it names, spec-type-alias-input-suffix-retired, is the same class (a TypeScript-surface change registered so the guide carries the mapping the compiler error omits).
  9. packages/spec/src/migrations/registry.ts is the generator's region — right. build-migration-registry.ts lifts the entry's leading // comment run and its literal verbatim at 4-space indent and orders by id within the major; the diff inserts exactly that between view-filter-rule-absent-value-refused and view-filter-rule-scalar-operator-array-refused, and touches nothing else. check:migration-registry runs in Lint & Repo Gates and inside check:generated. spec-changes.json and docs/protocol-upgrade-guide.md owe no regeneration: in-progress major-18 entries do not reach them (control at the head: the two step-18 siblings are absent from both, a step-17 entry is present in both), and the new id is absent from both, consistently.
  10. The disposition registered — right. ADR-0087 D8 predicates 2 and 3 close type-surface-only to a diff that touches packages/spec and a *.zod.ts; the changeset carries a FROM → TO prescription, so no-migration-prescription is refused statement-against-statement; the registered id is new in this diff and resolves at HEAD. The Check Changeset run on this head, which executes check-adr-0087-registration, concluded success.

② Semver level

③ Boundary flags

Dev flags (round 1 5877444573, round 2 5877521420), each answered:

  • deviations[0] site (the wrapper, not the export's parameter) — answered right, ① item 4.
  • deviations[1] one hand-written file beyond the claim's surface (the semantic entry) — answered by the seat (Q2 = A) and judged right, ① items 8–10.
  • deviations[2] Clause-② copied as yes — answered by the seat (Q1 = A); round 2 rewrote the changeset line and the seat rewrote the body's declaration. Residue for the seat, not a FAIL: the PR body's prose still says the changeset carries Clause-②: yes (narrowing) (the fourth bullet under What) and Acceptance note 2 still says "This body and the changeset carry the ruling's yes" — both sentences are now false about the head. No gate reads them (first key-line wins), but the seat owns the body: rewrite both before enqueue. No new head is needed.
  • deviations[3] a killed check:type-check-debt run left a half-built dist, rebuilt before any reading — no bearing on the diff; CI's Type Check · debt ledger runs that gate (not concluded, below).
  • deviations[4] model-free commit trailers — per AGENTS.md; no action.
  • deviations[5] consumer typecheck over 14 packages, not the 73-package closure — covered by the check-runs: Type Check · workspace runs turbo run typecheck over packages/*, packages/*/* and apps/* (not concluded, below). examples/* are not in that filter (CI builds them as dependencies only), so the examples rest on the dev's five local runs plus my census in ① item 6, which finds no alias on a carrier there. Accepted.
  • open_questions[0] (Clause-② value) and open_questions[1] (ADR-0087 route) — settled by 5877491493, both A; both right (② and ① items 8–10).
  • out_of_scope_findings[0] prose at four sites naming the old z.preprocess(normalizeFilterOperator, …) spelling (three in-repo, one in objectui) — noted, not filed: each statement stays true of behaviour; not a defect under Prime Directive 10. out_of_scope_findings[1] the case-fold branch's reach — deliberate per the table's docblock; no action.
  • NOT MEASURED locally: check:type-check-debt → CI (Type Check · debt ledger, not concluded); the check-changeset-no-major level axis → Check Changeset, concluded success.

Check-runs on the head, read once at 2026-09-28T20:05Z (32 runs; none failed):

  • Concluded success: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Check PR Size, Auto Label, Check Documentation Links, Flag docs affected by code changes, The card this PR closes must claim this branch, 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, filter.
  • Skipped by path filter: Console Pin Gate (no console path touched — see ① item 7 for what that leaves unmeasured), Build Docs, Packed-tarball smoke (opt-in).
  • Not concluded (in progress; not presumed green): Lint & Repo Gates (carries check:migration-registry, ESLint, check:doc-authoring), Type Check · source gates (carries check:spec-changes, check:upgrade-guide), Type Check · workspace (carries check:test-typecheck through spec's typecheck), Type Check · debt ledger, Type Check · consumer gates (carries check:api-surface), the rollup TypeScript Type Check (not yet started on this head; it needs the four), Build Core, Test Core 1–6, Dogfood Regression Gate 1–3, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL). The required set (Multi-agent discipline §7) is therefore not yet green; enqueue waits on it, and this record does not stand in for it.

Governance: the file list touches no governed surface (.changeset/ and four files under packages/spec/src/); 293 changed lines; head repo is the base repo; the queue guard concluded success. ESCALATED: none.

Implemented-by: claude/issue-20450-view-filter-operator-input-typed
Reviewed-by: session_014EJ1ED8X4MMrT18BhVx4tx

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 20:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit dc07593 Sep 28, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20450-view-filter-operator-input-typed branch September 28, 2026 20:52
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