Repository navigation
feat(lint): export the dataset dimension leg group-key classifier groupKeyClassOf - #23520
Conversation
…lassOf Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
…port-group-key-classifier
… discovery shape Claude-Session: https://claude.ai/code/session_01D6F9axfoPzR7t8G1J7Ytzz Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. 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 e85b07e919ed8077531faade3ac75600fcd9d290 && git checkout e85b07e919ed8077531faade3ac75600fcd9d290
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 222293f60a7fff2779269b20bb4e5ce28a34c935 dfe60e3486ef23d1c3da427d1964eb9adfd80158 && git checkout -B drift-repro 222293f60a7fff2779269b20bb4e5ce28a34c935 && git merge --no-ff dfe60e3486ef23d1c3da427d1964eb9adfd80158
node scripts/docs-audit/affected-docs.mjs --json 222293f60a7fff2779269b20bb4e5ce28a34c935 |
Contract reviewServed-tier: Inputs read: card #23235 (body; comments ① Derived judgmentsPublic-surface changes the diff implies (
Nothing in the diff is judged wrong. ② Semver levelChangeset ③ Boundary flags
Check-runs on Precedent: PR #23496 (#23272), merged Implemented-by: VERDICT: PASS Adopted by Generated by Claude Code |
Fixes #23235
Clause-②: yes
What
@objectstack/lintnow exports the dataset dimension leg's group-key classifier from the package barrel:groupKeyClassOf(shape: ValueShapeFieldDef): GroupKeyClass | null.nullmeans the column stores one scalar value and the analytics door serves it as a group key. Otherwise it names the JSON-stored class the door refuses with400 INVALID_FIELD:'structured-json'(STRUCTURED_JSON_TYPES) or'multi-value'(isMultiValueField). The two spec predicates are applied in the door's order.GroupKeyClass='structured-json' | 'multi-value', the named result type.jsonStoredDimensionFindingnow uses it in place of the inline literal union.dimension-json-stored-field-refusedkeeps calling the same function on both legs (dataset dimensions at thedatasets[i].dimensions[k].fieldsite, cube dimensions at theanalyticsCubes[i].dimensions.NAME.sqlsite). The barrel re-exports the function the module defines. There is no wrapper and no second copy. The rule's findings, paths and texts are unchanged: no existing assertion in the rule's suite was edited.Why this input shape, and no wrapper
The brief asked whether a consumer must pass
ValueShapeFieldDefor whether a wrapper should take a field definition. Measured onmain:ValueShapeFieldDefis not private.@objectstack/spec/dataexports it (field-value.zod.ts). It is the value contract's structural slice of a field definition:type,multiple,options. Its docblock says it is structural so callers holding their own field-def types can pass theirs as they are.Fieldis assignable to it, sogroupKeyClassOf(field)works with no adapter. A new case pins this at compile time: threeFieldliterals (tags,selectwithmultiple: true, a plainselect) are passed with no cast. The lint package's test layer is type-checked, andcheck:test-typecheckis OK forpackages/lint/tsconfig.test.json.groupedClassOfinservice-analytics/src/structured-json-dimension-door.ts) takes the sameValueShapeFieldDef. The export accepts exactly the shape the door judges, so the input is not widened. A wrapper over a field definition would add a second signature for the same verdict.Pin: the export and the rule agree on the rule's own fixtures
New describe block
[#23235] groupKeyClassOf — the exported classifier and the rule agree on the rule's own fixtures. It imports the classifier through./index.js, the consumer's door:toBeidentity).FieldType.optionsentry, with and withoutmultiple: true, the export's answer equals the class named in the rule's finding, ornullwhen the rule is silent, on the rule's owndimensionStackfixture. Floors are exact on the two refusing classes (2 × STRUCTURED_JSON_TYPES.sizeand2 × MULTI_OPTION_TYPES.size + MULTI_CAPABLE_TYPES.size) and more than 50 on the served side, so a classifier that always answers the same thing cannot make the agreement vacuous.json,text,multiselect,checkboxes,tags,selectwithmultiple: true,select) give the same expected class from both the export and the rule.cubeStackfixture object (fx_ledger) gives the same answer as the rule's cube dimension finding. All three answers are exercised.Fieldpasses verbatim (see above).The class is read from the finding's message words (
a structured-JSON value/a multi-value field) and the column name. Several findings, or words naming no class or both, read as'unreadable', which no classifier answer equals.Ablation (each leg's call site replaced by a type-only private copy)
Both runs went through
scripts/ablation-replace.mjsunder the verify lock. The anchor hit once and moved 1 -> 0. Each run restored: the blob after restore equals the HEAD blob3fc39652c3acandgit diff HEADis empty. The test file importssrcthrough relative paths, so nodistis involved.const cls = groupKeyClassOf(shape);replaced by aSTRUCTURED_JSON_TYPES-only check): 4 failed / 61 passed (65). Two of the failures are the new pins:agrees with the dataset leg on every declared FieldType…andagrees with the dimension leg on its named fixtures.const cls = groupKeyClassOf(column.shape);replaced the same way): 3 failed / 62 passed (65). One of the failures is the new pin:agrees with the cube leg on every column of the cube fixture.The observed direction is red, as expected.
Landing spot
As claimed:
packages/lint/src/validate-dataset-measure-aggregates.ts,packages/lint/src/index.ts, the rule's test file, and.changeset/23235-export-group-key-classifier.md(@objectstack/lint: minor,Clause-②: yes, additive).@objectstack/linthas no API-surface snapshot.check:published-readme-exportsis green, and the lint README lists no exports.Verification (final head
dfe60e348)pnpm --filter '@objectstack/lint^...' build: exit 0.pnpm --filter @objectstack/lint build: exit 0,check-dts-emitted6/6.groupKeyClassOfandGroupKeyClassare indist/index.d.ts, and the CJS (dist/index.cjs) and ESM entries both load and answermulti-value/structured-json/null.pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2: 139 files / 6731 tests passed.pnpm --filter @objectstack/lint typecheck(tsc --noEmit+check:test-typecheck): exit 0.pnpm --filter @objectstack/spec run test:repo: 60 files / 1029 tests passed (spec rebuilt after mergingorigin/main).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 62 commands, pluscheck:published-readme-exports,check-changeset-fixed.mjsandcheck:error-code-casingfrom the roster block. All exit 0 except:check:where-matcherfailed on the first draft of the pin's helper. Its structural discovery seated the helper as a WHERE matcher (Object.keys(...)over a same-file map plus an indexed first parameter). The helper was rewritten to destructure and to read aMap. The gate is now green: 0 new matchers, baseline unchanged.check:dual-build-cjs-loadsandcheck:lean-entry-closureprinted PREREQUISITE NOT MET (exit 3): they need a whole-workspacedist/. These are NOT MEASURED locally and are declared to CI. The lint package's own CJS/ESM load was probed directly (above).Acceptance notes
service-analytics'structured-json-dimension-door.tscomposes the same two predicates in a privategroupedClassOf. That is a second account of the door's order, inside the door itself. The two agree today: same predicates, same order, same input type. They are held equal only by each side's sweep againstSTRUCTURED_JSON_TYPES/isMultiValueField, not by a shared call.service-analyticsdoes not depend on@objectstack/lint(that direction is wrong), so one copy for both would have to live in@objectstack/spec/data. That is a different card's decision. Carrier: none.dataset-derive.tsisDimensionFieldto this export is cloud's card, per the triage direction.Generated by Claude Code