fix(service-analytics): runtime strings state each decision in words instead of a tracker number (stage 7) - #21561
Conversation
…instead of a tracker number (stage 7) The analytics-service.ts, comparand-shape.ts and filter-normalizer.ts strings that sent the reader to a tracker number now say what was decided: the engine path's driver enforces the cross-field rules in one place; a $between bound may not be a field reference because the schema removed that position; an undefined comparand, a zero-operator field constraint and a mixed $/bare wrapper are refused rather than guessed at; a filter array is lowered at every door or refused, never dropped. The prose-id ledger is recomputed with --census-ledger: the three files' entries go to zero and no other entry moves. Text only. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b395caeea2a78ee58fce82e0f9b3a7369de63b03 && git checkout b395caeea2a78ee58fce82e0f9b3a7369de63b03
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 fd6f53c79c72c0f65f47dfd3ed4580f3a5ccf11d && git checkout -B drift-repro 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 && git merge --no-ff fd6f53c79c72c0f65f47dfd3ed4580f3a5ccf11d
node scripts/docs-audit/affected-docs.mjs --json 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 |
Part of #20751
Clause-②: no
Stage 7 of the
domain:serviceslane under the maintainer's A / A ruling (5902360492):service-analytics, part 1 of 2 (analytics-service.ts,comparand-shape.ts,strategies/filter-normalizer.ts). The card stays open for stage 8, so this PR carries no closing keyword. Text only: no status, errorcode, field, route, export or control flow moves (the AST skeleton reads SAME for 4 of 4 changed.tsfiles, below).What this does
Seventeen strings in these three files sent the reader to a tracker number for the reason behind them. In form D, as stages 1 to 6 applied it, the number goes. Where the sentence already said what was decided, only the citation goes. Where it leaned on the number, it now says the decision in words.
All 21 ledgered occurrences in this stage's surface (claim
5966570749), re-derived from the ledger onorigin/mainat81e69cab(where the branch was cut):analytics-service.ts4 (4 pairs),comparand-shape.ts7 (3 pairs),strategies/filter-normalizer.ts10 (7 pairs), in 17 string sites. That matches the seat's reading (4 ids; 3 ids, 7 occurrences; 7 ids). The four files excluded at dispatch (read-scope-sql.ts,strategies/native-sql-strategy.ts,strategies/objectql-strategy.ts,preview-evaluator.ts, held by 21505) are not touched; their 13 occurrences stay for stage 8.Rewritten in words
Caller- and author-visible text first, the log line last. Line numbers are at the head
fd6f53c7.comparand-shape.ts:596-611,fieldReferenceComparandMessage(a{ $field }comparand the SQL lowering cannot render)$fieldquery so it routes to the engine path, where the driver enforces all four rulings with metadata it owns, so the security rules exist in one place, with no second copy and no newStrategyContexthook; the/analytics/sqlecho declines too ("one consistent loud answer, no half-rendering", which the sentence already said)comparand-shape.ts:650-664,fieldReferenceBetweenBoundMessage(a{ $field }used as a$betweenbound)FieldReferenceSchemafrom both$betweenendpoints and rule out$in/$ninmembers, declared = enforced by removal, with no member-resolution implementation without measured demand. 5222 and 7598 as aboveanalytics-service.ts:3865-3874, the no-strategy diagnostic for a cross-field filter on a deployment with no aggregate bridge (a thrownError, no code)filter-normalizer.ts:863-877, the undefined-comparand refusal (INVALID_FILTER/ 400)dateRangedrew a full-history chart with no error; the lesson the family carries is that a dropped predicate widens the query. 6050's ruling (2026-08-07, option B): anundefinedcomparand is refused loudly (INVALID_FILTER/ 400), never read as null, because the spec declares no such comparand and an undefined key cannot be told from an absent one. 6386 took the same refusal to thiswheredoor (its PR removed the drop-the-key line and addedassertDefinedComparands)filter-normalizer.ts:1013-1026, the mixed$-operator / bare-key wrapper refusalread-scope-sql, which already failed closed on the same input. The widening clause already said "a dropped conjunct does not narrow the query, it WIDENS it", so 3650 only dropsfilter-normalizer.ts:1115-1118, the zero-operator field constraint refusal{ field: {} }is refused (INVALID_FILTER) on every backend, neither TRUE nor FALSE, so a half-built filter fails at authoring instead of quietly returning more or fewer rowsfilter-normalizer.ts:1449-1457, the filter-array refusalFilterArrayis input-only authoring sugar, lowered throughparseFilterASTat the doors, so drivers keep no array dialect. 5334's ruling: the analyticswheredoor lowers the same way (an empty array is no filter, any other array it cannot lower is refused), so one dashboard filter answers the same onfind()and on a chartfilter-normalizer.ts:2067-2070, theisFilterAST/parseFilterASTdisagreement refusalanalytics-service.ts:3969-3976, the dotted-measure refusal (INVALID_FIELD/ 400)analytics-service.ts:3667-3670, the no-object-registry warning (warn, once)CUBE_NOT_FOUNDbefore any SQL forms; with no registry probe configured the gate stands down and warns onceEvery cited card (12: 3650, 3867, 5158, 5222, 5240, 5334, 5918, 6050, 6386, 6444, 7596, 7598) was read through REST, body and every comment, before its string was rewritten. All twelve answer 200.
Published contract check
None of these strings is a spec-declared message or an i18n key. They are refusal, diagnostic and log text built inside
service-analytics. A repository-wide search for each old fragment outside the three files finds no assertion and no doc quoting it; the other hits are code comments, test comments and two similar sentences of their own in the stage-8 strategy files.fieldReferenceComparandMessageandfieldReferenceBetweenBoundMessageare also emitted throughread-scope-sql.ts, a stage-8 file this PR does not touch; its own wrapper text is unchanged.Ledger (
scripts/doc-authoring-prose-id.baseline.json)Regenerated with
node scripts/check-doc-authoring.mjs --census-ledger(exit 0, no growth refusal). The diff deletes 20 lines and adds none: exactly the three file blocks of this stage. A scripted key-by-key comparison of the branch-point copy against the regenerated one reads 14 (file, id) pairs moved, all of them this stage's, each to absent; every other row is unchanged. No other open PR touches the file (open PRs read at 07:1xZ and again at PR-open time).81e69cab)analytics-service.tscomparand-shape.tsstrategies/filter-normalizer.tspnpm check:doc-authoringat the head: "sibling-package prose ids hold the baseline — 9 pinned site(s) across 3 file(s), 86207 string(s) read in 1252 parsed source(s), no growth, no burn-down unrecorded". No gate is added or loosened;scripts/check-doc-authoring.mjsis untouched.Changeset
.changeset/20751-services-strings-stage7-state-the-decision.md:patchfor@objectstack/service-analytics. Measured after the full build: every new sentence is indist/index.jsanddist/index.cjs, and none of the old citation fragments from these three files is (the one#6050 ruling Bhit left indistisread-scope-sql.ts's own refusal, a stage-8 string). A TypeScript scan of every string literal and template text in the built output finds 13 tracker ids in each file, and they are exactly the ids the remaining stage-8 ledger entries carry (read-scope 5, native-SQL 2, ObjectQL 6), which is also the scan's positive control.Text-only proof
A TypeScript-AST skeleton of each changed
.tsfile, where every string literal and template text is a placeholder, a run of adjacent string operands of a+chain is one string (only its embedded expressions are kept), identifiers and numbers keep their text, and comments are never read.81e69cabagainst the head: 4 of 4 SAME. Controls on scratch copies offilter-normalizer.ts, each mutation's marker counted once on disk first: a one-identifier rename reads DIFF; a text-only change reads SAME; a re-split of one string into two concatenated pieces reads SAME.Pins
cross-field-engine-fallback.test.ts:419: the no-aggregate-bridge diagnostic is found by "so those rules are enforced in one place" instead of the id. The two assertions beside it ("budget", "executeAggregate") and the narrowness control are unchanged. This string is a plainErrorwith no code or status, so no envelope assertion exists to keep. Reverse check at the committed head, under the lock, throughscripts/ablation-replace.mjs: the new clause put back to the citation form (anchor hit once, blob moved) turned exactly that case red (predicted 1, measured 1 of 93). Restored byte-identical toHEADwith an emptygit diff HEAD.code/statusand fragment assertions ("zero operators", "mixes $-operator keys", "WIDENS", "read-scope-sql.ts", "No strategy can handle"), all still present.Tests
All through
scripts/pm/os-verify-lock.sh, every verdictVERDICT command-exit 0, at the headfd6f53c7:turbo run build --concurrency=2 --filter=./packages/* --filter=./packages/*/*(71/71).@objectstack/service-analytics,vitest run --maxWorkers=2: 174 files, 4142 tests passed, 247 skipped.@objectstack/service-analyticstypecheck(tsc --noEmitoversrc): exit 0.--listFilescounts 174 test files in that program, the re-pinned one among them.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) atfd6f53c7(6 paths vs merge base81e69cab, 108 changed lines): 72 commands, run one at a time from the worktree after the full build, each exit code recorded before any pipe; 72 exit 0.--ran: "72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero — all 72 recorded an exit code and none of them is 3)".check-issue-citations: "no issue citations added against 81e69ca (3 file(s) read)";check:nul-bytes: OK, 9910 files;check:type-check-debt: "none above its recorded number";check:dual-build-cjs-loads: 106 require entry points across 66 packages load;check:dts-closure: 71 built packages, 169/169;check:sourcemap-no-sources-content: 68 packages, 532 maps;check:published-files: 69 publishable packages;check:engine-double-contract: OK;check-adr-0087-registration: no declared-breaking changeset.fd6f53c7: the eleven declared wide-population families (check:init-service-contract,check:live-db-isolation,check:meta-type-normalized,check:optional-error-sink,check:resume-authority-declared,check:route-envelope,check:runner-env-posture,check:settings-bind-window,check:startup-registry-verdict,check:verify-stand-in,check:wildcard-fallthrough), pluscheck:durability-log-levelandcheck:error-code-casing(log and refusal text moved; no level or code did).eslint --no-inline-config --format jsonover the 4 changed.tsfiles atfd6f53c7: 4 files linted, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict. Repo-widepnpm lintis CI's.origin/mainmoved to10454b3a(four commits: PRs 21555, 21539, 21473, 21554) after the branch point. None touchesservice-analyticsor the ledger, and none adds or removes an id-bearing line in a non-test package source, so the recomputed ledger stands on that tree; the branch is not merged. 21505 has no PR yet, so the dispatch's merge condition did not arise.Acceptance notes
Noted, not filed:
[#7598]and[#3867]docblocks); comments are outside the ledger and belong to the sibling comment card. Carrier: none.[#7598]describe title incross-field-engine-fallback.test.ts;measure-source-field-gate.test.ts:140quotes the old "Until analytics 自动推断路径:measures上的关系穿越点号 member 仍被剥成基表列 ——owner.region_count_distinct静默聚合基表region(#5739 裁决未覆盖的第四个铸造点) #5918" wording in a comment). Test bodies and comments are outside the ledger. Carrier: none.Generated by Claude Code