Repository navigation
feat(spec,analytics): a dataset answer's measure column states its aggregate, labelled or not (fields[].aggregate) - #22021
Conversation
…column's aggregate A dataset answer's measure column now states the aggregate its measure declares (`fields[].aggregate`, the closed AggregationFunction vocabulary), whether or not the author labelled it. `builtinAggregate` keeps its label-only meaning. Written in enrichResultColumns, so the live query and the draft-data preview agree by construction; absent on dimension columns, derived measures and cube query answers. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
…angeset The data-api page listed the optional fields[] members the response schema declares; it now names `aggregate` too and says how it differs from `builtinAggregate`. Changeset: @objectstack/spec and service-analytics minor. Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 139 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 69cf9c10c4be5556f604df22b55d89f56a7a1fa5 && git checkout 69cf9c10c4be5556f604df22b55d89f56a7a1fa5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8caa131e52717ef35fe71742d758f10b8f2b77ee cce091f511ea76031cced6e01798acdcae163feb && git checkout -B drift-repro 8caa131e52717ef35fe71742d758f10b8f2b77ee && git merge --no-ff cce091f511ea76031cced6e01798acdcae163feb
node scripts/docs-audit/affected-docs.mjs --json 8caa131e52717ef35fe71742d758f10b8f2b77ee
|
Contract reviewServed-tier: Inputs: card #21995 (body and all four comments, the ① Derived judgments
② Semver level
Clause-②: yes (widening) ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
CI note from the owning
Generated by Claude Code |
…asure-column-aggregate
|
CI note from the owning On the merged head
Generated by Claude Code |
…asure-column-aggregate
…tack/spec/ui; metadata-core re-exports the same bindings (objectstack-ai#22056) Fixes objectstack-ai#22047 Clause-②: yes (widening: new exports on the published `@objectstack/spec/ui` entry; `@objectstack/spec` changeset at least `minor`) ## What changes - **New on `@objectstack/spec/ui`:** `publicFormSlug`, `anonymousFormIntakeSlug`, `anonymousFormIntakeCandidates`, `anonymousFormIntakeSlugs` and the `AnonymousFormIntakeCandidate` type. This is the candidates half of the anonymous-form-intake rule, which the triage ruling on objectstack-ai/objectui#11545 (`5967405932`) asks a console to read instead of re-deriving "published" from the sharing keys. - **Moved, not copied.** The bodies are the ones that were in `packages/metadata-core/src/anonymous-form-intake.ts`, unchanged except for indentation (2 spaces, as in the rest of `packages/spec`). `diff -w` between the BASE lines 52-105 and the new module is empty. The scan still covers the three shapes in the same order: nested `form`, then `formViews` entries, then a `viewKind: 'form'` item's `config`. - **`@objectstack/metadata-core` re-exports the same bindings** (`export { … } from '@objectstack/spec/ui'`, plus `export type` for the interface), so one copy remains. Its remaining code imports `publicFormSlug` and the type from the spec. Its posture, withdrawal-layer and object-name parts stay where they were. `@objectstack/rest` and `@objectstack/metadata-protocol` are not edited and keep importing from `metadata-core`. `packages/metadata-core/src/index.ts` is unchanged. - Changeset `.changeset/22047-spec-ui-anonymous-form-intake.md`: `@objectstack/spec` `minor`, `@objectstack/metadata-core` `patch` (its built `dist/index.{js,cjs,d.ts,d.cts}` now import these functions from `@objectstack/spec/ui` instead of defining them). ## Where it lives, and why (H3) It goes in a new module, `packages/spec/src/ui/anonymous-form-intake.ts`, next to `sharing.zod.ts`. It is not added to `sharing.zod.ts`, for three reasons: - **The module imports nothing.** It has no zod import and no schema import, and does no work at load time. It stays as cheap as a browser can import, whichever entry reaches it. `sharing.zod.ts` imports `zod` and two schema helpers. - **This is the existing pattern for runtime helpers in `spec/ui` that do not live inside a schema module.** `chart-aggregate.ts`, `i18n-label-resolver.ts` and `view-grouping-query.ts` are sibling non-`.zod.ts` modules. `expandViewContainer` sits in `view.zod.ts` only because it reads that module's member constants. These four functions read no schema. - **The file name matches the one in `metadata-core`,** so the move is easy to follow in history. Trade-off: `files[]` ships `src/**/*.zod.ts` as source, so this module's source is not in the tarball. Its JS and declarations are, in `dist/ui/index.{mjs,js,d.mts,d.ts}`. Prime Directive 2 (no business logic in `packages/spec`: schemas, types and constants only) holds here the way ADR-0053 D-D2 reads it. A pure helper that states what the contract's own vocabulary denotes is protocol, not business logic. It lives beside that vocabulary, and a server package re-exports it: D-D2 moved `nextUtcCalendarDay` into `@objectstack/spec/data` and has `@objectstack/core` re-export it. These four functions only say which `sharing` declarations open a form. The two checks that read server state (another layer's withdrawal, the tenancy posture) stay in `metadata-core`. So does `anonymousFormObjectName`, a pure read of the form and the view, because that is where this export's surface was drawn. `expandViewContainer` (`view.zod.ts`) is the placement precedent: a pure helper beside the schema it serves. The reason two codebases must agree on this rule byte for byte is the triage ruling on objectui#11545 (`5967405932`), as the module's header now says (patch round 1, `6c5741c6`). ## Pins and measurements **Identity pin (3).** This is in `packages/metadata-core/src/anonymous-form-intake.test.ts`; the existing cases are unchanged, and the pin adds 3 import lines and 1 `describe`. For each of the four names it asserts that `./anonymous-form-intake.js[name]` and `./index.js[name]` are `toBe` (Object.is) `@objectstack/spec/ui[name]`. - **Ablation:** run once and not kept, through `scripts/ablation-replace.mjs` with the fix committed first. The re-export of `anonymousFormIntakeCandidates` was replaced by a wrapper that returns the spec function's answer. - Result: `Tests 1 failed | 45 passed (46)`. Only the identity case for `anonymousFormIntakeCandidates` failed (`expected [Function] to be [Function] // Object.is equality`). Every behaviour test passed against the wrapper, so only the identity pin can catch a copy. - Restore was verified by the tool: the blob equals HEAD (`c08671875`) and `git diff HEAD` is empty. metadata-core's test reads its own `src` directly, so the mutation needed no rebuild to take effect. **H5: identity in the built dual output.** A one-time node probe, run from `packages/rest`, compared the four functions in metadata-core's built output with `@objectstack/spec/ui`'s: | condition | metadata-core export === spec/ui export | |---|---| | ESM (`dist/index.js` vs `dist/ui/index.mjs`) | true for all four | | CJS (`dist/index.cjs` vs `dist/ui/index.js`) | true for all four | | ESM vs CJS (cross-condition) | false: the dual-package split every spec export already has | **Parity pin (1).** `packages/spec/src/ui/anonymous-form-intake.test.ts` (26 cases) pins each of the three shapes on its own (open; withdrawn by either switch; no link), all three in one body in scan order, a `config` without `viewKind: 'form'`, slug normalisation, and raw input against `SharingConfigSchema.parse` input. Its expectations are the same as metadata-core's existing ones. A one-time parity probe compared the BASE metadata-core functions (`git show 8caa131`, lines 52-105) with the built spec/ui and metadata-core functions now. It compared candidate keys, key presence, slugs, whether each candidate is a form object from the input, and the slug set: | shape | bodies | with an open slug | BASE = spec/ui = metadata-core | |---|---|---|---| | nested `form` | 13 | 4 | 13 | | `formViews` entry (plus an open sibling) | 13 | 13 | 13 | | `viewKind: 'form'` + `config` | 13 | 4 | 13 | | all three in one body | 13 | 4 | 13 | | `config` without `viewKind: 'form'` | 13 | 0 | 13 | | non-view input | 4 | 0 | 4 | | real producers: showcase `inquiry.view.ts`, crm `lead.view.ts`, as containers and as `expandViewContainer` items | 9 | 4 | 9 | That is 78 bodies with 0 mismatches, and 21 leaf inputs (`anonymousFormIntakeSlug`, `publicFormSlug`) with 0 mismatches. **H1.** Lines 52-105 call nothing from `@objectstack/spec/security`, `applyInjectedSystemColumns` or `resolveRecordWallOrganizationField`. The new module has no imports, and the four bodies compile unchanged in `packages/spec`. **H2.** `./ui` is in `browser-reachable-entries.json`'s `unjudged` list, so `check:browser-reachable-entries` asserts nothing about it (it passed). The module is plain `function` declarations with no top-level statements, and the package declares `"sideEffects": false`. **Declaration surface downstream.** `metadata-core`'s `dist/index.d.ts` and `.d.cts` now reference `@objectstack/spec/ui`. Measured with `tsc --noEmit --extendedDiagnostics --listFiles`, building metadata-core from BASE source and then from HEAD source. These are absolute numbers from a shared box: | program | files BASE → HEAD | memory BASE → HEAD | |---|---|---| | `packages/rest` | 579 → 580 | 1,246,583K → 1,268,472K | | `packages/objectql` | 574 → 574 | 983,063K → 994,921K | | `packages/plugins/plugin-security` | 495 → 495 | 1,079,234K → 1,084,561K | | `packages/metadata-protocol` | 808 → 808 | 1,319,331K → 1,318,969K | | `packages/qa/http-conformance` | 345 → 345 | 357,199K → 357,044K | The one new file in `rest` is `spec/dist/ui/index.d.ts`, the CJS barrel, reached through `metadata-core/dist/index.d.cts`. The chunks it re-exports were already in that program in both flavours. tsc exited 0 in every program on both builds. ## Prose that named the old home (H4) - `packages/spec/src/ui/sharing.zod.ts:19-23` said the rule lives in `anonymousFormIntakeCandidates` "in `@objectstack/metadata-core`". It now names `anonymous-form-intake.ts` beside that module, which metadata-core re-exports to the server's doors. - `content/docs/references/ui/sharing.mdx:22-26` was regenerated from that docblock by `gen:docs`, not edited by hand. - `packages/metadata-core/src/anonymous-form-intake.ts:13-20`: the module docblock says the candidates half is declared in `@objectstack/spec/ui`, whose docblock is now the authority on it. The scan-shape paragraph moved with the code. - Judged still true and left alone: - `packages/rest/src/rest-server.ts:10698` reads "(`anonymousFormIntakeCandidates`, `@objectstack/metadata-core`)". That is where rest imports the function from, and metadata-core still exports it. The file is also outside this card's surface. - `packages/metadata-core/src/index.ts:141-145` ("both read this one rule"). - The `docs/qa/platform-checklist` mechanism references name `anonymousFormIntakeWithdrawnIn` and `anonymousFormExplicitWithdrawals`, which stay in metadata-core. ## Verification (HEAD `cfdc8804f0`; the source tree is the same as `3e9a9e48b1` plus the changeset) - `pnpm --filter @objectstack/spec build` (JS + DTS): exit 0. `check-dts-emitted` reported 38/38. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts up to date. Before regeneration, 3 were stale: `api-surface/` (+5 rows in `ui.json`), `export-origins/` (+5) and `content/docs/references/**` (the H4 sentence). They were regenerated with `gen:api-surface`, `gen:export-origins` and `gen:docs`. - `check:api-surface`, `check:export-origins`, `check:docs`, `check:exported-any`, `check:dual-source-exports` (0 accepted dual-source), `check:entry-nameability`, `check:browser-reachable-entries`, `check:liveness`, `check:llms-txt`, `check:skill-examples`: exit 0. - `pnpm --filter @objectstack/spec test`: 620 files, 18511 passed, 1 todo. The new file alone: 26 passed. - `pnpm --filter @objectstack/metadata-core test`: 18 files, 415 passed. `src/anonymous-form-intake.test.ts` alone: 46 passed. - `pnpm --filter @objectstack/metadata-protocol test`: 219 files passed, 3 skipped; 28135 tests passed. The focused run of `runtime-authoring-gate.public-form-intake`, `protocol.runtime-authoring-gate` and `protocol.org-scoped-write-refused` passed 257. - `pnpm --filter @objectstack/rest test`: 260 files, 4914 passed, 326 skipped. The focused run of `public-form-routes`, `public-form-routes.stored-row`, `public-form-withdrawal` and `public-form-intake-availability` passed 76. - `pnpm --filter @objectstack/spec typecheck` and `pnpm --filter @objectstack/metadata-core typecheck`: exit 0. `--listFiles` shows both new test files are in their packages' test programs. - Gates derived by `node scripts/pm/dispatch-gates.mjs --commands` at `cfdc8804f0`, and checked with `--ran`: 110 derived, 109 run, 0 unrun, 1 NOT MEASURED. The NOT MEASURED one is `pnpm check:dual-build-cjs-loads`, exit 3, PREREQUISITE NOT MET: it needs every package built. The CJS half of the H5 probe above loaded `metadata-core/dist/index.cjs` with `require`. The 5 roster gates the lead list flagged under a touched directory also pass (`check:meta-url-spelling` and `check:spec-changes` through `check:generated`; `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`). - Lint is a narrowed run, and it measures something: `eslint --no-inline-config --format json` over the 6 changed TS files reported 6 files, 0 errors and 0 warnings. All 6 are in the population of `eslint.config.mjs` (`files: ['**/*.{ts,…}']` minus `NEVER_LINTED`, and `--print-config` resolves each one). That config never turns on type-aware linting (no `parserOptions.project`, no typed rules; see its own note near line 327), so this diff cannot change the result for any file it does not touch. The repo-wide `pnpm lint` is left to CI. ## Acceptance notes - **Not covered by this card** (the card's "Not this card" section): whether another layer withdraws a form, whether the posture makes a form unavailable, any server-side "published" answer, and the objectui page change. objectui#11545 restarts once objectui uses a release that carries these exports. - `main` gained objectstack-ai#22021 (a spec analytics change) after this branch was cut. It touches none of these files and none of the generated artifacts, so `main` is not merged here, and the merge queue rebuilds on the current `main`. --- _Generated by [Claude Code](https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…0 and follow its contract (objectui#12030) (objectstack-ai#12337) Fixes objectstack-ai#12030 Clause-②: yes — 22 published `@object-ui/*` packages declare a consumer-facing `@objectstack/*` range that moves from `^17.x` to `^18.0.0-next.0` (21 in `dependencies`, 1 in `peerDependencies`, counted from the manifests at the head), so a release would carry a published-contract change. `node scripts/check-changeset-presence.mjs` at `0cc4e8fcc`: "102 source file(s) and 1 published-contract change(s) of 18 released package(s) changed, and this change declares 3 changeset(s)". `.changeset/12030-objectstack-next-18.md` declares the 22 packages `minor`, with the breaking-for-consumers note in its body; the changeset coverage of the other touched packages is below. follow-up: objectstack-ai#12334 **Scope: the `@objectstack/*` move to `^18.0.0-next.0` and the fixes it forces. No objectui release.** Releasing objectui 18.x (the AGENTS.md §版本号策略 sync `major`, Changesets pre mode on `next`, and the two gate edits ruled in records 6148150305 and 6149296149) moves to the follow-up card above, per the scope split recorded as 6149376719. This branch carried the sync `major` and `.changeset/pre.json` for a while (`0b942ba`); they are reverted (`7ec3c2b`), and neither gate script is edited. **Known effect after merge, intended:** `Spec Range Floors` (`scripts/check-spec-range-floors.mjs`) cannot read `^18.0.0-next.0`: at the head it exits 1 with "The scan collapsed: 0 package(s) declare @objectstack/spec in a consumer-facing field". Its nightly alarm on `main` turns red, and `pnpm changeset:publish` cancels any publish until the follow-up lands. That is the fail-safe that stops a 17.x release declaring 18 ranges. The open version PR objectstack-ai#11600 (`changeset-release/main`) refreshes toward 17.8.0 and must not be merged before the follow-up. ## What this does - Every `@objectstack/*` range in `dependencies`, `devDependencies` and `peerDependencies` moves to `^18.0.0-next.0`: 40 ranges in 32 manifests (26 / 13 / 1). Raised textually; `pnpm-lock.yaml` regenerated by `pnpm install` only, never hand-edited. `pnpm install --frozen-lockfile` exits 0 and a second plain `pnpm install` leaves the lockfile byte-identical (measured at `e8e9a35`; lockfile and manifests unchanged since). - npm dist-tags read today for the four `@objectstack/*` packages objectui names (`spec`, `client`, `formula`, `lint`): `next: 18.0.0-next.0`, `latest: 17.7.0`; every `18.0.0-next.0` tag peels to objectstack `d889448b93`. `next` had not moved when the final head was pushed, so objectstack-ai/objectstack#23160 (`1912af47ba`, compare reads `behind`: not an ancestor) is not in this release and objectstack-ai#12325's reader removal stays out of scope. - Every break the bump caused that is a mechanical follow-through of a v18 contract change is fixed here. Where a break needed a behaviour or design choice, the PR keeps today's behaviour and says so in the test; the seat judged or routed each one (record 6148150305). ## Changeset coverage Every published package this change touches, measured on the net diff from `72594f29e` (the merge base since the `main` merge; the earlier rounds measured from `cea429b98`, before it, and the set is the same 32 published packages): - `.changeset/12030-objectstack-next-18.md`: the 22 packages whose consumer-facing ranges move, `minor`. - `.changeset/12030-i18n-capability-labels.md`: `@object-ui/i18n`, `minor`. The ten locale packs drop `capability.label.manage_org_presentation` and add `capability.label.manage_mailboxes` with spec 18's capability set. A key a consumer could read is removed, so the bump is `minor`, not `patch`. - `.changeset/12030-console-bundles-spec-18.md`: `@object-ui/console` and `@object-ui/runner`, `minor`. In both, the `@objectstack/*` ranges are `devDependencies`, but each is an app build whose published `dist` bundles them. The console bundles them in its `vendor-objectstack` chunk, measured under objectstack-ai#11762 below. The runner is a Vite app build (`vite build`, no `build.lib`, no `external`) whose aliases compile `@object-ui/components`, `core`, `types` and other `@object-ui/*` packages from `src`; those import `@objectstack/spec` at runtime. The runner line was added in the second patch round, after the opus re-review 6158463307 measured it. - `@object-ui/plugin-calendar` and `@object-ui/plugin-kanban` changed only a comment, and get no line. Measured: both rebuilt from source at `2c15e40cf` (`pnpm --filter … run build`, after removing `dist`); `grep -rF` over each `dist` (their `files[]` publish `dist`, `README.md`, `CHANGELOG.md`, `LICENSE`) finds neither the changed sentence nor the unchanged comment sentence beside it, while code strings such as `startDateField` and `ObjectKanbanRenderer` are found. - `@object-ui/collaboration`, `mobile`, `plugin-charts`, `plugin-report` and `sdui-parser` get no line. The test: does the package's published `files[]` bundle anything this change touched? In each, the only manifest change is `devDependencies["@objectstack/spec"]`, and the package's own sources are unchanged. `collaboration`, `mobile` and `sdui-parser` build with `tsc`, so their `dist` holds their own sources only. `plugin-charts` and `plugin-report` are Vite lib builds whose `rollupOptions.external` externalises every bare import, so no other package is compiled into them. None of them imports `@objectstack/spec` from non-test source. ## Pending changesets on `main` reworded (patch round 3) Commit `0cc4e8fcc`. Eight pending `.changeset/*.md` that this change did not add carried sentences the spec-18 move makes false at release time. Each false clause is corrected in place, mostly by one clause pointing at objectui#12030; history is kept. In the quotes below, a type argument is spelled in words (`PublicBlockNodeOf` for `'element:repeater'`), because GitHub's body sanitizer eats angle-bracket fragments; the files spell it as code. The wording is all that changed: every file's frontmatter (packages and levels) is byte-identical to `0d457fe1f` (sha1 of each frontmatter block compared), and `check-changeset-overwrite` reports "declared at base" equal to "declares now" for all eight. - `.changeset/6152-grid-default-filters-gantt-map-filter.md` (`@object-ui/types` minor; `core`, `plugin-grid`, `plugin-map`, `plugin-view` patch) - before: "`@objectstack/spec` has typed `ComponentPropsMap['object-grid'].defaultFilters` as the same `ViewFilterRule` array as `filter`, `[{ field, operator, value }, ...]`, since 17.6.0: the legacy fallback `ObjectGrid` reads only when `filter` is absent, …" - after: "`@objectstack/spec` typed `ComponentPropsMap['object-grid'].defaultFilters` as the same `ViewFilterRule` array as `filter`, `[{ field, operator, value }, ...]`, from 17.6.0 until 18 retired it (see objectui#12030): the legacy fallback `ObjectGrid` read only when `filter` was absent, …" - before: "… so `table: { defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }] }` now parses there, on the tolerant and the strict face and through `safeValidateSchema`." - after: "… so `table: { defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }] }` parsed there through 17.x, on the tolerant and the strict face and through `safeValidateSchema`; retired in 18, see objectui#12030: the row's tombstone refuses `defaultFilters` by name, so every value, the rule array included, is refused there now." - before: "… with the protocol's own message, which computes the rule array from the record's keys. Respell `defaultFilters: { status: 'open' }` as `defaultFilters: [{ field: 'status', operator: 'equals', value: 'open' }]`, or, better, move it to `filter`, which takes the same array and wins when both are written." - after: "… with the protocol's own message, which computed the rule array from the record's keys through 17.x (since 18, see objectui#12030, it is the retired-key message, which names `filter`). Respell `defaultFilters: { status: 'open' }` as `filter: [{ field: 'status', operator: 'equals', value: 'open' }]`; the rule-array `defaultFilters` spelling this entry first offered is retired in 18 too." - before: "`ObjectGrid` lowers `defaultFilters` through the same `toFilterNode` sink as `filter`, so a rule array there sends the same `$filter` and draws the same rows as the same array written as `filter`; the sink still lowers a record or an AST that reaches the slot at runtime, …" - after: "`ObjectGrid` lowered `defaultFilters` through the same `toFilterNode` sink as `filter`, so a rule array there sent the same `$filter` and drew the same rows as the same array written as `filter` (retired in 18, see objectui#12030: `ObjectGrid` and `ObjectView` no longer read `defaultFilters`, so a view that still carries it renders unfiltered); the sink still lowers a record or an AST that reaches `filter` at runtime, …" - before: "… and `@object-ui/plugin-grid` a pin of the above through the real renderer." - after: "… and `@object-ui/plugin-grid` a pin of the above through the real renderer (since 18 it pins that `defaultFilters` is not read)." - `.changeset/11880-grid-filter-handoff.md` (`@object-ui/plugin-view` patch) - before: "… else `table.filter` unless it lowers to nothing (absent, `[]` or `{}`), else the deprecated `table.defaultFilters`." - after: "… else `table.filter` unless it lowers to nothing (absent, `[]` or `{}`), else the deprecated `table.defaultFilters` (a rung retired in 18, see objectui#12030: `ObjectView` no longer reads it)." - before: "What did not change: which filter applies (each rung and each pair sends the same query and draws the same rows), and …" - after: "What did not change: which filter applies (each rung and each pair sends the same query and draws the same rows; in 18 the `table.defaultFilters` rung is retired, see objectui#12030), and …" - Left as written: the export and page-1 bullets, which say in the past tense what a `table.defaultFilters` did before this entry's change. - `.changeset/12056-repeater-object-waiver.md` (`@object-ui/types` minor) - before: "The zod arm `ElementRepeaterBlockSchema` now applies the same `object` waiver as the spec's props gate (objectui#12056)." - after: "The zod arm `ElementRepeaterBlockSchema` now applies the same `object` waiver as the spec's props gate (objectui#12056); superseded in 18, see objectui#12030: spec 18 retires the flat `object`, so the arm is the row by reference, a node may omit `properties.object` whether or not it binds one, and a `properties.object` is refused by name." - before: "`ComponentPropsMap['element:repeater']` requires `object`." … "a node refinement puts the requirement back wherever no binding names the object." - after: "`ComponentPropsMap['element:repeater']` required `object` through 17.x." … "a node refinement puts the requirement back wherever no binding names the object. Both waivers and both refinements are gone in 18 (objectui#12030): the row has no flat `object` left to waive." - before: "**Still refused at `properties.object`.** A bag with no `object` is refused when the node has no binding that names one: … The refusal is a `custom` issue with `params.code` `ELEMENT_REPEATER_OBJECT_REQUIRED`, and its message names both remedies. A `properties.object` of the wrong type is still refused by the row itself." - after: "**Still refused at `properties.object` through 17.x.** A bag with no `object` was refused when the node had no binding that names one: … The refusal was a `custom` issue with `params.code` `ELEMENT_REPEATER_OBJECT_REQUIRED`, and its message named both remedies; retired in 18, see objectui#12030: the refinement and its code are deleted, so such a bag is accepted. A `properties.object` of the wrong type is still refused by the row itself, and since 18 so is any `properties.object`, by name." - before: "**TypeScript.** The inferred `ElementRepeaterBlockSchema` type now types `properties.object` as optional, and so does the authoring type `PublicBlockNodeOf` for `'element:repeater'` derived from it." - after: "**TypeScript.** The inferred `ElementRepeaterBlockSchema` type typed `properties.object` as optional through 17.x, and so did the authoring type `PublicBlockNodeOf` for `'element:repeater'` derived from it; since 18 (objectui#12030) both follow the row, which retires `object`, so a typed `properties.object: string` no longer type-checks." - `.changeset/7611-approver-registry-read.md` (`@object-ui/app-shell` patch) - before: "A `data` binding (`user`, `team`, `department`, and `position` under the installed spec 17.7.0) still renders the record lookup … The type that admits the registry member before the installed spec declares it is marked for removal when objectui moves to spec 18." - after: "A `data` binding (`user`, `team`, `department`, and `position` under spec 17.7.0; spec 18, which objectui installs from objectui#12030, binds `position` to the registry) still renders the record lookup … The type that admitted the registry member before the installed spec declared it was marked for removal when objectui moves to spec 18; removed in 18, see objectui#12030." - `.changeset/11880-element-datasource-only.md` (`@object-ui/components`, `app-shell`, `i18n` patch) - before: "The flat keys stay published until `@objectstack/spec` retires them in v18; each input's description now says it is not read and names the binding member that is." - after: "The flat keys stayed published, each input's description saying it is not read and naming the binding member that is, until `@objectstack/spec` retired them in 18: the record picker's four inputs left with that retirement (objectui#12030), and `element:number`'s two with objectui#12085." - `.changeset/12056-repeater-datasource.md` (`@object-ui/app-shell` patch) - before: "… and `objectui validate` accepts the node the designer now writes, because `@object-ui/types` takes the spec gate's `object` waiver for this element in the same change." - after: "… and `objectui validate` accepts the node the designer now writes: `@object-ui/types` took the spec gate's `object` waiver for this element in the same change, and since 18 (objectui#12030) the row has no flat `object` left to require." - `.changeset/11880-repeater-datasource.md` (`@object-ui/types` minor, `@object-ui/components` patch), found by the grep beyond the six named - before: "Its flat `properties.object` / `filter` / `sort` / `limit` stay read, as the fallback, until `@objectstack/spec` retires them in v18, so a repeater that carries only the flat keys reads exactly as before." - after: "Its flat `properties.object` / `filter` / `sort` / `limit` stay read, as the fallback (spec 18 retires them as authored keys, see objectui#12030, and the renderer keeps this read for metadata written before), so a repeater that carries only the flat keys reads exactly as before." - before: "… by reference, and its bag's `object` stays required. Clause-②: the strict authoring face (`StrictAnyComponentSchema`) now ACCEPTS a repeater carrying `properties.object` plus a well-formed `dataSource`, which it refused as an unrecognized key; …" - after: "… by reference, and its bag's `object` stays required (until objectui#12056's waiver, and retired in 18, see objectui#12030). Clause-②: the strict authoring face (`StrictAnyComponentSchema`) now ACCEPTS a repeater carrying `properties.object` plus a well-formed `dataSource`, which it refused as an unrecognized key (since 18 it refuses `properties.object` by name, so the accepted node carries the binding alone); …" - before: "A repeater whose bag omits `object` is refused at `properties.object` on both faces, as before." - after: "A repeater whose bag omits `object` is refused at `properties.object` on both faces, as before; retired in 18, see objectui#12030: such a repeater is accepted." - One clause here went false with objectui#12085, not with this change, and is corrected in the same edit because it sits in the same bullet: before "The registration now publishes the injected `dataSource` input; `object` stays required." after "The registration now publishes the injected `dataSource` input; `object` stays required (until objectui#12085 dropped the flat query inputs)." Evidence: the `element:repeater` registration in `packages/components/src/renderers/basic/data-list.tsx` at the head publishes `titleField`, `fields`, `emptyText`, `divided` and no `object`, and `.changeset/12085-element-binding-inputs.md` states the drop. The seat may ask for this one clause back out. - `.changeset/12093-spec-main-compile.md` (empty frontmatter, no release) - before: "… a retired-key tombstone on objectstack `main`), and `@object-ui/app-shell`'s zh-CN overlay-scope label table keys `org` beside the spec's union (objectstack `main` narrows that union to `env`)." - after: "… a retired-key tombstone on objectstack `main`; the read itself is retired in 18, see objectui#12030), and `@object-ui/app-shell`'s zh-CN overlay-scope label table keys `org` beside the spec's union (objectstack `main` narrows that union to `env`; the `org` label is dropped in 18, see objectui#12030)." The grep over all 438 pending `.changeset/*.md`, besides this change's own three: `defaultFilters` hits 6152, 11880-grid-filter-handoff and 12093; `ELEMENT_REPEATER_OBJECT_REQUIRED` hits 12056-repeater-object-waiver only; `ELEMENT_NUMBER_OBJECT_REQUIRED` and `manage_org_presentation` hit nothing; the overlay-scope `org` hits 12093 only (12171-environment-overlay-copy names the environment overlay and stays true); the flat element query keys and the record picker hit 11880-element-datasource-only, 11880-repeater-datasource, 12056-repeater-datasource, 12056-repeater-object-waiver and 12085-element-binding-inputs. Read and left as written, each still true at release: `12085-element-binding-inputs.md` (the repeater renderer still reads its flat keys as the fallback; this change does not touch it), `11903-record-picker-layout.md` (the `RecordPickerDialog`, not `element:record_picker`), `12086-edit-inline-default-on.md`, `7611-capability-registry.md` (names no curated capability), `7611-decision-output-position-registry.md`, and the "since 17.7.0" mentions elsewhere, which date a fact and do not say what is installed. ## Merge of `main` `main` at `72594f29e` is merged into this branch (merge commit `1c95e13af`): six commits, merged cleanly, none touching a manifest or the lockfile. Files both sides changed: `packages/app-shell/src/views/metadata-admin/i18n.ts`, `packages/plugin-grid/src/ObjectGrid.tsx` and the ten `packages/i18n` locale packs. The re-run after the merge is under Gates. ## The break list (measured first, at `a3f41ed`: the bump and changeset alone) Full vitest suite in five lock-held groups: 50 files, 159 tests red (types/core 15 files / 58 tests, app-shell 21 / 45, plugins A 6 / 20, plugins B 1 / 4, console + scripts 7 / 32). `turbo run build` and every package's `type-check` were green at that commit (81/81). Two repo gates were red: `check:eager-closure` (exit 2, ceilings drifted to 1.74x / 1.21x) and `check:installed-pin-claims` (23 findings). By area, and how each is closed: - **Overlay scope is `env` only** (objectstack#15206 S5; card comment 6098299437): data-objectstack overlay fixtures and docblock, two app-shell fixtures, and the zh scope label table drops its `org` row (an `org` from an older server renders raw), as the ruling's seat judgment says. - **Index `unique: true` refused at protocol 18** (objectstack#22103): the index-fallback probe pins the refusal with a `false` control, the validation-preview help (en, zh) names `unique: 'organization'` / `unique: 'global'`, and the `data-protocol.ts` docblock says refused. These are the three sites objectstack-ai#11864 names; its legacy-row rendering case still passes and is left as is. - **Capabilities**: `manage_mailboxes` in, `manage_org_presentation` out; curated label set, default label, ten locale packs. - **Page blocks**: the element:number / element:repeater / element:record_picker flat query keys and object-grid `defaultFilters` are tombstones refusing by name (objectstack#11509). The number and repeater arms take the spec row by reference; the record_picker node and registration stop publishing the four keys; the parity, arm, harvest and ratchet pins read the tombstones. - **Retired reads removed** (seat judgment under the ruling): `ObjectGrid` no longer reads `defaultFilters` and `ObjectView` no longer reads `table.defaultFilters`; no in-repo producer writes the key. The tests that pinned it as a working alias are flipped to pin it inert beside a canonical `filter` control; the plugin-view README and docs page, the registration's `filter` description and the type's JSDoc say retired. The element:repeater renderer's flat `object` / `filter` / `sort` / `limit` fallback reads stay: `apps/console/src/dev/DevLists.tsx` writes `properties.object` and `properties.limit` and is routed in `App.tsx`, a live in-repo producer, reported instead of deleted. element:number and element:record_picker already read the binding only. - **New spec checks and keys**: `checkPageSlotPair` attached to both page node schemas beside the existing page checks (and listed among the chained refinements in the import-boundary census); `checkPagePrintComposition` and `checkListViewChartBinding` read a discriminator and are listed as not attachable; ObjectSchema's 45th key `attachedOnRead`; record:details sections' experimental `count` listed as deferred; the three new record component types stay out of the block palette, as today. - **ListView `requiredPermissions`**: pinned as the one pending declaration; the seat files a declare-and-enforce card. - **Flow dialect** (ADR-0032 §3): flow fixtures move to the spellings v18 accepts. The value-role ledger now names six key/value maps; each node's own config schema was measured to refuse a malformed envelope with exactly the sentences the editor shows. A `{token}` string in a value slot is refused by the spec and the editor shows that refusal; the recipient picker's `{…}` spellings and a blank Notify title are pinned as PENDING. The Studio cutover rides objectstack-ai#11824. - **Approver `position`** is a registry binding in v18: the spec-17 loosening is removed and the identity, directory and lookup pins read the registry arm. - **Submit-redirect / url action**: the refusal pins read the spec's own message; a url action target fixture is a path. - **Installed-spec claims**: re-measured and restamped where the fact holds, one historical mention ledgered; `e2e/live/ci/backend.env` moves to `18.0.0-next.0`; `QUICK_REFERENCE.md` re-synced by its script; lockfile-integrity samples re-keyed; the declared-floor reader in core's test reads a prerelease floor by its core triple. ## objectstack-ai#11762: the eager-closure line comes down by the payback objectstack#22044 (`93125aeeb8`) is an ancestor of the `18.0.0-next.0` tag commit (compare `ahead`, behind 0; control: the 17.7.0 tag commit `4e4e881427` also reads `ahead`). Two console builds, one container, one instrument (`CI=true pnpm exec turbo run build --filter=@object-ui/console... --concurrency=2` under the verify lock, reading `apps/console/dist/eager-closure.json`): | chunk | `cea429b` (main, 17.7.0) | `5488ae7` (this branch) | delta | |---|---:|---:|---:| | `vendor-objectstack` | 1,599,281 | 1,512,690 | -86,591 | | `ui-components` | 285,771 | 284,925 | -846 | | everything else (70 names) | 1,324,632 | 1,324,848 | +216 | | aggregate | 3,209,684 | 3,122,463 | -87,221 | `MAX_EAGER_CLOSURE_GZIP_BYTES` 3,281,467 to 3,194,246 and `BASELINE` onto `5488ae7`; `vendor-objectstack` ceiling 1,623,000 to 1,536,409 and its baseline onto the same build. Lowered by the amount, not re-derived and not rounded; `REGRESSION_THIS_GATE_MUST_CATCH_BYTES` untouched. Headroom 71,783 bytes = 0.79x on the aggregate, 23,719 = 0.26x on `vendor-objectstack`: what `main` had on `cea429b`. The ruling's two later edits reach bundler inputs; the console build of `5db13ba` reads 3,122,375 (-88), inside the band, recorded in the `BASELINE` prose and not re-pinned. The upstream figure (-226,301 on the chunk) was on an injected dist; the real release nets -86,591 here. ## Held cards: their Restart-when probes at the head | card | probe | at the head (`18.0.0-next.0`) | control | |---|---|---|---| | objectstack-ai#11773 | `MetadataConflictErrorSchema` in the spec's `.d.ts` | exit 0 (next tarball and installed tree) | `@latest` (17.7.0) tarball: exit 1 | | objectstack-ai#11545 | `anonymousFormIntakeCandidates` is a function | exit 0 | 17.7.0: exit 1 | | objectstack-ai#11681 | a release carrying the objectstack#21995 change (objectstack-ai/objectstack#22021, `c5658133fc`) | met: ancestor of `d889448b93` | | | objectstack-ai#11970 | lockfile resolves `@objectstack/core` and `@objectstack/objectql` carrying `4578c56e65` | half: core `18.0.0-next.0` carries it; objectql is in no objectui manifest or lockfile, so the probe cannot be met as written | | | objectstack-ai#11958 | spec/ui exports `PagePrintSchema` and `PRINTABLE_PAGE_COMPONENT_TYPES` | exit 0 | 17.7.0: exit 1 | | objectstack-ai#11864 | spec carries `ef1fcb26a2` | met; its three sites are changed in this PR | | | objectstack-ai#11762 | spec tag commit has `93125aeeb8` as ancestor | met; executed in this PR | | | objectstack-ai#12270 | `EmptyStateSchema` link + `InterfacePageConfigSchema.emptyState` | exit 0 | 17.7.0: exit 1 | | objectstack-ai#12271 | `ListDraftsResponseSchema` reads a draft's `member` | exit 1 as written: the probe's fixture has no `label`, which the schema requires (`drafts.0.label` invalid_union). With `label` added: exit 0 | with `label`, 17.7.0: exit 1 | | objectstack-ai#12192 | `SECTION_HEADING_COUNT_HOLE` is a RegExp | exit 0 | 17.7.0: exit 1 | | objectstack-ai#12314 | `stampCommentBodyEdit` in `@objectstack/plugin-audit` dist | the probe reads `@latest` (17.7.0): exit 1. Run against `@next` (18.0.0-next.0): exit 0 | | ## Gates (final head `0cc4e8fcc`; each reading names the commit it ran on) **Patch round 3** (`0cc4e8fcc`: pending-changeset wording only, eight `.changeset/*.md`, no code, manifest or lockfile): - At `0cc4e8fcc`, one script under the verify lock, `VERDICT command-exit 0` (the script exits 1 if any gate does), each gate exit 0: `check-changeset-presence` ("102 source file(s) and 1 published-contract change(s) of 18 released package(s) changed, and this change declares 3 changeset(s)"), `check-changeset-no-major` ("No changeset declares a `major` bump."), `check-changeset-fixed`, `check-changeset-overwrite` (report-only: "3 changeset(s) added, 8 modified, 0 deleted", and for each of the eight the declared packages and levels at base equal those now), `check:changeset-claims` ("No pending changeset names a file this change touches."), `check:pending-changeset-literals` ("No test source names a pending changeset."), `check:new-line-citations` (0 new), `check:control-bytes` ("OK (scanned 8636 tracked text file(s); skipped 85 binary)"), `check:installed-pin-claims` (`OK`). - The script tests that read the live `.changeset/` or run a changeset gate against this tree, at `0cc4e8fcc` under the verify lock: `pnpm exec vitest run --project unit --maxWorkers=2` over `changeset-polarity-census`, `check-changeset-no-major`, `body-dialect-census`, `dollar-dialect-alias-census`, `check-changeset-claims`, `check-pending-changeset-literals`, `check-control-bytes`, `check-changeset-presence` and `check-changeset-overwrite`: 9 files, 376 tests passed, `VERDICT command-exit 0`. No code moved, so no package test or type-check was re-run. **Patch round 2** (`0d457fe1f`: the runner changeset line, the three objectui-side moves named in the main changeset, and the last `defaultFilters`-is-read prose reworded, in the `ObjectGridSlotKey` docblock that ships in the published `.d.ts`, the zod mirror's member comment, the grid round-10 test header, the grid registration note and the `ObjectGrid` props note): - At `0d457fe1f`, under the verify lock, each exit 0: `check-changeset-presence` ("declares 3 changeset(s)"), `check-changeset-no-major`, `check-changeset-fixed`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:spec-symbols`, `check:installed-pin-claims`. - `pnpm exec turbo run type-check --filter=@object-ui/types --filter=@object-ui/plugin-grid --concurrency=2 --continue`: 16/16. Then `grid-default-filters-gantt-map-filter-round10-6152`, `object-view-slot-key-lists` and `imported-defaults-8317`: 3 files, 392 tests passed. - `git grep defaultFilters` over non-test sources (READMEs, docs and changelogs aside): what is left says retired, not read; describes history (objectui#6152, objectui#11880); or is the declaration typed by the row's tombstone. **Patch round, after the `main` merge** (`1c95e13af`; `a1ca2ad55` only rewords the i18n changeset): - Changeset family and i18n gates at `a1ca2ad55`, under the verify lock, each exit 0: `check-changeset-presence` (above, "declares 3 changeset(s)"), `check-changeset-no-major`, `check-changeset-fixed`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:i18n-keys` ("Every in-scope call-site key resolves against the en pack (3375 keys)"), `check:i18n-drift`, `check:i18n-designer-parity`, `check:i18n-dead-keys` (a report), `check:control-bytes`. - Type-check of the packages the merge touched, at `1c95e13af`: `pnpm exec turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/components --filter=@object-ui/data-objectstack --filter=@object-ui/i18n --filter=@object-ui/plugin-detail --filter=@object-ui/plugin-grid --filter=@object-ui/types --concurrency=2 --continue`, 36/36. - Tests the merge brings into contact with this diff, at `1c95e13af`. The set: every package `main` and this branch both changed (`plugin-grid`, `i18n`, `data-objectstack`, `types`) in full; every test file `main` added or changed, now running on spec 18; and every app-shell test that imports `metadata-admin/i18n`. Result: 920 files passed, 16,446 tests passed, 13 skipped. The merge touches no manifest and no lockfile, so the whole suite was not re-run; this head's CI shards are the whole-workspace reading. **Before the patch round:** - **Install:** `pnpm install --frozen-lockfile` exit 0, then a plain `pnpm install` leaves `pnpm-lock.yaml` byte-identical (sha256 compared), at `e8e9a35`; the lockfile and every manifest are unchanged since (`git diff e8e9a35 HEAD` over them is empty). Lockfile diff against `cea429b`: +106 / -106. - **Build:** `CI=true pnpm exec turbo run build --filter=@object-ui/console... --concurrency=2`, exit 0, 35/35 tasks, at `5db13ba` (the last commit touching a bundler input); the whole published set was built (43 tasks) by `pnpm check:published-dist` at `e8e9a35`. - **Type-check:** `pnpm exec turbo run type-check --concurrency=2 --continue`, 81/81, at `5db13ba` (later commits touch only scripts, tests and the changeset); `pnpm type-check:scripts && pnpm type-check:vitest-setup && pnpm type-check:e2e && pnpm type-check:vitest-config`, exit 0, at `2c15e40cf`. - **Tests** (`pnpm exec vitest run --maxWorkers=2`, five lock-held groups): - types, core, react, i18n, cli, data-objectstack and 12 more at `5488ae7`: 1019 of 1020 files; the red one (`imported-defaults-8317`, from attaching `checkPageSlotPair`) is fixed in `ef2054e` and re-ran green (267 tests). Later commits touch these packages only in one JSDoc. - app-shell at `ef2054e`: 1319 passed, 1 skipped (12,730 tests). After the `org` label drop, the four overlay-scope files re-ran green at `2e80824ea` (21 tests). - plugin-detail, plugin-grid, plugin-form, plugin-dashboard, components at `2e80824ea`: 1207 passed, 2 skipped (12,292 tests). - fields and the other 15 plugins at `7ec3c2b`: 1031 passed, 2 skipped (11,505 tests). - apps/console, scripts, eslint-rules, examples at `7ec3c2b`: 429 passed, 2 failed. `check-eager-closure-budget.test.ts` pinned the rendered `BASELINE` literal: re-taken in `2c15e40cf` and green there. `date-only-zone-pins-10183.test.ts` is red at `2c15e40cf` too, and is NOT caused by this diff: its forked child fails one case, the suite-zone control of `FormulaFunctions.dateOnlyZone-10866`, which reads `Asia/Shanghai` (11:00) instead of UTC (03:00). That is the zone another pin file sets last. The `unit` project runs `isolate: false`, and the file order comes from each worktree's vitest cache. Run alone in the same forked child at `2c15e40cf`, that file passes (39/39). The same driver passed 28/28 at `a3f41ed` in a fresh worktree. This diff touches neither the pin files nor the code they exercise. - **Repo gates at `2c15e40cf`**, exit 0 unless noted: `check-changeset-presence` (above), `check-changeset-no-major` ("No changeset declares a `major` bump."), `check-changeset-fixed`, `check:changeset-claims`, `check:pending-changeset-literals`, `check:installed-pin-claims` (`OK`), `check:spec-symbols`, `check:phantom-deps`, `check:new-line-citations` (0 new), `check:control-bytes`, `check:eager-closure` ("3049.2 KB gzipped across 72 of 2255 chunks (budget: 3119.4 KB, headroom: 70.2 KB)", every ceiling in range). `check:spec-floors` exits 1, the known effect above. `.changeset/pre.json` is absent. `check:lockfile-integrity`, `check:lockfile-dedupe`, `check:unused-deps` and `check:published-dist` were green at `2e80824ea` / `e8e9a35` and nothing they read has changed since. ## Acceptance notes - Records: the v18 ruling 6148150305 and the gate ruling 6149296149 both move to the follow-up card. This change is the dependency move only, per the split record 6149376719. The seat's judgments under 6148150305 are executed here: drop the zh `org` label, remove the retired reads. - Follow-up cards from the seat's judgments: - the list-view `requiredPermissions` mirror: objectstack-ai#12341; - the three record component types (`record:approval_decision` registration): objectstack-ai#12342; - the file field's `metadataRefused` marker: objectstack-ai#12339; - holding a blank Notify title: objectstack-ai#12343; - the `element:repeater` flat-key reads and their `DevLists.tsx` producer: objectstack-ai#12345; - the Studio flow-dialect cutover: objectstack-ai#11824; - the objectui 18.x release: objectstack-ai#12334. - Kept at today's behaviour and pinned as pending, for those cards: the recipient picker's `{…}` spellings; a blank Notify title, which is sent and shows the server's refusal; a `{token}` value-slot text cell, which commits as typed and shows the spec's refusal; the new record component types, which stay out of the palette; ListView `requiredPermissions`, still undeclared. - The element:repeater renderer keeps its flat-key fallback reads: `apps/console/src/dev/DevLists.tsx` (routed in `App.tsx`) writes `properties.object` / `properties.limit`, a live in-repo producer. - `isValueEnvelopeSlot` serves `KEY.*` ledger paths only, as before. The ledger's whole-value `value` slots (the record nodes' `filter`, loop/map `collection`, notify `recipients`) are not key/value maps, and this change leaves them alone. - objectstack-ai#11864's legacy-row case (`EmbeddedItemEditor.indexFallback`, a stored bare `unique: true` still renders) passes and is left as is: an 18 server serves such a row as `'global'`. - Two held cards' probes cannot exit 0 as written. objectstack-ai#11970 names `@objectstack/objectql`, which no objectui manifest or lockfile carries. objectstack-ai#12271's fixture lacks the required `label`. - The eager-closure readings above, and the `BASELINE` prose in `scripts/check-eager-closure-budget.mjs`, cover this branch's own commits through `2c15e40cf`. The `main` merge (`1c95e13af`) brings `main`'s own bundler inputs, which are `main`'s drift, not this change; CI's `Bundle Analysis` weighs them on this head. The console was not rebuilt locally after the merge. - AGENTS.md §版本号策略 names `OBJECTUI_ALLOW_MAJOR=1` as the sync-major switch, and nothing in `.github/` sets it. That is the follow-up card's to settle; this change leaves AGENTS.md alone. --- _Generated by [Claude Code](https://claude.ai/code/session_01CsyMRsFJBKNEMa4oPY1b5z)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: os-zhuang <jack@objectstack.ai>
Fixes #21995
Clause-②: yes (widening: a new member on a published response schema; @objectstack/spec changeset at least minor)
Dev report for this PR: session
session_01GV6oYwgc1kWiUCb1YaprQ7(PM loop round 2,domain:specseat 2), branchclaude/issue-21995-measure-column-aggregate, base1bc6ca1d.What this changes
A dataset answer's measure column now states its aggregate, whether or not the author labelled the measure:
packages/spec/src/api/analytics.zod.ts): one new closed member onAnalyticsResultResponseSchema.data.fields[],aggregate: AggregationFunction.optional(). TheAnalyticsResultcontract (packages/spec/src/contracts/analytics-service.ts) gains the same member, so the compile-time binding between the two (AnalyticsResultMatchesContract) holds.packages/services/service-analytics/src/analytics-service.ts,enrichResultColumns): one line beside thebuiltinAggregateline, read off the same dataset measure.enrichResultColumnsruns on both the live query and the draft-data preview, so the two answers agree by construction.!m.derived, because the compiler ignores a strayaggregatewritten besidederived); and the cube query answer (POST /analytics/query), which never passes throughenrichResultColumns. The.describe()says exactly this.builtinAggregatekeeps its label-only meaning, and every existing pin on it stays green. No authoring key is added.content/docs/api/data-api.mdxlisted "the optional membersAnalyticsResultResponseSchemadeclares". That list would be false after this change, so it now namesaggregatetoo and says how it differs frombuiltinAggregate. See Acceptance notes: this file is outside the claim's declared surface..changeset/21995-measure-column-aggregate.md, with@objectstack/specand@objectstack/service-analyticsbothminor. It says what a renderer can now read.What a renderer reads: on
POST /analytics/dataset/query, a measure column such as{ name: 'task_count', type: 'number', label: 'Tasks', aggregate: 'count' }. objectstack-ai/objectui#11681 is the consumer half: once a release carries this, it derives integer ticks from it.Mechanism readings (PM hypotheses H1 to H4, measured at
1bc6ca1d)enrichResultColumnsis the only writer of descriptor keys on a dataset answer'sfields[]. It has two call sites: the draft preview (analytics-service.ts:2504) and the live return (:2761). The column ENTRIES themselves are minted upstream as{ name, type: 'number' }by the strategies and byDatasetExecutor(dataset-executor.ts:1166for__compare,:1205for derived,:1333). The degraded "backing object unavailable" exit returnsfields: [], so it has no column to describe.queryDatasethas a single implementation repo-wide. The measure'saggregateis in hand at thebuiltinAggregateline (:2866).AnalyticsResultResponseSchemais the response schema ofPOST /analytics/query(plugin-rest-api.zod.ts:1324), and through theAnalyticsResultbinding it is also the dataset answer's shape. The cube door writesfields[]through the strategies,withDeclaredMeasureFormatsandwithMeasureResultTypes, and none of them writesbuiltinAggregateor the new member. So the describe states that the member is absent on a cube query answer. A pin callsAnalyticsService.query()and asserts that absence.dataset-executor.ts:1205and has no aggregate. The edge:DatasetSchemaacceptsaggregatebesidederived, and the compiler ignores it (dataset-compiler.ts:704). So the new member checks!m.derivedrather than relying onaggregatebeing absent, and a pin covers that case.res.json(result)(rest-server.ts:11319). No REST source change is needed.packages/rest/src/analytics-routes.test.tspasses (14 tests).Tests
All on HEAD
908f4f02, or on91e05bd9where noted. The commit between them touches only the.mdxand the changeset.packages/services/service-analytics/src/__tests__/measure-column-aggregate.test.ts(new, 7 tests, each run on BOTH the live and preview paths):countmeasure statescount, and an unlabelled one still carriesbuiltinAggregate;sumover a currency field statessum;aggregate;__comparecolumn states its measure's aggregate;query()answer.preview-column-enrichment.test.ts:aggregatewas added to its key-for-key live/preview parity descriptor.packages/spec/src/api/analytics.test.ts: the member parses beside alabel. Off-enumtotalis refused atdata.fields.0.aggregatewith issue codeinvalid_value.packages/spec/src/contracts/analytics-service.test.ts: the member types on a labelled column. Off-enum is a compile error (@ts-expect-error, compiled bycheck:test-typecheck).91e05bd9);analytics-routes.test.ts: 14 passed;typecheckfor@objectstack/spec(tsc, scripts-typecheck, test-typecheck) and for@objectstack/service-analytics: exit 0. service-analytics resolves@objectstack/spectypes fromdist/, so its compilingf.aggregateshows it read the rebuilt.d.ts.Reverse verification. The direction was predicted before running. The fix was committed first. The mutation went through
scripts/ablation-replace.mjs: the anchor hit 1 time and then 0, the blob changed, and the restore was proven by blob equal to HEAD and an emptygit diff HEAD. The subject is imported fromsrc/(relative import), so no dist leg applies.aggregateassertions go red, absences and everybuiltinAggregateassertion stay green. Observed: 4 failed and 47 passed across the 3 files. The red ones were labelled-count, sum, stray-aggregate (its control leg) and__compare.!m.derivedguard. Predicted: only the stray-aggregate case goes red. Observed: 1 failed and 50 passed (live: expected 'sum' to be undefined).Gates
These were derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon HEAD908f4f02, from a 9-path change set measured against merge base1bc6ca1d.--ranreconciliation exits 0.check:api-surface: "public API surface + factory signatures unchanged".check:docs: "226 generated files in sync".check:authorable-surface: green.check-adr-0087-registration: no declared-breaking changeset.check-changeset-no-major: no major.check:nul-bytes,check:docs-spec-enumerations,check:doc-authoring,check:cross-package-test-inputs,check:test-source-alias: OK.pnpm --filter @objectstack/spec run check:skill-examplesfirst exited 3 (PREREQUISITE NOT MET, noclient-reactdist). After a turbo build of@objectstack/client-reactit was re-run and exited 0 ("262 prose examples type-check").pnpm check:dual-build-cjs-loads(exit 3). Reason: it reads every package's built output, and 44 packages had nodist/here. A whole-repo build is CI's.check:authz-resolver,check:error-code-casing,check:filter-alias-parity, and speccheck:error-code-provenance. All exited 0.pnpm --filter @objectstack/spec check:generated: all 15 artifacts up to date. Thefieldsrow incontent/docs/references/api/analytics.mdxis truncated, so no generated artifact moves.eslint --no-inline-config --format jsonover the 7 touched.tsfiles.eslint.config.mjsenables no type-aware linting (noparserOptions.project, noprojectService), so this diff cannot move a verdict on any untouched file..mdand.mdxfiles are outside ESLint'sfilesglobs.pnpm lintis CI's.Acceptance notes
content/docs/api/data-api.mdxis not on the claim's declared surface. It was edited because its sentence naming "the optional membersAnalyticsResultResponseSchemadeclares" becomes false with this change. The claim's file list wants that path added.builtinAggregateon a derived measure with a strayaggregate(noted, not filed).builtinAggregatewith the stray value on such a column when it is unlabelled.builtinAggregate's own describe says it is absent on derived columns.DatasetSchemaacceptsaggregatebesidederivedat the REST door's parse, and the compiler then ignores it.examples/**and in non-testpackages/**sources.aggregatebesidederivedat the schema would close both this and the!m.derivedguard's reason to exist. That is a contract tightening, outside this card.aggregate. It is declared absent there. A cube measure'stypeis its aggregate, so stating it there would be one write besidewithMeasureResultTypes. No measured consumer pulls it; the dashboards on this card read dataset answers.origin/main. It is 2 commits behind (1fb274e6, touching rest/runtime, metadata-protocol,.claude). Neither touches a path here, somainwas not merged.Clause-②contract review is owed, as the claim records.Generated by Claude Code