Repository navigation
The draft-preview evaluator assumes every measure is numeric and every dimension is a string — a min/max over a date returns 0 there, and every dimension column is typed string #16203
Description
Activity
- addedbugSomething isn't workingSomething isn't workingand removed
on Sep 6, 2026 Claiming this card (
os-devseat, dispatched bydomain:servicesPM).- Branch:
claude/issue-16203-preview-evaluator-types(pushed, empty, at159dbad23) - Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
- Scope:
packages/services/service-analytics/src/preview-evaluator.tsonly. Staying out ofanalytics-service.ts(PR fix(service-analytics): a draft-preview dataset response describes its columns like the live one (#16097) #16192),text-match-sql.ts/like-pattern.ts($icontainsstill compilestranslate()on theunknowndialect arm, so a SQLite datasource whose dialect is unanswered still fails to parse — PR #16020's measured residue #16028), andmeasure-result-type.ts(Amin/maxover a text / select / lookup field is still described astype: "number"in the analytics response #16098). - Re-read the comments immediately before claiming: none existed.
Not touching the assignee field — that is the PM's.
Generated by Claude Code
- Branch:
os-dev-report
{ "issue": 16203, "status": "done", "branch": "claude/issue-16203-preview-evaluator-types", "pr": "https://github.com/objectstack-ai/objectstack/pull/16235", "premise_still_valid": true, "summary": "Both symptoms reproduced and fixed in preview-evaluator.ts only. (a) aggregate() no longer coerces min/max operands with Number(): they return the winning operand in its own type, ordered by this file's shared compare with a numeric arm, and a group whose operand is null throughout answers null (emptyGroupValueFor) instead of 0 — latest_spend goes 0 to '2026-05-12', matching the live path over the same rows. (b) a dimension column takes the cube dimension's own type, the same expression both live buildFieldMeta producers use, so a date dimension is 'time' on preview. Enumerating the closed AggregationFunction vocabulary also surfaced that the count_distinct arm was UNREACHABLE — the switch spelled it countDistinct, a word no producer mints — so it fell to the numeric default and answered a row count (3) where live says 2; fixed in place as the same function, same defect class, mechanically pinned by the spec spelling. sum/avg over a temporal operand is left measurably unchanged (#16099 owns refusing the pair) and a MEASURE column keeps the 'number' its producer mints (measureResultType at the descriptor pass owns that, #15768/#16101) — the descriptor is neither made to lie nor withheld. analytics-service.ts, measure-result-type.ts, text-match-sql.ts and like-pattern.ts are untouched.", "tests": "All at head 00e99d9d7, every exit code captured immediately after a single redirected command, never through a pipe. (1) NEW PINS: src/__tests__/preview-aggregate-operand-type.test.ts, 12 cases driven through the two-instance harness the card asks for — one dataset, one row set, two AnalyticsService instances differing only in draftRowsResolver, the live half being NativeSQLStrategy's SQL executed on a REAL SQLite (sql.js) seeded from the same rows. BEFORE the fix on 159dbad23: 'Tests 7 failed | 5 passed (12)' — latest_spend 0 vs live '2026-05-12', earliest_spend 0 vs '2026-04-21', first_payer/last_payer 0 vs 'ann'/'bob', min/max over an all-null group 0 vs null, max over a BSON Date 1785110400000 (epoch leak) vs the Date, distinct_payers 3 vs 2, derived ratio over a temporal max 0 vs null, and a date dimension typed 'string' vs 'time'. AFTER: 'Tests 12 passed (12)', exit 0. (2) PACKAGE SUITE: pnpm --filter @objectstack/service-analytics test — 'Test Files 95 passed (95) / Tests 2050 passed (2050)', exit 0. (3) TYPECHECK: exit 0, and tsc --noEmit --listFiles shows 1 hit each for preview-evaluator.ts and the new test file, so the green covers the edits rather than excluding them. (4) ABLATION: committed FIRST (4df5628e0), then preview-evaluator.ts restored to 159dbad23 with the pins kept. Direction predicted before running: RED on exactly the seven cases, five controls green — observed exactly that ('Tests 7 failed | 5 passed (12)', run exit 1). Mutation proven ON DISK by blob delta (HEAD 2edec66595ff8a22245ad1b46d191a5b57f16eca vs mutated cbe2c62df43a5b9639699fc37b1750836f710cbf, non-empty and different) plus anchored counts (extremumOf 0, Math.min 1); restore ran under trap on EXIT INT TERM against an ABSOLUTE path and is proven by blob equality with HEAD and an empty git diff HEAD. NO REBUILD LEG IS OWED and the reason is stated: the pins import ../preview-evaluator.js and ../analytics-service.js, relative same-package specifiers vitest compiles from src/, so nothing in this ablation resolves through a package dist/. (5) GATES: family derived mechanically by scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack and RE-DERIVED on the final head — identical, Reconciliation line 55 runnable families; ran those 55 plus the 4 artifact-roster gates whose roster sits under a path this change touches = 59 commands, 59 exit 0. Two answered 3 (PREREQUISITE NOT MET, read as UNMEASURED not as a pass) until the closure was built — turbo run build over ./packages/* and ./packages/*/*, 71/71 successful — after which check:dual-build-cjs-loads and check:type-check-debt both answered 0 (the latter re-measured 12 ledger entries, none above its recorded number). (6) ESLINT: the whole repo, not a narrowing — eslint . --no-inline-config --format json, 6195 files, 0 errors, 0 warnings, exit 0.", "mcp_calls": "10 — issue_read x2, pull_request_read x2 (the #16192 diff, and the read-back of my own PR body), add_issue_comment x2 (claim + this report), search_issues x1 (the mandatory dedup, one targeted call after repo-scoped REST answered 403), issue_write x2 (the two findings), create_pull_request x1", "open_questions": [], "out_of_scope_findings": [ "filed as #16218: the preview evaluator's `count` over a FIELD counts rows including nulls (2) while every SQL face lowers COUNT(payer) and answers 1 — measured on the same two-instance harness; the identical defect was measured and cleared on driver-mongodb by the AGGREGATION_CASES enrolment", "filed as #16219: the preview evaluator's `avg` over a group with no numeric values answers 0 while the live path answers null, contradicting emptyGroupValueFor's explicit ruling that averaging nothing must stay null — the min/max half of that same `: 0` idiom moved with this card because min/max are its subject, avg is not" ] }
Generated by Claude Code
Claim: PM loop round R1 — the clause-② declaration in the documented shape
Session:session_01XpTx2tbq3pZRYAdoGt6E6Y
Branch:claude/issue-16203-preview-evaluator-types
Domain:domain:services
File surface:packages/services/service-analytics/src/
Container & model: M,mode:subagent
Clause-②: no — conformance repair on one path;aggregate()is module-private with a single call site,evaluateAnalyticsQueryOverRowskeeps its signature, and no key is minted on the response
Serial constraints cleared: #16192 (analytics-service.ts), #16204 (text-match-sql.ts/like-pattern.ts), #16238 (measure-result-type.ts) — same package, disjoint files, fence verified by local merge-base diff (3 files, 0 hits on the other three)
Filed ahead of review rather than after it: @os-zhuang's holds on #16148 / #16192 / #16204 / #16216 all turned on this card's claim carrier being unreadable, and the same heading form was written here. Root cause and the parser-verified remedy are on #16097 (5557652999) — the defect is mine, in my own dispatch step.
⚠️ This one is closer to the line than the others, so I am stating the case against my own answer. The change does alter what the endpoint returns: amin/maxover adatecolumn goes from0to'2026-05-12', and adatedimension column'sfields[].typegoes from'string'to'time'. A reader could call that a payload change.My reading is still
no, on three measured grounds:- No key is minted and no signature moves.
aggregate()is module-private with exactly one call site (preview-evaluator.ts:339, writing into a row), so widening its return fromnumbertounknowncannot reach a caller outside the file; the only importer of this module takesevaluateAnalyticsQueryOverRows, unchanged. - The target is the answer the published contract already ships. Both values are brought to what the live path returns over the same rows —
d?.type || 'string'is literally the expression bothbuildFieldMetaproducers use. This is the preview path being brought onto the contract, the same conformance limb the reviewer accepted for fix(service-analytics): a draft-preview dataset response describes its columns like the live one (#16097) #16192 and fix(service-analytics): theunknowndialect arm folds$icontainswith a portable construct, nottranslate()#16204. - The pre-fix value was not a contract anyone was owed —
0for amaxover a date is a wrong answer, not a promise. Nobody can be relying on it except by relying on a defect.
If that is judged wrong, the correction is
yesplusneeds:contract-reviewon both carriers — ⛔ not a quiet re-file.PM verification of the delivering PR #16235 is at its comment 5557631259;
needs:contract-reviewis on it. ⭐ Note for the reviewer: enumerating the closedAggregationFunctionvocabulary is what surfaced a deadcountDistinctarm no producer could reach — sampling would not have found it.domain:servicesPM seat · claim carrier filed at the documented spelling before the review, not after
Generated by Claude Code
- No key is minted and no signature moves.
Filed by the
domain:servicesPM seat on behalf of theos-devseat that produced PR #16192 (card #16097). ⛔ That seat measured both symptoms, could not complete the mandatory dedup (repo-scoped REST answers 403 for this session, andsearch_issuesreturnedAPI rate limit already exceeded), and correctly handed them back rather than opening a card blind. This seat then hit the same limit twice and also declined to file. Filed now that the search answers. ⛔ Priority is triage's.One statement, two symptoms
Both live in
packages/services/service-analytics/src/preview-evaluator.ts, produced byevaluateAnalyticsQueryOverRowsbefore any enrichment runs, so ⛔ no descriptor pass can reach them — including the one PR #16192 adds.⭐ (a) — a WRONG VALUE, not a descriptor gap. This is the severe half.
aggregate()coerces operands withNumber()and filters out non-finite values. ⇒ amin/maxover a non-numeric field returns0on the draft-preview path.Measured on one dataset, one row set, two
AnalyticsServiceinstances differing only indraftRowsResolver:latest_spend'2026-05-12'0⇒ This is not a mislabelled column. It is a different, wrong answer for the same query — silently, with no refusal and no warning.
(b) — every dimension column typed
stringThe
fieldsthe evaluator mints type every dimension column'string', so a time dimension is'string'on preview and'time'on live.#16192 makes the preview path run the ADR-0021 column enrichment, so after it lands preview will describe
latest_spendcorrectly astype: 'time'— while the value sitting beside it is still0.⇒ That is exactly the shape #15768 was filed for: metadata that contradicts the value printed beside it, now reproduced on the preview path.
⛔ This is NOT an argument to withhold the descriptor. Withholding a correct descriptor to mask a producer defect is Prime Directive #12 in descriptor form, and #16192 rightly refuses it — the descriptor is right, the value is wrong. It is an argument for fixing (a).
What the taker owes
draftRowsResolver) isolates the evaluator as the sole difference and is the shape to reuse.min/maxreturn a value of the operand's own type;count/count_distinctare genuinely numeric;sum/avgover a temporal operand have no defined answer —avgover a datetime works on SQLite and errors on Postgres #16099, which is the card for refusing incoherent pairs, and ⛔ do not silently invent one here.Related
#16097 / PR #16192 (where both were found — the column-descriptor half, deliberately not absorbing these) · #15768 (the same metadata-vs-value contradiction on the live path) · #16098 (the live path's
min/maxover text/select/lookup still typednumber— the descriptor sibling of (b)) · #16099 (no layer refuses an incoherent aggregate/field-type pair).Dedup
Searched; ⛔ not a duplicate of #15768, #16098 or #16099 — those are all descriptor cards on the live path, whereas (a) is a wrong value on the preview path. ⭐ The zero-match is a reading rather than a silent zeroing: the same query returned #16097 and #16098, two adjacent cards known to exist, so the channel was answering at the time of filing.