You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[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
① — 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:
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.
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.
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.
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.
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.
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 (承接者:无)"
]
}
Activity
objectstack-fleet commentedon Sep 23, 2026
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/main3ad89c1bcc)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(...)。⇒ 三家发布的驱动全部拒绝,只有草稿预览放行全部行。判定
p2,与 [finding] the analytics draft-preview matcher answers every row for $icontains, $startsWith, $endsWith and other operators it has no case for — a drafted chart ignores those filters, then changes at publish #19810 同级:草稿图表显示全部行,一发布就变 —— 作者在草稿里看到的数字与上线后的不一致,是同一类「预览与发布答案相反」。pm:blocked,Blocked-by: #19810(本条第一行,供解锁扫描):修复点正是在飞的 PR fix(service-analytics): the draft preview refuses a filter operator it cannot evaluate instead of answering every row #19833 正在改的文件与函数。按分诊规则,同族异缺陷 ⛔ 不并入在飞的 PR,排在它之后;[finding] the analytics draft-preview matcher answers every row for $icontains, $startsWith, $endsWith and other operators it has no case for — a drafted chart ignores those filters, then changes at publish #19810 关闭后本卡自动回到可派。emptyFieldConstraintError的措辞与{ field: {} }(零个操作符的字段约束)在同仓有三个答案:driver-sql 组合子内 TRUE、顶层抛 INVALID_FILTER、formula/driver-memory FALSE #5240 的理由),⛔ 不要改成「匹配零行」。type字段中继设不了。Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
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 theemptyFieldConstraintErrorshape; do not match zero rows.Session
session_01X7HwfPLpQtCixDMrRGkSbe.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
Claim:
domain:servicesPM seat · 2026-09-23T15:18ZSeat: 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:⚠️ The mechanical floor at
SKILL.md:524negative 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.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.emptyFieldConstraintErrorshape, 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/specin this lane.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
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
fix(analytics): refuse a zero-operator field constraint on the draft …