Skip to content

fix(service-analytics): runtime strings state each decision in words instead of a tracker number (stage 7) - #21561

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20751-services-strings-stage7
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-20751-services-strings-stage7

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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:


Generated by Claude Code

…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>
@github-actions github-actions Bot added the size/m label Oct 3, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 10454b3afa94d49e6e424cc16fbbff3a898f3ad8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b395caeea2a78ee58fce82e0f9b3a7369de63b03 — the merge of head fd6f53c79c72c0f65f47dfd3ed4580f3a5ccf11d into base 10454b3afa94d49e6e424cc16fbbff3a898f3ad8, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit f9f9f91 Oct 3, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20751-services-strings-stage7 branch October 3, 2026 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants