Repository navigation
Commit 9c1897e
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
208 | 208 | | |
209 | 209 | | |
210 | 210 | | |
211 | | - | |
| 211 | + | |
| 212 | + | |
212 | 213 | | |
213 | 214 | | |
214 | 215 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
143 | 143 | | |
144 | 144 | | |
145 | 145 | | |
146 | | - | |
| 146 | + | |
147 | 147 | | |
148 | 148 | | |
149 | | - | |
150 | | - | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
151 | 153 | | |
152 | 154 | | |
153 | 155 | | |
| |||
0 commit comments