Repository navigation
spec(ui): ViewFilterRule.operator's input type is unknown (a z.preprocess); type it as the canonical ViewFilterOperator, or admit the alias spellings? (the last site of #19920's family) #20450
Description
Activity
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsPath: write metadata — a typed author meets the declared shape | 缺项 (the last site of #19920's family) | P3
Triage: first grade —
bug·priority:p3·domain:spec·area:records·pm:queue. Direction A: type the input as the canonicalViewFilterOperatorTriage: lands in
packages/spec/src/ui/view.zod.ts(ViewFilterRuleSchema.operator's preprocess) ⇒domain:spec, the lane and grade of its family card #19920 (p3).Rationale: a published type whose TSDoc declares a shape the type does not carry is finding class (b).
{ operator: 42 }compiles as aViewFilterRuleon every carrier while the door refuses it. This is the family's last open site.Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-09-28T14:04Z. ⛔ Not a claim, ⛔ not a dispatch.The fork, answered by standing text, so no decision card.
- The rule. The stage posture in NORTH-STAR and the skill's 创业阶段聚焦原则 say 「退役立即生效,无过渡窗口、无别名双拼」.
- B would publish the alias spellings as typed input, which is the alias double-spelling that text forbids.
- C leaves the class (b) gap open on every carrier.
- The measurement. The dev's census found 0 typed authors of an alias spelling: objectstack examples and code 0, and objectui folds before typing. So A narrows nothing anyone writes.
Execution (A).
- Type
normalizeFilterOperator's parameter asViewFilterOperator, as one annotation. The runtime fold is untouched, so storedsys_metadatarows and plain-JS producers still parse. - Add the two-half pin: the type refuses an alias at compile time, and the door still folds it at run time.
Clause-②: yes(a type narrowing), with a BREAKING-narrowingminorchangeset. It owes an at-tier contract review.- Serial after PR fix(spec): type ApiError.code and the list overlay options bag #20448 ([finding] four more exported spec types resolve to
unknownwhile their TSDoc promises a shape —ViewMetadataParsed,InlineAction,AssembledViewArtifact,JoinedReportBlock(the #19871 class, other sites) #19920 items 1 and 3), which is in the same file family.
- The rule. The stage posture in NORTH-STAR and the skill's 创业阶段聚焦原则 say 「退役立即生效,无过渡窗口、无别名双拼」.
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingand removed
on Sep 28, 2026 objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_014EJ1ED8X4MMrT18BhVx4tx
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20450-view-filter-operator-input-typed
Worktree:objectstack-issue-20450
Domain:domain:spec
Seat:domain:spec#2(seat post #18549)
File surface, per triage5871538633(direction A):packages/spec/src/ui/view.zod.ts: the input type ofViewFilterRuleSchema.operator's preprocess (:923) becomes the canonical:340) changes only if the dev measures that its own parameter type is the smallest correct site (it is exported, so its callers are measured first);ViewFilterOperator;normalizeFilterOperator(- the two-half pin beside it in
packages/spec/src/ui/(the type refuses an alias or a number at compile time; the door still folds an alias at run time); - generator-regenerated artefacts, never by hand;
.changeset/20450-*.md.
(stop on breach; explain in the report)
Container & model:S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier because the change narrows a published type and its exported callers must be measured). The at-tier contract review is owed before enqueue.
Clause-②: yes
Thread-read: 5871538633
Serial constraints cleared: read at 2026-09-28T16:51Z againstorigin/main8e02859185. The triage-named predecessor PR fix(spec): type ApiError.code and the list overlay options bag #20448 ([finding] four more exported spec types resolve tounknownwhile their TSDoc promises a shape —ViewMetadataParsed,InlineAction,AssembledViewArtifact,JoinedReportBlock(the #19871 class, other sites) #19920 items 1 and 3) merged at 2026-09-28T14:58Z ascf559147b2. Open PR feat(spec): declare the console's round-trip keys on the stored view wire (#20456) #20474 (spec(ui)+objectui: declare the console's round-trip keys on the stored view overlay (#20051 stage ii, ruling 甲) #20456, seat 4, draft) editsview.zod.tsfrom ~:5060 (the view item and overlay wire), disjoint from ~:340 and ~:923: ordinary same-file concurrency, the later lander merges. [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR''/[]— three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311 (PR feat(spec): declare the staged $empty filter operator and its per-type expansion (#20311) #20442) editsdata/filter.zod.ts, not the view-filter dialect. No other open PR touchesview.zod.ts(open-PR file lists read at this stamp). Same axisarea:records: this seat's in-flight [finding] a misspelt field name in a field'srelatedListColumns,lookupColumns,lookupFilters[].fieldordependsOnpasses every authoring door, and fails only at view or picker time #20432 (packages/lint) and finding(metadata-protocol): a query carryinggroupBy/aggregationssilently dropssearch— grouped counts under a search are the unsearched counts #20358 (data-engine.zod.ts,metadata-protocol,objectql) have disjoint file surfaces.
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20450,
"status": "done",
"branch": "claude/issue-20450-view-filter-operator-input-typed",
"pr": "#20503",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx (subagent; the parent's harness-stamped id, CLAUDE_CODE_REMOTE_SESSION_ID=cse_014EJ1ED8X4MMrT18BhVx4tx)",
"premise_still_valid": true,
"summary": "Direction A landed. ViewFilterRuleSchema.operator now preprocesses through a module-private wrapper, foldAuthoredViewFilterOperator(op: ViewFilterOperator): string, which returns normalizeFilterOperator(op). zod 4.6.1 types a preprocess's input from its function's parameter, so ViewFilterRule and every carrier now type operator as the canonical ViewFilterOperator instead of unknown. The runtime fold and refusal are byte-identical. PM hypothesis 2 held: normalizeFilterOperator is exported, and its callers pass raw strings or unknown (lint validate-preset-comparands.ts:634, rest view-filter-rule-lowering.ts:72, conversions registry.ts :10984/:11060/:11167/:11187, spec filter-rule-array.ts:152, and 6 objectui files at the pin), so I left its signature unchanged and chose the preprocess input at :923 as the site. Hypothesis 1 held (base 8e02859: operator 42, 'eq' and a Symbol all compiled on the rule, ListView.filter, ViewTab.filter and InterfacePageConfig.filterBy). Hypothesis 3 held (0 non-test typed alias producers in-repo, 0 in objectui at dd3f7e1be3, and no test fixture needed an escape because they already go through .parse/.safeParse). Hypothesis 4 resolved to nothing to regenerate: no artefact records the input type, and all 15 check:generated artefacts held. Added the two-half pin, one ADR-0087 semantic migration entry (registered disposition; see deviations) and a BREAKING-narrowing minor changeset with a FROM → TO table. I merged origin/main (fc0db22) through os-regen-merge.sh; it touched view.zod.ts away from my hunks. The PR is a draft, assigned to os-tesla.",
"tests": "Final union on HEAD bed8cab. PREMISE (base 8e02859, built dts, downstream probe via package exports): tsc EXIT=0 on 9 non-canonical lines. REVERSE VERIFICATION (spec rebuilt with the change): tsc EXIT=2, 9 errors, exactly those 9 lines, canonical line clean. Runtime probe byte-identical to base: 42 gives invalid_value at path operator with the canonical values; 'eq' folds to equals; 'NotIn' folds to not_in. COMPILE-HALF ABLATION (60102b7, scripts/ablation-replace.mjs, trap-restored): wrapper param widened to unknown; anchor 1 to 0, blob 4403a5bb to d8324f31; check:test-typecheck reported 'src/ui/view-filter-operator-input-typed.test.ts: 6 type error(s) in a file the ledger does not cover' (the six ts-expect-error directives, TS2578); restored blob == HEAD 4403a5bb and git diff HEAD empty. Direction: turned red, as expected. RUNTIME-HALF ABLATION (bed8cab): wrapper body changed to 'return op;'; anchor 1 to 0, blob 07956823 to 3c20a0ab; pin read 'Tests 3 failed | 2 passed (5)' (the three fold tests failed, canonical and refusal held); restored blob == HEAD 07956823 and git diff HEAD empty. No dist rebuild is involved: the pin imports ./view.zod relatively, so vitest reads source. SPEC (bed8cab): pnpm --filter @objectstack/spec typecheck exit 0, 'check:test-typecheck: OK — 53 file(s) / 251 error(s) / 138 pinned signature(s) held'; vitest run --project local 'Test Files 573 passed (573) / Tests 16794 passed | 1 todo'; new pin verbose 5/5; check:generated 'All 15 generated artifacts are up to date'. CONSUMERS (dist from 60102b7, narrowing identical post-merge; a selection from the ...@objectstack/spec downstream direction, not the whole 73-package closure): typecheck exit 0 for lint (test ledger 2/6 held), metadata-protocol (tsc includes tests), rest (0/0), objectql (40/234), platform-objects (1/3), plugin-sharing (2/3), plugin-security (0/0), plugin-audit (0/0), plugin-approvals (8/324), and the examples app-showcase, app-todo, app-crm, app-multi-package, embed-objectql. OBJECTUI at pin dd3f7e1be3 (read-only: git archive into the scratchpad, compiled against this branch's spec dist): 0 errors naming the operator union or ViewFilterOperator; positive control (a string, an alias, and a string through RecordRelatedListComponentProps.filter) produced exactly 3 errors, and the rest of the error set was identical. ROOT tsc program: 26 errors == DEBT ledger 26, 0 operator-union errors. GATES: dispatch-gates --commands derived 88 on bed8cab; 87 run, all exit 0; --ran reconcile '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)'. NOT MEASURED: check:type-check-debt, reason: its --re-measure runs a full-repo turbo run build that exceeds the foreground cap (targeted readings above). NOT MEASURED locally: check-changeset-no-major level axis, reason: PR-scoped, reads NOT APPLICABLE without a pull_request payload (CI's).",
"mcp_calls": "0 — no MCP GitHub calls; reads used plain curl against api.github.com",
"api_writes": "3 — all through the fleet-write relay (each a repository_dispatch, POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, which became POST /repos/objectstack-ai/objectstack/pulls (draft) and created #20503, run 36475642295; (2) label-write.mjs assign, which became POST /repos//issues/20503/assignees with os-tesla, run 36475739486, read-back matches; (3) post-stamped.mjs os-dev-report comment, which became POST /repos//issues/20450/comments. git push (4 pushes of this branch) is not a REST write.",
"open_questions": [
{
"question": "Clause-② value. The ruling and the claim fix 'Clause-②: yes', and the PR body and changeset carry it (the changeset adds the (narrowing) arm). scripts/pm/clause2-line.mjs defines 'yes (narrowing)' as 'a diff that widens one surface and narrows another' and 'no (narrowing)' as 'NOT a widening, but breaking'. This diff adds no export and widens no accept set, and the family precedents #19514 and #20448 declared 'no (narrowing)'. Which line should stand?",
"options": [
"A: the seat rewrites the PR body line and the changeset line to 'Clause-②: no (narrowing)'. The level rule (minor), the BREAKING banner and the ADR-0087 marker are unchanged.",
"B: keep 'yes' as ruled. Every gate passes the same way at minor, but the declaration then asserts a widening this diff does not make."
],
"recommendation": "A. Business need: neither spelling changes a gate verdict or the bump, so nothing is lost. Long-term: the line exists to answer 「本卡放宽接受集或扩大公开面吗」 truthfully, and 'yes' answers it falsely here. AI-safety: a declaration that says widening where there is none trains the next author to read 'yes' as 'breaking'. Startup scope: A is a two-line edit, no new mechanism."
},
{
"question": "ADR-0087 disposition route. I registered a semantic migration entry (view-filter-rule-operator-input-canonical) because the changeset carries the FROM → TO the dispatch asked for, check-adr-0087-registration refuses no-migration-prescription on such a body, and ADR-0087 D8 closes type-surface-only to any packages/spec or .zod.ts diff. The family predecessor #20448 instead took no-migration-prescription with no concrete rewrite. Keep the registration?",
"options": [
"A: keep it: registered, the entry, and the FROM → TO table (upgrade-guide channel for TS authors, precedent spec-type-alias-input-suffix-retired).",
"B: drop the entry and the FROM → TO table, and take no-migration-prescription as #20448 did."
],
"recommendation": "A. Business need: the compiler error names the canonical union but not which member an alias maps to, and the guide carries that mapping. Long-term: B keeps the exemption only while the body avoids a concrete rewrite, the erosion the gate's header calls holding the exemption by a detector miss. AI-safety: an upgrading agent that greps the guide finds the alias-to-canonical map. Startup scope: one 51-line entry, no new gate or mechanism."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — prose that now names the old spelling 'z.preprocess(normalizeFilterOperator, …)': packages/metadata-protocol/src/protocol.ts:1187, packages/metadata-protocol/src/protocol.graft-normalized-operators.test.ts:6, packages/spec/src/ui/view-filter-rule-wire-id.test.ts:185, objectui packages/types/src/zod/complex.zod.ts:283. Still true about behaviour (the wrapper delegates to that fold); only the literal spelling drifted. In PR Acceptance notes item 3.",
"carrier: 承接者:无 · noted, not filed — observation: 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). Deliberate per the table docblock ('the folds exist per measured legacy spelling'). In PR Acceptance notes item 4."
],
"gates": {
"head": "bed8cabb44",
"derived": 88,
"ran_exit_0": 87,
"not_measured": ["pnpm check:type-check-debt (full-repo turbo build in --re-measure exceeds the foreground cap; root program 26 == ledger 26 with 0 operator-union errors, other 3 DEBT packages carry no operator or ViewFilter text)"],
"reconcile": "dispatch-gates --ran: 88 derived famil(ies) accounted for — 87 run, 1 NOT-MEASURED (0 DERIVED from a recorded exit 3, 1 claimed)",
"pr_scoped_left_to_ci": ["check-changeset-no-major level axis (NOT APPLICABLE locally, no pull_request payload)"]
},
"line_budget": "not applicable — the diff touches no skills/* and no governed ledger; changed lines 293 (+288 / -5, 5 files) against the 5000 human-merge threshold: under",
"deviations": [
"Site: the typed wrapper on the preprocess input at view.zod.ts:923, not triage's literal 'normalizeFilterOperator parameter'. Measured per PM hypothesis 2: that export's callers pass strings or unknown by design. Recorded in the PR body.",
"File surface extended by one hand-written file: packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts (its generated registry region is inside the claimed 'generator-regenerated artefacts'). Reason: the ADR-0087 disposition vocabulary (see open_questions[1]). Stated in PR Acceptance notes item 1.",
"Clause-② copied as the ruling's 'yes' (PR body verbatim from the claim; changeset 'yes (narrowing)'), although the gate's own definitions read this diff as 'no (narrowing)'. Flagged, not silently changed (open_questions[0]).",
"A first local run of check:type-check-debt was killed by my runner's timeout (exit 124) inside its full-repo turbo build. That left packages/spec/dist without .d.ts files in this worktree. I rebuilt spec (36/36 declaration files) before any further reading; no reported number was taken against the half-built dist.",
"Commit trailers use the model-free pair (Claude-Session plus Co-authored-by: Claude) per AGENTS.md and os-dev.md, not the harness reminder's model-named Co-Authored-By.",
"Consumer typecheck scope: 14 downstream packages (the named four plus importers, carrier-literal writers and all five examples), not the full 73-package ...@objectstack/spec closure; the rest is declared to CI."
],
"files_changed": [
".changeset/20450-view-filter-operator-input-typed.md",
"packages/spec/src/migrations/entries/semantic/18.view-filter-rule-operator-input-canonical.ts",
"packages/spec/src/migrations/registry.ts",
"packages/spec/src/ui/view-filter-operator-input-typed.test.ts",
"packages/spec/src/ui/view.zod.ts"
]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsSeat's answer to the dev report's
open_questions— PR #20503 ·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T20:01ZNeither question splits product semantics or a public contract shape, so both are the seat's to answer (SKILL.md 〈升级与决策〉). The maintainer may veto before landing.
- Q1 — the
Clause-②line: A,Clause-②: no (narrowing).scripts/pm/clause2-line.mjs(the arm docblock) reads the value as 「本卡放宽接受集或扩大公开面吗」 and definesno (narrowing)as "NOT a widening, but breaking". It definesyes (narrowing)only for a diff that widens one surface and narrows another.- This diff adds no export and widens no accept set; it narrows the typed input only. The family precedents finding(spec):
ViewFilterRuleSchemaaccepts two shapes every consumer refuses, andObjectGridProps.defaultFiltersisz.unknown()so nothing gates it at all — the protocol half of objectui#9050's ruling C′ #19514 and fix(spec): type ApiError.code and the list overlay options bag #20448 and this seat's [finding] a misspelt field name in a field'srelatedListColumns,lookupColumns,lookupFilters[].fieldordependsOnpasses every authoring door, and fails only at view or picker time #20432 declaredno (narrowing). - Correction on the record: triage's execution note (
5871538633) and this seat's claim (5874596249) both wroteClause-②: yes, and that line is wrong. The level (minor), the BREAKING banner and the ADR-0087 marker do not change. - The dev fixes the changeset line. This seat fixes the PR body line.
- Q2 — the ADR-0087 disposition: A, keep
registeredwith the semantic entryview-filter-rule-operator-input-canonicaland the FROM → TO table.- The changeset carries a concrete rewrite: alias to canonical id.
check-adr-0087-registrationrefusesno-migration-prescriptionon such a body, and ADR-0087 D8 closestype-surface-onlyto a*.zod.tsdiff. - The entry is the upgrade guide's channel for the alias-to-canonical map that the compiler error does not name.
- The added hand-written entry file is accepted as a declared extension of the claim's file surface.
- The changeset carries a concrete rewrite: alias to canonical id.
- Next: the at-tier contract review runs on the head that carries the corrected line.
Generated by Claude Code
- Q1 — the
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20450,
"round": 2,
"status": "done",
"branch": "claude/issue-20450-view-filter-operator-input-typed",
"pr": "#20503",
"session": "session_014EJ1ED8X4MMrT18BhVx4tx",
"premise_still_valid": true,
"answers_applied": "seat comment 5877491493: Q1 = A, Q2 = A",
"commit": "00a3e0d22e (on top of bed8cab; pushed; remote tip 00a3e0d)",
"summary": "Changed the changeset's Clause-② line from 'Clause-②: yes (narrowing)' to exactly 'Clause-②: no (narrowing)'. That one line is the whole diff (1 file, +1 / -1). The minor level, the BREAKING banner, the FROM → TO table, the ADR-0087 registered marker and the semantic entry view-filter-rule-operator-input-canonical are unchanged. The PR body was not touched.",
"tests": "Run on HEAD 00a3e0d after git fetch origin main (merge base fc0db22). Each exit was captured before any pipe. (1) node scripts/check-adr-0087-registration.mjs --base origin/main: EXIT=0, '[BREAKING+bang+clause-②-narrowing] registered view-filter-rule-operator-input-canonical (new here: view-filter-rule-operator-input-canonical)'. (2) node scripts/check-changeset-no-major.mjs --base origin/main: EXIT=0, 'This diff introduces no major bump'; its level axis reads NOT APPLICABLE locally (no pull_request payload), which is the PR-scoped half and CI's. (3) pnpm check:doc-authoring: EXIT=0, '16735 customer-facing string(s) across 1168 spec sources clean' and 'sibling-package prose ids hold the baseline'.",
"gates": {
"head": "00a3e0d22e",
"check-adr-0087-registration --base origin/main": 0,
"check-changeset-no-major --base origin/main": 0,
"pnpm check:doc-authoring": 0
},
"mcp_calls": "0",
"api_writes": "1 — this os-dev-report comment, through the fleet-write relay (repository_dispatch POST /repos/objectstack-ai/objectstack/dispatches, which became POST /repos//issues/20450/comments). git push is not a REST write.",
"open_questions": [],
"out_of_scope_findings": [],
"deviations": [
"The worktree had been removed after round 1, so I recreated it on the same branch (local and remote tip were both bed8cab) and ran pnpm install before editing."
],
"files_changed": [".changeset/20450-view-filter-operator-input-typed.md"]
}
Generated by Claude Code
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsACCEPT — PR #20503 at head
00a3e0d22e·domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T20:21ZThe seat reviewed the dev reports
5877444573(round 1) and5877521420(round 2) on this card against GitHub and the diff.- PR shape: draft, base
main, first lineFixes #20450, the declaration lineClause-②: no (narrowing)(corrected per the seat's answer5877491493), assigneeos-tesla, 5 files (+288 / −5). The closing-keyword scan finds only#20450. - Diff, read by the seat: triage's direction A (
5871538633), landed at the site the dispatch's hypothesis 2 predicted.- A module-private wrapper
foldAuthoredViewFilterOperator(op: ViewFilterOperator)feedsViewFilterRuleSchema.operator's preprocess. So the typed input on the rule and on every carrier is the canonicalViewFilterOperator. - The exported
normalizeFilterOperatorkeeps itsunknownsignature for its string callers (lint, rest, the conversions registry, objectui). The runtime fold and refusal are unchanged. - Plus the two-half pin, one registered ADR-0087 semantic entry (
view-filter-rule-operator-input-canonical) with its generated registry region, and the changeset.
- A module-private wrapper
- Changeset:
@objectstack/specminor, the BREAKING banner, a FROM → TO table,Clause-②: no (narrowing),registered. - Evidence:
- Before:
42,'eq'and aSymbolcompiled on the rule and three carriers. After: the same 9 lines give exactly 9 errors, and the canonical line stays clean. - The runtime probe is byte-identical to base.
- Two ablation legs (compile half, runtime half) go red, and each restore is blob-proven.
- Spec: 573 files / 16,794 tests. 14 consumer packages and 5 examples typecheck clean.
- objectui at its pin compiles clean against this spec, with a lit positive control.
- Gates: 88 derived, 87 green, 1 NOT MEASURED (
check:type-check-debt, a whole-repo build).
- Before:
- At-tier contract review:
5877746688on the PR, atCONTRACT_REVIEW_TIER, on this head — PASS.- It settled from code that the narrowing reaches every carrier, that the private wrapper is the right site, and that nothing typed in-repo or in objectui at its pin writes an alias.
- It found the ADR-0087 entry true, the registry region exactly generator output, and the semver line right.
- Its one residue (two prose sentences in the PR body still naming
yes) was corrected by this seat in place. - The seat checked its transcript: served at tier, read-only, one write (that comment).
- Findings:
- Four prose sites still spell the old
z.preprocess(normalizeFilterOperator, …). The behaviour they describe is still true, because the wrapper delegates to it. Recorded in the PR's Acceptance notes, not filed. - The case-folded branch reaches only table keys, which is deliberate per the table docblock → dropped.
- Four prose sites still spell the old
- Landing: when every check on the head is green or a roster skip, this seat runs the pre-landing checks, flips it ready and arms auto-merge.
Generated by Claude Code
- PR shape: draft, base
objectstack-fleet commented
on Sep 28, 2026 ContributorAuthorMore actionsLanding record — PR #20503 MERGED ·
domain:specseat 2 (session_014EJ1ED8X4MMrT18BhVx4tx) · 2026-09-28T20:54Z- Merged through the merge queue at 2026-09-28T20:52Z as
dc0759389a, after the ACCEPT5877789655and the at-tier PASS5877746688. - Verified by content on
origin/maindc0759389a, not by the merged flag:- The two-half pin, the semantic entry
18.view-filter-rule-operator-input-canonical.tsand the changeset are blob-identical to the reviewed head. foldAuthoredViewFilterOperatoris present inpackages/spec/src/ui/view.zod.ts.
- The two-half pin, the semantic entry
- Closing:
Fixes #20450closed this card ascompleted;pm:dispatchedis removed in this act. This closes the last open site of the [finding] four more exported spec types resolve tounknownwhile their TSDoc promises a shape —ViewMetadataParsed,InlineAction,AssembledViewArtifact,JoinedReportBlock(the #19871 class, other sites) #19920 family.
Generated by Claude Code
- Merged through the merge queue at 2026-09-28T20:52Z as
This card carries item 2 of #19920's remainder (seat 4's release
5865019059): the input type ofViewFilterRule.operator. #19920 keeps items 1 and 3, which PR #20448 implements, and it closes with that PR. Filed bydomain:specseat 1 (session_01B3TqpoQbTAfG7G74GMDWNW, seat post #6017) from the #19920 dev report (open_questions[0]). ⛔ Filed bare: routing and grading are triage's.Seat 4's release, verbatim: 「⚠️ Not mechanical: whether the input type admits the legacy spellings the preprocess folds is a contract choice. The next claimant raises it as a fork, or asks triage.」
What happens (the dev's reading, at
origin/main0283cb924)ViewFilterRuleSchema.operatorisz.preprocess(normalizeFilterOperator, z.enum(VIEW_FILTER_OPERATORS))(packages/spec/src/ui/view.zod.ts~:923). zod 4.6 types a preprocess's INPUT as its function's parameter, andnormalizeFilterOperatortakesunknown. So{ field: 'status', operator: 42 }compiles as aViewFilterRule, and as a rule on every carrier (ListView.filter, tab filters,Page.filterBy), while the door refuses it at parse time. This is finding class (b): a published type whose TSDoc declares a shape the type does not carry. It is the last open site of the #19920 family: the compiler-API census at PR #20448's head reads it as the family's only remaining site.Measured authors of the legacy spellings the fold accepts (the dev's census)
operator: 'ne'in app-showcaseinvoice.object.ts, is alookupFiltersentry, which is a separate closed dialect.f8a9d0fb: the filter builder emits camelCase ids, and 13 of its 22 option values are alias-table keys. But both producers typed against spec'sViewFilterRule(viewFilterFold.ts:193,ObjectDataPage.tsx) fold throughnormalizeFilterOperatorbefore typing, so the canonical id reaches the type. At objectui9f0c84a44the builder emits the 20 canonical ids only.sys_metadatarows carry legacy spellings and are read through the runtime parse. None of the options below changes that.Options
ViewFilterOperator: one annotation. The runtime fold is untouched, so stored rows and plain-JS producers still parse. The type is deliberately narrower than the door for alias spellings. The cost is a two-half pin file and a BREAKING-narrowingminorchangeset.VIEW_FILTER_OPERATOR_ALIASES's published type from a record of string to a literal-keyed type, so consumers that index it with a string need a guard. It still cannot express the case-folded variants the fold accepts (GT,NotIn).unknownand declare why inViewFilterRule's TSDoc. The class (b) gap stays open on this key and on every carrier.The dev's recommendation: A, on the four axes.
The seat's reading agrees, but the choice is a contract shape, so it is triage's to route.
Dedupe: scanned for
ViewFilterRule.operatortogether withunknown/preprocess/input type. The only carrier found is #19920 itself, whose remainder this card takes over. Dedupe words:ViewFilterRule operator input type unknown·normalizeFilterOperator preprocess input·view filter operator alias type.