Repository navigation
fix(spec): the field → name respelling reaches a form view's subform columns as its own ADR-0087 step, and two step-18 entries read true (#20901) - #20953
Conversation
… carrier; step-18 entry texts name the identity-only sibling Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…pe-conditional rules, and the form-view respelling's reach Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…n the neighbour is placed by the rule Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…id-column-followup
📓 Docs Drift CheckThis PR changes 1 package(s): 2 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 — 137 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 e07a7d1ce021bedbe2375ac13f07924b052686be && git checkout e07a7d1ce021bedbe2375ac13f07924b052686be
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 95fed33a20dbefb8afdaa852724c3b78bccf93a0 06f079864abecbfefdefac2642e8a2ecb1438fd5 && git checkout -B drift-repro 95fed33a20dbefb8afdaa852724c3b78bccf93a0 && git merge --no-ff 06f079864abecbfefdefac2642e8a2ecb1438fd5
node scripts/docs-audit/affected-docs.mjs --json 95fed33a20dbefb8afdaa852724c3b78bccf93a0
|
Contract reviewServed-tier: ① Derived judgmentsInputs read: card #20901 (body; comments Accept-set and public-surface changes the diff implies
Author-shown and AI-facing text, tested sentence by sentence (true unless noted)
② Semver levelClause-②: no
③ Boundary flagsDev report
Check-runs on Implemented-by: VERDICT: PASS Adopted and posted by
Generated by Claude Code |
…d the showcase done rate moves to its dataset (objectstack-ai#20943) (objectstack-ai#20998) Closes objectstack-ai#20943 Clause-②: yes (narrowing) Ruling D on objectstack-ai#20943 (comment `5921156712`, execution parameters), dispatched by the claim `5921298734`: an analytics cube member's `sql` is a column reference, and a SQL expression there is refused at parse with a prescription naming the ADR-0021 dataset form. The showcase's one expression member, `done_rate`, moves to its dataset in the same PR. ## What changes **`@objectstack/spec`, `data/analytics.zod.ts`** - `MetricSchema.sql` and `DimensionSchema.sql` (every member of a cube's `measures` and `dimensions`) admit exactly the accept set the execution parameters name: a bare identifier (`amount`), a dotted identifier path (`account.amount`, `account.owner.region`), and `'*'`. Any other value is refused at `measures.METRIC.sql` / `dimensions.DIMENSION.sql` with code `invalid_format`. That covers a CASE expression, an aggregate or a ratio of aggregates, a quoted or `$`-prefixed spelling, an empty string and a broken path. A column reference parses byte-identically to before. - The identifier half is the pattern the readers already use to tell a column path from an expression: `IDENTIFIER_PATH` in `native-sql-strategy.ts`, and the field gate's bare-identifier / identifier-path pair. So the contract admits exactly the values those readers resolve to fields. - The rule is a `.regex()`, not a refinement, so the published JSON Schema carries it as a `pattern`. A first cut used refinements, and the build's dropped-refinement gate refused it: a JSON-Schema-validated document would have been judged differently from the parse. The pattern closes that gap, and `dropped-refinements.baseline.json` is untouched. - Each prescription opens with the contract sentence. It names the dataset form: a measure with its own structured `filter` for a conditional count or sum, and `derived: { op, of: [...] }` over named measures for a ratio, sum, difference or product. It also states the ratio's 0–1 scale. The dimension prescription says that a CASE bucket has no expression form in either layer. - Neighbouring prescriptions no longer offer "fold the condition into the metric's own `sql` expression" as a live channel. This covers the retired metric `filters` guidance, the analytics query `filters` guidance, the `metric-filters-removed` conversion summary, its D3 entry `cube-metric-filters-retired`, and its step-18 rationale fragment. All of them are unreleased major-18 text. **ADR-0087.** The D3 entry `cube-member-sql-expression-retired` (`migrations/entries/semantic/`) and its step-18 rationale fragment were written after merging a `main` that contains objectstack-ai#20953, which landed as `c6b3a01d5d`. `registry.ts` was regenerated by `gen:migration-registry`, never edited by hand. There is no D2 conversion, because an expression has no mechanical rewrite into a dataset. There is no `RETIRED_KEYS_BY_MAJOR` row, because no key left the shape. **Liveness.** The `analytics_cube` rows `measures.sql` and `dimensions.sql` stay `live`, re-verified 2026-09-30, with the narrowing recorded and the field gate's `fieldsOfColumnSql` added to the evidence. **Generated.** Only `content/docs/references/data/analytics.mdx` moved. The `authorable-surface`, `api-surface` and `json-schema.manifest` ratchets are byte-identical, as expected for a value narrowing. `spec-changes.json` and the upgrade guide stay at protocol 17, so major-18 entries do not project yet, and both checks are green. **Showcase.** The `showcase_delivery` cube loses `done_rate`. The `showcase_task_metrics` dataset gains `done_count` (`aggregate: 'count'`, `filter: { status: 'done' }`) and `done_rate` (`derived: { op: 'ratio', of: ['done_count', 'task_count'] }`, `format: '0.0%'`), in the same shape as the existing `paid_rate`. No dashboard read the cube's `done_rate`, so there was nothing to re-pin there. `test/gap-fill.test.ts` is re-pinned: the cube's measure list, the parse of the shipped cube, and the dataset's filtered-count-over-count form plus its `DatasetSchema` parse. **`@objectstack/service-analytics`.** One test and the README, nothing else: - `cube-authored-format-granularity.test.ts` built its custom-SQL fixture with `CubeSchema.parse`, which now refuses it. The fixture is built unparsed instead, plus one assertion that the parse refuses it. The engine-path refusal it pins is unchanged. - `README.md` no longer tells a reader to fold a per-metric condition into the metric's own `sql` expression. That text ships in the package, so it gets a `patch` line in the changeset. This file is outside the claim's declared surface; see the Acceptance notes. The gate's stand-down branch in `analytics-service.ts` is not touched. objectstack-ai#20965 remains open for it. ## The fork clause The fork clause did not fire. The one authored expression member in the tree is `done_rate`, and its structural equivalent exists and is measured (next section). A shape-level reading for the seat: a dimension CASE bucket has no dataset equivalent, because a dataset dimension names a field and its only bucketing is `dateGranularity`. No authored cube in the tree carries one. Two `service-analytics` gate fixtures do (`dimension-source-field-gate.test.ts`, `where-source-field-gate.test.ts`), but they are built without the parse to pin the runtime branch objectstack-ai#20965 owns. Out-of-repo cubes are NOT MEASURED. ## The measure-level filter, verified before relying on it - **Code path.** `compileDataset` reads `m.filter` into `measureFilters`. `DatasetExecutor` splits filtered measures off with `splitMeasuresByFilter` and runs one supplementary query per filtered measure over the base filter combined with that measure's filter. It then evaluates `derived` on the merged row, with `ratio` as `a / b` and null on a zero denominator. - **Unit pins.** The existing `dataset-executor.test.ts` cases cover a supplementary query for a measure-scoped filter and a derived measure over filtered and unfiltered dependencies, and `dataset-compare-measure-filters.test.ts` covers the number a reader sees. All are green in the `service-analytics` run below. - **Live.** An ephemeral showcase boot on a private port (`pnpm dev -- --fresh`, torn down after) queried the task dataset through the analytics dataset door as the seeded admin. `done_count` and `done_rate` reconciled against the raw `showcase_task` rows in every priority bucket and in the ungrouped total. `done_rate` equalled `done_count / task_count`, and the column carried `format: '0.0%'` and `percentScale: 'fraction'`. The cube's meta listed its three remaining measures. ## Tests (final head `fa979c57ed` unless stated) - `@objectstack/spec`: - `vitest run --project local src/`: 544 files, 16268 passed, 1 todo. - `--project local scripts/`: 41 files, 950 passed, 1 skipped (at `a32fd122dc`; the second merge brought no `scripts/` change under `packages/spec`). - The relevant `--project repo` files: 10 files, 300 passed. These are `step18-rationale-merge`, `conversions-major18-merge`, `liveness/evidence`, `liveness/proof-registry`, `cube-member-inner-name-retirement`, `cube-refresh-key-retirement`, `retired-key-migrate-sentence`, `root-index`, `file-description` and `category-title`. - `typecheck` exit 0. - `@objectstack/service-analytics`: `test` gave 149 files and 3449 passed; `typecheck` exit 0, and the edited test is in its program (`--listFiles`). - `@objectstack/example-showcase`: `vitest run` gave 29 files and 387 passed, after rebuilding its closure (60 tasks); `typecheck` exit 0, and the three edited files are in its program. - Gates: - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 116 families. All 116 were run, and `--ran` reconciled them as "116 derived, 116 run, 0 NOT-MEASURED, 0 UNRUN", each with an exit code. - 115 of them exited 0, including `check:generated`, `check:liveness`, `check:adr-0087-registration` (reads the new marker), `check:changeset-no-major`, `check:empty-changeset`, `check:doc-authoring` and `check:nul-bytes`. - The other one is `pnpm check:platform-checklist`, which exited 1 on an anchor this diff does not touch. `areas/identity-auth.json` cites `plugin-auth/src/auth-plugin.ts#twoFactor`, and that symbol is absent. This branch's bytes for `packages/plugins/plugin-auth`, `docs/qa/platform-checklist`, `scripts/check-platform-checklist.mjs` and `scripts/symbol-anchors.mjs` are identical to the merge base `05be352596`, so it is `main`'s state, not this diff's. - Lint, a declared narrowing: `eslint --no-inline-config --format json` over the 10 changed lintable files gave 10 files, 0 errors and 0 warnings. - The population comes from `eslint.config.mjs`: the `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` block. The other changed files are `.md`, `.mdx` and `.json`. - The file count comes from the JSON output. - Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rules), so this diff cannot move a verdict on an untouched file. The whole-repo `pnpm lint` is CI's. ## Ablations (each from the committed state; disk-verified through `scripts/ablation-replace.mjs`; restore proven by blob hash equal to HEAD and an empty `git diff HEAD`) | leg | mutation | result under mutation | restored | |---|---|---|---| | A1 | the member `sql` pattern admits anything | new spec pin: 9 failed / 6 passed (every refusal, door and pattern case red) | 15 / 15 green | | A2 | the metric `filters` guidance offers the `sql` expression channel again | 1 failed (the guidance case) | green | | A3 | the D3 id renamed in the generated registry region | 1 failed (the registration case) | green | | A4 | the showcase `done_count` loses its filter | `gap-fill`: 1 failed (the dataset-form case) | green | | A5 | the pattern admits anything, then `@objectstack/spec` rebuilt | `ablation-dist-preflight` marker present in 20 built files; the service-analytics parse assertion failed (`expected true to be false`) | rebuilt; marker absent from all 230 files; tree clean; 14 / 14 green | The expected direction was "turns red" in all five, and that is what was observed. No ablation file is left in the tree. ## Acceptance notes - **Surface breach, declared.** `packages/services/service-analytics/README.md` is outside the claim's file surface. The dev contract's rule says published text this change makes false is fixed in the same round, and the dev contract outranks the dispatch words where they conflict. The edit is one paragraph, and the changeset carries `'@objectstack/service-analytics': patch` for it. The seat can drop both if it prefers objectstack-ai#20965 to carry the fix. - **Not fixed here, governed (Tier H).** `skills/objectstack-ui/rules/dashboards.md` still escalates past a dataset to "a hand-authored Cube (raw SQL / explicit joins)" (≈`:78`), and lists "any custom-SQL metric" as a dataset gap (≈`:70`). Both are now false: a cube member takes no SQL expression, and joins were already derived. The natural carrier is the separate Tier H docs PR the ruling names for ADR-0021's dated note. It is not mixed in here. - **Not fixed here, internal.** `docs/qa/platform-checklist/areas/dashboards.json` (the cube meta item, ≈`:976`, `:991`, `:1037`) names `done_rate` among "exactly its four measures" and as a variant. The cube now declares three. No gate reads that text. Carrier: the checklist's next revision. - **Boundary, left as ruled.** `'*'` is admitted on a dimension and on a non-count measure, because the execution parameters admit it on both members. Neither was ever a working query. Separately, the `number` / `string` / `boolean` measure types existed to carry an expression. With an identifier `sql`, the raw-SQL path emits the column unaggregated and the ObjectQL path refuses it, so retiring those three types is the natural follow-up. That is a decision for the seat, not something this PR does. - **Not added: a tree-scoped text pin.** The narrowing is enforced at every parse door. The one authored cube in the tree is parsed at import (`defineCube`) and pinned by the showcase test. The tree's other expression members are deliberate unparsed runtime fixtures in `service-analytics`, owned by objectstack-ai#20965's cleanup. A text scan would have needed a directory exclusion and a new cross-package radius. - `tsc` does not catch an expression member, because `sql` is still `string`; the parse is the judge. Stored `analytics_cube` rows and built artifacts that carry an expression are NOT MEASURED here. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…osed fragment (objectstack-ai#21090) Part of objectstack-ai#20901 Clause-②: no One `STEP18_RATIONALE` fragment in `packages/spec/src/migrations/registry.ts`: id `form-view-subform-columns-closed`, `order` 54 (the highest at base `5dbeb7d7b7` was 53), inserted where its id sorts. It covers the form-view carrier closure, the D2 conversion `form-view-subform-columns-canonicalized`, and the identity-only sibling `inline-grid-column-identity-only-currency-scale-refused`. Each sentence restates the two entry sources and the conversion source. The list sits outside the generated markers and is hand-written; `gen:migration-registry` is a no-op on this diff. A `patch` changeset for `@objectstack/spec` is included because the rationale string ships in `dist/migrations`. This is review item 4 of the at-tier record `5921114874` on objectstack-ai#20953. The card stays open, and the seat closes it by hand. ## Verification at head `8ebaf237c4` (merges origin/main `dff98c1f51`) - Rationale pins (`step18-rationale-merge`, `compliance-families-retirement`, `conversions-major18-merge`): 31 passed. `migrations.test.ts` and `inline-grid-column-carriers.test.ts`: 179 passed. Spec local suite: 588 files, 17318 passed. Spec typecheck: exit 0. - `check:generated`: all 15 artifacts are up to date after a spec build. The upgrade guide prints majors up to `PROTOCOL_MAJOR` (17), so it does not print step 18 and has no diff here. - `dispatch-gates --ran`: 82 derived, 82 run, 0 NOT-MEASURED. - Ablation (fragment removed, anchor 1 to 0, restored to the HEAD blob): no pin went red. The merge test pins the sort order and the joined text; no test pins this fragment by id. ## Acceptance notes - The review counted entries without a fragment of their own id: 207 of 253 at `06f079864a`. Fragments are per retirement family, so the number of families without one is not counted. `carrier:` none named. Noted here under the filing gate, with no `reach:` measured today. The generated guide prints major 17 only (`build-upgrade-guide.ts` loops to `PROTOCOL_MAJOR`, 17 at this head). Step 18's rationale does reach `os migrate meta --step`, whose default chain ends at major 18 (`packages/cli/src/commands/migrate/meta.ts`). There, a family without a fragment is left out of the paragraph, not misstated. This was read from the source and not measured at that door. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #20901
Clause-②: no
The follow-up the contract review of PR #20927 owes (review record
5919009567, items 5, 6 and 9, and the ① text findings), dispatched by landing record5919493523. #20901 stays open for the seat to close by hand.What changed
inline-grid-column-currency-scale-refused. Its surface said a column that declares notype"is untouched", and its acceptance said such columns "keep theirscale". Sincebee75cebe6that is false for an identity-only column over acurrencychild field, whichdefineStackrefuses. The surface, the reach sentence of the reason and the acceptance now say so, and each names the sibling entryinline-grid-column-identity-only-currency-scale-refused. The registry was regenerated withgen:migration-registry.field→namerespelling is now an ADR-0087 chain step on the form-view carrier. New D2 conversionform-view-subform-columns-canonicalized(protocol 18,retiredFromLoadPath: true,retiredAfter: '17.5.0'). It walks every FORM payloadmapViewPayloadsreaches, which isform, eachformViewsentry, a form view item'sconfigand a flattened form overlay (the stored-row seam wraps aviewrow as{ views: [row] }in any of those spellings). It also walks the assembledviewItemschannel. It uses the same respelling rule asfield-column-lists-canonicalized, now one shared function (respellInlineGridColumns) called by both entries. An entry already spellednameis left alone, and so is one carrying both keys, exactly as the existing walk behaves. The D3 entryform-view-subform-columns-closedsaid "NOT mechanically converted". It now names this conversion, and its comment no longer says a stored row is "refused": a stored row is diagnosed at rehydration.inline-grid-column-carriers.test.tsprobesInlineGridColumnSchemafor every (type, key, value) that parses with the key alone and with the type alone but not with both. Today it finds exactlycurrency+scale(at0and2), and it asserts thatdefineStackrefuses each such rule on an identity-only column over a field of that type. A new type-conditional rule without aHYDRATED_INLINE_COLUMN_TYPErow goes red, and the failure message names the table.stack.zod.tsitself is unchanged apart from comments.stack.zod.ts: the objectuihydrateColumns/fieldTypeToColumnTypeclaim is now a historical pin citation (.objectui-shapindb11afd4967c, re-read at that sha:hydrateColumnsleaves a typed column alone, andfieldTypeToColumnType's onlycurrencyarm iscase 'currency'), andcheck:objectui-pin-citationslists it. The over-broad "like the view data-source check" is cut. The instruction "adds its row here" now names the pin that holds it.Route change on item 6: its own entry, not a wider walk inside
field-column-lists-canonicalizedThe ruling's intent is carried out exactly: a lossless respelling on the new carrier, landed as a
retiredFromLoadPathchain step in the same release. The vehicle differs from the ruling's wording, for one ledger fact the gates cannot see.retiredAfteris one value per entry: "the last published@objectstack/specwhose authoring surface still accepted the old shape" (ADR-0087 amendment of 2026-09-30, maintainer ruling A on #20390).field-column-lists-canonicalizedis published with17.0.0, andretired-after.census.test.tspins that value, while the form-view carrier acceptedfieldthrough 17.5.0. The artifact-ingestion door opens its window per entry by that value. Measured at this head, withapplyArtifactForwardConversions, an artifact declaringengines.protocol: ^17.5.0, and runtime label17.5.0:converted-retired-after;{ field: 'quantity' }became{ name: 'quantity' }through the new entry (listed inreplayedRetirementswithretiredAfter17.5.0);{ field: 'quantity' }on a relationship field'sinlineColumnsstayed as authored, becausefield-column-lists-canonicalized(17.0.0) stays closed at that floor. That is correct for that carrier, and it is what a folded-in subform walk would have done to the new carrier: the refusal instead of the rewrite.A second, smaller reason: the per-release section of
spec-changes.jsonreports conversion ids that are new in a release, so a widened, already-published id would be invisible there. If the seat still wants one id, the fold-in is mechanical: move themapViewPayloadswalk intofield-column-lists-canonicalizedand delete the sibling. The cost is the artifact-door gap above.Verification (at
06f079864a, which carriesorigin/main3fbf3ca617)packages/spec/src/inline-grid-column-carriers.test.ts: 21 passed (13 existing + 8 new).scripts/ablation-replace.mjs,registry.ts: the new entry'srespellInlineGridColumns(subform.columns, …)call replaced bysubform.columns; anchor x1 → x0; blob909148b74725→3b07d9f2d145), run over the pins plusconversions.test.ts:Tests 4 failed | 251 passed (255). The red ones are the fixture pair ofform-view-subform-columns-canonicalized, the stored-row test in all threeviewspellings, theos migrate metachain test, and the two-carriers-one-rule test. The controls (an entry spelledname, one carrying both keys, and the authoring funnel's refusal) stayed green. Restored: blob == HEAD,git diff HEADempty.stack.zod.ts: thecurrencyrow deleted fromHYDRATED_INLINE_COLUMN_TYPE; blobf93870ec6a88→d3aca2ec1d38):Tests 4 failed | 17 passed (21). The new table pin is red with "…add the row to HYDRATED_INLINE_COLUMN_TYPE in stack.zod.ts", and so are the three existing hydrated refusals. Restored: blob == HEAD.ca5ad202a9). The subject resolves tosrcthrough relative imports, so no dist leg was owed.vitest run --project local:Test Files 584 passed (584) / Tests 17200 passed | 1 todo. Typecheckpnpm --filter @objectstack/spec typecheck: exit 0 (check:test-typecheck: OK;tsconfig.test.json --listFilesincludes the pin file). Repo-project subset (major-18 merge, step-18 rationale merge, retired-key migrate sentence, two view retirement pins): 82 passed. The fulltest:repoproject is left to CI.check:generated: all 15 artifacts up to date after a freshpnpm --filter @objectstack/spec build.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 90 derived, 90 run, all exit 0.--ranreconciliation reports "0 NOT-MEASURED (a DERIVED zero — all 90 recorded an exit code)". Four first exited 3 (PREREQUISITE NOT MET:lint check:doc-formula-expressions,check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt) and were green on a re-run afterturbo run buildover./packages/*and./packages/*/*.eslint --no-inline-config --format jsonover the 7 changed.tsfiles reports files 7, errors 0, warnings 0. The population iseslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}. That config never enables type-aware linting (its own comment: noparserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file.One test touched outside the dispatched files
packages/spec/scripts/conversions-major18-merge.test.tswent red on this diff. It builds its synthetic retirement pair beside the entry at the middle ofMAJOR_18_CONVERSIONS. At the base that neighbour washookTimeoutToTimeoutMs, which predates the placement rule. With this entry added, the neighbour isformViewSubformColumnsCanonicalized, which is placed by the rule. The "defined after every other conversion" variant then reports the neighbour too, because its entry is now followed by the synthetic one. The expectation now derives that neighbour finding when, and only when, the neighbour is not exempt. At the base it reduces to the old single-element list. 12/12 pass.Changeset
@objectstack/spec: patch. The act adds no exported symbol (check:api-surfacegreen) and no accepted key or value on an authoring surface: authored sources are refused exactly as before. It rewrites stored and assembled data that the contract already names (name), so under the "WHICH LEVEL" rule it is a fix and stayspatch.Clause-②: no, per the gate's definition (a new key on a published payload): the conversion adds no key, and the step-18 edits are prose. It is not a narrowing either, since the data-at-rest seams now accept strictly more.Acceptance notes
retiredAfter: '17.5.0'is the package label, asretired-after.census.test.tsrequires for an unpublished entry. The census is refreshed after the next publish.rationalegains no fragment. PR feat(spec)!: a form view's subform columns are the inline grid column contract, and an identity-only column is judged as the type it renders (#20901) #20927 added none for its two D3 entries either, and no gate requires one per entry.subforms[].childObject", recorded by the adoption for its next touch;packages/spec/src/data/field.zod.ts, which the dispatch keeps out of scope; finding(spec):ObjectMasterDetailFormPropsSchema.detailsisz.unknown(), a third unjudged carrier of the inline grid column: a bogus key or a typedcurrencycolumn withscalepublishes green #20928 and finding(lint):field-no-consumerscalls a field "inert" when an inline grid column names it (form.subforms[].columns[].name), becausenameis inLITERAL_KEYS#20929; and objectui#11266.Generated by Claude Code