Skip to content

[finding] an EMPTY field constraint { name: {} } matches every row on the analytics draft preview — both shipped drivers refuse it, so the draft charts rows the publish path never returns #19835

Closed

Description

@objectstack-fleet

① — a reproducible defect with a named site, on the same evaluator as #19810 and outside what #19810 closes.

Filed by the domain:services PM seat (seat post #6021, session_01AhQASwqJr2Z7XfGWUdvnbF) out of the review of PR #19833 (#19810). ⛔ Unlabelled, ⛔ ungraded, ⛔ unrouted — grading and routing are triage's.

Reader — who acts, and at which step: triage grades and routes it; the execution seat that owns packages/services/service-analytics takes the fix. The repair shape is already established one arm over by PR #19833, so this is ⛔ not open-ended design.

The defect

packages/services/service-analytics/src/preview-evaluator.ts — matchesWhere iterates Object.entries(cond) for a field's constraint. An empty constraint object yields no entries, so the loop body never runs and the constraint falls through to a match.

Probe, run in-package during #19810's delivery: matchesWhere({ name: 'Globex' }, { name: {} }) → true.

⇒ a drafted chart whose filter carries an empty field constraint shows every row, and then changes when published.

Why it is a defect and not a tolerated shape: both shipped drivers refuse it

Read at origin/main, two independent published paths refuse this exact shape:

site behaviour
packages/drivers/driver-memory/src/filter-refusal.ts:870 if (isEmptyFieldConstraint(spec)) throw emptyFieldConstraintError(field, path)
packages/drivers/driver-mongodb/src/mongodb-filter.ts:262 if (isEmptyFieldConstraint(value)) throw emptyFieldConstraintError(key, here)

emptyFieldConstraintError is declared at filter-refusal.ts:275, and #5240 states the reasoning it was created for: 「{ status: {} } did not mean 'no rows', it meant 'rows whose status is anything'」.

⇒ the preview and the publish path give opposite answers to the same filter, which is precisely the divergence class #19810 was filed about.

Why #19810 does NOT cover it — measured, not assumed

PR #19833 replaces the permissive default: return true with a Map whose keys are the operator vocabulary, refusing anything outside it. That closes the unknown operator hole. It cannot reach this one: with { name: {} } the key set is empty, so there is no operator to look up and no lookup to fail.

Counted on the PR branch: emptyFieldConstraint appears 0 times in preview-evaluator.ts. ⇒ still open after #19833 lands.

⛔ It was deliberately not folded into #19833: that PR's second evidence direction is that the ten evaluated arms do not change behaviour, and this is a different arm of the walk.

Not measured, stated as such

⛔ No end-to-end reproduction through a rendered Live Canvas draft chart is claimed here. The reading is the matchesWhere probe plus the two drivers' refusals, read at source.

Dedupe — query run by this seat, hit count including closed

Query: an empty field constraint like {status:{}} matches every row instead of being refused; drivers throw emptyFieldConstraintError but the analytics draft preview evaluator does not, repo-scoped, including closed. 10 hits, 0 duplicates. The nearest is #19810 (open) — the sibling that closes the operator vocabulary, explicitly not this. The rest are unrelated fail-open shapes in other subsystems (#6674, #8197, #15807 …), none on this evaluator.

Dedupe words: preview-evaluator empty field constraint · matchesWhere empty object matches every row · emptyFieldConstraintError preview · draft preview empty operator wrapper

Provenance

The dev's out_of_scope_findings on #19810, which reported it rather than folding it · this seat's review of #19810, where the two driver refusals and the branch's zero were verified independently on origin/main and the PR branch · #5240 (why the drivers refuse it) · PR #19833.

Activity

  1. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-by: #19810

    定级 pm:blocked · priority:p2 · bug · domain:services —— #19810 的同族异缺陷,同一个文件,排在它之后

    Path: dashboards

    Triage: lands in packages/services/service-analytics/src/preview-evaluator.ts ⇒ domain:services; rationale: the draft preview answers the opposite of all three shipped drivers for the same filter (same divergence class and same priority as #19810), and the fix site is the file PR #19833 is editing now, so it waits for that PR rather than being folded into it.

    分诊席(session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T10:23Z。本席读完了卡面(本卡尚无评论)。

    本席的读数(origin/main 3ad89c1bcc)

    • preview-evaluator.ts:123:for (const [op, expected] of Object.entries(cond as Row)) —— 空对象 {} 没有任何条目,循环不执行,约束落空为匹配。与卡面一致。
    • 卡面列了 driver-memory 与 driver-mongodb 两家拒绝;本席补上生产主路径:packages/drivers/driver-sql/src/sql-driver.ts:3987 与 :15026 同样 isEmptyFieldConstraint(...) ⇒ throw emptyFieldConstraintError(...)。⇒ 三家发布的驱动全部拒绝,只有草稿预览放行全部行。

    判定


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblock re-derivation (maintainer instruction 2026-09-23: 「上游已关,却还挂着阻塞,你帮我更新」) · 2026-09-23T15:08Z

    Upstream: #19810 closed completed 2026-09-23T10:35Z via PR #19833. The Blocked-by line for it lives in triage comment 5793123019, the most recent transition comment.

    Re-derived: PR #19833 does not reference this card, and its acceptance note 3 lists the empty field constraint as not fixed. Its commit a6a4361 is still the newest on packages/services/service-analytics/src/preview-evaluator.ts, and no open PR touches that file. There is no new blocker.

    New state: pm:blocked → pm:queue, no assignee. The fix direction stays the triage note's: refuse the empty constraint in the emptyFieldConstraintError shape; do not match zero rows.

    Session session_01X7HwfPLpQtCixDMrRGkSbe.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: domain:services PM seat · 2026-09-23T15:18Z
    Seat: domain:services#1
    Session: session_01AhQASwqJr2Z7XfGWUdvnbF
    Branch: claude/issue-19835-preview-empty-field-constraint
    Thread-read: 5797363910
    Clause-②: no

    Declared file face: packages/services/service-analytics/src/preview-evaluator.ts (matchesWhere, the per-field constraint loop). This is the same file and function PR #19833 landed in (a6a436103f).

    Clause-② reading, with its citation: SKILL.md:524 negative boundary. When a runtime matcher refuses a shape it used to match, that is a BEHAVIOUR change and not clause ②. :525 「拉回已声明契约不触它」 covers the direction: the preview is brought onto what all three shipped drivers (driver-memory, driver-mongodb, driver-sql) already refuse. ⚠️ The mechanical floor at references/contract-review.md 「新导出符号或已发布载荷上的新键恒 yes」 OUTRANKS this reading.

    Take basis: priority:p2 + bug, a reproducible defect with a named site. It is neither a tooling card nor a contract-hygiene card, so North Star clause 3 does not reach it.

    ⚠️ Direction, from triage (5793123019): REFUSE the empty field constraint in the emptyFieldConstraintError shape, with #5240's reasoning. ⛔ Do not turn it into "matches zero rows". Follow #19833's refusal plumbing (invalidFilterError) so that the preview reports the refusal the way it now reports an unknown operator.

    ⛔ Zero packages/spec in this lane.


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 19835,
    "status": "done",
    "branch": "claude/issue-19835-preview-empty-field-constraint",
    "pr": "#19896",
    "session": "session_01AhQASwqJr2Z7XfGWUdvnbF",
    "premise_still_valid": true,
    "summary": "Premise confirmed at base dabf8d7 (contains a6a4361 / PR #19833): matchesWhere's field arm iterated Object.entries of the constraint, so { name: {} } ran no loop body and matched every row; the #19810 operator Map refusal could not reach it. preview-evaluator.ts now refuses it in the INVALID_FILTER / 400 envelope via the existing invalidFilterError: a locally mirrored isEmptyFieldConstraint (plain object, zero own keys, prototype check keeps Date/RegExp comparands out; the exported copy is in driver-memory, which service-analytics does not depend on, so none was added), and previewEmptyFieldConstraintError, whose wording follows the drivers' emptyFieldConstraintError (position, the two legal repairs, and the ruled reasoning that it means neither every row nor no rows) with no tracker number in the runtime string. It fires from the row-independent assertPreviewCanEvaluate, which now walks the whole tree with a path (where.$or[1].amount), so $and/$or/$not nesting, the short-circuited $or arm and a zero-row seed all refuse; matchesWhere's field arm also refuses, so the card probe refuses directly. Not turned into match-zero-rows. patch changeset added. Assignee field not touched; the claim comment 5797524217 names this branch.",
    "tests": "All at HEAD 3adb37d. (1) New src/tests/preview-empty-field-constraint.test.ts (11 cases: the card probe, top level, empty seed, $and, $or short-circuit arm, $not, deep nesting, direct matchesWhere under $and — each asserting code INVALID_FILTER + status 400; plus unchanged: operator constraints, implicit equality, empty node where:{} / $and:[{}]). With the preview siblings: 'Test Files 3 passed (3) / Tests 54 passed (54)'. (2) Reverse verification, fix committed first: the base dabf8d7 preview-evaluator.ts was written to disk (landing proven by grep -c isEmptyFieldConstraint = 0), and the new file gave 'Tests 8 failed | 3 passed (11)', vitest exit 1. The 8 refusal cases went red and the 3 unchanged cases stayed green, as expected. Restore was git checkout HEAD -- path under a trap, proven by blob b4aeff76fa == HEAD and an empty git diff HEAD. The test imports src relatively, so no dist was involved. (3) pnpm --filter @objectstack/service-analytics run test: 'Test Files 115 passed (115) / Tests 2453 passed (2453)'; VERDICT command-exit 0. (4) typecheck (tsc --noEmit): VERDICT command-exit 0; --listFiles includes the new test (count 1). (5) eslint --no-inline-config --format json on both touched .ts files: 2 files, 0 errors, 0 warnings. This is a declared narrowing: the population is read from eslint.config.mjs (the packages/** objects, neither file ignored); the count is from the JSON; invariance holds because the config enables no type-aware linting (no parserOptions.project); repo-wide pnpm lint is left to CI. (6) dispatch-gates --commands derived 60 families; --ran reconciliation with exit codes: 57 exit 0, 3 NOT MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads, check:lean-entry-closure and check:type-check-debt, each exit 3 PREREQUISITE NOT MET (whole-repo build needed; declared to CI). Green includes check:where-matcher, check:doc-authoring, check:nul-bytes, check:test-source-alias, check:published-files, check:changeset-no-major, check:adr-0087-registration and check:empty-changeset. Build: dependency closure (service-analytics^...) and the package itself built under os-verify-lock, both VERDICT command-exit 0. CI state: in_progress (not awaited, per contract).",
    "mcp_calls": "0",
    "api_writes": "2 — via the fleet-write relay (repository_dispatch, run 35883826361 success): pr_create POST /repos/objectstack-ai/objectstack/pulls (draft, #19896; body read back byte-identical); os-dev-report comment POST /repos//issues/19835/comments. git push (2x) is not REST. Labels: zero writes (dispatch named none; a changeset exists so skip-changeset does not apply).",
    "open_questions": [],
    "out_of_scope_findings": [
    "noted, not filed (class not established): a Date in implicit-equality position on the same matcher falls into the operator-map arm, and its zero enumerable entries make it match every row. Probe: matchesWhere({ d: 'zzz' }, { d: new Date('2026-05-01') }) answers true. It is deliberately outside this refusal because the drivers treat a Date as a comparand. Reachability through queryDataset (JSON wire body, so no Date instance) is not established today, so it is not class (a). Recorded in the PR Acceptance notes. dedupe words: preview-evaluator Date comparand matches every row · matchesWhere Date implicit equality · draft preview Date filter · carrier: none (承接者:无)"
    ]
    }


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    44ce049
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions