Repository navigation
feat(spec)!: object-metric aggregate and trend take the shape the tile reads; drillDown, compareTo held for a ruling (#21464, stage 4) - #21622
Conversation
…reads (#21464, stage 4) aggregate takes the chart aggregate's own field / function members and groupBy union by reference, groupBy optional; trend takes the badge's measured shape. drillDown and compareTo are held for a ruling. Artifacts not yet regenerated. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ric-aggregate-trend-typed Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
… the tile draws Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…d-refinements ledger, as the chart aggregate's does Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ess counts for the typed metric members Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…r, Clause-② no (narrowing) Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ree; the metric rationale fragment takes order 69 Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 138 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 9004cece5272f594588521852afa4d37d7d8dd46 && git checkout 9004cece5272f594588521852afa4d37d7d8dd46
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 045b946256d988653fdca185c7fd33d6d86bd78d 48aa1fa2538519e643310faa324b260f8299c871 && git checkout -B drift-repro 045b946256d988653fdca185c7fd33d6d86bd78d && git merge --no-ff 48aa1fa2538519e643310faa324b260f8299c871
node scripts/docs-audit/affected-docs.mjs --json 045b946256d988653fdca185c7fd33d6d86bd78d
|
…rrowing), per the re-issued claim Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…gen step 3 hand-off) The os-regen driver kept this branch's side of content/docs/references/ui/component.mdx in the merge commit and dropped main's object-metric rows from #21622. Step 2 restored main's side; this commit regenerates the page from the merged source with gen:schema and gen:docs. Result: main's side plus this branch's one `sortField` row, and the delta against the branch side is exactly #21622's own delta on the page. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…s order 70 #21622 (#21464 stage 4) landed on main as 3f1bc81 with its step-18 rationale fragment `ui-object-metric-aggregate-trend-typed` at order 69, the order this branch's fragment also held. Re-read on the merged tree's main side (5b5e83f) the highest STEP18_RATIONALE order is 69, so this fragment takes the next free one, 70, and now renders after #21622's. The D2 conversion keeps order 60: the highest MAJOR_18_CONVERSIONS order on main is still 59 (`elementTextVariantHeadingLevels`). Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…eld — the console derives the line-position field from the child object (objectstack-ai#21589) (objectstack-ai#21632) Fixes objectstack-ai#21589 Clause-②: no (narrowing) ## What this does Retires `sortField` from the `object-master-detail-form` detail entry (`ComponentPropsMap['object-master-detail-form'].details[]`). The console reads no authored value for it. At the `.objectui-sha` pin `89cad75d5570` (and unchanged at the current pin `ab1879721595`: `MasterDetailForm.tsx` and `deriveMasterDetail.ts` are byte-identical between the two), `MasterDetailDetailConfig` has no `sortField` member (`plugin-form/src/MasterDetailForm.tsx:83`). The field the line grid stamps with each line's position is the one `deriveDetail` derives from the child object (`deriveMasterDetail.ts:540`), and it reaches the grid as `sort_field` (`:874`). The spec still declared the key, so an authored `sortField` went through `os validate` clean and was then dropped. This executes the direction triage confirmed (`5969870827`), the one recorded on objectstack-ai#21220's landing (`5937457620`) and mirrored on objectstack-ai/objectui#11396 ③: a `retiredKey()` tombstone, one ADR-0087 D2 conversion and one D3 entry, with no staged window because the writer census is zero. There is no objectui edit here: the objectui#11396 mirror follows once a published `@objectstack/spec` carries this. ## The retirement kit - **Tombstone** on the strict detail entry (`packages/spec/src/ui/component.zod.ts`). The refusal is the prescription, and the key's input type is the retired-key mark, so `tsc` refuses it too. The prescription opens with the fully qualified key: `object-master-detail-form` property `details[].sortField`. It ends with the house `os migrate meta --from 17` sentence. - **One declaration of the derived sort-field names.** `INLINE_GRID_SORT_FIELDS` (the same six names in the same order as the renderer's `SORT_FIELD_NAMES`) moves out of `data/inline-grid-columns.ts` into `data/inline-grid-sort-fields.ts`, a module reached only by relative import, like `ui/action-target-aliases.ts`. Exported from `inline-grid-columns.ts`, it would have reached the published API through the `data` barrel. `inline-grid-columns.ts` imports it, and the tombstone's prescription and `record:line_items`' answer print the list from it (`INLINE_GRID_SORT_FIELD_LIST`). `check:api-surface` is unchanged, as expected. - **D2 conversion `object-master-detail-form-detail-sort-field-removed`** (step 18, `retiredFromLoadPath`, `retiredAfter: '17.6.0'`, order 60). It is a lossless delete of `sortField` from each `properties.details[]` entry of an `object-master-detail-form`, scoped by component type and by position. Its fixture covers region, nested-card and slotted positions, an open-namespace control with the same shape, and an entry that is not an object. - **`RETIRED_KEYS_BY_MAJOR[18]`** registers the nested key `ui/ObjectMasterDetailFormProps:details.sortField` (an array member, spelled without its `[]`). It has no `authorable-surface/` line, so checks (b2) and (b3) resolve it against the emitted schema, and `check:authorable-surface` is green on it. - **D3 entry `object-master-detail-form-detail-sort-field-retired`**, plus a `STEP18_RATIONALE` fragment at order 70. - **Texts that became false, changed in this PR:** - the entry docblock: "exactly the twelve keys" becomes eleven plus the tombstone, re-read at the pin, and "the three it adds … and `sortField`" now says the line-position field is derived; - `record:line_items`' guidance for `sortField`, which used to say the key "belongs to an `object-master-detail-form` detail entry" and now says no block takes it; - the generated reference page row (`content/docs/references/ui/component.mdx`); - the carriers test's every-key control (`FULL_DETAIL_ENTRY`, now 11 keys); - two landed step-18 D3 entries. `ui-object-master-detail-form-details-closed` listed `sortField?` among the keys to write, and `ui-record-line-items-props-closed` sent `sortField` to the detail entry. Both entries shipped in 17.6.0's `registry.ts`. Step 18 is still open, so the 17 → 18 upgrade guide will be generated from these texts. Reviewer, please confirm that this correction belongs in this PR. - **Changeset** `@objectstack/spec` `minor`, with a BREAKING banner, the FROM → TO table and the ADR-0087 `registered` disposition. ## Premise checks (measured on origin/main 9a4182a, objectui at the pin 89cad75d5570) - Locations: the key was at `component.zod.ts:5566`, the docblock at `:5539` and the `record:line_items` guidance at `:2201`–`:2202`, as the claim said. - Renderer: `MasterDetailForm.tsx:83` says "⛔ No `sortField` member". The only `sortField` on that path is the derived one (`:372`, `:874`, `:1066`, `:1081`; `deriveMasterDetail.ts:55` and `:540`). A fully configured entry (FK plus every column typed) loads no child schema (`:977`–`:979`), so it stamps no line position. The prescription says this. - Writer census, `git grep -n -w sortField` over `examples apps packages skills content/docs`, excluding `packages/spec/**` and CHANGELOGs: 1 hit, the generated reference page. Same instrument, control `addLabel`: 6 hits, including the showcase project workspace's detail entry. At the objectui pin, the only detail entries that write the key are probes asserting that nothing reads it (`masterDetailDetailsMembers-8071.test.tsx` rows 2c and `:295`/`:304`). No objectui source assigns it through a spec type. - Landing site: `packages/spec` is the producer of the declared key. No other package was involved. ## Evidence All on the final head `f765e8caaf` unless noted. The branch merged `origin/main` three times: at `e367002e11` (which carries the sibling element:text landing `36ad3210d4`), at `5b5e83f446` (which carries objectstack-ai#21622's landing `3f1bc816a2`), and at `15fe567c9c` (which carries objectstack-ai#21625's pin bump `1cbe165bfc` and objectstack-ai#21642's `15fe567c9c`). Readings marked `69f42d393d` predate the second merge. - `@objectstack/spec` local project at `f765e8caaf`: 609 files and 18059 tests passed (1 todo), with no skip. - `@objectstack/spec` repo project at `f765e8caaf`: the three merge-shape and retirement files (`step18-rationale-merge`, `conversions-major18-merge` and `master-detail-detail-sort-field-retirement`) passed 41 tests. The wider repo-project run is a `69f42d393d` reading: 52 of 53 files passed. NOT MEASURED: `scripts/build-schemas-check-mode.test.ts`, which alone exceeds the 590 s foreground cap. `check:authorable-surface`, the gate whose mode it tests, is green at `f765e8caaf`. - `pnpm --filter @objectstack/spec typecheck`: exit 0, including `check:test-typecheck`. The new pin's `@ts-expect-error` is compiled there, so an unused directive would be red. - Consumer packages that author an `object-master-detail-form` `details[]` entry: - `@objectstack/lint` at `f765e8caaf`: `validate-component-props.test.ts` passed 51 tests (including the new advisory pin). At `69f42d393d`, all 119 files and 5621 tests passed, and its typecheck passed; - `@objectstack/example-showcase` at `69f42d393d`: its typecheck passed, and 32 files and 399 tests passed. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts are up to date. - Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 114 commands at `f765e8caaf` (the same set as at `a1b0552acd`), and all 114 exited 0 on the first attempt after a full build. `--ran` reconciled them as 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN. At `a1b0552acd`, the round report that first quoted the same reading had stale provenance; the seat's note `5974199078` on the card corrects it. - ESLint, as a proven narrowing rather than the repo-wide `pnpm lint`: - Population: `eslint.config.mjs` lints `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`. - `--no-inline-config --format json` over the 12 changed lintable files at the final head `f765e8caaf` reported 12 files, 0 errors and 0 warnings (no ignored-file warnings, so all 12 are in the population). An earlier run, before the two D3 entry text edits, read 10 files with the same result. - Invariance: the config never enables type-aware linting (no `parserOptions.project` and no `projectService`), and the diff touches neither the config nor its baselines, so no verdict on an untouched file can move. - Ablations: one-shot runs through `scripts/ablation-replace.mjs`, wrapped in a trap. Each restore is proven by blob == HEAD and an empty `git diff HEAD`. - The tombstone reverted to `z.string().optional()`: 5 of the pin's tombstone rows went red (refusal, list, props-lint row, tombstone-not-strict-arm, tsc channel), and 15 stayed green. - `sortField: 'position'` planted on the showcase workspace's detail entry: the tree-scoped absence walk went red, naming `examples/app-showcase/src/ui/pages/project-workspace.page.ts authors sortField: 'position'`. ## New pins - `packages/spec/src/ui/master-detail-detail-sort-field-retirement.test.ts`, in the repo project because it walks the tree. It covers: - the tombstone: issue `code` `invalid_type`, path `details.0.sortField`, the prescription, and the derived names drawn from the one declaration; - that the props-lint row is the same schema; - the tombstone against the strict-arm control; - the walked shape keeping the key; - the tsc channel; - that the page parse never refuses; - `record:line_items`' answer; - the D2 strip: stored row, built artifact in every position, by-reference control, type and position scoping, idempotence, retired from the load path; - the ledger row and the D3 entry; - a tree-scoped absence walk over the radius `@objectstack/spec` already declares. `check:cross-package-test-inputs` is green, and the test is listed in `vitest.repo-tests.json`. - `packages/lint/src/validate-component-props.test.ts`: a detail-entry `sortField` is a `component-props-invalid` warning at `properties.details.1.sortField`, carrying the prescription. ## Acceptance notes - The D3 entry's `replacement` spells out the six derived names literally. The generator copies entry values into `registry.ts` and keeps no imports, so it cannot read the declaration. The pin asserts that every member of `INLINE_GRID_SORT_FIELDS` appears in it. - Deployed metadata is NOT MEASURED. Stored pages are converted by the D2 strip when they are read, and the props lint reports live sources. - Merge order with this seat's siblings: the element:text variant retirement (`36ad3210d4`) landed first at conversion order 59 and rationale order 68. objectstack-ai#21622 (`3f1bc816a2`) landed next with rationale order 69. This PR takes 60 and 70. All three merges went through `scripts/pm/os-regen-merge.sh`. Each time, the os-regen driver kept this branch's copy of `component.mdx` and dropped main's rows: the variant row, then objectstack-ai#21622's `object-metric` rows, then objectstack-ai#21625's `object-grid` `keyboardNavigation` row. Each time, the page was regenerated from the merged source in its own commit (the last is `f765e8caaf`). Measured by comparing change lines, the head's delta against `main` is this PR's own delta at `a1b0552acd`, file by file; and the head's delta against `a1b0552acd` is `main`'s own delta. objectstack-ai#21625 did not touch the detail entry or its docblock. - CI at `f765e8caaf`: complete and green, 33 success and 2 skipped (`Console Pin Gate` and the opt-in packed-tarball smoke, by their own filters). `Lint & Repo Gates`, red at `a1b0552acd` only because GitHub's issues API answered HTTP 503 in `check-issue-citations`, is success on this head. --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #21464
Clause-②: yes (narrowing)
What this does
Stage 4 (S-metric) of the
ComponentPropsMapz.unknown()close-out, per triage5961300594, the seat answers5963787404(staging A) and5966636964(the writer test: a writer is a value the renderer draws), and the claim5970896052. Of the metric tile's fourz.unknown()members, two are typed and two are held for a ruling, because each one's by-reference candidate disagrees with what the tile reads. Read points are at the.objectui-shapin89cad75d55, underpackages/plugin-dashboard/src/unless named.object-metricmemberaggregatez.unknown(){ field?, function, groupBy? }(module-privateObjectMetricAggregateSchema):functionis the query AST'sAggregationFunctionby reference,groupByisChartGroupBySchemaby reference and optional, and a function other thancountneeds afieldObjectMetricWidget.tsx:403-443(computeOne: the request and the result column),:405-427(a structuredgroupBygoes to the spec-shape query; an absent one floors at_all),:305and:344-353(functionpicks the unit and the min / max width)trendz.unknown(){ value: number, label?: I18nLabel, direction?: 'up' | 'down' | 'neutral' }(module-privateObjectMetricTrendSchema), the measured shapeMetricWidget.tsx:437-450drawsvalueas a percentage anddirectionas the arrow;:332-337resolveslabel; the declared type is these three (MetricWidget.tsx:242-246,ObjectMetricWidget.tsx:176-181)drillDownz.unknown()ObjectMetricWidget.tsx:602-651readsenabled,title,target,columns,maxRowsandreport;DrillDownDrawer.tsx:77-114drawsreportas a report bodycompareToz.unknown()kindalone, throughshiftFilterByCompareToandcompareToTrendLabelKey(objectuicore/src/utils/compare-to.ts:99-137), fromObjectMetricWidget.tsx:468-469and:581Neither typed member carries a default or a transform, so a parse returns the authored value byte for byte, and
ObjectMetricPropsParsedstill differs from the authored state onfilteralone.ObjectMetricPropscarries the two types instead ofunknown. No new export: both schemas are module-private, as stage 2's lane and stage 3'smobilewere.The by-reference candidates, measured member by member
The claim's mechanism assumption 2 named the chart's
aggregate/drillDownand the dashboard widget'scompareToas the by-reference candidates. None of the three accept sets equals the tile's read:aggregateChartAggregateSchemacount_distinctfunction (the chart's five leave it out; the tile forwards the engine's six, and objectui's unit-rule pin mounts one tile perAggregationFunctionmember,__tests__/ObjectMetricWidget.countNotCurrency-10356.test.tsx:102-124)groupBy, and every authored showcase tile writes{ field, function }with nonegroupByunion is taken by reference, andfunctionis the engine's own enum by referencetrenddrillDownChartDrillDownSchemareport(refused there, with guidance pointing at the metric; drawn here,__tests__/objectMetricDrillDownMembers-8071.test.tsx:277-294)filter(objectui's ownObjectMetricDrillDownConfigrefuses it by name;__tests__/ObjectMetricWidget.drillDownRefusal-9002.test.tsx:56authors one and parses under the chart's shape while the tile ignores it)compareToDashboardWidgetSchema.shape.compareTodimension, which this path never reads (compare-to.ts:28-33;__tests__/ObjectMetricWidget.compareTo.test.tsx:105-115pins it inert)So
aggregateis typed to the tile's read with its vocabulary by reference (one declaration per shape: the shape is the metric's, each member's vocabulary is declared once elsewhere), and the two members whose reference would declare a key the tile never reads are reported as forks rather than shipped. Both are put to the seat in the dev report's open questions.The census
A writer is a value written on an
object-metric: a page-component node (an object literal naming the type, a literal annotated with the block's type, a direct parse through the row), the block's React component with the member as a prop or insideschema={{…}}, or the argument of a local test helper that mounts one. Values resolve through same-file constants; helper arguments andit.eachrows were resolved by hand. The instrument is a TypeScript-AST walk over every.ts,.tsx,.js,.jsx,.mjs,.json,.yaml,.mdand.mdxfile, with fenced code in documents parsed too, plus a loose pass over every property or JSX attribute named for one of the four members in a file that names the block, triaged by hand. Each static value was parsed through the BUILT row.aggregatevalues · static (distinct) · parse · refusedtrenddrillDown(held)compareTo(held)b610eabf72(examples/,packages/withpackages/apps/,content/,skills/,apps/)89cad75d55(whole tree)Re-checked at the final merge base
36ad3210d4: the.objectui-shapin is unchanged, and the commitsmainbrought sinceb610eabf72add no line namingobject-metricorObjectMetric(0 hits in 5095 moved lines).objectstack writers. The showcase's thirteen KPI tiles (
examples/app-showcase/src/ui/pages/index.ts:75-78,my-work.page.ts:39-41, thekpi()helper's six calls atcommand-center.page.ts:147-152), two YAML examples incontent/docs/protocol/objectui/layout-dsl.mdx(:688,:1046), and six test copies (packages/spec/src/ui/component.test.ts:3479-3482,packages/lint/src/validate-component-props.test.ts:561,:565): all{ field, function }withcountorsum, all parse, all drawn. The one refused value is not a door of this row:skills/objectstack-ui/rules/pages.md:160'skind: 'html'example writesaggregate="count", which the html tier compiles against the component manifest, not throughComponentPropsMap(see Acceptance notes).objectui writers. Every one is a test fixture, under
packages/plugin-dashboard/src/unless named: the query, drill-down, trend and structured-groupBy member pins (__tests__/objectMetricQueryMembers-8071,objectMetricDrillDownMembers-8071,objectMetricTrendMembers-8071,objectMetricStructuredGroupBy-8613), the widget pins (ObjectMetricWidget.*andObjectMetric.*),packages/app-shell/src/views/__tests__/PageView.refreshInPlace-10519.test.tsx:240, and the two arms parsed through objectui'sObjectMetricBlockSchema, which takes this row by reference (packages/types/src/__tests__/registered-type-arms-10859-b2.test.ts:149,:198;flat-props-refusal-10872.test.ts:160). Under the writer test:aggregate: { field: 'amount', function: 'sum', groupBy: ['stage'] }__tests__/objectMetricStructuredGroupBy-8613.test.tsx:225invalid_unionataggregate.groupBy— correcttrend: { value: 12, direction: 'up', label, percent: 99, caption: 'Off-list caption' }__tests__/objectMetricTrendMembers-8071.test.tsx:20599and the caption are not drawnunrecognized_keysattrend— correctaggregate: { field: 'amount', function: 'count_distinct' }(twoit.eachrows)__tests__/ObjectMetricWidget.countNotCurrency-10356.test.tsx:121,:127functionis the engine's six, not the chart's fiveaggregateandtrendvalue (62 and 10)The two values that are not static are the dashboard relays' run-time hand-offs (
DashboardRenderer.tsx:858,DashboardGridLayout.tsx:363), which build a node from a dashboard widget's provider and pass through no parse.The held members (for the seat)
drillDown. The chart's drill-down shape refuses thereportthe tile draws and declares thefilterit ignores, so taking it by reference fails the writer test and declared = enforced at once. Typing it to the read needs a spec declaration of the drillreport(objectui's structural{ name, objectName, type?, columns, … } | { name }, whose named armDrillDownDrawernever draws), which is the S-objectui-held stage's definition.compareTo. Every measured writer parses under the dashboard widget's{ kind, dimension? }, so the reference refuses nothing; but it would declare adimensionthe tile accepts and ignores. The alternative is the tile's projection,{ kind }, withkindby reference anddimensionrefused with a prescription.Both stay
staged('object-metric', …)in the enumeration pin, with the read each rests on, and the stage's text now says why.Changes
packages/spec/src/ui/component.zod.ts: the two module-private schemas, the two typed members, the fork docblocks on the two held members, and an import ofChartGroupBySchemafrom./chart.zod(no import cycle:chart.zod.tsimports onlyi18n,retired-key,strict-objectandlazy-schema).packages/spec/src/ui/component-props-unknown-members.pin.test.ts: theaggregateandtrendlines leave the ledger; thedrillDownandcompareTolines cite the corrected reads; theobject-metricstage's text records the fork instead of the three by-reference candidates the census disproved.packages/spec/src/ui/component-metric-family-typed-members.pin.test.ts(new): §1 byte-identical parses of the drawn writers, §2 refusals bycodeand path, §3 thefunctionandgroupBydefs by identity, the restated count-needs-a-field rule held equal to the chart's over the chart's vocabulary, andtrend's three members, §4 the D3 registration.packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts(new) andpackages/spec/src/migrations/registry.ts(the hand-written step-18 rationale fragment at order 69, the next free order once spec(ui): release 2 of the objectui#7450 convergence —element:text.variant'sheading/subheadingbecome named refusals through the value-level retirement mechanism #21015's PR took 68; the semantic region regenerated bygen:migration-registry).packages/spec/dropped-refinements.baseline.json:ui/ObjectMetricPropsgains the siteaggregate(the count / field refinement does not project into JSON Schema, the same gapui/ChartAggregaterecords for the chart's identical rule); the site count 659 → 660.content/docs/references/ui/component.mdx(gen:docs) anddocs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md(gen:strictness-ledger:ui/192 → 194 sites, +2 strict)..changeset/21464-component-props-metric-family-typed.md:@objectstack/specminor, BREAKING banner, theClause-②line, the FROM → TO table and the ADR-0087 markerregistered ui-object-metric-aggregate-trend-typed.Measurements
All on
b29eafa062, the head after mergingorigin/main36ad3210d4(which carries #21015's PR, the other edit toui/component.zod.tsandmigrations/registry.ts; disjoint regions, both sides' entries present after the merge). Heavy runs went throughscripts/pm/os-verify-lock.sh; every exit code was written to a file before it was read.pnpm --filter @objectstack/spec test: Test Files 609 passed (609); Tests 18057 passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0;check:test-typecheck: OK(52 files / 246 errors / 135 signatures held).tsconfig.test.jsoncompiles both pins (tsc --listFilesOnly: 2 of 2 named).node scripts/ablation-replace.mjsin wrap mode, inside a script with its ownEXIT INT TERMrestore trap on absolute paths):trend: ObjectMetricTrendSchema.optional()→trend: z.unknown().optional(), no ledger line. Anchor x1 → x0, bloba85eb289b7→95ff70573c. Red: Tests 8 failed, 83 passed (91); the enumeration pin's §1 received exactly[ "object-metric trend" ], its census-equals-ledger control read 94 against 93, and the companion pin failed its fivetrendrefusals and its member-set case. Restore: blob after restorea85eb289b7equals the HEAD blob,git diff HEADempty (tool check and trap check both). The pins import./component.zodfrom source, so no build or dist preflight sits between mutation and run. Run at072d08d8e7;component.zod.tsis byte-identical in its metric region since (the later merge touchedElementTextPropsSchemaonly).object-metric, plus the row's one import side):pnpm --filter @objectstack/lint test119 files, 5620 tests passed;pnpm --filter @objectstack/example-showcase test32 files, 399 tests passed;pnpm --filter @objectstack/example-showcase validate(objectstack validate): "Validation passed", 0component-propsfindings over the showcase's thirteen metric tiles;packages/cli/test/platform-page-i18n-parity.test.ts(itsobject-metricnodes settitleonly): 27 passed.content/docsis judged by the derivedcheck:yaml-examplesbelow. objectui is read, not run: every value its pinned fixtures author was parsed through this built row (the census), and the arms that parse throughObjectMetricBlockSchemaall parse.validateComponentPropsfrom the built@objectstack/lint) on a page with oneobject-metric: the showcase tile and acount_distincttile report nothing;aggregate: 'count'reportscomponent-props-invalidatproperties.aggregate;trend: { direction: 'up' }reports it atproperties.trend.value.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) atb29eafa062: 114 commands, 9 paths, 553 changed lines (+536 / -17), under the 5000 threshold.--ran: "Run reconciliation — 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. All 114 exit 0. One run ofpnpm check:type-check-debtwas cut at my own 420 s cap (exit 124) and rerun alone: exit 0, "1 ledger entr(ies) re-measured in 100.5s". At the earlier head072d08d8e7,check:skill-examplesandcheck:dual-build-cjs-loadsfirst answered PREREQUISITE NOT MET (exit 3, unbuilt packages in a fresh worktree); after building those packages both exit 0, and those lines are kept apart.pull_requestpayload:check-changeset-no-majorexit 0 — on the corrected body (fix round48aa1fa253): "LEVEL AXIS: this PR declares clause-②yes (narrowing)" (it first read the originalno (narrowing)body with the same exit 0: "…, and no package whosepackages/**/src/**it moves is gradedpatch· direction arm:narrowing— a BREAKING change; during the launch window it shipsminor");check-adr-0087-registrationexit 0 (1 declared-breaking changeset,registered ui-object-metric-aggregate-trend-typed);check-empty-changesetexit 0.check-widening-tells --declaration no: exit 4, six tells — see Acceptance notes.--declaration yes: exit 0.eslint --no-inline-config --format jsonover the 5 changed TypeScript files: 5 files, 0 errors, 0 warnings, atb29eafa062. Population from eslint's own--print-config(the 5 resolve a config; the changeset,component.mdx, the strictness counts page anddropped-refinements.baseline.jsonresolveundefined); count from the JSON output; invariance:eslint.config.mjsnever enables type-aware linting (its own text, lines 326-328), so this diff cannot move a verdict on an untouched file. The fullpnpm lintis CI's.pnpm lint, reason: CI-owned.origin/mainmoved four commits after the last merge (to045b946256); none touches this PR's 9 paths or anything underpackages/spec, and none adds a line naming the block, so it was not merged again — the merge queue rebuilds onto currentmain.Acceptance notes
skills/objectstack-ui/rules/pages.md:160(akind: 'html'example) writesaggregate="count". Measured through the html tier's own compile with the committed component manifest (the pathobjectstack buildwires): onewarning,type-mismatch("prop "aggregate" expected an object"), and the compiled node carriesaggregate: "count"; the tile then readsaggregate.fieldandaggregate.functionas undefined. The same tag withaggregate={{"function":"count"}}compiles clean. Reported in the dev report for the seat to file;skills/**is a governed surface, so the fix is not a rider here.count_distinct. objectuidata-objectstack/src/index.ts:6740-6765(aggregateClientSide) has nocount_distinctarm, so itsdefaultsums the field. The tile draws acount_distinctcorrectly wherever the analytics service answers it (service-analyticsresolves the_count_distinctsuffix), and a wrong number on a deployment without it. Read-only reading of objectui code, not filed.check-widening-tells --declaration noexits 4 on this diff, six tells: five T1 at the two new schemas' keys inside the formerz.unknown()bags, and one false T2 at thedateGranularityguidance string. The gate's own text rules the T1 shape a true refusal that should declareyes; the original claim declaredno (narrowing).--declaration yesexits 0. Raised in the dev report and resolved: the seat re-declaredClause-②: yes (narrowing)(seat answer5972917488on spec(ui): theComponentPropsMaprows still type renderer-read members asz.unknown()—navigationon object-map / object-gantt / object-tree andconditionalFormattingon object-kanban accept42— the family close-out after #21445 #21464); the re-issued claim, this body's line 2 and the changeset (48aa1fa253) all carry it.