feat(spec)!: object-metric drillDown and compareTo take the shape the tile reads; object-grid columns exits its hold (#21464, stage 5) - #21673
Conversation
… tile reads; object-grid columns exits its hold (#21464, stage 5) — WIP schemas and pins Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…8 fragments and changeset (#21464, stage 5) Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ss counts; the metric drill's enabled describe (#21464, stage 5) Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…abled describe (#21464, stage 5) Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 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 b4bae5b2f79f04319bb83f5328328fb46016eeae && git checkout b4bae5b2f79f04319bb83f5328328fb46016eeae
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55e6f14f8d87004c50b0f35eaafba36ac23cf32b a10f94d7c482d8759e4d687341f778682ac7846c && git checkout -B drift-repro 55e6f14f8d87004c50b0f35eaafba36ac23cf32b && git merge --no-ff a10f94d7c482d8759e4d687341f778682ac7846c
node scripts/docs-audit/affected-docs.mjs --json 55e6f14f8d87004c50b0f35eaafba36ac23cf32b
|
Part of #21464
Clause-②: yes (narrowing)
What this does
Stage 5 of the
ComponentPropsMapz.unknown()close-out: the S-metric remainder and the exit of theobject-gridcolumnshold, per the claim5975220557, the seat's S-metric answer5972917488(§1drillDown: B; §2compareTo: B), the staging answer5963787404, the holds' carrier answer5969669440, stage 4's landing5973565761anddomain:devx's pointer5974144504. Three narrowings, one ADR-0087 D3 entry each. Read points are at the.objectui-shapinab1879721595(this repo's pin), underpackages/plugin-dashboard/src/orpackages/plugin-grid/src/unless named.ab1879721595object-metriccompareToz.unknown(){ kind }(module-privateObjectMetricCompareToSchema):kindisDashboardWidgetSchema.shape.compareTo's ownkindby reference;dimensionrefused by name with the prescriptionObjectMetricWidget.tsx:522-523(shiftFilterByCompareTo) and:675(compareToTrendLabelKey); both dispatch onkind === 'previousYear'alone (objectuicore/src/utils/compare-to.ts:104,:128), and the file's header recordsdimensionas never read on this path (:28-33)object-metricdrillDownz.unknown(){ enabled?, title?, target?, columns?, maxRows?, report? }(module-privateObjectMetricDrillDownSchema): the first five areChartDrillDownSchema.shape's members by reference;filterandmoderefused by name with objectui's prescriptions;reportHELD atz.unknown()ObjectMetricWidget.tsx:696(isDrillEnabled),:708(resolveDrillTitleagainst an empty click event),:732-744(the sharedDrillDownDrawer:target,columns,maxRows,report); objectui's ownObjectMetricDrillDownConfigrefusesfilterandmodeby name (types/src/data-display.ts:2628)object-gridcolumnsz.array(z.unknown()), held at stage 2ListViewSchema.shape.columnsby reference (the reference stage 2 held it from)ObjectGrid.tsx:819(normalizeColumns),:3367/:3451(the draw path keeps only an entry with a stringfield, readingListColumn's own members);groupValueFormatter(:2979) now reads the column'stypealone, typed asListColumn(:3004-3007), and the labels fromobjectDefField?.optionsonly (:3010)enabledandtitlekeep the chart drill-down's defs (_zod.defidentity, pinned) with describes of their own, because the chart's speak of a clicked segment and its${event.*}tokens, and a metric tile has no click context. No new member carries a default or a transform, soObjectMetricPropsParsedstill differs from the authored state onfilteralone; a parsed grid column'sprefix.typecarries the list view's'text'default, as stage 2 originally recorded. No new export: both new schemas are module-private.The holds, re-read at
ab1879721595object-gridcolumns: exit condition met. Stage 2's records held the member on one read (colOverride?.options || objectDefField?.optionsat89cad75d55), and the writer test (5966636964) held it only if a renderer path at the pin readseditableoroptionsoff an authored column. objectuib0bf413(finding(plugin-grid): the grouped grid's header labels readcolumns[].options, a keyListColumnSchemarefuses, blocking the spec from typingobject-gridcolumnsobjectui#11544) retired theoptionsread, and no read takeseditable(orreference) off an authored column: the emitted columns are built key by key fromListColumnmembers (:3676-3702), and every object spread at:4384,:4483,:4628,:4728is of an EMITTED column. The three records that said the grid readscolumns[].optionsare corrected: the docblock incomponent.zod.ts, the enumeration pin's held line (removed) and the list-family pin's header. One finding beside the exit, see Acceptance notes: the footer summary still reads four undeclared column keys.object-metricdrillDown.report: stays held, as ruled. Its docblock is re-anchored: the drawer draws a dataset-bound report whenisDatasetBoundReportholds (DrillDownDrawer.tsx:92, used at:115; objectui#11506,8366acc), and objectui#11517 (9ed8d0f) refuses the{ name }arm. Expressed in the house shape: the enumeration pin's walk reaches it asobject-metric drillDown.report, astaged('objectui-held', …)ledger line, and theobject-metricstage leavesSTAGES(its four members are typed). No fork was needed.The census
A writer is a value written on the block: a page-component node (an object literal naming the type, a literal annotated or asserted with the block's type, a direct parse through the row), the block's React component with the member as a prop or inside an object-literal
schema, or the argument of a local helper that mounts one, with positional helper parameters resolved at every call site. Values resolve through same-file constants. Instrument: a TypeScript-AST walk over every.ts,.tsx,.js,.jsx,.mjs,.cjs,.json,.yaml,.mdand.mdxfile (fenced code in documents parsed too). Each static value was parsed through the row at this branch's source, and the grid's refused set was cross-checked againstListViewSchema.shape.columnsdirectly (identical).compareTodrillDownobject-gridcolumns6ec54f00ba(examples/,packages/withpackages/apps/,content/,skills/,apps/,docs/)ab1879721595(whole tree,git archiveof the pin)objectstack writers. The showcase's two grids (
examples/app-showcase/src/ui/pages/my-work.page.ts:65,command-center.page.ts:172) and three test copies (packages/lint/src/validate-component-props.test.ts:553,:577,packages/spec/src/ui/component.test.ts:3433): all field-name strings, all parse, all drawn.origin/mainmoved four commits since the base (tofea67065a3); none touchespackages/spec, and none adds a line namingobject-grid,object-metric,drillDownorcompareTo.objectui writers, under the writer test. Every value is a test fixture or a run-time hand-off. Each refused value:
compareTo: { kind: 'previousYear', dimension: 'close_date' }plugin-dashboard/src/__tests__/ObjectMetricWidget.compareTo.test.tsx:109drillDown: { enabled: true, filter: { stage: 'won' } }and{ enabled: true, mode: 'record' }plugin-dashboard/src/__tests__/ObjectMetricWidget.drillDownRefusal-9002.test.tsx:56,:58@ts-expect-error)accessorKey/headercolumnDeclaredSpellingOnly×4,columnSpellingDiagnosticRender×9,ObjectGrid.malformedFilter-9050×3,authoredColumnsFls-6799:409resolvesToDataColumnneedsfield; the spelling diagnostic names themnameprojectionFls-6898:206,authoredColumnsFls-6799:333,columnIdentity×3field(the identity fold readsnamefor the projection and field security only,fieldwinning)editable: falsegridDependentValues-7165,lookupPickerKeys-7154×5,relationalMetaCopySet-7166×4,inlineEditExitMode×2,inlineEditPersistence,inlineEditLookupReproeditableoff an authored columnreferenceObjectGrid.linkCellAnchor.test.tsx:252applyRelationalMeta), and the test asserts the cell stays plainoptionsgridGroupingMembers-8071.test.tsx:293,:312optionsare NOT readfieldserverGroupedSelectIdentity-11105.test.tsx:162resolvesToDataColumn)columns: 5types/src/__tests__/element-data-source-objectname-waiver-11117.test.ts:154The not-static values: one
drillDownit.eachover two report probes (both parse:reportis held open); forcolumns, the run-time hand-offs inplugin-view/src/ObjectView.tsx:2487,plugin-designer/src/ObjectManager.tsx:116andFieldDesigner.tsx:188(the designers' lists are{ field, label, width }), and six test lists built from{ field, label[, type] }entries, which parse by construction. Stage 2 counted 40 refusedcolumnsvalues with a different instrument; the 41st here, thereferencecolumn, was present at89cad75d55too.Changes
packages/spec/src/ui/component.zod.ts:ObjectMetricDrillDownSchemaandObjectMetricCompareToSchema(module-private) with their docblocks and prescriptions; the two members;columnsback toListViewSchema.shape.columnswith the exit docblock; the grid row andObjectGridPropsParseddocblocks; imports ofChartDrillDownSchema(besideChartGroupBySchema) andDashboardWidgetSchema(dashboard.zod.tsimports nothing that reachescomponent.zod.ts; the eagergen:schemabuild is green).packages/spec/src/ui/component-props-unknown-members.pin.test.ts: thedrillDown,compareToandcolumns[]lines leave the ledger;drillDown.reportjoins asobjectui-held; theobject-metricstage leavesSTAGES, whoseobjectui-heldtext now names the drill report.packages/spec/src/ui/component-metric-family-typed-members.pin.test.ts: §1 seven new byte-identical parses (objectui's drill and comparison writers), §2 ten new refusals bycodeand path plus three prescription checks, §3 def identity for the five drill members andkind, and each shape's exact member set, §4 the two new D3 ids.packages/spec/src/ui/component-list-family-typed-members.pin.test.ts: thecolumnscases stage 2's fix round removed come back (§1 two parses and theprefixdefault, §2 nine union refusals includingoptions,referenceand a footerprecisionhint, §3 def identity), and the header no longer says the grid reads a column'soptions.packages/spec/src/migrations/entries/semantic/18.ui-object-metric-compare-to-typed.ts,18.ui-object-metric-drill-down-typed.ts,18.ui-object-grid-columns-typed.ts(new) andpackages/spec/src/migrations/registry.ts: three hand-written step-18 rationale fragments at orders 71, 72 and 73 (70, feat(spec)!: retire the object-master-detail-form detail entry sortField — the console derives the line-position field from the child object (#21589) #21632's, is the last taken onmain), and the semantic region regenerated bygen:migration-registry. No D2 conversion: nothing stored needs rewriting, since page-componentpropertiesis not parsed on the save or load path. NoRETIRED_KEYS_BY_MAJORrow: the refused keys are nested in member shapes, not top-level row keys.content/docs/references/ui/component.mdx(gen:docs) anddocs/audits/2026-07-unknown-key-strictness-ledger.counts/ui.md(gen:strictness-ledger:ui/194 → 196 sites, +2 strict)..changeset/21464-component-props-metric-rest-and-grid-columns-typed.md:@objectstack/specminor, BREAKING banner, theClause-②line, the FROM → TO table, the census, and the ADR-0087 marker registering the three ids.Measurements
All on the head
a10f94d7c4(base6ec54f00ba; 11 files, +765 / −119, 884 changed lines, under the 5000 threshold). Heavy runs went throughscripts/pm/os-verify-lock.sh, and every exit code was written to a file before it was read.pnpm --filter @objectstack/spec test: exit 0 — Test Files 609 passed (609); Tests 18095 passed, 1 todo.pnpm --filter @objectstack/spec typecheck: exit 0 —check:test-typecheck: OK(52 files / 246 errors / 135 signatures held, unmoved);tsc -p tsconfig.test.json --listFilesOnlynames all three pins.pnpm --filter @objectstack/spec buildandcheck:generated --fix: the build is green (the eagergen:schemapass included);--fixregenerated only what it proved stale (gen:docs,gen:strictness-ledger), and the derivedcheck:generatedthen exits 0.pnpm --filter @objectstack/lint exec vitest run src/validate-component-props.test.ts— 51 passed (after building lint's closure; the first try answeredFailed to resolve entry for package "@objectstack/formula", an unbuilt dependency, not a measurement);pnpm --filter @objectstack/lint test— Test Files 119 passed, Tests 5627 passed;pnpm --filter @objectstack/example-showcase validate(objectstack validate) — "Validation passed".validateComponentPropsfrom@objectstack/lintover the built spec, one block per page): a drill withenabled/target/columns/maxRows, a heldreport,compareTo: { kind: 'previousYear' }, the showcase's string columns and a{ field, label, width }column report nothing; a drillfilterreportscomponent-props-unknown-keyatproperties.drillDown.filter, a comparisondimensionatproperties.compareTo.dimension, a barecompareTo: 'previousYear'component-props-invalidatproperties.compareTo, anaccessorKeycolumncomponent-props-invalidatproperties.columns, and a columnoptionscomponent-props-unknown-key.node scripts/ablation-replace.mjsin wrap mode inside a script with its ownEXIT INT TERMrestore trap on absolute paths, HEAD-blob comparison, an empty hash read as failure). Each leg reverts one member to its formerz.unknown()form and runs the three pins (175 tests):compareTo→z.unknown().optional(): anchor x1 → x0, blobb3bba43a35→4ef63a8559. Red: 8 failed, 167 passed. The enumeration pin's §1 received exactly[ "object-metric compareTo" ]and its census-equals-ledger control read 92 against 91; the companion pin failed its four comparison refusals, thedimensionprescription and thekinddef identity.drillDown→z.unknown().optional(): blob →51d55b8e7b. Red: 12 failed, 163 passed. §1 received[ "object-metric drillDown" ], and its second half[ "object-metric drillDown.report" ](a ledger line outliving its member); the companion pin failed its six drill refusals, the two prescriptions and both §3 cases.columns→z.array(z.unknown()).optional(): blob →8f09c64675. Red: 13 failed, 162 passed. §1 received[ "object-grid columns[]" ], 92 against 91; the list-family pin failed theprefix-default parse, its ninecolumnsunion refusals and the def identity.b3bba43a35equals the HEAD blob andgit diff HEADis empty. Green control after the three legs: 175 passed (175). The pins import./component.zodfrom source, so no build or dist preflight sits between mutation and run.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) — 114 commands;--ran— "Run reconciliation — 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN", exit 0, all 114 exit 0. Six first answered PREREQUISITE NOT MET (exit 3) in the fresh worktree (@objectstack/lintcheck:doc-formula-expressionsandcheck:doc-security-posture, speccheck:skill-examples,check:docs-transcript-drift,check:dual-build-cjs-loads,check:lean-entry-closure); after oneturbo run build(72 tasks, 71 cached) each exits 0, and those reruns are what the reconciliation records.pull_requestpayload:check-changeset-no-majorexit 0 — "LEVEL AXIS: this PR declares clause-②yes (narrowing), and no package whosepackages/**/src/**it moves is gradedpatch";check-adr-0087-registrationexit 0 — "1 declared-breaking changeset(s), each carrying an ADR-0087 disposition";check-empty-changesetexit 0.eslint --no-inline-config --format jsonover the 8 changed TypeScript files — 8 files, 0 errors, 0 warnings. Population from eslint's own--print-config(the 8 resolve a config; the changeset,component.mdxand the strictness counts page resolveundefined); count from the JSON output; invariance:eslint.config.mjsnever enables type-aware linting (its own text, about lines 326-328), so this diff cannot move a verdict on an untouched file. The fullpnpm lintis CI's.pnpm lint, reason: CI-owned; objectui was read at the pin, not built or run against this spec.Acceptance notes
ListColumndoes not declare.plugin-grid/src/useColumnSummary.ts:558-568at the pin formats a column's summary with(col as any).currency,.defaultCurrency,.precisionand.scaleahead of the field's (the same file readscurrencyConfigandmaxoff the field only, saying a column-level read "would be a second, undeclared spelling"). The read was there at89cad75d55too. No measured writer authors one on anobject-gridcolumn, so under the writer test it holds nothing, and a list view's columns (the same entry) already refuse all four; with this PR the grid row refuses them as well (pinned: theprecisioncase in the list-family pin). Reported for the seat as an objectui finding, the shape finding(plugin-grid): the grouped grid's header labels readcolumns[].options, a keyListColumnSchemarefuses, blocking the spec from typingobject-gridcolumnsobjectui#11544 had.report's contract may be cheaper than the hold suggests. At the pin objectui typesDrillDownConfig.reportas this spec's ownReportSchemaauthor input (types/src/data-display.ts:23-24,:2592,SpecReportInput), and the drawer applies the spec report'sruntimeFilter. So the S-objectui-held step forreportmay reduce to the spec declaring the member by reference toReportSchema. Not done here, as ruled; raised for the seat.@objectstack/specbump: its block schemas take these rows by reference (ObjectMetricBlockSchema,ObjectGridBlockSchema, throughpropsBag), but no objectui fixture that authors a refused value parses through them: the refused metric values mount the widget directly, and the refused grid columns mountObjectGriddirectly. objectui source does not import the narrowedObjectMetricProps/ObjectGridPropstypes. Not built here: the Console Pin Gate builds objectui at the pin againstmain, and its filter does not listpackages/spec/src/**.check-widening-tells --declaration noexits 4 with seven T1 tells, every one a key inside a formerz.unknown()bag (the drill's six members,compareTo.kind), the shape the gate rules a true refusal;--declaration yesexits 0. The claim, this body and the changeset declareyes (narrowing).Generated by Claude Code