Skip to content

fix(lint): field-no-consumers credits every field an analytics member's column path reads (#21439) - #21460

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21439-field-consumers-relationship-path
Oct 2, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21439-field-consumers-relationship-path

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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's field, and a cube dimension's and measure's sql. 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 (fieldsOfColumnSql in service-analytics).

  • One hop resolver. A cube's path goes through resolveCubeColumn, the resolver the cube leg of validate-dataset-measure-aggregates.ts already uses: the cube's declared join for the hop, else the lookup's reference. That function is now exported from its module. It is not re-exported from the package barrel: resolveCubeColumn appears 0 times in dist/index.d.ts, dist/index.d.cts and dist/runtime.d.ts. A dataset's path goes through resolveFieldPath plus joinablePrefixes(include), the pair validate-dataset-references.ts uses. The rule asks the resolver about each prefix of the path (account, then account.region, then account.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 one export keyword and one docblock paragraph, so nothing it refuses changes.
  • One reading per slot. The general text walk now skips those four config paths. It read a dotted path as one token and credited neither end. It also credited nothing for a bare cube column, because a cube names its object in its own sql and the walk's object context never reads that.
  • Paths the door does not read. A path counts as read only when every prefix resolves and, on a dataset, include declares the relationship prefix. Otherwise:
    • The door refuses a path whose hop or column does not resolve, and a dataset path whose join include does not declare. (compileDataset refuses the dataset, and dataset-field-not-included already 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: an inlineColumns entry with no inlineEdit is the precedent. So the verdict is carrier-only and 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.
    • The object graph cannot judge some paths: a lookup to an object this stack does not define, a hop through an injected column, a relationship with no target. Such a path credits the fields it does resolve. The door joins through them, and "cannot answer" is no evidence that nothing reads them.
    • The row wildcard '*' reads no field and credits none.
  • Ruling B (5950947151) stands. Only explicit, authored member paths are credited. Nothing drawn by default is counted.
  • The finding's message names cube members among the consumers, and refused analytics paths among the carriers.
  • One @objectstack/lint patch 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.account is a lookup to fx_account, and fx_account.region is a lookup to fx_region. Fields prefixed c_ are read only by a cube member, d_ only by a dataset member (include: ['d_account.d_region']), and u_ only by a dataset measure whose join is not declared. Each slot has a bare, a one-hop and a two-hop member.

before (CLI and lint built at BASE 53fd35e3e3) after (lint rebuilt at 51819a6aaf; later commits are comment-only)
cube: c_kind, c_amount (bare) inert not reported
cube: c_account, c_tier, c_revenue, c_region, c_code, c_zone inert not reported
dataset: d_kind, d_amount (bare) not reported not reported
dataset: d_account, d_tier, d_revenue, d_region, d_code, d_zone inert not reported
undeclared join: u_account, u_revenue inert carrier-only, carrier datasets[1].measures[0].field

Both runs exit 1 on the fixture's one error, dataset-field-not-included on the undeclared measure. The field-no-consumers warnings 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 the tsconfig.test.json program (--listFiles count 1), with 0 errors in it.
  • The rule's test file has 153 tests, 27 of them new. Among them is the enumeration pin: 4 slots × 3 shapes, each asserting that exactly the fields the path reads leave the report. The slot list is read off the spec's own CubeSchema and DatasetSchema through z.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.mjs with a trap restore. Every anchor hit 1 time and then 0. Every restore was proven by blob equal to HEAD and an empty git diff HEAD. Final git status was clean. The subject resolves through relative src imports, so no dist was in the path.

leg mutation result
A1 credit only the leaf, no hop 16 red / 137 green: all 8 one- and two-hop pin cells, plus the join, refused and not-judgeable cases
A2 drop the cube leg 11 red: all 6 cube pin cells (bare included), plus the join and cube refused cases
A3 cube hops through resolveFieldPath instead of resolveCubeColumn 1 red: the declared-join case
A4 refused paths recorded as reads 5 red: every refused-path case
A5 rename one pin builder key 1 red: the spec census equality

Gates.

  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (no paths) at 6ae7c521fe derived 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 --diff exited 0, with "0 judged against a declared surface, 4 NOT MEASURED": no declared surface covers packages/lint or .changeset. So Clause-②: no rests on this reading, not on that tool: no spec key, no enum arm, no barrel export, no registration.

eslint, narrowed and proven.

  • Population: the 3 changed .ts files. eslint.config.mjs lints **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}, and the changeset .md is ignored by it.
  • npx eslint --no-inline-config --format json on those 3 files: 3 files, 0 errors, 0 warnings.
  • The config sets no parserOptions.project (no type-aware linting), so this diff cannot move the verdict on any untouched file. The repo-wide pnpm lint is left to CI.

Acceptance notes

  • Observation, not filed. include entries and cube joins keys are not in this card's slot list, and they are unchanged. A bare include: ['account'] is credited by the general walk as a read of the base lookup. A dotted include: ['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.
  • Observation, not filed. resolveCubeColumn answers ok for a member path through a join keyed by an alias that is no field on the base object. CubeJoinSchema.name describes such a join as one that "joins on a column the base object does not have, so nothing resolves". This rule does not take that ok on 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.
  • Not merged with origin/main. The 3 commits that landed since BASE touch the service-analytics native SQL strategy, a TSDoc block in packages/spec/src/data/field-scale.ts and the spec liveness README. None of them touches packages/lint, the hop resolver or the analytics schemas.

Generated by Claude Code

claude added 4 commits October 2, 2026 18:53
…'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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint, touching 10 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/validating-metadata.mdx (via analyticsCubes (literal, a string literal in creditAnalyticsColumns))
  • content/docs/getting-started/quick-start.mdx (via analyticsCubes (literal, a string literal in creditAnalyticsColumns))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-5.mdx (via analyticsCubes (literal, a string literal in creditAnalyticsColumns))
  • content/docs/releases/v17/17-6.mdx (via analyticsCubes (literal, a string literal in creditAnalyticsColumns))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 4 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 1fd56645af247808e66f64b1ebb63c4ad614c84f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 75781cdd5a96924afa4012726ae4c2d1c620e4a0 — the merge of head 6ae7c521fe7071b507336c035144a35560e1aeb5 into base 1fd56645af247808e66f64b1ebb63c4ad614c84f, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1fd56645af247808e66f64b1ebb63c4ad614c84f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 2, 2026 20:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 2, 2026 20:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 6210f88 Oct 2, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21439-field-consumers-relationship-path branch October 2, 2026 21:03
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

2 participants