Commit f9f9f91
fix(service-analytics): runtime strings state each decision in words instead of a tracker number (stage 7) (#21561)
Part of #20751
Clause-②: no
**Stage 7 of the `domain:services` lane 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, error
`code`, field, route, export or control flow moves (the AST skeleton
reads SAME for 4 of 4 changed `.ts` files, 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 on `origin/main` at `81e69cab`
(where the branch was cut): `analytics-service.ts` 4 (4 pairs),
`comparand-shape.ts` 7 (3 pairs), `strategies/filter-normalizer.ts` 10
(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`.
| Where | Cited | The text now says | Decision read from |
|---|---|---|---|
| `comparand-shape.ts:596-611`, `fieldReferenceComparandMessage` (a `{
$field }` comparand the SQL lowering cannot render) | 5222, 7598 x2 |
"driver-sql / driver-sqlite-wasm compile it to a same-table column
comparison for the six scalar operators, and the analytics native-SQL
strategy DECLINES such a query so it routes to the ObjectQL engine path
... the driver enforcing declared-only enumeration, the tenant-isolation
ban and the comparison class with metadata it owns, so those rules are
enforced in one place, next to the metadata they read"; the closing
"(/analytics/query) to get its rows" drops its citation | 5222's
maintainer rulings (2026-08-06, restated 2026-08-11): same-table columns
only, dotted paths refused; declared-only enumeration; the
tenant-isolation column forbidden on both sides; plus the
comparison-class rule the implementation added. 7598's ruling
(2026-08-12, Q1 = B): native SQL declines a `$field` query 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 new `StrategyContext` hook; the `/analytics/sql` echo
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 `$between` bound) | 5222 x2, 7596, 7598 |
"@objectstack/spec no longer declares the position at all
(FieldReferenceSchema was removed from the $between endpoint union
rather than implemented there, since nothing asked for it, ADR-0049
declared = enforced)"; "on the ObjectQL engine path, where the driver
enforces the cross-field rules (declared same-table columns only, never
the tenant-isolation column, one comparison class)"; "driver-sql and
driver-sqlite-wasm refuse both endpoints" drops its citation | 7596's
ruling (2026-08-11): remove `FieldReferenceSchema` from both `$between`
endpoints and rule out `$in` / `$nin` members, declared = enforced by
removal, with no member-resolution implementation without measured
demand. 5222 and 7598 as above |
| `analytics-service.ts:3865-3874`, the no-strategy diagnostic for a
cross-field filter on a deployment with no aggregate bridge (a thrown
`Error`, no code) | 5222, 7598 | "the ObjectQL engine path, whose driver
compiles it and enforces the cross-field rules (declared same-table
columns only, never the tenant-isolation column, one comparison class)
with metadata it owns, so those rules are enforced in one place, next to
the metadata they read" | as the first row |
| `filter-normalizer.ts:863-877`, the undefined-comparand refusal
(`INVALID_FILTER` / 400) | 3650, 6050, 6386 | "(a dropped predicate
WIDENS the query, which this module refuses everywhere else)"; "An
undefined comparand is refused rather than read as null, on the SQL
drivers and on this door alike." | 3650: a silently dropped `dateRange`
drew 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): an `undefined` comparand 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 this `where` door (its PR removed the drop-the-key line
and added `assertDefinedComparands`) |
| `filter-normalizer.ts:1013-1026`, the mixed `$`-operator / bare-key
wrapper refusal | 3650, 6444 | "the failure mode this module refuses
everywhere else"; "already fails closed on this exact shape, so both
doors refuse it: one shape, one answer." | 6444's ruling (2026-08-08,
option A): refuse the mixed wrapper in this module's envelope rather
than flatten it, converging with `read-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 drops |
| `filter-normalizer.ts:1115-1118`, the zero-operator field constraint
refusal | 5240 | "neither reading is the author's intent (a filter that
recorded a field and never its operator), so this shape is refused on
every backend." | the maintainer's ruling on 5240 (2026-08-04): `{
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 rows |
| `filter-normalizer.ts:1449-1457`, the filter-array refusal | 5158,
5334 | "lowered to a FilterCondition by @objectstack/spec
parseFilterAST() at every door, this one included, so it means the same
rows whichever door it enters." | 5158's ruling (2026-08-04, option C):
`FilterArray` is input-only authoring sugar, lowered through
`parseFilterAST` at the doors, so drivers keep no array dialect. 5334's
ruling: the analytics `where` door 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 on `find()` and on a chart |
| `filter-normalizer.ts:2067-2070`, the `isFilterAST` / `parseFilterAST`
disagreement refusal | 5158, 5334 | "Refusing rather than charting the
dataset unfiltered: a filter array is lowered at every door or refused,
never dropped." | as the row above |
| `analytics-service.ts:3969-3976`, the dotted-measure refusal
(`INVALID_FIELD` / 400) | 5918 | "Before this refusal the prefix was
silently dropped" (citation only: the sentence already says measures do
not traverse relationships, so there is no related column to aggregate)
| 5918's ruling (2026-08-07, option 3): refuse a dotted measure loudly,
naming the caller's spelling, because a measure has no traversal answer
to converge on |
| `analytics-service.ts:3667-3670`, the no-object-registry warning
(`warn`, once) | 3867 | "the cube-inference existence gate, which
answers 404 CUBE_NOT_FOUND for a name that is neither a registered cube
nor a registered object, is INACTIVE for this service" | 3867's landed
change (PR 3875): an inferred cube must name a registered object, and a
name that is neither a registered cube nor a registered object answers
404 `CUBE_NOT_FOUND` before any SQL forms; with no registry probe
configured the gate stands down and warns once |
Every 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. `fieldReferenceComparandMessage` and
`fieldReferenceBetweenBoundMessage` are also emitted through
`read-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).
| | before (`81e69cab`) | after |
|---|---|---|
| `analytics-service.ts` | 4 occurrences, 4 pairs | 0 |
| `comparand-shape.ts` | 7 occurrences, 3 pairs | 0 |
| `strategies/filter-normalizer.ts` | 10 occurrences, 7 pairs | 0 |
| whole ledger | 34 occurrences, 27 pairs, 6 files | 13, 13, 3 (the
stage-8 files) |
`pnpm check:doc-authoring` at 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.mjs` is untouched.
## Changeset
`.changeset/20751-services-strings-stage7-state-the-decision.md`:
`patch` for `@objectstack/service-analytics`. Measured after the full
build: every new sentence is in `dist/index.js` and `dist/index.cjs`,
and none of the old citation fragments from these three files is (the
one `#6050 ruling B` hit left in `dist` is `read-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 `.ts` file, 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. `81e69cab` against the head: 4 of 4 SAME. Controls on scratch
copies of `filter-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 plain `Error` with no code or status, so no envelope
assertion exists to keep. Reverse check at the committed head, under the
lock, through `scripts/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 to `HEAD`
with an empty `git diff HEAD`.
- No other test asserts any of the seventeen strings by an id or by a
fragment this PR rewrites. The refusal tests keep their `code` /
`status` and 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 verdict `VERDICT
command-exit 0`, at the head `fd6f53c7`:
- Build: `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-analytics` `typecheck` (`tsc --noEmit` over
`src`): exit 0. `--listFiles` counts 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) at `fd6f53c7` (6 paths vs merge base `81e69cab`,
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.
- Outside the derived set, all exit 0 at `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`), plus
`check:durability-log-level` and `check:error-code-casing` (log and
refusal text moved; no level or code did).
- Lint, narrowed as a measurement: `eslint --no-inline-config --format
json` over the 4 changed `.ts` files at `fd6f53c7`: 4 files linted, 0
errors, 0 warnings. `eslint.config.mjs` enables no type-aware linting
(no `parserOptions.project`, no typed rules), so this diff cannot move
any untouched file's verdict. Repo-wide `pnpm lint` is CI's.
- `origin/main` moved to `10454b3a` (four commits: PRs 21555, 21539,
21473, 21554) after the branch point. None touches `service-analytics`
or 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:
- Code comments beside the rewritten strings still carry ids (for
example the `[#7598]` and `[#3867]` docblocks); comments are outside the
ledger and belong to the sibling comment card. Carrier: none.
- Test titles and comments still carry ids (the `[#7598]` describe title
in `cross-field-engine-fallback.test.ts`;
`measure-source-field-gate.test.ts:140` quotes the old "Until #5918"
wording in a comment). Test bodies and comments are outside the ledger.
Carrier: none.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 5dbcee8 commit f9f9f91
6 files changed
Lines changed: 58 additions & 50 deletions
File tree
- .changeset
- packages/services/service-analytics/src
- __tests__
- strategies
- scripts
Lines changed: 17 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
416 | 416 | | |
417 | 417 | | |
418 | 418 | | |
419 | | - | |
| 419 | + | |
420 | 420 | | |
421 | 421 | | |
422 | 422 | | |
| |||
Lines changed: 9 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3664 | 3664 | | |
3665 | 3665 | | |
3666 | 3666 | | |
3667 | | - | |
3668 | | - | |
3669 | | - | |
| 3667 | + | |
| 3668 | + | |
| 3669 | + | |
| 3670 | + | |
3670 | 3671 | | |
3671 | 3672 | | |
3672 | 3673 | | |
| |||
3864 | 3865 | | |
3865 | 3866 | | |
3866 | 3867 | | |
3867 | | - | |
3868 | | - | |
| 3868 | + | |
| 3869 | + | |
| 3870 | + | |
| 3871 | + | |
3869 | 3872 | | |
3870 | 3873 | | |
3871 | 3874 | | |
| |||
3965 | 3968 | | |
3966 | 3969 | | |
3967 | 3970 | | |
3968 | | - | |
| 3971 | + | |
3969 | 3972 | | |
3970 | 3973 | | |
3971 | 3974 | | |
| |||
Lines changed: 15 additions & 12 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
601 | 601 | | |
602 | 602 | | |
603 | 603 | | |
604 | | - | |
605 | | - | |
606 | | - | |
607 | | - | |
608 | | - | |
609 | | - | |
610 | | - | |
| 604 | + | |
| 605 | + | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
611 | 612 | | |
612 | 613 | | |
613 | 614 | | |
| |||
648 | 649 | | |
649 | 650 | | |
650 | 651 | | |
651 | | - | |
| 652 | + | |
652 | 653 | | |
653 | 654 | | |
654 | | - | |
655 | | - | |
| 655 | + | |
| 656 | + | |
| 657 | + | |
656 | 658 | | |
657 | 659 | | |
658 | 660 | | |
659 | 661 | | |
660 | | - | |
661 | | - | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
662 | 665 | | |
663 | 666 | | |
664 | 667 | | |
| |||
Lines changed: 16 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
865 | 865 | | |
866 | 866 | | |
867 | 867 | | |
868 | | - | |
869 | | - | |
870 | | - | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
| 871 | + | |
871 | 872 | | |
872 | 873 | | |
873 | 874 | | |
874 | 875 | | |
875 | | - | |
| 876 | + | |
| 877 | + | |
876 | 878 | | |
877 | 879 | | |
878 | 880 | | |
| |||
1019 | 1021 | | |
1020 | 1022 | | |
1021 | 1023 | | |
1022 | | - | |
1023 | | - | |
1024 | | - | |
| 1024 | + | |
| 1025 | + | |
| 1026 | + | |
1025 | 1027 | | |
1026 | 1028 | | |
1027 | 1029 | | |
| |||
1111 | 1113 | | |
1112 | 1114 | | |
1113 | 1115 | | |
1114 | | - | |
1115 | | - | |
| 1116 | + | |
| 1117 | + | |
| 1118 | + | |
1116 | 1119 | | |
1117 | 1120 | | |
1118 | 1121 | | |
| |||
1447 | 1450 | | |
1448 | 1451 | | |
1449 | 1452 | | |
1450 | | - | |
| 1453 | + | |
| 1454 | + | |
1451 | 1455 | | |
1452 | 1456 | | |
1453 | 1457 | | |
| |||
2062 | 2066 | | |
2063 | 2067 | | |
2064 | 2068 | | |
2065 | | - | |
| 2069 | + | |
| 2070 | + | |
2066 | 2071 | | |
2067 | 2072 | | |
2068 | 2073 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | | - | |
3 | | - | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | 2 | | |
14 | 3 | | |
15 | 4 | | |
16 | 5 | | |
17 | 6 | | |
18 | 7 | | |
19 | 8 | | |
20 | | - | |
21 | | - | |
22 | | - | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | 9 | | |
30 | 10 | | |
31 | 11 | | |
| |||
0 commit comments