Skip to content

feat(spec): a dataset dimension may name its null bucket with nullLabel - #25397

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-qbbntk
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-qbbntk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #25391

Clause-②: yes (widening)
Metadata-protocol: added

The spec half of ruling 6358843349 on #25330 (letter A, maintainer 「同意」), verbatim: "new spec key DatasetDimension.nullLabel (I18nLabel, optional; final spelling settled at spec review), the objectui consumer reads it over chart.nullCategory, and system.datasets.ts sets user_id's null label to 「System」." The consumer half and the system.datasets.ts producer follow after this lands; #25330 remains open for them.

What changes

  • DatasetDimensionSchema.nullLabel (packages/spec/src/ui/dataset.zod.ts): optional, typed I18nLabelSchema (the label slot's own type, so a plain string or an inline locale map), .meta({ title: 'Null Label' }). The describe says it names the bucket of rows whose value for the dimension is null, that it is display copy only (grouping is unchanged), and that consumers fall back to their own default when it is absent. No alias and no guidance entry: no sibling contract spells this slot, so there is nothing to anchor one to.
  • Liveness row dataset.dimensions.nullLabel: planned, evidenceScope: cross-repo, authorWarn: true with an authorHint. Measured: objectstack's dataset compiler copies name / label / field / type / dateGranularity into the Cube dimension and nothing else; objectui at the .objectui-sha pin 0d12e953 and at main a14737b has no reader (its only nullLabel spelling is the local parameter of chart-series.ts pivotSeriesBuckets, fed from ChartSeriesOptions.nullCategoryLabel).
  • Form reconciliation ledger (metadata-form-zod-reconciliation.test.ts): one omit row for dataset / dimensions / nullLabel, in the planned-not-rendered group (page.print, page.record, field.conditionalFormatting). Offering a repeater column now would let an author set a label nothing shows. It also keeps packages/platform-objects out of this diff: a new repeater row child would owe four locale catalog entries there.
  • Generated: content/docs/references/ui/dataset.mdx, packages/spec/authorable-surface/ui.json (ui/DatasetDimension:nullLabel), packages/spec/liveness/state-counts/dataset.md (planned 0 to 1), via check:generated --fix.
  • Changeset .changeset/25391-dataset-dimension-null-label.md: @objectstack/spec minor, with both declaration lines.

Verification (head b22602309)

  • New packages/spec/src/ui/dataset-dimension-null-label.test.ts, 6 cases: the string form and the locale-map form parse and keep their value; an absent key parses to the same object with no nullLabel materialized; nullLabel: 42 is refused as invalid_union at ['nullLabel']; an undeclared sibling key is still refused (unrecognized_keys); a whole DatasetSchema carrying it parses clean.
  • pnpm --filter @objectstack/spec test: 711/711 files, 21633 passed, 1 todo. The first run (before the ledger row) failed exactly one case, dataset.dimensions: accepted by the Zod but unauthorable in the form, which the ledger row answers.
  • pnpm --filter @objectstack/spec typecheck: exit 0. pnpm --filter @objectstack/lint test: 155/155 files, 7178 tests.
  • Author warning measured end to end: lintLivenessPropertiesFromLedgerDir over a stack whose dataset dimension sets nullLabel: 'System' returns one liveness-planned-property finding carrying the row's hint; the same stack without the key returns none.
  • node scripts/pm/dispatch-gates.mjs --commands derived 112 families for this diff; all 112 ran and exit 0. --ran reconciliation: 112 run, 0 NOT-MEASURED, 0 UNRUN. On the first sweep 11 families answered exit 3 (a stale packages/spec/dist after the test edit, and one pinned control commit missing from the shallow clone) and check:generated answered 1 for the same stale dist; after pnpm --filter @objectstack/spec build and a fetch of that one commit, all 12 re-ran and exit 0.

Acceptance notes

  • How the key reaches the renderer is the consumer half's decision. A dataset response describes a dimension column as { name, type, label }, and the compiler does not copy nullLabel into the Cube. The objectui half can read it from the served dataset document, or the response's dimension descriptor can carry it, which would be a service-analytics change. The ruling does not pick one; noted here, not filed.
  • Translation. translation.datasets.NAME.dimensions.DIM declares only label, so a translated null label is written today as an inline locale map on the key itself. A bundle face for it would come with the consumer, if wanted.
  • Final spelling stays open to the spec review, per the ruling.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fd2ukZvGZZbccf6CSDPzuv

…ull bucket

An optional I18nLabel on DatasetDimensionSchema that names the bucket of
rows whose dimension value is null; consumers fall back to their own
default when it is absent. Liveness row lands as planned (authorWarn),
with no reader on either side yet.

Claude-Session: https://claude.ai/code/session_01Fd2ukZvGZZbccf6CSDPzuv
Co-Authored-By: Claude <noreply@anthropic.com>
…ounts for nullLabel

Claude-Session: https://claude.ai/code/session_01Fd2ukZvGZZbccf6CSDPzuv
Co-Authored-By: Claude <noreply@anthropic.com>
…planned-not-rendered

Claude-Session: https://claude.ai/code/session_01Fd2ukZvGZZbccf6CSDPzuv
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/dataset.json, packages/spec/liveness/state-counts/dataset.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/dataset.json, packages/spec/liveness/state-counts/dataset.md) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 144 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b22ab8d6fb3462bff05142efc6b6cc040c95f646 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6317570d7503c463893b6dde9aee6380036b0052 — the merge of head b22602309e10291c37b646e051f0af005cdc1470 into base b22ab8d6fb3462bff05142efc6b6cc040c95f646, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6317570d7503c463893b6dde9aee6380036b0052 && git checkout 6317570d7503c463893b6dde9aee6380036b0052
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b22ab8d6fb3462bff05142efc6b6cc040c95f646 b22602309e10291c37b646e051f0af005cdc1470 && git checkout -B drift-repro b22ab8d6fb3462bff05142efc6b6cc040c95f646 && git merge --no-ff b22602309e10291c37b646e051f0af005cdc1470

node scripts/docs-audit/affected-docs.mjs --json b22ab8d6fb3462bff05142efc6b6cc040c95f646

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b22602309e10291c37b646e051f0af005cdc1470
Local-runs: none
Scope: packages/spec/src/ui/dataset.zod.ts + packages/spec/src/ui/i18n.zod.ts (I18nLabelSchema, the value type), packages/spec/liveness/dataset.json + packages/spec/liveness/state-counts/dataset.md (the row and its count), packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (the form-side gate the new key trips), packages/spec/src/ui/dataset-dimension-null-label.test.ts, packages/spec/authorable-surface/ui.json, content/docs/references/ui/dataset.mdx, .changeset/25391-dataset-dimension-null-label.md; read for the row's claims: packages/services/service-analytics/src/dataset-compiler.ts (compileDataset), packages/spec/src/system/translation.zod.ts (datasets.*.dimensions.*), .objectui-sha and objectui packages/core/src/utils/chart-series.ts at 0d12e953 and 5fef032b. The remainder of the diff is empty: every path of the PR is in scope.

Inputs read at 2026-10-23T07:36Z: card #25391 (body, claim 6358989326, os-dev-report 6359831583), ruling 6358843349 on #25330, PR #25397 (body, 8-file list, net diff origin/main...b22602309), the 39 check-runs on the head.

① Derived judgments

  • Accept set: DatasetDimensionSchema widens by exactly one key. nullLabel: I18nLabelSchema.optional() on the strictObject (dataset.zod.ts:195-200). I18nLabelSchema is z.union([z.string(), InlineLocaleMapSchema]) (i18n.zod.ts:272), so the plain string and the inline locale map both parse, which is what the ruling asked for ("I18nLabel, optional"). Every dimension that parsed before parses unchanged (optional, no default, no transform), and the object stays closed. Right. The six-case test pins each edge the card's Acceptance names: string form, map form, absent key not materialized, 42 refused as invalid_union at ['nullLabel'], an undeclared sibling still unrecognized_keys, a whole DatasetSchema parses. Right.
  • No alias, no guidance entry. Judged right: the only neighbouring spelling in the spec is emptyText, an element:record_picker property (translation.zod.ts:1154, 1233), a different surface; nothing a dataset author would plausibly reach for competes with nullLabel. The spelling itself is the one the ruling left to this review; it matches objectui's own vocabulary for the slot (nullLabel / nullCategoryLabel in chart-series.ts) and sits beside label, whose type it reuses. Spelling accepted.
  • Public surface. authorable-surface/ui.json gains ui/DatasetDimension:nullLabel in sorted position (between :name and :type); content/docs/references/ui/dataset.mdx gains the row in both tables that render DatasetDimension. Both are generator output and the generating gates ran green (Check Changeset, Spec property liveness, Lint · scoped · tooling self-tests succeeded; check:authorable-surface sits in the still-running Lint families, see ③). Right.
  • Liveness row dataset.dimensions.nullLabel: planned, evidenceScope: cross-repo, authorWarn: true, authorHint, dated 2026-10-23. The shape matches the two spec-first precedents (app.navigation.dataSource, field.conditionalFormatting). Its measured claims hold: compileDataset builds the Cube dimension from label, type, sql: d.field and (time only) granularities from dateGranularity and nothing else (dataset-compiler.ts:689-702), so the key is parsed and dropped; objectui at 0d12e953 has no dataset-dimension reader, its only nullLabel spellings being the local parameter of pivotSeriesBuckets fed from ChartSeriesOptions.nullCategoryLabel ?? NULL_CATEGORY_LABEL (chart-series.ts:546-600, 847-868). planned rather than dead is right: the consumer is ruled and sequenced (Setup → System Overview: "Events by User"'s largest bar (~740 events) is labelled "(None)" and each bar has its own colour — label system writes "System" and use one colour per series (hotcrm#2194 T2-U01) #25330), not missing. authorWarn is right: a dimension authored with the key today shows the renderer's default and nothing says so otherwise. Right, with one prose nit: the note calls 0d12e953 "the .objectui-sha pin". That was true at the branch point (c109bfc1), but main moved the pin to 5fef032b in 9a48ae8d at 2026-10-23T06:07Z (chore(console): move the objectui pin 0d12e95361b2 → 5fef032b1ada, so the 18.1.0 console makes one request per open for a nav entry and a page element bound to the same read #25358), so on landing the note names a superseded pin. I read objectui at 5fef032b: the no-reader verdict is unchanged (same four local-parameter hits, nothing reading a dataset dimension), so the status stands; only the pointer is stale. Not a gate matter; the consumer half's live flip rewrites this note anyway.
  • State counts planned 0 → 1, classified 27 → 28: generated by gen:liveness-counts and verified by check-liveness.mts (countsArtifactErrors), which is the Spec property liveness run, success on this head. Right.
  • Form reconciliation ledger: one omit row dataset / dimensions / nullLabel in the planned-not-rendered group beside page.print, page.record, field.conditionalFormatting, with a non-vacuous why. The gate is the one the new key trips (the dev's first run reddened exactly dataset.dimensions: accepted by the Zod but unauthorable in the form), so rule and gate land as one change. The alternative, a repeater column in the dataset form, would offer an author a label nothing shows and would owe locale catalog entries in packages/platform-objects, which the claim forbids. Right.
  • Changeset body and PR body prose match the diff: "parses unchanged", "still refuses any other undeclared key", "nothing reads it yet", "os lint and os validate warn when you set it" (the authorWarn fan-out the dev measured through lintLivenessPropertiesFromLedgerDir). Right.
  • Dedupe: nullLabel has no other spelling anywhere on origin/main. New, as the card says.

② Semver level

.changeset/25391-dataset-dimension-null-label.md: '@objectstack/spec': minor with Clause-②: yes (widening) and Metadata-protocol: added, both line-initial, both repeated line-initial in the PR body. AGENTS.md §3: a new key an author can write is added; a kind takes at least minor on the package the diff moves; yes (widening) is the non-breaking arm. The diff publishes exactly that: one optional key on a published schema, nothing removed, no default or meaning changed. No ADR-0087 marker is owed (not breaking). Check Changeset success on the head. Right.

Clause-②: yes (widening) — matches the diff.

③ Boundary flags

  • Dev deviation 1 (file surface: metadata-form-zod-reconciliation.test.ts, not named in the order): answered in ①. The file is inside packages/spec, the edit is the ledger row the order's own change forces, and the only alternative crosses into the forbidden package. Accepted.
  • Dev deviations 2 and 3 (no MCP assignee / reviewer request; not subscribed to PR activity): process posture, no contract bearing. Noted, not judged here.
  • open_questions: []: none to answer.
  • Ruling clause "final spelling settled at spec review": settled above, nullLabel stands.
  • Out-of-scope observation A (channel to the renderer: the compiler does not copy the key into the Cube, and the served response's dimension descriptor does not carry it): correctly left to Setup → System Overview: "Events by User"'s largest bar (~740 events) is labelled "(None)" and each bar has its own colour — label system writes "System" and use one colour per series (hotcrm#2194 T2-U01) #25330's consumer half; the row's note says so in as many words. Nothing for this PR.
  • Out-of-scope observation B (translation bundle face: translation.datasets.NAME.dimensions.DIM declares label only, translation.zod.ts:1006, so a translated null label is an inline locale map on the key today): correct reading of the file. Escalated to the seat, below, as a question for Setup → System Overview: "Events by User"'s largest bar (~740 events) is labelled "(None)" and each bar has its own colour — label system writes "System" and use one colour per series (hotcrm#2194 T2-U01) #25330's scope, not a defect here: I18nLabelSchema already carries the inline map, and the key reads nothing yet.
  • Stale pin spelling in the liveness note (①): a prose nit, the verdict is unaffected at either pin. Fix at the dev's option or at the live flip.
  • Check-runs on b22602309 (39): 15 success, 6 skipped (Dogfood matrix shards, Temporal Conformance, Console Pin Gate, Dogfood Verify CLI, the two opt-in harnesses), 0 failed, 18 in_progress: Build Core, Build Docs, Test Core 1-6, Type Check × 4 (consumer gates, debt ledger, source gates, workspace), Lint × 6 (query-options erasure, slot lookup & corpus, CI/release/engine gates, ESLint, repo & tooling gates, docs & code guards). Every completed gate is green and the content is right, so this record is a PASS; merge waits on those 18 families. Should any of them fail, this record does not cover that head state and the seat re-reads before the queue.

Implemented-by: claude/project-thread-qbbntk
Reviewed-by: contract-review subagent of session_013odhA26dvzy7pQmevdTTvy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (seat post #6017) · os-tesla · session session_013odhA26dvzy7pQmevdTTvy · 2026-10-23T08:04Z

Head b22602309e · card #25391 · claim 6358989326 · report 6359831583 · ruling 6358843349 on #25330 (letter A) · contract review 6359948450 (PASS, spelling nullLabel settled).


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants