Repository navigation
fix(service-analytics, lint)!: a dataset or cube dimension over a formula field is refused where it is authored, instead of on the first query that groups by it - #30815
Conversation
…ld at compile Claude-Session: https://claude.ai/code/session_016WV4cz1PAaWWtcjFnbNsbW Co-authored-by: Claude <noreply@anthropic.com>
… a virtual field at authoring Claude-Session: https://claude.ai/code/session_016WV4d1SZmabyrNd3YLCq3J Co-authored-by: Claude <noreply@anthropic.com>
…la dimension end to end Claude-Session: https://claude.ai/code/session_016WV4d1SZmabyrNd3YLCq3J Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1SZmabyrNd3YLCq3J Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): ⛔ 1 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 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 9905b1a4e47b580bb9d695444a7ee8b0704a4703 && git checkout 9905b1a4e47b580bb9d695444a7ee8b0704a4703
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b 43db93dfabc5a2e6da08abb884644d2c69a01263 && git checkout -B drift-repro 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b && git merge --no-ff 43db93dfabc5a2e6da08abb884644d2c69a01263
node scripts/docs-audit/affected-docs.mjs --json 9b24f3ca8734b5e0ea77cc3cb282c3f8f55a862b
|
|
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5 Contract reviewServed-tier: Inputs read: card #30773 body and its three comments (triage 6675591690, seat claim 6676304413, os-dev-report 6677212943); #24426 body and its PR #24505's file list; the dispatch order and the dev's PR body, report, gates and reconcile files on the project mount; PR #30815 body, 10-file list, commit list with trailers, and the net diff vs ① Derived judgments
② Semver levelBoth carriers read Ledgers. ADR-0087 disposition. The Check-runs on ③ Boundary flags
④ Design judgment
Findings (none must-fix):
Implemented-by: VERDICT: PASS |
…N-stored dimension rule Claude-Session: https://claude.ai/code/session_016WV4d1SZmabyrNd3YLCq3J Co-authored-by: Claude <noreply@anthropic.com>
…follow-up #30824 The changeset still said validating-metadata.mdx did not list the id; a97ed9f moved it, and the skills formula row is follow-up #30824. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4d1SZmabyrNd3YLCq3J
|
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5 ACCEPT · #30773 · 2026-11-06T05:35Z · Head-sha: 席位核对
|
Requested by Elon · project thread
domain:server#3 · os-elon-musk · session_016WV4cytQjG9X1CrCgytUM5
Fixes #30773
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: updated here —
content/docs/deployment/validating-metadata.mdxnamesdimension-virtual-field-refusedbesidedimension-json-stored-field-refusedand in its cube paragraph (seat-approved surface addendum); follow-up #30824 —skills/objectstack-data/rules/field-types.md's formula row (Tier H, the skills lane).维护者速读
registerDataset/queryDataset)给DATASET_INVALID/ 400,点名数据集、维度、位置和修法;os validate/os build/os lint与运行时保存数据集(Studio、REST/meta、MCP 共用的那道门)报新规则dimension-virtual-field-refused。packages/services/service-analytics/src/dataset-compiler.ts的assertDimensionFieldStored;packages/lint/src/validate-dataset-measure-aggregates.ts的virtualDimensionFinding及数据集、cube 两腿的调用点;preview-date-bucket-type.test.ts里公式维度从共享数据集拆出(原 analytics: a date bucket over a formula field with no stored column renders strftime in the /analytics/sql dry run and serves one null group in the draft preview, while both live faces refuse from the driver #29167 / analytics: a plain groupBy over a formula field answers DATABASE_ERROR 500 on the native composition, INVALID_FIELD 400 on ObjectQL, and a null group on the preview and in-memory engine #29304 的数据集面钉现在钉编译期拒绝)。pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/aggregate-nontemporal-measure-refusal.test.ts,预期全部通过,含 6 条[#30773]用例。What changes
@objectstack/service-analytics: the compile refuses the dimension.compileDataset's dimension loop now callsassertDimensionFieldStoredafterassertDeclared, the way the measure loop calls its type gate after the join check. A dimension whosefieldthe base object declares VIRTUAL, judged by the spec'sisVirtualSearchField(SEARCH_VIRTUAL_TYPES, todayformula), whatever itsreturnTypeand with or withoutdateGranularity, throwsdatasetInvalidError(DATASET_INVALID/ 400). The message names the dataset, the dimension, its position (dimensions.N), the field and its declared type, and the remedy the query-time doors name: denormalise the value onto the object (a stored field, written when the source changes) and point the dimension at that field.compileDatasetthrough the service's onecompile():registerDataset(the configuration door),queryDataseton every face (native, ObjectQL, the draft preview), and the build probes that query through it.declaredValueShapehook, a field the hook does not resolve, or a dotted relationship path (the hook answers for the base object only) is not judged here. A dotted dimension over a joined formula stays the query-time doors' (analytics: a plain groupBy over a formula field answers DATABASE_ERROR 500 on the native composition, INVALID_FIELD 400 on ObjectQL, and a null group on the preview and in-memory engine #29304) and the lint leg's.@objectstack/lint:dimension-virtual-field-refused.dimension-json-stored-field-refusedinvalidate-dataset-measure-aggregates.ts, so it rides the one registry entry: gating, onos validate/os build/os lint, and at the runtimedatasetwrite door (422). One verdict for a dataset dimension (datasets[N].dimensions[M].field) and an authored cube dimension (analyticsCubes[N].dimensions.KEY.sql), resolved on the object graph exactly as the JSON-stored leg resolves it, dotted paths included (judged on the leaf's object), with the same skips. The predicate is the spec'sisVirtualSearchField, never a local list. A formula is never JSON-stored, so the two dimension ids never fire on one member.field-no-consumerswarning is one 856-character line, printed by validate, build and dev alike — one-line verdict +rule:id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161 siblings. Verdict: groups by field "FIELD" (formula on object "OBJECT"), a value computed on read and never stored, so no query can group by it. Fix: Store the value in a stored field of "OBJECT" (written when its source changes) and point this dimension at that field, or remove the dimension. The reasoning (which doors refuse it, why a computed value is no group key) isos explain dimension-virtual-field-refused(rule-explanations.ts). The id's constant is exported from the package barrel beside its two siblings.Measured before the change (on
origin/main2d04cd2b, probes since deleted):formula:registerDatasetACCEPTED;queryDatasetselecting a stored dimension of it SERVED (1 statement); selecting the formula dimension refused only at query time.sqlnames a formula column: no authoring door judged it (cubes are registered without a compile; the query-time door refuses it since analytics: a plain groupBy over a formula field answers DATABASE_ERROR 500 on the native composition, INVALID_FIELD 400 on ObjectQL, and a null group on the preview and in-memory engine #29304).runAuthoringRuleson validate / build / lint: zero findings for either (the only finding on the probe stack was an unrelatedexpression-invalidon the probe's own formula, fixed in the probe).Tests
service-analyticsaggregate-nontemporal-measure-refusal.test.ts(the lint: the measure-aggregate-field-type-refused hint for a formula total says "aggregate it with count", where the spec's own remedy is "store the value as a number field" (cloud#2861 ruling B item 4) #24426 harness), new top-level describe[#30773]: the request door refusesDATASET_INVALID/ 400 with the exact message and no SQL emitted, for a selection naming the formula dimension and for one naming only the stored dimension; the configuration door (registerDataset) refuses in the same words; a date dimension bucketed by month over a formula is refused at its position; controls (a stored, a date-bucketed and a numeric dimension compile and query); the three cannot-answer tiers do not block; the measure gate over the same formula is unchanged (acountcompiles, asumkeeps the stored-number remedy).service-analyticspreview-date-bucket-type.test.ts: the sharedDATASETdeclared three formula dimensions beside its stored ones, so every case in the file went red under the compile gate (19 red, measured). The formula dimensions moved toFORMULA_DATASET/FORMULA_TEXT_DATASET; the analytics: the draft preview serves a time bucket over a text column as one null bucket, where the live faces refuse INVALID_FIELD #28852 / analytics: the /analytics/sql dry run renders a time bucket over a text, select, number or boolean column, where the query refuses INVALID_FIELD #28977 cases are byte-unchanged and green, and the five analytics: a date bucket over a formula field with no stored column renders strftime in the /analytics/sql dry run and serves one null group in the draft preview, while both live faces refuse from the driver #29167 / analytics: a plain groupBy over a formula field answers DATABASE_ERROR 500 on the native composition, INVALID_FIELD 400 on ObjectQL, and a null group on the preview and in-memory engine #29304 dataset-face cases now pin the compile refusal alike on every face (native, ObjectQL, preview), plus a case for a selection naming only the stored dimension. The query-time refusal itself stays pinned where no dataset compile stands in front of it: the engine door,objectql-echo-non-date-bucket.test.ts(the dry run) andfallback-delegate-date-bucket-type.test.ts(both untouched, green).lintvalidate-dataset-measure-aggregates.test.ts, new top-level describe for the id: the dataset pin with exact message and hint plus a text control; a date-typed dimension bucketed over a formula ofreturnType: 'date'; a sweep over everyFieldTypeagreeing withSEARCH_VIRTUAL_TYPES(floors on both sides); the later position beside a stored dimension, never under the JSON-stored id; a dotted dimension judged on the joined object; a cube dimension over a formula column plus a stored control; the skips; the measure leg over a formula unchanged; throughrunAuthoringRuleson all three commands. The [maintainer] validate: thefield-no-consumerswarning is one 856-character line, printed by validate, build and dev alike — one-line verdict +rule:id + a pointer to the full reasoning (os explain, which today takes only schema names) #22161 one-line verdict / fix blocks now cover three ids.lintruntime-gate.dataset-writes.test.ts: a LIT case through the real dataset write door (runRuntimeAuthoringRules), refusal atdatasets.acme_invoice_metrics.dimensions[1].field;OBJECTSgained one formula field, every other case unchanged and green.dogfoodanalytics-formula-dimension-authoring.dogfood.test.tson the stock showcase (showcase_project.budget_remaining, a formula):PUT /meta/datasetanswers 422 naming the id atdimensions[1].field, andPOST /analytics/dataset/querywith the inline dataset answers400 DATASET_INVALIDnaming the dimension and the remedy, for a selection with and without the formula dimension; controls save and serve the dataset without it. 5/5, plus the analytics: a plain groupBy over a formula field answers DATABASE_ERROR 500 on the native composition, INVALID_FIELD 400 on ObjectQL, and a null group on the preview and in-memory engine #29304 sibling 5/5.Package runs
b100c406(the implementation commit, before theorigin/mainmerge; the commits after it touch only the dogfood pin and the changeset, and the merge brought in no file of either package):@objectstack/service-analyticsfull suite 211 files / 5015 passed, 300 skipped (lock VERDICT command-exit 0);@objectstack/lintfull suite 195 files / 8236 passed (VERDICT 0);typecheckfor both packages exit 0.preview-date-bucket-type.test.ts, 19 cases (the shared fixture declared formula dimensions), which is what the fixture split answers.6c77bf99(the merge commit); the pin alone 5/5 at0dd6d595, and both inside the full battery below.Ablations (fix committed first; `scripts/ablation-replace.mjs`, which restores to HEAD and checks `git diff HEAD` is empty)
assertDimensionFieldStored(...)call incompileDataset(anchor 1 → 0, blob9bf5497fcd8b→6e7d8fe55a4a). 10 red: the three new request / configuration / bucket door cases inaggregate-nontemporal-measure-refusal.test.tsand the seven[#30773]cases inpreview-date-bucket-type.test.ts; 66 green, including the controls, the tiers and every measure pin. Restored: blob == HEAD.if (isVirtualSearchField(shape)) {with a constant-false guard (blob5f981455daa6→5e2fc683fa4f). 7 red: six cases of the new describe and the runtime write-door LIT case; the cube case stayed green (its own leg). Restored: blob == HEAD.if (isVirtualSearchField(column.shape)) {(blob →0e485f1cf802). 1 red, the cube case. Restored: blob == HEAD.src/by relative path, so nodist/rebuild was needed.Acceptance notes
queryDatasetcompiles before the draft preview evaluates, and both read the samesourceFieldMetahook, so the preview's call toassertDatasetGroupTargetsStoredno longer sees a direct formula dimension (and itsdeclaredTypereads the base object only, so it never judged a dotted one). It is left in place as the date-bucket door's partner; carrier: none (noted, not filed).validating-metadata.mdxparagraph is folded in here ata97ed9f0as a surface addendum; the skills formula row is follow-up skills: the formula row of objectstack-data field-types should say a formula is no group key either (follow-up to #30773) #30824 (Tier H, the skills lane); an ADR-0087 semantic upgrade-guide entry mirroring18.dataset-measure-aggregate-field-type-refusedis follow-up spec: an upgrade-guide entry for a dataset or cube dimension over a formula field, now refused at authoring (follow-up to #30773) #30822 (the spec lane), to land before the release that ships this change.098b2b92carries aClaude-Session:trailer naming the wrong session id (the coordinator's); later commits carry this run's. The branch is aclaude/project-thread-*branch, so it is not rewritten; the seat ruled it has no landing effect (squash merge).Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat0dd6d595(after mergingorigin/mainat172eebb2,pnpm install --frozen-lockfileand a whole-treeturbo run build, 119/119 tasks) derived 64 commands. All 64 ran and exited 0;--ranreports 64 run, 0 NOT-MEASURED, 0 UNRUN.check-adr-0087-registrationreads the changeset as[BREAKING+bang+clause-②-narrowing+metadata-protocol-narrowed] not-required (no-migration-prescription).After the docs commit
a97ed9f0(content/docsonly), the derivation grew to 89 families; the 25 new ones plus seven docs-reading families from the first pass were run ata97ed9f0, all exit 0, and--ranover the combined record reports 89 run, 0 NOT-MEASURED, 0 UNRUN. The other families read no file the docs commit changed and keep their0dd6d595readings.Dogfood battery (title carries
!):0dd6d595·pnpm --filter @objectstack/dogfood testunder the verify lock, VERDICT command-exit 0 · Test Files 424 passed, 3 skipped (427) · Tests 3537 passed, 41 skipped (3578) · no failure.Generated by Claude Code