Skip to content

feat(spec)!: object-metric aggregate and trend take the shape the tile reads; drillDown, compareTo held for a ruling (#21464, stage 4) - #21622

Merged
objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-21464-s-metric
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 11 commits into
mainfrom
claude/issue-21464-s-metric

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Part of #21464
Clause-②: yes (narrowing)

What this does

Stage 4 (S-metric) of the ComponentPropsMap z.unknown() close-out, per triage 5961300594, the seat answers 5963787404 (staging A) and 5966636964 (the writer test: a writer is a value the renderer draws), and the claim 5970896052. Of the metric tile's four z.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-sha pin 89cad75d55, under packages/plugin-dashboard/src/ unless named.

object-metric member was now read point
aggregate z.unknown() strict { field?, function, groupBy? } (module-private ObjectMetricAggregateSchema): function is the query AST's AggregationFunction by reference, groupBy is ChartGroupBySchema by reference and optional, and a function other than count needs a field ObjectMetricWidget.tsx:403-443 (computeOne: the request and the result column), :405-427 (a structured groupBy goes to the spec-shape query; an absent one floors at _all), :305 and :344-353 (function picks the unit and the min / max width)
trend z.unknown() strict { value: number, label?: I18nLabel, direction?: 'up' | 'down' | 'neutral' } (module-private ObjectMetricTrendSchema), the measured shape MetricWidget.tsx:437-450 draws value as a percentage and direction as the arrow; :332-337 resolves label; the declared type is these three (MetricWidget.tsx:242-246, ObjectMetricWidget.tsx:176-181)
drillDown z.unknown() held, unchanged, docblock records the fork ObjectMetricWidget.tsx:602-651 reads enabled, title, target, columns, maxRows and report; DrillDownDrawer.tsx:77-114 draws report as a report body
compareTo z.unknown() held, unchanged, docblock records the fork kind alone, through shiftFilterByCompareTo and compareToTrendLabelKey (objectui core/src/utils/compare-to.ts:99-137), from ObjectMetricWidget.tsx:468-469 and :581

Neither typed member carries a default or a transform, so a parse returns the authored value byte for byte, and ObjectMetricPropsParsed still differs from the authored state on filter alone. ObjectMetricProps carries the two types instead of unknown. No new export: both schemas are module-private, as stage 2's lane and stage 3's mobile were.

The by-reference candidates, measured member by member

The claim's mechanism assumption 2 named the chart's aggregate / drillDown and the dashboard widget's compareTo as the by-reference candidates. None of the three accept sets equals the tile's read:

member candidate the tile reads it does not declare it declares the tile does not read verdict
aggregate ChartAggregateSchema a count_distinct function (the chart's five leave it out; the tile forwards the engine's six, and objectui's unit-rule pin mounts one tile per AggregationFunction member, __tests__/ObjectMetricWidget.countNotCurrency-10356.test.tsx:102-124) nothing, but it REQUIRES groupBy, and every authored showcase tile writes { field, function } with none not taken whole: it would refuse all thirteen showcase tiles. Its groupBy union is taken by reference, and function is the engine's own enum by reference
trend none in the spec — — typed to the read
drillDown ChartDrillDownSchema report (refused there, with guidance pointing at the metric; drawn here, __tests__/objectMetricDrillDownMembers-8071.test.tsx:277-294) filter (objectui's own ObjectMetricDrillDownConfig refuses it by name; __tests__/ObjectMetricWidget.drillDownRefusal-9002.test.tsx:56 authors one and parses under the chart's shape while the tile ignores it) fork, held
compareTo DashboardWidgetSchema.shape.compareTo — dimension, which this path never reads (compare-to.ts:28-33; __tests__/ObjectMetricWidget.compareTo.test.tsx:105-115 pins it inert) fork, held

So aggregate is 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 inside schema={{…}}, or the argument of a local test helper that mounts one. Values resolve through same-file constants; helper arguments and it.each rows were resolved by hand. The instrument is a TypeScript-AST walk over every .ts, .tsx, .js, .jsx, .mjs, .json, .yaml, .md and .mdx file, 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.

corpus aggregate values · static (distinct) · parse · refused trend drillDown (held) compareTo (held)
objectstack b610eabf72 (examples/, packages/ with packages/apps/, content/, skills/, apps/) 22 · 22 (4) · 21 · 1 0 0 0
objectui 89cad75d55 (whole tree) 66 · 64 (19) · 63 · 1 12 · 12 (9) · 11 · 1 26 · 26 (14) 5 · 5 (3)

Re-checked at the final merge base 36ad3210d4: the .objectui-sha pin is unchanged, and the commits main brought since b610eabf72 add no line naming object-metric or ObjectMetric (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, the kpi() helper's six calls at command-center.page.ts:147-152), two YAML examples in content/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 } with count or sum, all parse, all drawn. The one refused value is not a door of this row: skills/objectstack-ui/rules/pages.md:160's kind: 'html' example writes aggregate="count", which the html tier compiles against the component manifest, not through ComponentPropsMap (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.* and ObjectMetric.*), packages/app-shell/src/views/__tests__/PageView.refreshInPlace-10519.test.tsx:240, and the two arms parsed through objectui's ObjectMetricBlockSchema, 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:

writer file:line at the pin drawn or probe verdict
aggregate: { field: 'amount', function: 'sum', groupBy: ['stage'] } __tests__/objectMetricStructuredGroupBy-8613.test.tsx:225 probe: the test's own title says an array is not the union's object arm, and the data adapter refuses it at the producer refused, invalid_union at aggregate.groupBy — correct
trend: { value: 12, direction: 'up', label, percent: 99, caption: 'Off-list caption' } __tests__/objectMetricTrendMembers-8071.test.tsx:205 probe: "reads no member the registration does not declare", asserting 99 and the caption are not drawn refused, unrecognized_keys at trend — correct
aggregate: { field: 'amount', function: 'count_distinct' } (two it.each rows) __tests__/ObjectMetricWidget.countNotCurrency-10356.test.tsx:121, :127 drawn: the tile shows the distinct count as a plain number parses — this is why function is the engine's six, not the chart's five
every other aggregate and trend value (62 and 10) the files above drawn parse

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 the report the tile draws and declares the filter it 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 drill report (objectui's structural { name, objectName, type?, columns, … } | { name }, whose named arm DrillDownDrawer never 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 a dimension the tile accepts and ignores. The alternative is the tile's projection, { kind }, with kind by reference and dimension refused 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 of ChartGroupBySchema from ./chart.zod (no import cycle: chart.zod.ts imports only i18n, retired-key, strict-object and lazy-schema).
  • packages/spec/src/ui/component-props-unknown-members.pin.test.ts: the aggregate and trend lines leave the ledger; the drillDown and compareTo lines cite the corrected reads; the object-metric stage'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 by code and path, §3 the function and groupBy defs by identity, the restated count-needs-a-field rule held equal to the chart's over the chart's vocabulary, and trend's three members, §4 the D3 registration.
  • packages/spec/src/migrations/entries/semantic/18.ui-object-metric-aggregate-trend-typed.ts (new) and packages/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's heading / subheading become named refusals through the value-level retirement mechanism #21015's PR took 68; the semantic region regenerated by gen:migration-registry).
  • packages/spec/dropped-refinements.baseline.json: ui/ObjectMetricProps gains the site aggregate (the count / field refinement does not project into JSON Schema, the same gap ui/ChartAggregate records for the chart's identical rule); the site count 659 → 660.
  • Regenerated: content/docs/references/ui/component.mdx (gen:docs) and docs/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/spec minor, BREAKING banner, the Clause-② line, the FROM → TO table and the ADR-0087 marker registered ui-object-metric-aggregate-trend-typed.

Measurements

All on b29eafa062, the head after merging origin/main 36ad3210d4 (which carries #21015's PR, the other edit to ui/component.zod.ts and migrations/registry.ts; disjoint regions, both sides' entries present after the merge). Heavy runs went through scripts/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.json compiles both pins (tsc --listFilesOnly: 2 of 2 named).
  • The two pins, inside the suite above: 91 tests at the earlier head, unchanged in content since.
  • Ablation (committed tree, node scripts/ablation-replace.mjs in wrap mode, inside a script with its own EXIT INT TERM restore trap on absolute paths): trend: ObjectMetricTrendSchema.optional() → trend: z.unknown().optional(), no ledger line. Anchor x1 → x0, blob a85eb289b7 → 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 five trend refusals and its member-set case. Restore: blob after restore a85eb289b7 equals the HEAD blob, git diff HEAD empty (tool check and trap check both). The pins import ./component.zod from source, so no build or dist preflight sits between mutation and run. Run at 072d08d8e7; component.zod.ts is byte-identical in its metric region since (the later merge touched ElementTextPropsSchema only).
  • Consumer packages (every package whose fixtures author an object-metric, plus the row's one import side): pnpm --filter @objectstack/lint test 119 files, 5620 tests passed; pnpm --filter @objectstack/example-showcase test 32 files, 399 tests passed; pnpm --filter @objectstack/example-showcase validate (objectstack validate): "Validation passed", 0 component-props findings over the showcase's thirteen metric tiles; packages/cli/test/platform-page-i18n-parity.test.ts (its object-metric nodes set title only): 27 passed. content/docs is judged by the derived check:yaml-examples below. objectui is read, not run: every value its pinned fixtures author was parsed through this built row (the census), and the arms that parse through ObjectMetricBlockSchema all parse.
  • The public door, lit both ways: the component-props gate (validateComponentProps from the built @objectstack/lint) on a page with one object-metric: the showcase tile and a count_distinct tile report nothing; aggregate: 'count' reports component-props-invalid at properties.aggregate; trend: { direction: 'up' } reports it at properties.trend.value.
  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at b29eafa062: 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 of pnpm check:type-check-debt was 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 head 072d08d8e7, check:skill-examples and check:dual-build-cjs-loads first 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.
  • Changeset gates with this body as the pull_request payload: check-changeset-no-major exit 0 — on the corrected body (fix round 48aa1fa253): "LEVEL AXIS: this PR declares clause-② yes (narrowing)" (it first read the original no (narrowing) body with the same exit 0: "…, and no package whose packages/**/src/** it moves is graded patch · direction arm: narrowing — a BREAKING change; during the launch window it ships minor"); check-adr-0087-registration exit 0 (1 declared-breaking changeset, registered ui-object-metric-aggregate-trend-typed); check-empty-changeset exit 0.
  • check-widening-tells --declaration no: exit 4, six tells — see Acceptance notes. --declaration yes: exit 0.
  • Narrowed lint: eslint --no-inline-config --format json over the 5 changed TypeScript files: 5 files, 0 errors, 0 warnings, at b29eafa062. Population from eslint's own --print-config (the 5 resolve a config; the changeset, component.mdx, the strictness counts page and dropped-refinements.baseline.json resolve undefined); count from the JSON output; invariance: eslint.config.mjs never enables type-aware linting (its own text, lines 326-328), so this diff cannot move a verdict on an untouched file. The full pnpm lint is CI's.
  • NOT MEASURED: the Console Pin Gate, the Dogfood Regression Gate and the full pnpm lint, reason: CI-owned. origin/main moved four commits after the last merge (to 045b946256); none touches this PR's 9 paths or anything under packages/spec, and none adds a line naming the block, so it was not merged again — the merge queue rebuilds onto current main.

Acceptance notes

  • A published skill teaches a value no door refuses and the tile cannot draw. skills/objectstack-ui/rules/pages.md:160 (a kind: 'html' example) writes aggregate="count". Measured through the html tier's own compile with the committed component manifest (the path objectstack build wires): one warning, type-mismatch ("prop "aggregate" expected an object"), and the compiled node carries aggregate: "count"; the tile then reads aggregate.field and aggregate.function as undefined. The same tag with aggregate={{"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.
  • The client-side aggregate fallback sums a count_distinct. objectui data-objectstack/src/index.ts:6740-6765 (aggregateClientSide) has no count_distinct arm, so its default sums the field. The tile draws a count_distinct correctly wherever the analytics service answers it (service-analytics resolves the _count_distinct suffix), and a wrong number on a deployment without it. Read-only reading of objectui code, not filed.
  • check-widening-tells --declaration no exits 4 on this diff, six tells: five T1 at the two new schemas' keys inside the former z.unknown() bags, and one false T2 at the dateGranularity guidance string. The gate's own text rules the T1 shape a true refusal that should declare yes; the original claim declared no (narrowing). --declaration yes exits 0. Raised in the dev report and resolved: the seat re-declared Clause-②: yes (narrowing) (seat answer 5972917488 on spec(ui): the ComponentPropsMap rows still type renderer-read members as z.unknown() — navigation on object-map / object-gantt / object-tree and conditionalFormatting on object-kanban accept 42 — the family close-out after #21445 #21464); the re-issued claim, this body's line 2 and the changeset (48aa1fa253) all carry it.

claude added 10 commits October 3, 2026 16:26
…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>
…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>
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 6 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/api/error-catalog.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/index.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/data-modeling/queries.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/deployment/validating-metadata.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/kernel/contracts/data-engine.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/kernel/runtime-services/data-service.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/kernel/error-handling.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectql/query-syntax.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectui/concept.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/protocol/objectui/layout-dsl.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/ui/react-pages.mdx (via groupBy (literal, a string literal in a comment on a changed line))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v15.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-0.mdx (via groupBy (literal, a string literal in a comment on a changed line))
  • content/docs/releases/v17/17-6.mdx (via groupBy (literal, a string literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 138 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 045b946256d988653fdca185c7fd33d6d86bd78d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9004cece5272f594588521852afa4d37d7d8dd46 — the merge of head 48aa1fa2538519e643310faa324b260f8299c871 into base 045b946256d988653fdca185c7fd33d6d86bd78d, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 045b946256d988653fdca185c7fd33d6d86bd78d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…rrowing), per the re-issued claim

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 20:40
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 20:40
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit 3f1bc81 Oct 3, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21464-s-metric branch October 3, 2026 21:15
os-project-manager pushed a commit that referenced this pull request Oct 3, 2026
…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>
os-project-manager pushed a commit that referenced this pull request Oct 3, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants