Skip to content

Commit 9c1897e

Browse files
docs: name the reference dimension class instead of listing two of its four members (#18474)
Part of #17483. Two authored pages stated the label-resolving dimension class by naming two of its four members. PR #17470 (card #16390) deleted `service-analytics`'s private two-member `LOOKUP_TYPES` subset and routed every site that classified a dimension through spec's `referenceTargetOf` instead, so a `user` or `tree` dimension now yields a label exactly as a `lookup` one already did. Both sentences were true when the card was filed and are false on `main` today; the card's `Blocked-by:` fence is spent (#16390 is closed, PR #17470 merged as `113050e2c2`). ## The two sentences, before and after All tree readings below were taken in this worktree at `8fe5cb8e5` (the merge base of this branch) between 14:13Z and 14:39Z on 2026-09-16; the diff itself is commit `39528063a`. `origin/main` had moved on to `0fe38a92f` (committed 14:08:52Z) by the last fetch at 14:39Z. ### Site 1 — `content/docs/data-modeling/analytics.mdx`, line 211 (before), line 211 (after) Before: > - **Dimensions** — a `select` dimension returns its option **label** (not the stored > value), a `lookup` / `master_detail` dimension returns the related record's **display > name** (not the FK id), and a `date` dimension with a `dateGranularity` returns a human > bucket label … After: > - **Dimensions** — a `select` dimension returns its option **label** (not the stored > value), a **reference** dimension (any field whose stored value is another record's id, > a `user` person axis included) returns the related record's **display name** (not the FK > id), and a `date` dimension with a `dateGranularity` returns a human bucket label … ### Site 2 — `content/docs/ui/dashboards.mdx`, line 146 (before), lines 146–152 (after) Before: > - A `sortBy` naming a **select** or **lookup** dimension orders by the **display label** > the rows render (the option label / the related record's name), not the stored value or > foreign-key id — and the label is resolved before `limit` applies, so a top-N by name > truncates the right N. Sorting by a measure (the common case) involves no label lookup > and is unaffected. After: > - A `sortBy` naming a **select** or **reference** dimension orders by the **display label** > the rows render (the option label / the related record's name), not the stored value or > foreign-key id — and the label is resolved before `limit` applies, so a top-N by name > truncates the right N. **Reference** is the whole class of fields whose stored value is > another record's id, so a `user` person axis sorts by name too. Sorting by a measure (the > common case) involves no label lookup and is unaffected. ## Why the class is named rather than the members listed Triage's sharpest instruction was: add the missing two, but do not restate the class by listing members if a name for it exists. A name does exist, in three places at once: - spec declares the set as `REFERENCE_VALUE_TYPES` (`packages/spec/src/data/field-value.zod.ts:153`); - the implementation's own header calls it "**the reference class**" (`packages/services/service-analytics/src/dimension-labels.ts:11`), and `createOrderLabelResolver`'s doc comment says a dimension is label-bearing when its field "carries select `options` or belongs to the reference class and names a target"; - `content/docs/**` already uses the same noun in prose — "a reference field" (`content/docs/api/data-api.mdx:199`), "the reference field types" (`content/docs/data-modeling/queries.mdx:327`, `content/docs/protocol/objectql/query-syntax.mdx:739`). So both sentences now name the class. Appending `user` and `tree` to each list would have produced a four-item prose copy of a four-item `const` — a second copy that goes stale the next time the set moves, which is exactly the failure this card is. A named class with a definition ("any field whose stored value is another record's id") stays true when a fifth member lands. Two deliberate choices inside that: - **"reference" is bolded, not backticked.** There is no `reference` *field type*; there is a `reference` *property* on a reference field. Code-formatting the word would have invented a fifth field type. Bold marks it as the class noun, matching how `dashboards.mdx` already bolds **select** and **derived measure**. - **`user` is named as an example, not as a list member.** The card's finding is directional: a reader asking "does a person axis render a name?" read the old enumeration and concluded *no*. A class name alone would leave that reader to classify `user` themselves, which is the very belief gap #16390 was filed to close. An open example ("a `user` person axis included") answers the question inside the paragraph and — unlike a closed enumeration — claims no completeness, so a future fifth member does not falsify it. ## How `REFERENCE_VALUE_TYPES`' members were confirmed By **module import** of the built package, not by grepping the declaration — a grep over that file also matches four TSDoc comments that quote the same members, and a comment miscounted as a row is a known way to get this wrong by one. Run from `/home/user/objectstack-issue-17483/packages/spec` after `pnpm --filter @objectstack/spec build`: ``` import { REFERENCE_VALUE_TYPES, referenceTargetOf } from '@objectstack/spec/data'; ``` ``` MEMBERS=["lookup","master_detail","user","tree"] SIZE=4 has(user)=true has(tree)=true CONTROL has(select)=false has(text)=false referenceTargetOf({type:user})=sys_user referenceTargetOf({type:tree,reference:"dept"})=dept CONTROL referenceTargetOf({type:select})=undefined ``` `SIZE=4` is read from the live `Set`, so it counts members and cannot count a comment. The two negative controls show the probe discriminates rather than answering `true` to everything. ## Site 2's absence from drift output is structural `content/docs/ui/dashboards.mdx` names neither `master_detail` nor any symbol in PR #17470's diff — it states the rule by its **inputs** (`sortBy`, `select`, `lookup`, "display label"), and shares no identifier with the **emitter** that implements it. An emitter-only diff therefore cannot list it, on that run or on any run; the drift bot's own "What this run could not see" section states that limit by name. This page was found by a hand sweep over output phrasings, not by the tool. **Its absence from any drift output is structural and is never evidence that it is fine.** Both pages were re-derived by hand for this PR: neither contained the string `user` or `tree` as a field type anywhere (0 hits in each) before this change. ## Changeset: none, and the `files[]` readings behind that Measured, not assumed — the criterion is whether anything **published** moves. | reading (all taken 14:35Z, tree `39528063a`) | value | |---|---| | paths this PR touches | `content/docs/data-modeling/analytics.mdx`, `content/docs/ui/dashboards.mdx` | | package directories containing a touched path | **0** — `content/docs/**` is at the repo root, inside no package | | non-private packages under `packages/**` and `apps/**` | **70** | | of those, packages declaring no `files[]` at all | **0** — every one declares it | | `files[]` entries that escape their own package directory (begin with `..` or `/`) | **0** | | `apps/docs` (`@objectstack/docs`, the renderer of `content/docs/**`) | `private: true`, `files` absent ⇒ never published | | root `package.json` (`@objectstack/spec-monorepo`) | `private: true`, `files` absent | `files[]` globs resolve relative to their own package directory, and no package declares an escaping entry, so no published tarball can contain a repo-root `content/docs/**` path. The one package that consumes these files is private. ⇒ nothing published moves; `skip-changeset` applies. This was measured rather than taken from the docs-only reflex, which has been wrong before on a package whose `files[]` ships its own `CHANGELOG.md` — that shape needs the touched path to sit *inside* a package directory, and neither of these does. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` over the two touched paths, from this worktree. **39 families derived, 39 run, 0 NOT MEASURED, 0 unrun** — reconciled by handing the exit codes back to the same tool (`--ran`, exit 0, 14:38Z). - **38 exit 0.** - Four of them first exited **3 (PREREQUISITE NOT MET)** in an unbuilt fresh worktree (`check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`). Exit 3 is neither a pass nor a finding; after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react` (verify-lock VERDICT command-exit 0, 418 s) all four re-ran at **exit 0**. - **One exit 1, not caused by this diff:** `check:cross-package-test-inputs` reports `packages/cli/test/init-created-files-summary.e2e.test.ts` descending `packages/spec/dist/` with no declared glob reaching inside it. That gate is a scan of test sources and `turbo.json`; this diff contains **0** files with a `.ts/.mts/.mjs/.js/.json` extension, so its inputs are byte-identical to the merge base. Re-run after the builds above: same finding, same exit 1. Repo-wide `pnpm lint` (`eslint . --no-inline-config`) was **narrowed, and the narrowing is measured**, not assumed: 1. **Population, read from eslint's own config:** every `files` entry in `eslint.config.mjs` is `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or narrower. `.mdx` appears in none of them. 2. **File count, read from `--format json`:** running eslint directly on both touched paths returns, for each, `File ignored because no matching configuration was supplied.` — `errorCount: 0`, `warningCount: 1`, **zero rules applied**. This diff contributes 0 files to the linted population. 3. **Invariance for untouched files:** type-aware linting is not enabled anywhere in this config — no `parserOptions.project`, no typed `@typescript-eslint` rules (stated at `eslint.config.mjs:328`) — so no `.mdx` edit can move the verdict on any file it does not contain. Also run as an MDX parse check: `pnpm exec fumadocs-mdx` in `apps/docs`, **exit 0** — both pages still compile. CI's two path-scheduled jobs (`Test Core` shards and `Build Docs`) have no local invocation and are declared to CI. ## Risk Prose only, in two bullets, on two pages. No `packages/**` file is touched; the spec fact the prose now names was read out of the built package, not authored here. Rollback is a revert of one commit. --- _Generated by [Claude Code](https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent b6471ba commit 9c1897e

2 files changed

Lines changed: 7 additions & 4 deletions

File tree

‎content/docs/data-modeling/analytics.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ The query result is presentation-ready — authors do not format dimension or
208208
measure values by hand:
209209

210210
- **Dimensions** — a `select` dimension returns its option **label** (not the
211-
stored value), a `lookup` / `master_detail` dimension returns the related
211+
stored value), a **reference** dimension (any field whose stored value is
212+
another record's id, a `user` person axis included) returns the related
212213
record's **display name** (not the FK id), and a `date` dimension with a
213214
`dateGranularity` returns a human bucket label (`month` → `2026-04`,
214215
`quarter` → `2026-Q2`, `year` → `2026`). Unresolved values pass through

‎content/docs/ui/dashboards.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,11 +143,13 @@ Notes on behaviour:
143143
truncates a reproducible window instead of an arbitrary subset.
144144
- Ordering is applied to the finished grid, so a **derived measure** is a valid
145145
`sortBy` even though no single SQL statement computes it.
146-
- A `sortBy` naming a **select** or **lookup** dimension orders by the
146+
- A `sortBy` naming a **select** or **reference** dimension orders by the
147147
**display label** the rows render (the option label / the related record's
148148
name), not the stored value or foreign-key id — and the label is resolved
149-
before `limit` applies, so a top-N by name truncates the right N. Sorting by
150-
a measure (the common case) involves no label lookup and is unaffected.
149+
before `limit` applies, so a top-N by name truncates the right N.
150+
**Reference** is the whole class of fields whose stored value is another
151+
record's id, so a `user` person axis sorts by name too. Sorting by a measure
152+
(the common case) involves no label lookup and is unaffected.
151153
- A `funnel` with no declared stage order falls back to sorting by value
152154
descending.
153155

0 commit comments

Comments
 (0)