Repository navigation
fix(service-analytics)!: judge and present a relationship-path cube measure by its column's declaration on the object the path reaches - #21230
Conversation
…asure by the declaration on the object its last hop reaches The cube door's aggregate x field-type judgment, the measure result type and the native-SQL presenter now locate a measure column written as a relationship path on the object the one hop resolver (hop-object.ts, columnObjectOf) names for its last hop, and read the declaration there. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ent and presentation on both faces Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…d hook answered for every object Both cases pinned the compile check's stand-down on a dotted field with a sourceFieldMeta stub that described the joined object too. The cube door now reads the column there, so the stand-down case gets a hook that describes the base object alone, as its comment says, and a sibling case pins the cube door's query-time refusal where the joined object is described. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…a relationship-path measure Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…tted-measure-type Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
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 eb6d889c0d756a7e719a9da8d8db512e820496c2 && git checkout eb6d889c0d756a7e719a9da8d8db512e820496c2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3ddd3d0c4a355b58d074245ae6732ac91489c2a7 c632454740e26177da76986fd5e849725ada4adf && git checkout -B drift-repro 3ddd3d0c4a355b58d074245ae6732ac91489c2a7 && git merge --no-ff c632454740e26177da76986fd5e849725ada4adf
node scripts/docs-audit/affected-docs.mjs --json 3ddd3d0c4a355b58d074245ae6732ac91489c2a7
|
…r the claim revision Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…inct over a relationship path the cube declares no join for, located through the one hop resolver (objectstack-ai#21247) Fixes objectstack-ai#21232 Clause-②: no (narrowing) ## What changed The structured-JSON door (`structured-json-dimension-door.ts`) now locates a dotted path's column through the one hop resolver, the way every other reader in the package does. - **`columnOf`** asks `columnObjectOf` (`hop-object.ts`, consumed unchanged) with the host's hop reference. The answer is the cube's declared join at that path, else the relationship field's declared `reference`, else the alias. That is the object both strategies join and read for the path. Before, `columnOf` read `cube.joins` alone and stood down on a path the cube declares no join for. ⛔ No second resolver: the hop walk is `hop-object.ts`'s, as for the measure side in PR objectstack-ai#21230. - **`assertNoStructuredJsonDimension`** takes one more argument, `referenceOf` (the `HopReference` type from `hop-object.ts`). Its one caller, `AnalyticsService.assertDimensionsGroupScalarColumns`, passes `this.hopReference`. That is the same function the field gate, the admitted and scoped set, and both strategies resolve a hop with. The function is internal to the package (not exported from `index.ts`). - The refusal words and the envelope are unchanged. A member over an undeclared-join path now gets the refusal a member over a declared join already got: `INVALID_FIELD` / 400, `member`, `param`, `cube`, `field` (the path), `object` (the object the lookup declares as its target). ## Measured: `POST /api/v1/analytics/query` and `/sql` on the real dispatcher route Setup: `AnalyticsServicePlugin` over a real `ObjectQL` engine and `SqlDriver`, a signed-in caller, the real `dispatcher-plugin` mount, SQLite in memory and a private PostgreSQL 16.14. A configured cube over `os21232_deal` declares a join for `account` (`hq` `json`, `name` `text`) and none for `owner`. `owner` is a lookup whose `reference` is `os21232_person` (`prefs` `json`, `labels` `tags`, `email` `text`). Before: `origin/main` at `3a7b6eb0`. After: this branch's `service-analytics` build. There are 80 cells (2 drivers x 2 faces x 2 doors x 10 members). 32 changed and 48 are byte-identical. The scratch probe was deleted. | member | face | SQLite before → after | PostgreSQL 16.14 before → after | |:--|:--|:--|:--| | dimension `owner.prefs` (json) / `owner.labels` (tags) | native | 200, one group per serialized value → **400 `INVALID_FIELD`** | **500 `DATABASE_ERROR`** → **400** | | the same | ObjectQL | 400 `INVALID_FIELD` from the engine, `groupBy[1]` → **400 from this door, naming the member** | the same | | `count_distinct` over `owner.prefs` / `owner.labels` | native | 200, `2` / `2` → **400** | **500** → **400** | | the same | ObjectQL | 400, the cross-object refusal (no `field` / `object`) → **400 from this door** | the same | | any of the above on `/sql` (dry run) | both | 200 (native, and the ObjectQL dimension) → **400** | the same | | control: the same members over `account.hq` (declared join) | both | 400 `INVALID_FIELD`, this door → unchanged | unchanged | | control: `owner.email` dimension | both | 200 → unchanged | unchanged | | control: `owner.email` / `account.name` count_distinct | native / ObjectQL | 200 / 400 cross-object refusal → unchanged | unchanged | ## Pins **New file:** `packages/services/service-analytics/src/__tests__/json-stored-door-undeclared-join.test.ts`. It uses the plugin's own composition over a real engine, a SQLite cell and a PostgreSQL cell (a named skip without `OS_TEST_POSTGRES_URL`), and both faces. It has 10 tests, 5 per cell. - A dimension and a `count_distinct` over `owner.prefs` / `owner.labels` are refused `INVALID_FIELD` / 400 on both faces, with nothing read. The checked fields are `code`, `status`, `member`, `param`, `cube`, `field` (the path) and `object` (`os21232_person`), and the raw-SQL and engine-aggregate counters stay at 0. The two faces' envelopes must be equal. Neither face's own refusal carries this envelope, so equality shows the door answered. - An ad-hoc query's inferred cube declares no join at all. A dotted dimension on it (`owner.prefs`) is refused the same way. - The dry-run door refuses what the query door refuses. - Controls: the same members over the declared join `account.hq` get the same refusal (`object` the joined object). The scalar `owner.email` dimension is served on both faces (one group per owner), and its `count_distinct` answers `3`. **Unit file** `dimension-structured-json-door.test.ts`: a new block with 5 tests. The relationship field's declared reference names the object, on both faces (`crm_account`, not the alias). The reference wins over an alias that names a described object (the door stands down and the statement joins `"crm_account"`). A host with no reference resolves the alias, and both doors refuse there. Control: the referenced object's text column is served. ## Ablations The tests import the subject by relative path (`../analytics-service.js`, `../plugin.js`), so each run reads `src` and there is no `dist` leg. Every mutation went through `scripts/ablation-replace.mjs` in WRAP mode, with an outer `trap` restore on `EXIT INT TERM` against the absolute path. Predictions were written before each run. They ran from committed `08ea135b`, and `git diff 08ea135 b6e6418 -- packages/services/service-analytics` is empty. The PostgreSQL cell was live. | ablation | mutation | predicted | observed | |:--|:--|:--|:--| | A1 | `columnOf` gets back the joins-only resolution (the removed code, byte for byte) | 9 red: the undeclared-join, ad-hoc and dry-run tests on each cell, the two reference-tier face tests and the alias-tier test | **9 failed / 19 passed** | | A2 | the call site passes `undefined` in place of `this.hopReference` | 9 red: the same six live tests, the two reference-tier face tests, and "the reference, not the alias" (the alias tier stays green) | **9 failed / 19 passed** | The prediction's total (30) was an arithmetic slip: 28 tests ran. Each mutation landed: anchor 1 → 0, and the blob changed (A1 `04bf4095e712` → `db237bdc284e`, A2 `16d63d721b6e` → `5d257e0a59d3`). Each was restored and proven: the blob equals the HEAD blob, `git diff HEAD` is empty, and porcelain shows 0. An earlier pair of runs at `50d5511f`, before the ad-hoc pin existed, gave 7 failed / 19 passed for each, as predicted then. ## Fixture triage The full `service-analytics` suite turned up exactly one fixture that pinned the removed stand-down: `dimension-structured-json-door.test.ts`, "a dotted path the cube declares no join for is a synthetic traversal … not judged". It pinned the branch this PR deletes, so it was replaced, not respelled. Its stand-down now has an honest reason, a host that describes nothing on the object the hop reaches (`describes: null`), and the new block above pins the judged tiers. Consumer radius: the analytics fixtures with dotted members in `packages/rest` (8 files), `packages/runtime` (4) and `packages/driver-memory` (3) were run against this branch's build. They gave 107 passed / 3 skipped, 24 / 10 and 239 / 0, with no failures. ## Other readers of `cube.joins` in `service-analytics` (dispatch Zone 2, item 2) No other reader resolves a dotted path through `cube.joins` alone. Every path-splitting reader goes through `resolvePathHops` / `columnObjectOf`. Six sites enumerate `cube.joins` without splitting a path. They are listed for the seat and not touched here. - `strategies/native-sql-strategy.ts` `qualifyAndRegisterJoin`, `canJoin`: it qualifies a bare base column only when the cube declares a join. **Measured** at `b6e64185` (this file is byte-identical to `origin/main`). Take a configured cube with no declared join and the dimensions `note` + `owner.email`, where the lookup's target also declares `note`. The native face answers **500 `DATABASE_ERROR`** on SQLite ("ambiguous column name: note") and PostgreSQL (42702). The ObjectQL face answers 200. This is reported to the seat as a finding and is not handled in this PR. - `strategies/native-sql-strategy.ts` `canHandle`, the federated-object decline: it asks `isExternalObject` of the declared join targets only, not of an object reached through a path with no declared join. Reach not measured. - `native-sql-strategy.ts` (three sites) and `analytics-service.ts` `cubeObjects`: these read the declared joins as a fallback for a context built without `readScopedObjects`, or beside `namedQueryFields`, which adds the path-reached objects. No defect was found. ## Docs `git grep -nE "declares no join|declared join|structured-JSON|structured JSON|count_distinct"` over `content/docs/**`, excluding `releases/` and `references/`, gave 32 hits. None says the door stands down on a path without a declared join, and none speaks about a `count_distinct` over a related field's JSON-stored column. Positive control: the pattern hits `content/docs/deployment/validating-metadata.mdx:228`, the dataset-dimension door sentence. That page speaks about datasets, whose dotted fields must traverse a declared `include` (a declared join), and it stays true. No page edited. ## Deviation from the declared file surface The claim names `structured-json-dimension-door.ts` (`columnOf`) and its tests. Routing `columnOf` through the resolver's reference tier, which is the triage direction, needs the host's `HopReference`, and only the door's one caller holds it. So `analytics-service.ts` changes by one argument (`this.hopReference`) and three docblock lines in `assertDimensionsGroupScalarColumns`, the door's caller. Nothing else in that file moved. A2 above pins that line. ## Verification at `b6e64185` The branch head `fafbf053` carries a tree byte-identical to `b6e64185` (`git diff b6e6418 fafbf05` is empty), so every reading below holds for it. `b6e64185` has `origin/main` (`ef96c9ed`) merged in. Install and a full build were refreshed after the merge, and `pnpm --filter @objectstack/spec check:generated` reports all 15 artifacts up to date. - `pnpm --filter @objectstack/service-analytics test`: 164 files, 3758 passed / 56 skipped (the live-PostgreSQL cells), 0 failed. `typecheck` (`tsc --noEmit`): exit 0. - PostgreSQL 16.14 live (`OS_TEST_POSTGRES_URL`): `json-stored-door-undeclared-join`, `json-stored-door-live-drivers`, `cube-measure-relationship-path-type`, `dimension-structured-json-door` and `multi-value-json-stored-door` gave 72 passed / 0 skipped. `runtime` `analytics-json-dimension-door` and `analytics-cube-measure-field-type-door` gave 20 passed / 0 skipped (run at `9a32e950`, whose analytics source is the same). - Gates: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives 62 commands, and all 62 exit 0 at `b6e64185`. `--ran` reconciliation: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN", with every exit code recorded. `check:adr-0087-registration` was red once, on the changeset's "FROM → TO" label, which the gate reads as a rewrite prescription. The section describes behaviour, not a rewrite, so it was relabelled "Before and after". The gate is now green with `not-required (no-migration-prescription)`, the disposition the door's earlier entries carry. - Lint, a declared narrowing: `eslint --no-inline-config --format json` over the 4 touched `.ts` files at `b6e64185` reports 4 files, 0 errors and 0 warnings. ① All 4 are inside the population `eslint.config.mjs` lints (`packages/**/*.{ts,tsx,mts,cts}`; none ignored), and the changeset `.md` is in no `files` glob. ② The count of 4 is read from the JSON output. ③ The config enables no type-aware linting (`--print-config` gives `parserOptions` `{ecmaVersion:'latest', sourceType:'module'}`, with no `project`), so this diff cannot move a verdict on an untouched file. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Carrier note for release compilation (not filed).** The two pending release notes for this door, `20807-analytics-json-dimension-refused.md` and `20912-analytics-multi-value-distinct-refused.md`, list a dotted path the cube declares no join for as Unchanged. This PR makes that clause untrue, and its own changeset states the reversal. When the CHANGELOG is assembled, the release compiler should drop that clause from both entries. They cannot be corrected from this PR: editing another PR's pending changeset is a foreign-changeset edit that stays refused until a person confirms it (objectstack-ai#17712). - The ObjectQL face's own refusals of these members (the engine's `groupBy[1]`, the cross-object measure refusal) are now unreachable for them, because the door answers first. Neither refusal changes. - No route-level pin was added. The dispatcher relays this door's envelope generically, and `packages/runtime/src/analytics-json-dimension-door.test.ts` already pins that relay for this door. The route-level readings above were taken through the real route. --- _Generated by [Claude Code](https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #21129
Clause-②: no (narrowing)
What changed
A configured cube measure whose
sqlis a relationship path (account.name) is now judged, described and presented by the declaration on the object the path's last hop reaches, exactly as a measure over the cube's own column already was (#21044).analytics-service.ts).declaredMeasureColumnlocates a relationship-path column on the object its last hop reaches throughcolumnObjectOf(hop-object.ts, PR fix(service-analytics)!: a relationship-path hop with no declared join reads the object its lookup field declares (#20986) #21088, consumed unchanged), with the service'shopReference: the cube's declared join, else the relationship field's declaredreference, else the alias. That is the answer the field gate admits the path with and both strategies join it by. ⛔ No second path walk.cube-measure-field-type-door.ts).assertCubeMeasureFieldTypesAcceptednow takes the located column (MeasureColumn: object, column, path) and asksisAggregateCompatibleWithFieldTypewith that object's declaration.fieldon the error is the path as the measure spells it, andobjectis the related object that declares the column: the convention the [finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and acount_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 door already uses for a joined dimension. The words for a base-object column are byte-identical to before.withMeasureResultTypes). It reads the samedeclaredMeasureColumn, so a relationship-pathmin/maxover a temporal column is describedtime, as a base-object one is.strategies/native-sql-strategy.ts). A new module helper,measureColumnOf, is read by the presenter and by the aggregand operand policy. The policy is the same code, moved onto the helper with unchanged behaviour. A relationship-pathmin/maxover a numeric column now goes throughpresentAsNumber, as a base-object one does.Why "refused" rather than "presented as text" (dispatch Zone 2, item 2)
The base-object door's answer for a base
textmaxis a refusal on both faces, and following it needs no new surface. The ObjectQL face already refused the related pair as a cross-object measure. The native face now refuses it through the same door, so the two faces give one envelope. "Presented as text" would need two things. First, the ObjectQL face would have to serve cross-object measures, a widening and so a Clause-② question. Second,fields[]would need a new word for a text measure. It would also disagree with the base-object door on the same declared type.Measured:
POST /api/v1/analytics/queryon the real dispatcher routeSetup:
AnalyticsServicePluginover a realObjectQLengine andSqlDriver, the cube handed in ascubes, and a signed-in caller. The cube is overdealwith a declared joinaccount(nametext,revenuenumber,opened_atdatetime,tierselect). Readings were taken before atc6b6889193(base) and after with this branch'sservice-analyticsbuild. There are 40 cells (2 drivers x 2 faces x 10 measures): 20 changed and 20 are identical. The scratch probe was deleted.sqlmax/minoveraccount.name(text)"zeta"/"alpha",fields[]number → 400INVALID_FIELDmaxoveraccount.tier(select)"b", number → 400sumoveraccount.name0→ 400DATABASE_ERROR→ 400max/minoveraccount.revenue(number)250/100→ unchanged"250.000000000000000000000000000000"(string) → 200250(number)maxoveraccount.opened_at(datetime)fields[]number →timetimeINVALID_FIELD(cross-object refusal) → 400INVALID_FIELD(this door, withfield/object)maxovernote(base text),maxoveramount(base number)32→ unchangedPins
New file:
packages/services/service-analytics/src/__tests__/cube-measure-relationship-path-type.test.ts. It uses the plugin's own composition over a real engine, on a SQLite cell and a PostgreSQL cell (a named skip withoutOS_TEST_POSTGRES_URL), and both faces. It has 13 tests: 6 per cell and 1 more.INVALID_FIELD/ 400 on both faces, with nothing read. The checked fields arecode,status,member,param,cube,field(the path) andobject(the related object), and the raw-SQL and engine-aggregate counters stay at 0. The two faces' envelopes must be equal. The ObjectQL face's own cross-object refusal carries nofield/object, so equality proves the door answered and not two refusals that happen to agree. The pairs includemaxoverowner.email, whereowneris a lookup the cube declares no join for. Only the field's declaredreferencereachesos21129_person(tier 2).min/maxis a JS number typednumber(250,100,41), and a related temporalmaxis typedtime.maxis refused the same way, and a base numbermaxanswers32on both faces.Ablations (from committed
37196767; predictions written before each run)The tests import the subject by relative path (
../analytics-service.js,../plugin.js), so each run readssrc. There is nodistleg. Every mutation went throughscripts/ablation-replace.mjsin WRAP mode, with an outertraprestore onEXIT INT TERMagainst the absolute path.declaredMeasureColumn: a relationship path answerscolumn: null(the old stand-down)timeand dry-run on each cell, plus the dataset query-time refusal casenative max_acct_name must not be served,expected 'number' to be 'time'target = { object: objectName, field: measure.sql }(the old base-object lookup)expected '250.000000000000000000000000000000' to be 250columnObjectOf(..., undefined): the host's reference answer is droppedmax_owner_emailnative max_owner_email must not be servedEach mutation landed (anchor 1 → 0, and the blob changed: A1
34fb71b6048c→c55b95c96e8a, A2d7c20d35c3ba→aebd3cb1f1fc, A334fb71b6048c→ee3a18cc777f). Each was restored and proven: the blob equals the HEAD blob,git diff HEADis empty, and porcelain shows 0.Fixture triage
The full
service-analyticssuite turned up exactly two fixtures that pinned the removed stand-down. Each drove a dataset measure overaccount.FIELDthroughqueryDataset, with asourceFieldMetastub that answered by field name for every object, so the stub described the joined object too.aggregate-nontemporal-measure-refusal.test.ts, tier 2 of "the three cannot-answer tiers": the stub now describes the base object alone, as the comment says, and the case also asserts that the statement ran.aggregate-datetime-measure-refusal.test.ts: the stand-down case gets a base-only hook, the same way. A sibling case keeps the any-object hook and pins the new behaviour: the dataset's compile check still stands down on the dotted field, and its query is refused by the cube door withINVALID_FIELD/ 400,fieldaccount.submitted_atandobjectaccount, and no statement runs.Consumer radius: no fixture outside
service-analyticsfeeds the door a relationship-pathmin/max/sum/avgwith field metadata wired. Agit grepoverpackages/rest,packages/runtime,packages/qa,examples,packages/cli,packages/pluginsandappsturned up only dimensions, plus onerestdataset measure (sumoveraccount.balance) whose service wires nosourceFieldMeta.Docs
content/docs/deployment/validating-metadata.mdx:216-217. Old: "The analytics service refuses the same pair with400 DATASET_INVALIDwhen a query is built; this is the identical verdict," New: "The analytics service refuses the same pair with400 DATASET_INVALIDwhen a query is built — or, for a field reached through a relationship path, with400 INVALID_FIELDwhen the query runs, judged on the object the path reaches; this is the identical verdict,". The lint rule on that page resolves relationship paths, and the compile check does not, so the pair is now refused one door later with the member-level code. No other sentence undercontent/docs/**(outsidereleases/andreferences/) speaks about the type of a measure over a related field.Open question (not decided here, per the dispatch)
The ObjectQL face still refuses a relationship-path pair the table accepts (
maxoveraccount.revenue) as a cross-object measure: the engine aggregate cannot join. The native face serves it. The two faces now agree on every refused pair and disagree only on this capability, and the refusal names its remedy (run on a native-SQL driver). The four-axis options are in the dev report on #21129.Acceptance notes
packages/runtime/src/analytics-cube-measure-field-type-door.test.tsalready pins that relay for this door. The route-level readings above were taken through the real route.assertAggregateFieldTypeCompatible(dataset-compiler.ts) still returns early on a dotted field, so such a dataset is refused when its query runs (INVALID_FIELD), not at compile (DATASET_INVALID). It is refused loudly before anything is read, so this is not a defect, and it is not changed here.Clause-②: no (narrowing), as claim revision5939237083sets it, in this body and in the changeset: the change narrows the native face's accept set, the same class as analytics: a config cube min/max over a text column is served at the cube door under fields[] type number; the dataset door refuses the pair by the compatibility table, the cube door consults nothing #21044. The changeset also carries a**BREAKING**banner and a!summary;check-adr-0087-registrationreads[BREAKING+bang+clause-②-narrowing]with analready-registereddisposition. The package is gradedminor.jsonfield, compiled by NativeSQLStrategy, answers one group per serialized document on SQLite and 500 on PostgreSQL; the engine door #20783 closes does not see it #20807 / [finding] analytics NativeSQL answers a multi-value dimension one group per serialized array on SQLite and 500 on PostgreSQL, and acount_distinctover a JSON-stored field 2 / 500; the engine door #20808 adds does not see it #20912 door (structured-json-dimension-door.ts,columnOf) resolves a relationship path through the cube's declared join only. It reads no relationship field's declaredreference, so it does not use the one hop resolver. A cube that reaches a relatedjsonfield only through the lookup'sreferencegets 500DATABASE_ERRORon PostgreSQL when grouping by it or takingcount_distinctover it on the native face. On SQLite it answers 200 (one group per serialized document). The declared-join control answers 400INVALID_FIELD. The measurement is in the dev report. Not touched here: the file is outside this card's surface.Local verification (at
17e58d67, after mergingorigin/maind6d6e872and refreshing install and build)pnpm --filter @objectstack/service-analytics exec vitest run --maxWorkers=2withOS_TEST_POSTGRES_URL(private PostgreSQL 16.14): 163 files, 3799 passed.pnpm --filter @objectstack/service-analytics typecheck: exit 0.tsc --listFileslists all three touched test files.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 91 families, all exit 0. Two of them (check:skill-examples,check:dual-build-cjs-loads) first answered exit 3 (PREREQUISITE NOT MET, nodist/) and were re-run after a full turbo build.--ran: "91 derived famil(ies) accounted for — 91 run, 0 NOT-MEASURED (a DERIVED zero — all 91 recorded an exit code and none of them is 3)".17e58d67:eslint --no-inline-config --format jsonover the 7 touched.tsfiles reads 7 files, 0 errors, 0 warnings. The population comes fromeslint.config.mjs(**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minusNEVER_LINTED). Invariance: the config never enables type-aware linting (noparserOptions.projectorprojectService), so this diff cannot move a verdict on an untouched file.packages/qa/dogfood(CI's Dogfood Regression Gate), and the fullrest/runtimesuites (CI).Generated by Claude Code