fix(lint): field-no-consumers credits every field an analytics member's column path reads (#21439) - #21460
Conversation
…'s column path reads A dataset dimension's or measure's `field` and a cube dimension's or measure's `sql` now credit the lookup on the base object, each intermediate lookup and the leaf column, each hop resolved the way the analytics door resolves it: resolveCubeColumn for a cube (exported from the cube leg, not from the barrel), resolveFieldPath plus the `include` gate for a dataset. A path the door refuses records carriers, not reads. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
…-hop, two-hop The slot list is read off the spec's own CubeSchema and DatasetSchema (every string whose pattern is the analytics column path) and held equal to the pin's builders, so a slot the spec adds fails the pin. Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 4 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 75781cdd5a96924afa4012726ae4c2d1c620e4a0 && git checkout 75781cdd5a96924afa4012726ae4c2d1c620e4a0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1fd56645af247808e66f64b1ebb63c4ad614c84f 6ae7c521fe7071b507336c035144a35560e1aeb5 && git checkout -B drift-repro 1fd56645af247808e66f64b1ebb63c4ad614c84f && git merge --no-ff 6ae7c521fe7071b507336c035144a35560e1aeb5
node scripts/docs-audit/affected-docs.mjs --json 1fd56645af247808e66f64b1ebb63c4ad614c84f
|
Fixes #21439
Clause-②: no
What changed
field-no-consumers(packages/lint/src/validate-field-consumers.ts) now owns the four analytics slots that name a column: a dataset dimension's and measure'sfield, and a cube dimension's and measure'ssql. Each path credits every field it reads. Those fields are the lookup on the base object, each intermediate lookup, and the column on the object the last hop reaches. They are the same fields the analytics door's field-level read gate names (fieldsOfColumnSqlinservice-analytics).resolveCubeColumn, the resolver the cube leg ofvalidate-dataset-measure-aggregates.tsalready uses: the cube's declared join for the hop, else the lookup'sreference. That function is nowexported from its module. It is not re-exported from the package barrel:resolveCubeColumnappears 0 times indist/index.d.ts,dist/index.d.ctsanddist/runtime.d.ts. A dataset's path goes throughresolveFieldPathplusjoinablePrefixes(include), the pairvalidate-dataset-references.tsuses. The rule asks the resolver about each prefix of the path (account, thenaccount.region, thenaccount.region.zone). The leaf of each prefix is one field the path reads, so the rule walks no hop itself. The cube leg's body is unchanged: its diff is oneexportkeyword and one docblock paragraph, so nothing it refuses changes.sqland the walk's object context never reads that.includedeclares the relationship prefix. Otherwise:includedoes not declare. (compileDatasetrefuses the dataset, anddataset-field-not-includedalready reports it.) Each field such a path names is recorded as a carrier, not a reader. That is the rule's existing word for a site that names a field and reads it nowhere: aninlineColumnsentry with noinlineEditis the precedent. So the verdict iscarrier-onlyand the finding lists the member path. It is not a false "inert — no site of any kind names it", and it is not a guessed read.'*'reads no field and credits none.5950947151) stands. Only explicit, authored member paths are credited. Nothing drawn by default is counted.@objectstack/lintpatch changeset. That is the family's convention for the lint side of a false-"inert" fix: the four earlier changesets of this family were lint patch three times and minor once, the minor being the first, which added two consumer kinds. A false advisory verdict in a released package is a bug fix. No export reaches the published entry points.Evidence (final commit
6ae7c521fe)os validate, before and after, on a scratch fixture that is not committed. Three objects:fx_ledger.accountis a lookup tofx_account, andfx_account.regionis a lookup tofx_region. Fields prefixedc_are read only by a cube member,d_only by a dataset member (include: ['d_account.d_region']), andu_only by a dataset measure whose join is not declared. Each slot has a bare, a one-hop and a two-hop member.53fd35e3e3)51819a6aaf; later commits are comment-only)c_kind,c_amount(bare)c_account,c_tier,c_revenue,c_region,c_code,c_zoned_kind,d_amount(bare)d_account,d_tier,d_revenue,d_region,d_code,d_zoneu_account,u_revenuecarrier-only, carrierdatasets[1].measures[0].fieldBoth runs exit 1 on the fixture's one error,
dataset-field-not-includedon the undeclared measure. Thefield-no-consumerswarnings went from 16 to 2.Tests.
pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 119 files and 5619 tests passed.pnpm --filter @objectstack/lint typecheck: exit 0,check:test-typecheck: OK. The test file is in thetsconfig.test.jsonprogram (--listFilescount 1), with 0 errors in it.CubeSchemaandDatasetSchemathroughz.toJSONSchema: every string whose pattern is the analytics column path. It is held equal to the pin's builders, so a column slot the spec adds on either root fails the pin.Ablations. Each leg ran on the committed tree through
scripts/ablation-replace.mjswith atraprestore. Every anchor hit 1 time and then 0. Every restore was proven by blob equal to HEAD and an emptygit diff HEAD. Finalgit statuswas clean. The subject resolves through relativesrcimports, so nodistwas in the path.resolveFieldPathinstead ofresolveCubeColumnGates.
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) at6ae7c521federived 62 commands, the same list the seat derived at dispatch. All 62 exited 0.--ran: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN".check-widening-tells --declaration no --diffexited 0, with "0 judged against a declared surface, 4 NOT MEASURED": no declared surface coverspackages/lintor.changeset. SoClause-②: norests on this reading, not on that tool: no spec key, no enum arm, no barrel export, no registration.eslint, narrowed and proven.
.tsfiles.eslint.config.mjslints**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the changeset.mdis ignored by it.npx eslint --no-inline-config --format jsonon those 3 files: 3 files, 0 errors, 0 warnings.parserOptions.project(no type-aware linting), so this diff cannot move the verdict on any untouched file. The repo-widepnpm lintis left to CI.Acceptance notes
includeentries and cubejoinskeys are not in this card's slot list, and they are unchanged. A bareinclude: ['account']is credited by the general walk as a read of the base lookup. A dottedinclude: ['account.region']credits nothing on its own. The pin uses the dotted form, so the member's own credit of each lookup is what it measures. No carrier.resolveCubeColumnanswersokfor a member path through a join keyed by an alias that is no field on the base object.CubeJoinSchema.namedescribes such a join as one that "joins on a column the base object does not have, so nothing resolves". This rule does not take thatokon trust: it also resolves the hop prefix on the base object, finds no field, and records the leaf as a carrier. The cube leg's own reading was not changed (out of scope), and this was read from code only. No carrier.origin/main. The 3 commits that landed since BASE touch theservice-analyticsnative SQL strategy, a TSDoc block inpackages/spec/src/data/field-scale.tsand the spec liveness README. None of them touchespackages/lint, the hop resolver or the analytics schemas.Generated by Claude Code