Skip to content

spec: FilterConditionSchema (the save-time door) admits a non-boolean $null / $exists flag, so a stored dataset or widget filter carrying one saves clean and is refused 400 on every query face #20116

Closed

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site, measured. Finding class (c): two faces of one contract disagree.

The contract

The defect

Measured by the #20040 dev at 66266c2a3b:

The field entry is typed z.unknown() at that position. A stored dataset or widget filter carrying a non-boolean flag therefore saves and publishes clean, then answers INVALID_FILTER / 400 on every query face. The author finds out at run time, not at save.

Producer: whoever authors a dataset filter, through defineStack metadata or the Studio. Stored deployments were NOT measured for the shape.

Seam: spec:FilterConditionSchema (checkFilterConditionComparands) → runtime:driver-sql reduceFilterKey | service-analytics normalizeWhereComparands

Filing-gate answers

Dedupe words: FilterConditionSchema $null non-boolean admitted at save · dataset filter $exists string saves then 400 · checkFilterConditionComparands boolean flag


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20116,
      "status": "done",
      "branch": "claude/issue-20116-filter-save-door-face-parity",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20247",
      "session": "session_01Rjy9MeetSfq34PKn81CRiN (subagent: the PM's session)",
      "premise_still_valid": true,
      "round": "patch round 1 (CI Test Core (4/6) at c300e80e; review record 5856977110). New head ab146a9a.",
      "summary": "Blocking item fixed. packages/rest/src/analytics-filter-refusal-envelope.test.ts: 'a $between with one bound' moved from the normalizer REFUSALS table (400 INVALID_FILTER) to the [#17551] AT_THE_DOOR table. It now asserts 400, VALIDATION_FAILED, exactly one details.fields[] entry at selection.runtimeFilter.amount.$between, the face's sentence (regexp 'Operator \"$between\" on field \"amount\" requires a [min, max] value array. Received array ([10]). A range needs exactly two bounds'), and no ' at where.'. The sibling-schema control now also asserts the AnalyticsQueryRequestSchema refusal at where.amount.$between in the same sentence. The pass-list control is three spellings (the one-bound $between removed). The #17551 docblock's count and its table row for this spelling were updated, and the #20010 note moved with the row, reworded so it stays true. Changeset gains a 'FROM → TO at the HTTP doors' table (POST /analytics/dataset/query: 400 INVALID_FILTER becomes 400 VALIDATION_FAILED located on the member; same for any refused slot in selection.runtimeFilter; POST /analytics/query refuses the shape in where at its request schema). Non-blocking flag (a): changeset FROM → TO rows added for $nin: ['lost', null], the producer's $in: [null, ''] ('is empty') and $nin: [null, ''] ('is not empty'). Each TO spelling was checked to pass both FilterConditionSchema and the face. Flag (b): filter-ne-array-schema-door.test.ts §5 it.todo turned into real pins (path stage.$ne, message equal to the face's less its location, the $nin remedy, a DatasetSchema carrier at filter.$or.0.stage.$ne, a null/scalar control). Flag (c): PR body Clause-② line left verbatim 'no', as the review found coherent. Consumer sweep: 62 non-spec test files carry a refused member shape; only the rest file above drives a schema door with one. PR body revision prepared (no relay op pr_update exists and os-dev forbids a dev PATCH): /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-20116/pr-body-v2.md, for the seat to apply.",
      "tests": "Batch F (os-verify-lock VERDICT command-exit 0, held 1824s; tree at its spec step = ab146a9a, tree clean at the end; header reads dfca00f0 because ab146a9a, a test and changeset commit, landed before the spec step): rest closure build 0; rest flipped file 31 passed; spec check:generated 0; rest full suite (--project local) 201 files / 3576 passed / 1 skipped; service-analytics full suite 129 files / 3041 passed; spec full suite 581 files / 16770 passed / 1 todo (was 2: the $ne §5 todo is now a pin, which proves the edited file ran). Gates on ab146a9a: dispatch-gates --commands 87 (unchanged set); 85 exit 0; check:dual-build-cjs-loads and check:type-check-debt exit 3 PREREQUISITE NOT MET (whole-repo build) = NOT MEASURED; --ran with recorded codes: 87 accounted, 85 run, 2 NOT-MEASURED derived. SWEEP: git grep -P for every refused member shape over packages/**/*.test.ts, packages/**/test/** and packages/**/__tests__/** outside spec found 62 files (service-analytics 20, driver-memory 9, driver-sql 7, driver-turso 6, driver-mongodb 5, objectql 4, formula 4, rest 3, plugin-security 2, runtime 1, driver-sqlite-wasm 1). Schema doors at runtime: rest /analytics/dataset/query (DatasetSchema.parse on the posted dataset, DatasetSelectionSchema), runtime domains/analytics.ts (AnalyticsQueryRequestSchema), objectql registry ObjectSchema.parse. Only analytics-filter-refusal-envelope.test.ts sends a refused shape through one. The other rest and runtime hits are an RLS scope ({ restricted_region: { $in: 'emea' } }, never schema-parsed) or a comment. objectql hits are an engine where, not a registered object's carrier. metadata-protocol has no member-shape fixture. A second pass (any operator mention in runtime, rest, metadata-protocol, cli and client tests that name an analytics or dataset door) found only legal shapes ($ne: 'EU', $gt: { $field }). CI c300e80e final: Test Core (4/6) and its aggregate failed, all else green or skipped. CI at ab146a9a: not yet read (just pushed).",
      "mcp_calls": 0,
      "api_writes": 1,
      "api_writes_endpoints": "POST /repos/objectstack-ai/objectstack/issues/20116/comments (this os-dev-report, via post-stamped through the fleet-write relay dispatch). Plus 1 paced git push (c300e80e..ab146a9a), not REST.",
      "files_changed_this_round": [
        "packages/rest/src/analytics-filter-refusal-envelope.test.ts",
        "packages/spec/src/data/filter-ne-array-schema-door.test.ts",
        ".changeset/20116-filter-save-door-face-parity.md"
      ],
      "gates": "ab146a9a: 87 derived; 85 exit 0; check:dual-build-cjs-loads exit 3, check:type-check-debt exit 3 (PREREQUISITE NOT MET, NOT MEASURED); spec check:generated exit 0",
      "line_budget": "n/a — no skills/** or governed surface touched",
      "deviations": [
        "PR body not edited: the relay has no pr_update op and os-dev forbids a dev PATCH. The revised body is at /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-20116/pr-body-v2.md. It changes: (1) the Tests section's NOT MEASURED paragraph becomes the patch-round readings and sweep; (2) the pin-sweep list gains the rest flip and the $ne todo fulfilment; (3) a paragraph before 'Changed docblocks' names the changeset's HTTP-door and producer rows. Everything else is byte-identical.",
        "origin/main was merged once this round (0d3ec471, merge f3fee77d) before any edit. main then moved 4 more commits (a91d12af #20245, 5983d112, e0f17a37, 08c8484a) while batch F ran. The only path those commits share with this diff is the generated packages/spec/src/migrations/registry.ts, and git merge-tree --write-tree HEAD origin/main exits 0 (no conflict). Not merged again, because that would owe another locked re-verification; the merge queue rebuilds on main.",
        "Worktree /home/user/objectstack-issue-20116 kept (the PM's instruction for this round), not removed."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "unchanged from the round-0 report (comment 5856795387): the objectui producer (class a), and two family members for collector #20116 (the widget/report nested-relation reach, and the comparand-type face). No new finding this round."
      ]
    }

    Generated by Claude Code

  2. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: session_01Rjy9MeetSfq34PKn81CRiN (domain:spec seat 1) · reason: partial landing; PR #20247 merged as Part of · to: pm:queue, assignee cleared in this act · 2026-09-27T16:16Z

    Landed: PR #20247 → main cfc3bcf15fba8986d44449e6fe34b78c2e9b1054 (Part of #20116)

    How it got here

    • Seat ACCEPT 5856822806.
    • Round-0 at-tier review FAILED (5856977110): two published pins in packages/rest had been flipped but not swept.
    • Patch round, then a delta at-tier PASS (5857406453) at head ab146a9a, which is the head that landed.
    • CI at that head: 37 success, 5 expected skips.
    • Enqueued 15:54Z, merged 16:14Z.
    • Pre-landing checks on main e4621867: git merge-tree clean; registry drift 0 missing; not governed.

    Verified on main, two readings

    • cfc3bcf1 is an ancestor of origin/main, and no queue branch for fix(spec)!: a filter carrying a comparand the query faces refuse is refused when it is saved (#20116) #20247 remains.
    • The diff against its first parent is 11 files, +1288/−123, matching the PR.
    • Present at cfc3bcf1 and absent at its parent (the control): the judge module packages/spec/src/data/filter-save-door-refusals.ts, and the semantic entry filter-query-face-comparands-refused-at-save.
    • The merge closed no card: Part of carries no closing keyword. No other lane card closed in the window.

    Delivered

    • FilterConditionSchema now refuses, at the shared face's reach, every comparand slot the face refuses:
      • $ne with an array;
      • a null ordering comparand;
      • a non-list $in / $nin;
      • a null list member;
      • a malformed $between;
      • a null, blank or { $field } endpoint;
      • a non-boolean $null / $exists flag.
    • The reach covers field entries plus $and / $or / $not members. Nested relations are covered on Dataset.filter and the measure filter, through fix(spec)!: a dataset or measure filter with a list inside a nested relation is refused on save (#20080) #20207's walk.
    • The HTTP-door code move (INVALID_FILTER → VALIDATION_FAILED at /analytics/dataset/query and /analytics/query) is declared in the changeset.

    Remainder: this collector's open members (both appended in ACCEPT 5856822806)

    1. M-widget. DashboardSchema widget filter and ReportSchema.runtimeFilter still save a face-refused shape INSIDE a nested relation (for example { acct: { stage: { $in: ['won', null] } } }). The analytics where door refuses the same shape on chart. fix(spec)!: a dataset or measure filter with a list inside a nested relation is refused on save (#20080) #20207's carrier refinement covers only Dataset.filter and the measure filter. A list inside a nested-relation condition in a dataset or measure filter ({ account: { region: ['a'] } }) passes the save-time schema door and is refused only when the chart runs #20080's equality-list shape has the same gap on these two carriers.
    2. M-type. The comparand-TYPE face (normalizeFilterComparandTypes) refuses a plain-object or Map comparand ({ stage: { $eq: { a: 1 } } }, { stage: { $in: [{ a: 1 }] } }), and the analytics door refuses it in every position. Every save door accepts it.

    Carried elsewhere: the objectui producer (FilterConditionField writes $in: [null, ''] / $nin: [null, '']) is objectui#10790. The next claimant re-claims from the queue under the full claim protocol and re-measures both members on the day's main.

  3. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-09-27T17:57Z
    Session: session_01Rjy9MeetSfq34PKn81CRiN
    Account: os-zhuang
    Branch: claude/issue-20116-stage-2-type-and-widget
    Worktree: objectstack-issue-20116-2
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: stage 2 of the collector, M-type and the dashboard half of M-widget from release 5857575995. The files: packages/spec/src/data/filter.zod.ts checkFilterConditionComparands (the faces filter-comparand-shape.ts / filter-comparand-type.ts are called read only) and the DashboardSchema widget filter carrier in packages/spec/src/ui/dashboard.zod.ts, with tests; one ADR-0087 semantic entry plus the generated regions of packages/spec/src/migrations/registry.ts; dropped-refinements.baseline.json if the build names it; generated artifacts; .changeset/. ⛔ Not packages/spec/src/ui/report.zod.ts: both runtimeFilter carriers sit in the regions PR #20238 (#20161, seat 4) edits, so that half stays on this card. ⛔ Not packages/formula/** (#19886 stage 2e). (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default judgment tier (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable). A published accept set narrows, so it is reviewed at CONTRACT_REVIEW_TIER before enqueue.
    Clause-②: no
    Thread-read: 5857575995
    Serial constraints cleared: Read at {{NOW}} on origin/main 17bd3187. Stage 1 (PR #20247) landed as cfc3bcf1. #20207 (the dataset carrier refinement, #20080) and #20143 (likePatternToRegexSource) are both closed, so their fences are gone. Open-PR census (19 PRs): only PR #20238 touches this family's files (packages/spec/src/ui/report.zod.ts, +77/-14, the JoinedReportBlockSchema and ReportSchema regions that hold both runtimeFilter carriers at :239 and :366), so it is fenced and the report half stays on the card. pm:dispatched claims hitting filter/dashboard/report: #20161 (that PR) and #20168 (seat 4, ruling 5856786357 item 1, schemaless-node-config DecisionConfigSchema; disjoint). This seat's #19886 stage 2e reads filter-comparand-shape.ts read only; this stage also calls it read only.

  4. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20116,
      "status": "blocked",
      "branch": "claude/issue-20116-stage-2-type-and-widget",
      "pr": null,
      "session": "session_01Rjy9MeetSfq34PKn81CRiN (mode:subagent, the PM's session; identity = the branch)",
      "premise_still_valid": true,
      "blocked_on": "The order's fence rule fired: 'Merge origin/main before opening the PR. If PR #20238 has landed by then, stop and ask the seat whether to fold the report half. Do not fold it on your own.' PR #20238 landed on main as 6a6a17b6 (read at the pre-PR fetch). So the work is complete, verified and pushed at 54b99f3c, and NO PR was opened and 6a6a17b6 was NOT merged. Only the seat can answer the fold question (open_questions 1).",
      "summary": "Both remaining members are implemented and verified on the branch (54b99f3c, which merges origin/main 3f86dc52). M-type: reportQueryFaceRefusals (the one judge both walks call) now asks the comparand-TYPE face normalizeFilterComparandTypes read-only after the shape face, so a plain object where one value belongs, a Map, a class instance, a function, a Symbol, undefined and a bigint beyond 2^53 are refused on save (comparand, implicit comparand or list member) at every reach the save doors have, in the face's own sentence less its ' at where.SLOT' clause; one slot raises one issue in the query doors' order (shape, type, flag), and the #19514/#8793 arms stay silent on a face-refused slot. A field value is a comparand unless it is a PLAIN object (the face's structure test), so an implicit Map reaches the face; the whole field entry is shown to the face first, so a spec it steps around as a $field reference stays accepted. M-widget dashboard half: #20207's refinement was inline and module-private in dataset.zod.ts; it moved verbatim (own commit dfa424f6) into the non-barrel packages/spec/src/ui/analytics-carrier-filter.ts, byte-neutral for Dataset (z.toJSONSchema of Dataset, DatasetMeasure, Dashboard, DashboardWidget: sha256 246850f2b9efdf1b at base 17bd3187, at the extraction commit and at the final head; probe verdicts identical base vs extraction; the ui/Dataset and ui/DatasetMeasure ledger rows unchanged), and DashboardWidgetSchema.filter now declares it. The report half (ReportSchema.runtimeFilter, JoinedReportBlockSchema.runtimeFilter) is untouched and pinned as EXPECTED_OPEN rows naming #20161. Zone 2 answered: (1) re-probe at 17bd3187 confirmed both members open on every carrier (table in the PR body draft); (2) the type face judges nothing only at request time: it is context-free, {placeholder} strings are strings at save time and resolveWhereTokens runs after both faces on the engine seam; $field, Date, placeholders and bigints within 2^53 pinned accepted and kept; (3) extraction in scope and byte-neutral, pinned (parity section 7); (4) the enumerating pin now derives the type face's arms (every declared operator must be type-judged over the battery) and walks FILTER_COMPARAND_TYPE_CASES; carriers x positions include the widget; (5) producer census 0 with lit controls in all four corpora plus a runtime walk of the example stacks.",
      "tests": "All at 54b99f3c through os-verify-lock (VERDICT command-exit 0). spec: build 0, check:generated 0 ('All 15 generated artifacts are up to date'), typecheck 0, full suite 585 files / 16930 passed / 1 todo. Consumer closures built (exit 0), then service-analytics 129 files / 3041 passed; lint 111 / 4273 passed; rest --project local 202 / 3599 passed / 1 skipped; rest typecheck 0 (test layer 0 errors); the two new rest AT_THE_DOOR rows ran by name (verbose). ABLATION (scripts/ablation-replace.mjs WRAP, from committed 54b99f3c; each anchor x1 to x0 on disk and blob changed; each restore blob == HEAD blob and git diff HEAD empty; script trap restores all four files on EXIT/INT/TERM): parity pin 152 tests: leg1 type face off = 48 failed; leg2 widget carrier off = 18 failed; leg3 shared walk data-object classification off = 6 failed; leg4 nested walk classification off = 3 failed; leg5 one-issue continue off = 1 failed, direction MORE diagnostics (two issues at name.$icontains); restored 152 passed. No build needed for the legs: the pin imports spec src. PROBE: base 17bd3187 vs head, member x carrier x face table in the PR body draft. ESLint narrowed: --no-inline-config --format json over the 9 changed .ts files (count from the JSON) 0 errors 0 warnings; population eslint.config.mjs, which never enables type-aware linting, so no untouched file's verdict can move. NOT MEASURED: objectql, metadata-protocol, runtime and example suites (CI); repo-wide pnpm lint (CI); the rest pin's red-before leg (would need a base spec dist rebuild; the base probe shows the analytics door answered INVALID_FILTER for both rows).",
      "gates": "54b99f3c: dispatch-gates --commands derived 88 on the actual paths (identical set to the dispatch lead gates-20116b.txt); 86 exit 0; pnpm check:dual-build-cjs-loads exit 3 and pnpm check:type-check-debt exit 3, both PREREQUISITE NOT MET (whole-repo build) = NOT MEASURED; --ran with 'CMD :: exit N' records: 88 derived accounted, 86 run, 2 NOT-MEASURED derived. check-adr-0087-registration: 1 declared-breaking changeset [BREAKING+bang+clause-2-narrowing] registered filter-comparand-types-and-widget-nested-slots-refused-at-save.",
      "line_budget": "n/a — no skills/** or governed surface touched",
      "files_changed": [
        ".changeset/20116-filter-save-door-type-face-and-widget.md (new)",
        "packages/spec/src/data/filter-save-door-refusals.ts",
        "packages/spec/src/data/filter.zod.ts",
        "packages/spec/src/ui/analytics-carrier-filter.ts (new, not in the ui barrel; verbatim move from dataset.zod.ts plus the stage-2 edits)",
        "packages/spec/src/ui/dataset.zod.ts",
        "packages/spec/src/ui/dashboard.zod.ts",
        "packages/spec/src/data/filter-save-door-face-parity.test.ts",
        "packages/spec/src/migrations/entries/semantic/18.filter-comparand-types-and-widget-nested-slots-refused-at-save.ts (new)",
        "packages/spec/src/migrations/registry.ts (generated)",
        "packages/spec/dropped-refinements.baseline.json (6 sites the build named, measured 604 to 610)",
        "packages/rest/src/analytics-filter-refusal-envelope.test.ts"
      ],
      "deviations": [
        "No PR opened and origin/main 6a6a17b6 not merged: the order's stop-and-ask rule fired (PR #20238 landed). Driverless merge-tree probe (bare shared clone, no os-regen driver) of 54b99f3c against 6a6a17b6: exactly one conflict, packages/spec/dropped-refinements.baseline.json (a ratchet ledger, not driver-managed); migrations/registry.ts auto-merges. The PR body is drafted at /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-20116b/pr-body.md; its remainder names the report carriers at :239/:366 of 17bd3187, which are :284/:413 on 6a6a17b6.",
        "File surface beyond the claim, each required by the order's own mechanism: data/filter-save-door-refusals.ts (stage 1's judge, where the shape face is asked: 'the way stage 1 calls the shape face'); ui/analytics-carrier-filter.ts and ui/dataset.zod.ts (Zone 2.3's extraction); packages/rest/src/analytics-filter-refusal-envelope.test.ts (the published HTTP-door code move for the type face's JSON cells, pinned on both analytics routes).",
        "Behaviour beyond the two members, pinned: one issue per slot. Where a face refuses a slot, the #19514 $icontains arm and the #8793 preset arms no longer add a second issue on that slot (a $between that the shape face refuses no longer also reports a preset endpoint). No accept/refuse verdict moves by this; it removes double reports.",
        "Worktree /home/user/objectstack-issue-20116-2 is KEPT (clean, everything pushed; spec and consumer closures built) for the round that answers the fold question; my temporary base-comparison worktree was removed."
      ],
      "mcp_calls": "0 — no MCP GitHub tool used",
      "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/20116/comments (this os-dev-report, via scripts/pm/post-stamped.mjs through the fleet-write relay). Plus 7 git pushes (not REST): the empty-branch probe (plain git push), then dfa424f6, cfc0d741, eaf7a925, 56285cb3, 3ec67d04 (merge), 54b99f3c through scripts/pm/write-pace.mjs. No pr_create and no label-write (no PR).",
      "open_questions": [
        {
          "question": "PR #20238 (#20161) landed as 6a6a17b6, so the report.zod.ts regions that fenced the report half are free. Fold the report half of M-widget into this PR, or open this PR as is (Part of #20116) and leave the report half on the card?",
          "options": [
            "A — fold: next round merges 6a6a17b6 (resolve the one ledger conflict), sets runtimeFilter: analyticsCarrierFilter() on JoinedReportBlockSchema (:284) and ReportSchema (:413), moves parity section 5's two EXPECTED_OPEN rows into CARRIERS, adds the dropped-refinement sites the build names (ui/Report, ui/JoinedReportBlock, the manifests' reports runtimeFilter), widens the changeset and the semantic entry (the one-open-position warning goes away), and asks whether the body becomes Fixes #20116. Cost: one build/regen/suite round and a CONTRACT_REVIEW_TIER review that now covers report.zod.ts.",
            "B — open as is: next round merges 6a6a17b6, opens the draft PR with Part of #20116; the report half stays on the card for a stage 3 dispatch (two lines plus pins). Cost: a further dispatch, review and queue lap for a two-line change, and the report carriers keep saving what the analytics door refuses meanwhile."
          ],
          "recommendation": "A. Real business need: the report runtimeFilter is charted through the same analytics where door, the gap is measured (report nested ACCEPT vs door 400), and the census found 0 producers, so no stored document pays. Long-term soundness: one carrier declaration for every charted filter, the fence's only reason is gone, and the pin already names the exact flip. Guarding AI authoring: an AI writing a report filter gets the same loud save-time refusal as on a widget, instead of a chart that fails later. Startup focus: two declaration lines and moved pin rows, no new gate, no new capability; it closes the collector rather than spawning another stage."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the analytics request doors keep two codes for one family: DatasetSelectionSchema.runtimeFilter and AnalyticsQueryRequestSchema.where carry the shared reach, so a refused slot at the top level answers 400 VALIDATION_FAILED from the schema door while the same slot inside a nested relation answers 400 INVALID_FILTER from the analytics normalizer. Both 400 and located; not a save door; no class a/b/c (no wrong answer, no silent drop). Recorded in the drafted PR body's Acceptance notes."
      ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat answer to the stage-2 fold question: A (fold the report half), and the PR body becomes Fixes #20116 · 2026-09-27T20:11Z

    domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), answering report 5859419341 on claim 5858335082.

    Why this is the seat's call. It is ordering between technical tasks, which is a no-escalation class. The order fenced the report half only because PR #20238 (Fixes #20161) held the report.zod.ts regions. That PR has landed as 6a6a17b6, so the fence's only reason is gone. The fold adds no capability: two carrier declarations (runtimeFilter: analyticsCarrierFilter() on JoinedReportBlockSchema and on ReportSchema), the dropped-refinement sites the build names, and moving the pin's two EXPECTED_OPEN rows (which already name the flip) into CARRIERS. The census found 0 producers. The analytics where door already refuses these shapes on chart, so today the report carriers save a document that later fails.

    Fixes #20116. The collector's open members, per the release 5857575995, are M-widget and M-type. With the fold both are closed. The objectui producer is carried by objectui#10790, not by this card.

    Scope, recorded here as the claim's amended surface (the claim comment is not edited):

    • Now in: packages/spec/src/ui/report.zod.ts (the two runtimeFilter carriers only), plus its dropped-refinement baseline sites.
    • Accepted as the order's own mechanism:
    • The "one issue per slot" change removes double reports only; no verdict moves. It is accepted, pinned, and must be named in the changeset.

    Next round.

    1. Merge origin/main with scripts/pm/os-regen-merge.sh. Resolve the one dropped-refinements.baseline.json conflict by regenerating, not by hand.
    2. Fold the report half, then widen the changeset and the semantic entry.
    3. Re-run the build, the regenerated artifacts, the parity pin (report rows in CARRIERS), and the spec, analytics, rest and lint consumer suites under the lock. Re-derive the gates on the actual paths.
    4. Open the draft PR with Fixes #20116 and Clause-②: no (narrowing). The PR is reviewed at CONTRACT_REVIEW_TIER before enqueue, and that review now includes report.zod.ts.
  6. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20116,
      "status": "done",
      "branch": "claude/issue-20116-stage-2-type-and-widget",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20325",
      "session": "session_01Rjy9MeetSfq34PKn81CRiN (mode:subagent, the PM's session; identity = the branch)",
      "premise_still_valid": true,
      "round": "fold round, answering the seat's 5859432781 (A: fold the report half; Fixes #20116). Previous report 5859419341.",
      "summary": "Both collector members are done in draft PR #20325 (head e9f93902, which merges origin/main 6ac33a57), with Fixes #20116 and Clause-②: no (narrowing), BREAKING. M-type: reportQueryFaceRefusals, the one judge both walks call, asks the comparand-TYPE face (normalizeFilterComparandTypes) read-only after the shape face. So a plain object where one value belongs, a Map, a class instance, a function, a Symbol, undefined and a bigint beyond 2^53 are refused on save, as the comparand, an implicit comparand or a list member, in the face's sentence less its location. M-widget: DashboardWidgetSchema.filter, ReportSchema.runtimeFilter and JoinedReportBlockSchema.runtimeFilter declare analyticsCarrierFilter(), the #20207 nested-relation walk. That walk moved verbatim into the non-barrel ui/analytics-carrier-filter.ts. So all five analytics carriers refuse inside a nested relation what the analytics where door refuses on chart. In report.zod.ts only the two runtimeFilter lines and the import changed. The fold is byte-neutral for every carrier's published JSON Schema: z.toJSONSchema of Dataset, DatasetMeasure, Dashboard, DashboardWidget, Report and JoinedReportBlock hashes 35ba34f964bb8fd6 before the fold (6a0cfb05) and after it. The parity pin's two EXPECTED_OPEN rows now sit in CARRIERS, with a pin that the carrier list is exactly those five. The changeset and the semantic entry were widened to five carriers, the one-open-position warning is gone, and the one-issue-per-slot dedupe is named in the changeset with an example and the statement that no verdict moves. The producer census stays 0: its nested-relation scan covered the runtimeFilter key, and its runtime walk covered runtimeFilter values.",
      "tests": "Final head e9f93902, through os-verify-lock (VERDICT command-exit 0): spec build 0; check:generated 0 ('All 15 generated artifacts are up to date'); spec typecheck 0; spec full suite 585 files / 16988 passed / 1 todo. At 3d9621a8 (the fold plus the ledger, before the second main merge; that merge touched no file of this diff's behaviour): spec 585 / 16955 passed; consumer closures built (exit 0); service-analytics 129 files / 3041 passed; lint 111 / 4297 passed; rest --project local 202 / 3664 passed / 1 skipped. rest typecheck 0 at 54b99f3c (the rest file is unchanged since). ABLATION of the report-carrier arm, at 3d9621a8, through scripts/ablation-replace.mjs (WRAP), with a script trap restoring on EXIT/INT/TERM. Leg 1: ReportSchema.runtimeFilter carrier stripped (analyticsCarrierFilter().unwrap().optional()); anchor x1 to x0 on disk, blob 007fe3e5 to cb0bef28; parity pin 18 failed / 153. Leg 2: JoinedReportBlockSchema.runtimeFilter stripped; blob 007fe3e5 to e143ffaf; 18 failed / 153. Each restore: blob 007fe3e5 == HEAD blob and git diff HEAD empty; 0 unwrap markers on disk; restored run 153 passed. The earlier legs at 54b99f3c still stand: type face off 48 red, widget carrier off 18, shared classification off 6, nested classification off 3, one-issue continue off 1 (MORE-diagnostics direction). PROBE after the fold (src): every nested M-type and M-widget cell is refused on all five carriers (for example runtimeFilter.acct.stage.$in.1 and blocks.0.runtimeFilter.acct.stage.$in.1); controls are accepted. ESLint, narrowed: --no-inline-config --format json over the 10 changed .ts files (count from the JSON) gave 0 errors and 0 warnings; the population is eslint.config.mjs, which never enables type-aware linting. NOT MEASURED: objectql, metadata-protocol, runtime and example suites (CI); repo-wide pnpm lint (CI). CI at e9f93902 when this was written: 12 success, 3 skipped, 17 in_progress, 0 failing (in_progress is the honest reading).",
      "gates": "At e9f93902: dispatch-gates --commands derived 89 on the actual paths (the lead's 88 plus pnpm check:skill-identifier-liveness). 87 exited 0. pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt exited 3 (PREREQUISITE NOT MET: they need a whole-repo build) = NOT MEASURED. --ran with 'CMD :: exit N' records: 89 derived accounted, 87 run, 2 NOT-MEASURED. check-adr-0087-registration: [BREAKING+bang+clause-2-narrowing] registered filter-comparand-types-and-widget-nested-slots-refused-at-save. The derivation printed a STALE TREE notice (main had moved 5 commits past e9f93902, and two gate scripts changed there). A driverless merge-tree probe (bare shared clone, no os-regen driver) of e9f93902 against de091b50 is clean.",
      "line_budget": "n/a — no skills/** or governed surface touched",
      "files_changed": [
        ".changeset/20116-filter-save-door-type-face-and-widget.md (new)",
        "packages/spec/src/data/filter-save-door-refusals.ts",
        "packages/spec/src/data/filter.zod.ts",
        "packages/spec/src/ui/analytics-carrier-filter.ts (new, not in the ui barrel)",
        "packages/spec/src/ui/dataset.zod.ts",
        "packages/spec/src/ui/dashboard.zod.ts",
        "packages/spec/src/ui/report.zod.ts (the two runtimeFilter carriers and the import only)",
        "packages/spec/src/data/filter-save-door-face-parity.test.ts",
        "packages/spec/src/migrations/entries/semantic/18.filter-comparand-types-and-widget-nested-slots-refused-at-save.ts (new)",
        "packages/spec/src/migrations/registry.ts (generated)",
        "packages/spec/dropped-refinements.baseline.json (+17 build-named sites, 0 removed; measured 605 to 622)",
        "packages/rest/src/analytics-filter-refusal-envelope.test.ts"
      ],
      "deviations": [
        "The dropped-refinements.baseline.json conflict: os-regen-merge.sh classes this ledger as a NON-generated file and prescribes a by-hand semantic merge. The ledger has no gen script by design. The coordinator asked for regeneration, not a hand edit. What I did: took main's side (git checkout --theirs) to finish the merge commit 6a0cfb05, then applied build-schemas' own printed 'corrected entries' with a script, which also recomputed the measured totals. No site was hand-picked: the result is exactly the build's +17, and the committed-ledger totals test passes in the spec suite.",
        "Second merge before opening the PR: origin/main had moved to 6ac33a57 (f415bcf1 added a D3 census pin to migrations.test.ts). I merged it through os-regen-merge.sh (clean), regenerated the registry (no diff), and re-ran spec build, check:generated, typecheck and the full suite at e9f93902. The consumer suites were not re-run after this merge (their reading is at 3d9621a8), because it touched nothing on this diff's behaviour path.",
        "The PR body's Clause line reads 'Clause-②: no (narrowing)', as the coordinator asked. The claim's own line is 'Clause-②: no'. The arm agrees with the changeset.",
        "The stored PR body differs from what was sent at exactly one character. Line 94, in the Tests section's 54b99f3c ablation bullet ('nested walk's classification off → 3'), stores three U+FFFD replacement characters where the → arrow was (UTF-8 byte offset 14311). Everything else is byte-identical. os-dev forbids a dev PATCH, so the seat should change that one character back to '→'. I have one observation here, not a reproduction of a relay defect.",
        "Worktree cleanup follows this report's read-back: node_modules removed and worktree /home/user/objectstack-issue-20116-2 removed (its tree is clean and everything is pushed). A patch round re-creates it from the remote branch."
      ],
      "mcp_calls": "0 — no MCP GitHub tool used",
      "api_writes": "3 relay writes this round, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft, #20325; run 36353675919); label-write --assign os-zhuang = POST /repos/objectstack-ai/objectstack/issues/20325/assignees (run 36353734930; read back: assignee os-zhuang); os-dev-report = POST /repos/objectstack-ai/objectstack/issues/20116/comments (this comment, via post-stamped). Plus 4 git pushes through scripts/pm/write-pace.mjs (not REST): 6a0cfb05 (merge), ebb1cd47, 3d9621a8, e9f93902 (merge). Previous round: 1 relay write (report 5859419341) and 7 pushes.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the analytics request doors give one family of refusals two codes. DatasetSelectionSchema.runtimeFilter and AnalyticsQueryRequestSchema.where have only the shared reach. So a refused slot at the top level answers 400 VALIDATION_FAILED from the schema door, while the same slot inside a nested relation answers 400 INVALID_FILTER from the analytics normalizer. Both are 400 and both name the location. No class a/b/c. Recorded in the PR body's Acceptance notes.",
        "carrier: the seat (one-character PR body fix) · noted, not filed — one multi-byte character (→) in the PR body arrived as three U+FFFD characters through the pr_create relay path, at UTF-8 byte offset 14311. One observation only: no repro, so no class (a) card."
      ]
    }

    Generated by Claude Code

  7. objectstack-fleet commented on Sep 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Review: ACCEPT (stage 2, folded) · PR #20325 at head e9f93902 · 2026-09-27T22:03Z

    domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), reviewer of record, on claim 5858335082 and fold ruling 5859432781 (A). Checked against GitHub and the diff, ⛔ not against the report alone.

    Checklist

    • Shape. Draft, base main, first line Fixes #20116, Clause-②: no (narrowing), BREAKING. Not governed. 12 files, +1024/−252; the bulk is the carrier extraction and the parity pin.
    • M-type. reportQueryFaceRefusals, the one judge both save walks call, now asks the comparand-TYPE face (normalizeFilterComparandTypes) read only, after the shape face. The refusal is the face's own sentence.
    • M-widget, all carriers. DashboardWidgetSchema.filter, ReportSchema.runtimeFilter and JoinedReportBlockSchema.runtimeFilter now declare analyticsCarrierFilter(), the fix(spec)!: a dataset or measure filter with a list inside a nested relation is refused on save (#20080) #20207 nested-relation walk moved verbatim into the non-barrel ui/analytics-carrier-filter.ts.
      • Read in the diff: report.zod.ts changes only its two runtimeFilter lines and the import.
      • Byte-neutral: the z.toJSONSchema hash of all six schemas is the same before and after the fold (report).
    • The one-issue-per-slot dedupe is named in the changeset with an example, and no verdict moves.
    • Measured (report fields).
      • spec at the head: build, check:generated (15/15), typecheck, and the full suite 16988.
      • Consumers at 3d9621a8: service-analytics 3041, lint 4297, rest 3664.
      • Ablation of each report carrier: 18 of 153 red, and restored to the HEAD blob. The earlier legs stand.
      • Gates: 89 derived, 87 run, 2 NOT MEASURED (the whole-repo-build pair).
    • Accepted deviations.
      • dropped-refinements.baseline.json has no generator. The dev took main's side to finish the merge, then applied exactly the build's printed corrected entries (+17 sites) by script, with no site hand-picked. The committed-ledger totals test passes.
      • A second merge of main is re-verified on the spec side. The consumer suites were read at 3d9621a8, and that merge touches nothing on the behaviour path.
    • PR body. The dev reported one → stored as U+FFFD. The seat re-read the stored body and found 0 U+FFFD characters; line 94 reads → correctly. No edit is needed.
    • CI at this head. In progress at review time.

    Contract review. Owed: a published accept set narrows at the save doors of five carriers. An isolated at-tier reviewer is dispatched, covering report.zod.ts as the fold ruling requires. needs:contract-review is added to PR #20325 in this act. ⛔ The PR is not readied before a same-shape PASS.

    Findings. The analytics request doors answer one refusal family with two codes, VALIDATION_FAILED at the top level and INVALID_FILTER nested. Both are 400 and both name the location → Acceptance notes, noted, not filed.

  8. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 20116,
      "status": "done",
      "branch": "claude/issue-20116-stage-2-type-and-widget",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/20325",
      "session": "session_01Rjy9MeetSfq34PKn81CRiN (mode:subagent, the PM's session; identity = the branch)",
      "premise_still_valid": true,
      "round": "Post-review round after the at-tier PASS 5860495664: the main merge, the text correction of the dedupe claim, and the docs-drift re-read (github-actions 5860213966). Previous report: 5860227795.",
      "summary": "The round is done at head 830a071a, which is CI-green (35 check-runs: 32 success, 3 skipped, 0 failing). (1) I merged origin/main dfd8e398 through os-regen-merge.sh (merge 64dd083b, no conflict). gen:migration-registry left registry.ts unchanged, and the dropped-refinements ledger did not move (the spec build passes with it). (2) Text only, no behaviour change: the changeset and the semantic entry now state the dedupe at the reach where it holds. At the top level and in $and/$or/$not members, a face-refused slot carries one issue. Inside a nested relation on an analytics carrier, the shared walk's own $icontains and date-preset arms still judge the slot beside the faces. So there a nested $icontains with a type-refused comparand gets two issues at one path, and a nested one-bound $between of a preset name gets the range at $between plus the preset at $between.0. I measured both with tsx on Dataset.filter. The changeset also says no verdict moves. The registry was regenerated from the entry. (3) I re-read the four pages the docs-drift check named against this PR's behaviour: all four are unchanged, because no sentence is made false (verdicts in docs_verdicts). No content/docs/releases page names these shapes, and none was touched. (4) I did NOT update the PR body through the relay; see the conflict in deviations. The revised body is ready for the seat.",
      "docs_verdicts": {
        "content/docs/api/data-api.mdx": "unchanged — its INVALID_FILTER sentences are about the data routes' ?filter (the engine normalizer, not a FilterConditionSchema parse). The /analytics/query section only says where is the FilterCondition find() accepts, which is still true. No sentence names the code a type-face cell gets.",
        "content/docs/api/error-catalog.mdx": "unchanged — INVALID_FILTER and VALIDATION_FAILED are defined generically. No sentence claims the analytics routes answer INVALID_FILTER for a plain-object comparand.",
        "content/docs/data-modeling/analytics.mdx": "unchanged — the new doors accept the widget filter example ($nin list) and the report runtimeFilter example ({current_quarter_start} placeholder). The placeholder paragraph and the dataset-bound section stay true, and the page never says a nested-relation filter saves on those carriers.",
        "content/docs/protocol/objectql/query-syntax.mdx": "unchanged — the FilterConditionSchema, field-reference and relation-traversal text stays true ({ $field: 'col' } is still accepted). The page makes no claim that a plain-object, Map or undefined comparand passes validation."
      },
      "tests": "At 830a071a (the merge plus the text commit), through os-verify-lock (VERDICT command-exit 0): spec build 0; check:generated 0 ('All 15 generated artifacts are up to date'); spec typecheck 0; spec full suite 587 files / 17031 passed / 1 todo; parity pin alone 1 file / 153 passed. check-adr-0087-registration --base origin/main exit 0 ('1 declared-breaking changeset(s) ... [BREAKING+bang+clause-②-narrowing] registered filter-comparand-types-and-widget-nested-slots-refused-at-save'). check-changeset-no-major --base origin/main exit 0 ('no major bump'). ESLint, narrowed, at 830a071a: --no-inline-config --format json over the 10 changed .ts files (count from the JSON) gave 0 errors and 0 warnings; the population is eslint.config.mjs, which never enables type-aware linting. Dedupe probe (tsx, spec src): Dataset.filter with { acct: { name: { $icontains: new Map() } } } gives 2 issues at filter.acct.name.$icontains. { acct: { created_at: { $between: ['last_7_days'] } } } gives issues at filter.acct.created_at.$between and at .$between.0. Top level: 1 issue each. That is the restated sentence. NOT re-run this round: the consumer suites (their reading stands at 3d9621a8; this round changed only the merge and text) and the ablation (no source changed since the review reproduced it). CI at 830a071a: 32 success, 3 skipped, 0 failing; the client flake #20327 did not recur.",
      "gates": "At 830a071a: dispatch-gates --commands derived 90 on the actual paths: the review head's 89 plus 'node scripts/check-issue-citations.mjs', which main's 7338efe0 now runs locally. 88 exited 0. pnpm --filter @objectstack/lint run check:doc-formula-expressions and pnpm check:lean-entry-closure first exited 3, because the formula and objectql builds were absent in the re-created worktree. After that closure was built under the lock they exited 0 ('Admitted set held exactly (15 packages)'). pnpm check:dual-build-cjs-loads and pnpm check:type-check-debt exited 3 (PREREQUISITE NOT MET: they need a whole-repo build) = NOT MEASURED. --ran with 'CMD :: exit N' records: 90 derived accounted, 88 run, 2 NOT-MEASURED.",
      "line_budget": "n/a — no skills/** or governed surface touched",
      "files_changed_this_round": [
        ".changeset/20116-filter-save-door-type-face-and-widget.md (the dedupe paragraph, split into its true reach plus the nested exception)",
        "packages/spec/src/migrations/entries/semantic/18.filter-comparand-types-and-widget-nested-slots-refused-at-save.ts (the same sentence in reason)",
        "packages/spec/src/migrations/registry.ts (regenerated)",
        "merge of origin/main dfd8e398 (64dd083b)"
      ],
      "pr_body": "NOT updated. The revised body is at /tmp/claude-0/-home-user/d7055c86-c1e6-5545-851c-a50c5d9fd367/scratchpad/issue-20116b/pr-body-v2.md (19512 bytes, no angle-bracket fragments, no U+FFFD), for the seat to apply. It changes five things. (a) The 'One judge' paragraph and the behaviour-table row state the dedupe at its true reach, with the nested exception. (b) The Tests section leads with the 830a071a readings and the 90/88/2 gate reconciliation, and records ESLint at 830a071a. (c) A new section records the four docs verdicts (all unchanged) and that no release page was touched. (d) Line 94's three U+FFFD characters are restored to '→'. (e) The stale note about main moving after e9f93902 is replaced. Everything else is byte-identical.",
      "deviations": [
        "Conflict named, not silently resolved. The coordinator asked me to update the PR body's Tests section through the relay. .claude/agents/os-dev.md says a dev writes the PR body once, at pr_create, and never PATCHes it; later changes go in the report for the seat to write. It also says the standing file wins over dispatch wording and that a conflict must be named. The relay does have issue_patch, which would work on a PR. I followed os-dev.md: no PATCH. The seat can apply pr-body-v2.md with one issue_patch (issue 20325, body).",
        "Scope held: text only. The nested double-report was not changed in behaviour, as instructed. Making the words true by silencing the shared walk's arms on a slot the carrier walk judged would be a behaviour change and is not in this PR.",
        "Worktree /home/user/objectstack-issue-20116-2 was re-created from the branch at e9f93902 and is removed again after this report's read-back (its tree is clean and pushed at 830a071a)."
      ],
      "mcp_calls": "0 — no MCP GitHub tool used",
      "api_writes": "1 relay write this round: os-dev-report = POST /repos/objectstack-ai/objectstack/issues/20116/comments (this comment, via post-stamped through the fleet-write relay). Plus 2 git pushes through scripts/pm/write-pace.mjs (not REST): 64dd083b (merge) and 830a071a.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed — the nested double-report the review measured (303 cells): inside a relation on an analytics carrier, the shared walk's $icontains and preset arms judge a slot the carrier walk already asked the faces about. No verdict moves and it is not a class a/b/c defect. The words now state it; making it true in behaviour (the shared walk skipping depth > 0 slots of an analytics carrier) would be its own change, with no carrier named.",
        "carrier: none · noted, not filed — the analytics request doors give one family of refusals two codes (VALIDATION_FAILED at the schema door at the top level, INVALID_FILTER from the normalizer inside a relation). Already recorded in the PR's Acceptance notes."
      ]
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Sep 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #20325 → main dd1b80314322871a8142c8282f15d0cd858b79f2 (Fixes #20116) · 2026-09-28T00:54Z

    domain:spec seat 1 (session_01Rjy9MeetSfq34PKn81CRiN), on the stage-2 claim, the fold ruling (seat answer 5859432781) and release 5857575995 (the collector's two open members, M-widget and M-type).

    How it got here

    • The at-tier review PASSED at e9f93902 (5860495664), over a 74,880-cell door battery per worktree, the analytics door, a three-corpus census with 0 flips, byte-neutral JSON schemas, the ledger and an ablation.
    • The final round corrected the one flagged sentence (the one-issue-per-slot dedupe, now stated at the reach it holds) and merged main.
    • Delta at-tier review PASS 5861271836 at 830a071a, which is the head that landed. The code files are byte-identical to e9f93902; the change is text only in the changeset, the semantic entry and the regenerated registry.ts.
    • CI at that head: 37 success and 5 expected skips (check-expected-skips OK). Not governed; 1286 changed lines.
    • Pre-landing: merge-tree clean against d3958bac, with the registry resolved and sorted.
    • Queue: enqueued at 00:32Z, merged at 00:53Z.

    Verified on main, two readings

    Carried elsewhere

    • objectui#10790: the objectui producer stage 1 found.
    • Not filed (Acceptance notes):
      • the request doors' two codes for one refusal family (not a save door);
      • GlobalFilterOptionsFromSchema.filter, which keeps the shared reach;
      • the nested two-issue count has no pin (the words are now true).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:specpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions