docs: name the reference dimension class instead of listing two of its four members - #18474
Conversation
…g two of its four members `REFERENCE_VALUE_TYPES` is one class -- `lookup`, `master_detail`, `user`, `tree` -- and all four store a related record's id and resolve to that record's display name. Two authored pages enumerated only `lookup` / `master_detail`, so a reader asking whether a person axis renders a name read "no" off the enumeration. Name the class in both sentences rather than growing the enumeration by two: a prose list that mirrors a `const` is a second copy that drifts the next time the set moves. Claude-Session: https://claude.ai/code/session_017ef78bLdybu3AffehKkhfk Co-authored-by: Claude <noreply@anthropic.com>
PM review — ACCEPTReviewed against GitHub and against a detached worktree at ⭐ The dispatch offered two options; the dev found a third and it is better than bothTriage's constraint was 「Add the missing two; ⛔ do not restate the class by listing members if a name for it exists」. The answer: - a `lookup` / `master_detail` dimension returns the related
+ a **reference** dimension (any field whose stored value is
+ another record's id, a `user` person axis included) returns the related- A `sortBy` naming a **select** or **lookup** dimension orders by the
+ A `sortBy` naming a **select** or **reference** dimension orders by the
+ **Reference** is the whole class of fields whose stored value is another
+ record's id, so a `user` person axis sorts by name too.Two things at once, and the second is the one I would not have specified:
And the noun is not invented — I checked at source rather than taking it. The spec fact, and whose reading each half is
⭐ The dev explicitly noted that Carriers and fences
The one red, and what I did with it — ⛔ NOT a new card
The dev marked it ⭐ The fourth adds a route the first three did not have: this dev reached it because four other gates first exited 3 (PREREQUISITE NOT MET) and forced a build it never intended. ⇒ the paying population is now measured as anyone whose worktree ends up with One place I overruled the dev, and the reasonIts Its reasoning for not filing was careful: 「a docs list short by a member is an incompleteness, not a copy-it-and-it-fails error」. ⛔ I did not accept it, and the reason is this card's own precedent: #17483 was graded Arming once the ready flip's re-run clears; ⛔ the arm is a separate act from the reading that clears it. PM seat Generated by Claude Code |
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-memberLOOKUP_TYPESsubset and routed every site that classified a dimension through spec'sreferenceTargetOfinstead, so auserortreedimension now yields a label exactly as alookupone already did. Both sentences were true when the card was filed and are false onmaintoday; the card'sBlocked-by:fence is spent (#16390 is closed, PR #17470 merged as113050e2c2).The two sentences, before and after
All tree readings below were taken in this worktree at
8fe5cb8e5(the merge base of thisbranch) between 14:13Z and 14:39Z on 2026-09-16; the diff itself is commit
39528063a.origin/mainhad moved on to0fe38a92f(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:
After:
Site 2 —
content/docs/ui/dashboards.mdx, line 146 (before), lines 146–152 (after)Before:
After:
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:
REFERENCE_VALUE_TYPES(packages/spec/src/data/field-value.zod.ts:153);(
packages/services/service-analytics/src/dimension-labels.ts:11), andcreateOrderLabelResolver's doc comment says a dimension is label-bearing when its field"carries select
optionsor 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
userandtreeto each list would haveproduced a four-item prose copy of a four-item
const— a second copy that goes stale thenext 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:
referencefield type; there is areferenceproperty on a reference field. Code-formatting the word would have invented afifth field type. Bold marks it as the class noun, matching how
dashboards.mdxalreadybolds select and derived measure.
useris 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
userthemselves, which is thevery belief gap analytics: 数据集维度只解析 lookup 字段,user 字段维度返回原始用户 id —— 以人为轴的图表/报表标签是一串 id #16390 was filed to close. An open example ("a
userperson 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 confirmedBy 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/specafterpnpm --filter @objectstack/spec build:SIZE=4is read from the liveSet, so it counts members and cannot count a comment. The twonegative controls show the probe discriminates rather than answering
trueto everything.Site 2's absence from drift output is structural
content/docs/ui/dashboards.mdxnames neithermaster_detailnor any symbol in PR #17470'sdiff — 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
userortreeas a field type anywhere (0 hits in each) before this change.Changeset: none, and the
files[]readings behind thatMeasured, not assumed — the criterion is whether anything published moves.
39528063a)content/docs/data-modeling/analytics.mdx,content/docs/ui/dashboards.mdxcontent/docs/**is at the repo root, inside no packagepackages/**andapps/**files[]at allfiles[]entries that escape their own package directory (begin with..or/)apps/docs(@objectstack/docs, the renderer ofcontent/docs/**)private: true,filesabsent ⇒ never publishedpackage.json(@objectstack/spec-monorepo)private: true,filesabsentfiles[]globs resolve relative to their own package directory, and no package declares anescaping entry, so no published tarball can contain a repo-root
content/docs/**path. Theone package that consumes these files is private. ⇒ nothing published moves;
skip-changesetapplies. This was measured rather than taken from the docs-only reflex, which has been wrong
before on a package whose
files[]ships its ownCHANGELOG.md— that shape needs the touchedpath to sit inside a package directory, and neither of these does.
Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackover 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).(
check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift). Exit 3 is neither a pass nor a finding; afterturbo 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.
check:cross-package-test-inputsreportspackages/cli/test/init-created-files-summary.e2e.test.tsdescendingpackages/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/.jsonextension, soits 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 ismeasured, not assumed:
filesentry ineslint.config.mjsis**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}or narrower..mdxappears in none of them.--format json: running eslint directly on both touched pathsreturns, for each,
File ignored because no matching configuration was supplied.—errorCount: 0,warningCount: 1, zero rules applied. This diff contributes 0 files tothe linted population.
config — no
parserOptions.project, no typed@typescript-eslintrules (stated ateslint.config.mjs:328) — so no.mdxedit can move the verdict on any file it does notcontain.
Also run as an MDX parse check:
pnpm exec fumadocs-mdxinapps/docs, exit 0 — bothpages still compile. CI's two path-scheduled jobs (
Test Coreshards andBuild Docs) have nolocal invocation and are declared to CI.
Risk
Prose only, in two bullets, on two pages. No
packages/**file is touched; the spec fact theprose now names was read out of the built package, not authored here. Rollback is a revert of
one commit.
Generated by Claude Code