Repository navigation
fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level options is refused by name (stage iv of #20051) - #20868
Conversation
…every key its body carried; a ViewItem record's top-level options is refused by name Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
… is representable Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…stored view projection Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…arsed body Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…bag-refused Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
… refusal at the spec layer Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…tions bag Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
… refusal family Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…w save now say what the save stores Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…view-item-options-bag-refused Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
…rsist-parsed-view-body
📓 Docs Drift CheckThis PR changes 2 package(s): 7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 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 3c9bd02f49c574bd820e35ed9bcb41e4a3097829 && git checkout 3c9bd02f49c574bd820e35ed9bcb41e4a3097829
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 72f8c3820154c69cdf9faa6f887dc544e4c4b823 9924c95b0b603c34729ac2706d48b6bb7d7ff24b && git checkout -B drift-repro 72f8c3820154c69cdf9faa6f887dc544e4c4b823 && git merge --no-ff 9924c95b0b603c34729ac2706d48b6bb7d7ff24b
node scripts/docs-audit/affected-docs.mjs --json 72f8c3820154c69cdf9faa6f887dc544e4c4b823
|
…, its non-converging fail-safe, and the once-per-view fallback warning Claude-Session: https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs read: card #20051 (body and all 26 comments, rulings 甲 Check-runs on this head, read 2026-09-30T14:18Z: 35 runs. 32 ① Derived judgments
② Semver levelChangeset
③ Boundary flagsEvery dev flag and every
Implemented-by: VERDICT: PASS |
…the connector sync retirement The merge of origin/main (which carries #20868's registry entry) owed a regeneration of three os-regen artifacts; they are regenerated here from the merged tree. The same commit writes this branch's two registry hunks, which the serial constraint held until that landing: the generated regions of migrations/registry.ts (gen:migration-registry over the entry files), the D2 conversion connector-sync-keys-removed in conversions/registry.ts (MAJOR_18_CONVERSIONS, order 50) and its STEP18_RATIONALE fragment (order 52). gen:schema cannot run without the second half (build-schemas gate (b)), so the two cannot be split. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
… options bag, as the interface page does (objectui#10380) (objectstack-ai#11571) Fixes objectstack-ai#10380 Clause-②: no. No published schema, type, input or accept set moves. `@objectstack/spec` 17.6.0 already declares and judges the list overlay's legacy `options` bag (`ListViewOverlayOptionsSchema`), and this change makes the object page render what that door accepts, the way the interface page already does. The one new export, `storedLegacyOptions` in app-shell's `ObjectView` module, is a helper exported for its pin, like its siblings `kanbanViewOptions` and `timelineViewOptions`. ## What was wrong The flattened list overlay in `@objectstack/spec` 17.6.0 declares a legacy `options` bag. The view write door judges each `options.KIND` block key by key with that kind's own block schema. It refuses an out-of-contract key by name and stores what it accepts. The spec describes the bag as an underlay: the top-level `KIND` block "wins per key where both set one". `InterfaceListPage` forwarded a stored row's bag to `ListView`, and `ObjectView` dropped it. So one stored row handed the renderer two different configs. Measured on `origin/main` `ab18797`, with rows the 17.6.0 door accepts: - a timeline was grouped by `region` on the interface page and ungrouped on the object page (`options.timeline.groupByField`); - a timeline item or kanban card was titled by `region` on one page and by `name` on the other (`options.timeline.titleField`, `options.kanban.titleField`); - a map had a `locationField` binding on the interface page and none on the object page (`options.map`). Ruling A (5824043998) keeps the interface page's forward ("what passes the door is legal"). The seat's answer 5971906253 directs the object page to honour the bag too, and to reuse `ListView`'s existing per-key merge, with no second merge in app-shell. ## What changed - **`ObjectView` (app-shell).** A new helper, `storedLegacyOptions(viewDef)`, returns the stored row's bag, or `{}` when the row has no bag or the bag is not an object. The relay then: - forwards the bag in `options`. For each kind the bag carries, the bag's block replaces this page's synthesized block (`kanbanViewOptions` and its siblings). This is the rule `InterfaceListPage` applies to its `mapCfg` (`view.options.map ?? derived`). A synthesized block stands in only for a kind the bag does not carry, so a row with no bag is relayed exactly as before; - forwards the view's own top-level block for each kind the bag carries, at the top level, as `InterfaceListPage` does. `ListView`'s existing per-key merge (`{ ...options.KIND, ...KIND }` in each render branch) then lays the declared block over the bag. That is the spec's precedence, and no merge is written in app-shell. - **`InterfaceListPage` (app-shell).** A default binding the page derives from the object (`defaultKanbanFromObject`, `defaultCalendarFromObject`, `defaultGalleryFromObject`, `defaultGanttFromObject`) no longer fills a kind the row's bag carries. Before this change, the derived block sat at the top level and outranked the row's own bag declaration on every key it set. The object page, which derives nothing, therefore rendered such a row differently. This is the rule `mapCfg` already follows, extended to the other derived kinds. The forward of `options` is unchanged (ruling A), and so is the `options.map` legacy path and its CONTROL pin. - **The relay census** (`ObjectView.relayRungCensus-7559.test.ts`). The eight per-kind blocks now have a conditional top-level rung that reads `viewDef`, so the census counts them as relayed. It requires their `relayed-nested` ledger rows to go ("an absence that came back is not an absence"), and they are replaced by a comment that says why. Their nested `options.KIND` paths are still written for every row, and are still asserted by name in the census's derivation checks. - **Changeset:** `@object-ui/app-shell` patch. ## What an existing row that still carries `options` does Both pages forward the stored bag in the same way, including a key the 17.6.0 door now refuses (for example `options.timeline.metaFields`). The renderers ignore the retired keys: `ObjectTimeline`'s chip list has been the constant status / priority pair since the `metaFields` retirement (objectui#10222), and `ListView` strips the stray kanban `groupBy` and the calendar `dateField` / `endField`. The row is served as stored until its next save, which the door refuses by name. No migration is done, per the maintainer's 「20051 不考虑现有的数据」. ## Tests New pin: `ObjectView.storedOptionsBag-10380.test.tsx`. Each case renders one stored flat overlay row through both pages, captures the schema each page hands `ListView`, mounts the REAL `ListView` on it with a spy renderer for the kind, and compares what the renderer receives. The cases are: - four rows carrying a bag (`options.timeline.titleField`; `options.map`; `options.timeline.groupByField` under a declared timeline; `options.kanban.titleField` under a declared kanban); - a CONTROL row with no bag; - two collision rows where the top-level block must win per key on both pages (the timeline `titleField`; the kanban lane); - a unit case for `storedLegacyOptions`. Every row is one the 17.6.0 door accepts, judged with the spec's own `ViewMetadataSchema.safeParse` and `diagnoseViewMetadata` from the installed `@objectstack/spec` 17.6.0: `success=true`, `branch=listOverlay`, with `options` kept in the parse. The direction of each run was written before it ran. Each run used repo-root `pnpm exec vitest run FILE`, under `os-verify-lock`: - **Red on `origin/main` first.** The fix was committed (`4d0668d`), then `ObjectView.tsx` and `InterfaceListPage.tsx` were checked out at `ab18797`. `grep -c legacyOptions` read 0 in both files. Result: `Tests 6 failed | 2 passed (8)`. - The four bag rows are RED, as predicted. The control and the timeline collision are GREEN, as predicted. `storedLegacyOptions` is RED because the export does not exist there. -⚠️ The kanban collision was RED, which was NOT predicted. Its lane agreed (`region` on both pages). It failed on the card title: the object page floored `titleField: 'name'` and the interface page sent none for a row whose bag carries a kanban block. The fix removes that difference, because the bag's block replaces the synthesized one. - Restored with `git checkout HEAD --`. The blob hashes matched HEAD and `git diff HEAD` was empty. - **Ablation on the fixed tree.** The declared block was merged UNDER the bag in app-shell (`{ ...viewDef.KIND, ...legacyOptions.KIND }` at the top level, for timeline and kanban). That is the second merge, with the wrong precedence, that the direction forbids. The anchors were checked on disk before the run: 1 → 0, and the injected text 0 → 1. Predicted: both collision cases RED. Observed: `Tests 3 failed | 5 passed (8)`, which is both collisions plus the `options.timeline.groupByField` row. That row turned red because `ListView` reads `groupByField` as a flat timeline key off the top-level block only, so the merged block exposed it on the object page alone. Restored and verified the same way. - **On the final head `44e55cf`:** - `pnpm --filter @object-ui/app-shell type-check` exit 0. Its `tsconfig.test.json` covers the new test: it reported a type error there on an earlier commit. - `pnpm exec vitest run packages/app-shell/src/views/ObjectView packages/app-shell/src/views/InterfaceListPage packages/app-shell/src/views/__tests__/PageView packages/app-shell/src/no-refresh-key-remount.ratchet.test.ts packages/app-shell/src/views/view-filter-fold.ratchet.test.ts` gives `Test Files 55 passed (55)` and `Tests 504 passed (504)`. This covers every `ObjectView.*` and `InterfaceListPage.*` suite, including the `options.map` CONTROL pin, the relay census, `titleFieldConvergence`, the calendar / gallery / gantt / timeline binding pins and `hostRerenderRefetch-10046`. - NOT MEASURED: the full app-shell suite (979 test files). The run was narrowed to the suites that render or read the two edited modules, and CI runs the rest. - **Build:** `turbo run build --filter='@object-ui/app-shell^...' --concurrency=2` gave 28 of 28 tasks successful. - **Lint, narrowed and stated as such:** - `eslint --no-inline-config` on the four touched `.ts`/`.tsx` files gives `files=4`, 0 errors. - The population is `eslint.config.js`'s `**/*.{ts,tsx}` block. It uses `tseslint.configs.recommended` with no `parserOptions.project` and no `projectService`, so linting is not type-aware and this diff cannot move a verdict on an untouched file. - Warnings against `ab18797`, base → head: `ObjectView.tsx` 171 → 173 (one `any` in the helper's return type, and the `react-refresh/only-export-components` warning every sibling exported helper carries), `InterfaceListPage.tsx` 41 → 41, the census 0 → 0. The new test has 19 `no-explicit-any` warnings, in line with its siblings. No `--max-warnings` is set in this repo. - **Gates on `44e55cf`, all exit 0:** - `pnpm check:control-bytes`, `pnpm check:new-line-citations` (0 new citations), `pnpm check:pending-changeset-literals`, `pnpm check:spec-symbols`, `pnpm check:self-import` and `pnpm check:phantom-deps`; - `pnpm check:changeset-claims`: report-only. It names `7070-no-invented-gantt-date-fields.md` and `7218-rowcolor-host-relay.md`. Both paragraphs were re-read and both are still true; - `node scripts/check-changeset-presence.mjs`, `check-changeset-no-major.mjs`, `check-test-path-roots.mjs`, `check-vi-mock-specifiers.mjs`, `check-vi-mock-inherit.mjs` and `check-vi-mock-override-shape.mjs`. ## Acceptance notes - **Residual, the chart view's own route.** A `type: 'chart'` row whose chart block lives ONLY in the bag passes the 17.6.0 door. The object page renders chart views through its dedicated `ObjectChart` route, which reads `viewDef.chart` alone and never reaches `ListView`. So that row still renders the bag's chart on the interface page and the default binding on the object page. Aligning it means either a second chart resolution in app-shell (the `chart || options.chart` rule of `ListView`'s `resolveListChartBinding`), which the direction forbids, or exporting that resolver from `@object-ui/plugin-list`, which adds a published name. That is left to the seat. Evidence: the door acceptance was measured, and the route was read from source and not rendered. - **Pre-existing, not this card: the interface page drops a declared top-level `tree` / `chart` block.** `InterfaceListPage`'s schema literal forwards `kanban`, `calendar`, `gallery`, `timeline`, `gantt` and `map`, and has no `tree` or `chart` rung. `tree` and `chart` are allowed visualizations, and `CreateViewDialog` writes both blocks on the views it creates. A view's `tree.parentField` or chart binding therefore reaches `ListView` on the object page and not on the interface page. This is reported for the seat and not fixed here, because it is independent of the bag and would change the interface page for rows without one. Read from source. - **Pre-existing, posture: page-derived defaults.** An interface page still derives a binding for a whitelisted kind that the row declares nowhere, and the object page never invents one (objectui#3129, objectui#7029). That is a deliberate difference between the pages and is untouched. This change only stops the derivation from outranking a bag declaration. The kanban `titleField: 'name'` floor of `kanbanViewOptions` likewise still applies to rows without a bag. - **Legacy rows (the dev report's out-of-scope finding).** A stored row whose bag holds a key the door now refuses is served as stored until its next console save (rename or pin, through data-objectstack's `updateView`, which merges the current row). The door then refuses that save with a 422 that names the key. This is landed objectstack behaviour (objectstack-ai/objectstack#20868, `9905e61c`). Noted here and not filed, per the seat's answer. - **Docs:** none touched. No page under `content/docs/` or the app-shell README describes the bag or the interface page's derived defaults. The two pages that mention a `listViews` entry's `options` bag describe the authored object schema, where the bag is still refused, and they stay true. Session: `https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2` (dispatched dev of `domain:ui` seat 1, claim 5971770388, as amended by 5971906253). --- _Generated by [Claude Code](https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…ew's own shape) and keyboardNavigation (objectstack-ai#20882) Fixes objectstack-ai#20694 Clause-②: yes `ComponentPropsMap['object-grid']` (`ObjectGridPropsSchema`) gains the three members named in triage's direction (`5897622232`). This is a widening. Nothing that parsed before is refused, and the row still refuses every key it does not declare. ## What changes - **`description`**: `I18nLabelSchema.optional()`. At the `.objectui-sha` pin `db11afd4967c`, `plugin-grid/src/ObjectGrid.tsx:5677` resolves it with `resolveInlineI18nLabel(schema.description, displayLocale)` and draws it above the rows at `:5784`, `:6303` and `:6342`. The card calls the function `resolveI18nLabel`; at the pin it is named `resolveInlineI18nLabel`. - **`emptyState`**: the list view's own empty-state shape, taken **by reference**. It used to be an inline `strictObject` at `ListViewShapeSchema.emptyState` (`view.zod.ts`). It is now the exported `EmptyStateSchema` (author type `EmptyState`), and `list-view` and `object-grid` both hold that one schema. At the pin it is read at `ObjectGrid.tsx:6225` and drawn through `DataEmptyState` at `:6233`. - **`keyboardNavigation`**: `z.boolean().optional()`. Its describe is the one triage specified: arrow-key cell navigation on the WAI-ARIA grid pattern, on by default when `editable` is set, and a read-only grid keeps its Tab behaviour unless this is `true`. It is prefixed `[EXPERIMENTAL — not enforced]` because nothing reads it at the pin. Under objectui `packages/` and `apps/`, excluding tests, CHANGELOGs, READMEs and `packages/types` (which holds its type and zod twin), it has 0 hits; the control `schema.editable` has 3 hits in `ObjectGrid.tsx`. - The block docblock records each key's read point at the pin, following its convention, and each member repeats its own. ### The extraction, and the one text change - A parse corpus, run as a scratch script and not committed, covers 28 `emptyState` values on each of the four list-view doors: `ListViewSchema`, `ObjectListViewSchema`, `ViewSchema.list` and the flattened overlay arm of `ViewMetadataSchema`. That is 112 cells, compared between BASE `72f8c38201` and head. - `success` and parsed `data` are identical in 112 of 112 cells. - Issue codes, paths and keys are identical in 112 of 112 cells (every `message` field stripped at every depth). - After substituting only the guidance sentence, the full JSON of each result is byte-identical in 112 of 112 cells. - The 12 cells that differ at all are the `action`, `button` and mixed inputs on each of the four doors. In those cells the `action`/`button` guidance is reworded so that it also reads true on `object-grid`, which has no `addRecord` block. It still points at `addRecord` (`view-strictness-batch18.test.ts` still passes), and it follows the `VIEW_ROW_BOUND_GUIDANCE` precedent for text that two doors share. - The members gain short describes. The list view's reference docs lose the inherited sentence claiming the inline locale map is "resolved at render time"; see Acceptance note 1 for why that claim did not hold. ### Liveness record for `keyboardNavigation` The liveness ledger has no row for any `ComponentPropsMap` key. The page ledger leaves `page/regions` undrilled (it is listed in `undrilled-containers.baseline.json`), and `PageComponent.properties` is an open record, so the walk never reaches `ObjectGridPropsSchema`; a row in `page.json` would be an orphan. The record is therefore the describe marker `[EXPERIMENTAL — not enforced]`, which is the ledger's own spelling for a declared key that is not enforced (`MARKER_RE` in `check-liveness.mts`). The member docblock says to drop the marker in the change that lands the reader. `check:liveness` is green. ### Files outside the claimed surface - `packages/spec/src/type-alias-convention.pin.test.ts`: `gen:docs` refuses to document a schema that has no type alias, so `EmptyState` is exported. ADR-0122 then requires an isomorphism pin (`Iso_ui_view__EmptyStateSchema`), which moves the pin count from 779 to 780. The schema's tree has no default and no transform, so a synonym `EmptyStateParsed` would be wrong. ## Evidence (head `98dad84e21`, which includes a merge of `origin/main` at `07356a6ab0` through `os-regen-merge.sh`) - `object-grid` corpus: 3 of 36 cases parsed at BASE and 14 of 36 parse at head. The newly accepted cases are exactly the valid values of the three members, and no case is newly refused. Both `examples/app-showcase` `object-grid` nodes (command-center `cc_queue_g` and the my-work queue) parse before and after. - **Pins:** `component-object-grid-declared-keys.pin.test.ts` passes 18 of 18. §1 checks that the three members parse byte-identically. §2 checks that an unknown top-level key is refused with `unrecognized_keys` at the row, and that a wrong value in each member is refused (code and path). §3 checks that the grid and the list view hold `EmptyStateSchema` by identity, and that the grid answers exactly what that schema answers. - **Ablations**, run on the committed state through `scripts/ablation-replace.mjs`. Each restore was proven: the blob equals HEAD and `git diff HEAD` is empty. - Renaming the `keyboardNavigation` member: 4 tests fail and 14 pass. - Replacing the grid's `EmptyStateSchema` with a copied `z.strictObject`: only the identity pin fails, and 17 pass. - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: 581 files, 17154 passed, 1 todo. - `pnpm --filter @objectstack/spec typecheck`: exit 0. This includes `check:test-typecheck`, and the debt ledger is unchanged. - **Consumer `@objectstack/lint`.** Its closure was built upstream with `turbo run build --filter='@objectstack/lint...'` (lint plus its dependencies). - Tests: 117 files, 5437 passed. `typecheck`: exit 0. - Component-props reading through `validateComponentProps` on an `object-grid` node: - The three keys produce 0 findings. - The control `emptyStat` produces `component-props-unknown-key` at `properties.emptyStat`. - The control `emptyState.heading` produces the same rule at `properties.emptyState.heading`. That path exists only through the new declaration, which shows the rule read the rebuilt dist. - `pnpm --filter @objectstack/spec check:generated`: all 15 artifacts are up to date. - **Derived gates** (`dispatch-gates.mjs --commands`, 108 families, run at head): 104 exited 0. Four exited 3 with PREREQUISITE NOT MET, so they are **NOT MEASURED**: - `check:skill-examples`: the client SDK closure is not built. - `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:type-check-debt`: each needs the whole-repo package build that CI does. - Reconciled with `--ran`: all 108 accounted for, 104 run, 4 NOT MEASURED. ## Acceptance notes 1. **objectui renderer gap: an inline locale map in `emptyState.title` or `.message`.** At the pin, `ObjectGrid` passes both to `DataEmptyState` unresolved (`:6241-6242`), unlike `description`. React throws on an object child, measured on react 19.2.8: "Objects are not valid as a React child". `ListView` guards with `typeof === 'string'` (`plugin-list/src/ListView.tsx:5112` and `:5114`), so a map there silently falls back to the default text. No producer in `examples/**` or `platform-objects` writes a map there; all of them use strings. So this is not filed here. **Carrier:** the objectui follow-up the seat files at ACCEPT, which lists the three keys in `GRID_QUERY_INPUTS`. Its fix is one resolver, `resolveInlineI18nLabel`, applied at both renderers. Meanwhile the changeset tells authors to write strings on `object-grid`. 2. `origin/main` moved to `9905e61ca2` after the merge; objectstack-ai#20868 edits `view.zod.ts` and `view.mdx`. A merge-tree probe from a bare clone with no merge driver shows a clean merge, and the hunks in both files are disjoint from this branch's. CI validates the merge ref. 3. The `object-grid` follow-up in objectui, per triage: after the release is installable, list `description`, `emptyState` and `keyboardNavigation` in `GRID_QUERY_INPUTS` (the reverse direction of the parity gate). --- _Generated by [Claude Code](https://claude.ai/code/session_018fxqvRJW12TaHC7DUQ89Y6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…g / fieldMappings retired, mapping gains the connectorSource pull binding (objectstack-ai#20281 stage 1) (objectstack-ai#20903) Part of objectstack-ai#20281 Clause-②: yes (narrowing) Stage 1 (spec) of ruling A (`5904845660`) on objectstack-ai#20281, as claimed in `5913026572`: - `mapping.connectorSource` — the target-side pull binding (`connector`, `action`, optional `input` / `recordsPath` / `watermark { field, param }`); `rest` / `openapi` only, one-way, no cadence key (a `job` owns it), no credential. Declared, not executed: every row `planned`, the container `authorWarn`. - The 14 connector keys retire: `syncConfig` and `fieldMappings` are `retiredKey()` tombstones pointing at that binding and a `job`; `DataSyncConfig`, `SyncStrategy`, `ConnectorConflictResolution`, `ConnectorFieldMapping` leave whole; D2 `connector-sync-keys-removed` (strip, never writes a mapping); D3 `connector-sync-keys-retired`. - `packages/spec/docs/SYNC_ARCHITECTURE.md` stops presenting connector sync as delivered (rewritten, 494 to 464 lines). - The serial constraint held: the two registry hunks were written only after merging a `main` containing objectstack-ai#20868's landing (`9905e61ca2`), via `scripts/pm/os-regen-merge.sh`. Local, at `674abedfd9`: spec `local` 583/583 files green (17163 passed); spec typecheck exit 0; `check:generated` 15/15 current. The derived-gate union ran at `cb54c889b9`, before the second `main` merge: 111 of 114 green. `check:platform-checklist` is red identically on `origin/main` (not this diff); `check:dual-build-cjs-loads` and `check:type-check-debt` are NOT MEASURED (they need the whole-repo build CI does). Measurements, ablations and findings are in the `os-dev-report` comment on the card. ## Acceptance notes - `packages/lint` still says the `mapping` ledger warns on nothing (a comment in `authoring-rules.ts` and a group-C test title in `runtime-gate.inert-type-writes.test.ts`). The ledger now warns on `connectorSource`. The tests stay green because they do not author the key. Carrier: stage 2 of objectstack-ai#20281. - `shared/FieldMapping` (`./shared` `FieldMappingSchema`) no longer has a carrier in the spec, because its only extender left. No carrier. - `packages/spec/scripts/build-schemas.ts` comments still cite `integration/DataSyncConfig` as a live specimen. No carrier. objectstack-ai#20281 remains open for stages 2 and 3. --- _Generated by [Claude Code](https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
… view, and a saved view stores the parsed value of every key its body carried (ruling B) (objectstack-ai#20899) Fixes objectstack-ai#20879 Clause-②: no This is the documentation step that ruling `5910106571` on objectstack-ai#20051 names (batch objectstack-ai#256 item 3, Q2 = B, maintainer 「同意 批次 objectstack-ai#256」): "ADR-0005 appendix (c)'s `parsed.data` wording and its two "not yet in force" markers change in a separate Tier H documentation PR for the maintainer's hand." Tier H (`docs/adr/**`): this PR stays draft until the maintainer's word. ## What this changes One file, `docs/adr/0005-metadata-customization-overlay.md`, four places: 1. **Status line.** - The ruling 甲 entry's clause "the storage switch itself is the ruling's last stage, not yet landed" now says the switch "is in force for `view` since PR objectstack-ai#20868 (`9905e61ca2`)". It points at "the amendment notes" (plural). - One new `**Amended** (2026-09-30, objectstack-ai#20051 ruling B — …)` entry: the parsed body a `view` stores is the parsed value of every key its request body carried, not the whole parse output. Undeclared keys are dropped, schema defaults are not materialised, and a moved key is stored under its canonical spelling. Every other type still stores its request body. 2. **Appendix (c)'s persisted-document bullet** is rewritten from the landed code: - A `view` stores the parsed value of every key its request body carried (`projectStorableViewBody`). - The stored row re-parses to exactly what the save accepted. - Every other type still stores its request body (`request.item`). - It has been in force since PR objectstack-ai#20868. Rows stored before it are not migrated. - The "⚠️ Ruled, not yet in force" marker is gone, because the bullet now states the rule in force. The bullet links ADR-0087's addendum 2026-08-01b for the `storable` rule. 3. **The stage (iii) note keeps its text.** A "⚠️ **Historical — superseded.**" paragraph now sits above its items 5 (the interim state) and 6 (the stage (iv) plan). Both items are kept verbatim. This is ADR-0005's own convention: its "Overlay-uniqueness index" block is left standing as written under the same marker, because an accepted record is not edited to make the past look like the present. 4. **A new dated note, "Amended (2026-09-30)"**, directly after the first one, in the stage (iii) form: - **Provenance.** Ruling B `5910106571` with the maintainer's 「同意 批次 objectstack-ai#256」. The maintainer's 「20051 不考虑现有的数据」 (`5909085121`), which waived the stored-row count. PR objectstack-ai#20868, merged as `9905e61ca2`. - Item 1 quotes the stage (iii) bullet and the Status clause verbatim, marked **Superseded**. - Item 2: what a `view` save stores. - Item 3: the two grafts no longer run on a `view` save, and every other type still stores its request body. - Item 4: a view item record's top-level `options` is refused by name (Q3 = A). This settles the judgement the first note's item 6 left open. - Item 5: the pins. The heading `Addendum — 2026-05-16 (c): spec validation on overlay save` is byte-stable: 0 hits in the diff and 1 in the file. So `scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.json` and the code comments that cite that heading stay as they are, and nothing outside this file changes. Per the triage direction (`5914934930`), the text says nothing about types the switch does not cover, beyond one fact: every other type still stores its request body, as the landed code does. ## Each sentence against the landed code (`origin/main` `00a92e18d`) - `saveMetaItem` (`packages/metadata-protocol/src/protocol.ts`) stores `projectStorableViewBody(request.item, parsed.data, reparse)` inside `if (singularType === 'view')`. Every other type goes to the `else` branch, which keeps `graftNormalizedOperators(graftFoldedFormSections(request.item, parsed.data), parsed.data)`. - The rules come from `buildStorableViewNode`'s docblock and body: - an authored key the parse kept is stored with its parsed value; - an authored key the parse did not keep is dropped; - a key the parse added is stored only when the re-parse does not reproduce it (a moved key), and is not stored when it does (a default). - The loop returns `converged: true` only when `storableEqual(again.data, parsed)`. Otherwise it stores `parsed` whole and warns once per view (`storableViewFallbackWarned`). Item 2 of the new note says both. - "with the normalizations `saveMetaItem` applied to it before the switch" (item 3) covers what the other types already had, and nothing new: - the two grafts; - the page `type` default (`withDeclaredPageTypeDefault`, objectstack-ai#20101, which predates objectstack-ai#20868); - the flow canonicalization that runs before the gate. - The `options` refusal: `VIEW_ITEM_OPTIONS_REFUSED` and `options: z.never(…)` in `packages/spec/src/ui/view.zod.ts`, and the ADR-0087 entry `view-item-options-bag-refused` in `packages/spec/src/migrations/registry.ts`. - The pins: `protocol.project-storable-view-body.test.ts` covers the converged arms and the fail-safe. `protocol.graft-folded-form-sections.test.ts` has the stage (iv) describe blocks: W1–W5 store no `type`, an undeclared key is dropped, the persisted body parses to exactly what the save parsed, and a page is stored as sent. - `protocol-meta.test.ts` re-judged "preserves Studio-only auxiliary fields verbatim" as "stores the console's declared round-trip keys, and drops the `objectName` alias". ## Census of the file after the change `not yet in force` · `not yet landed` · `has not landed` · `parsed.data` · `request.item`. Each remaining hit is either true or a dated quote: | line | text | why it stays | |---|---|---| | 6 | ruling 甲 entry: "reversing the verbatim `request.item` save" | true, since the same entry now says it is in force for `view` | | 349 | bullet: "Every other type still stores its request body (`request.item`)" | true (the `else` branch) | | 367–370 | first note, item 1: the 2026-05-16 bullet quoted | dated quote (stage (iii)) | | 406 | first note, item 5: "has not landed" | under the new "Historical — superseded" marker, verbatim | | 436–443 | new note, item 1: the stage (iii) bullet quoted | dated quote, marked Superseded | ## Evidence (head `950180d84`) - **Gates.** Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, with no paths. The tree is `950180d84` and the change set is 1 path. It derived 19 commands, the same 19 as the dispatch. All 19 exit 0: - `check-adr-links` (+ `--self-test`); - `check-adr-symbol-anchors` (+ `--self-test`); - `check-ci-filter-parity`; - `check-closing-keyword-parity` (+ `--self-test`); - `check-comment-mask-corpus`; - `check:doc-formula-expressions`; - `check:adr-anchors`; - `check:cross-package-test-inputs`; - `check:doc-authoring`; - `check:driver-memory-census`; - `check:gitlink-declared`; - `check:nul-bytes`; - `check:pm-governed-merges`; - `check:pm-prior-rulings`; - `check:refd-timer-probe`; - `check:watch-hint-literal`. - **Reconciliation.** `--ran` with `COMMAND :: exit CODE` lines: "19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)". - **`check-adr-symbol-anchors`:** "2165 anchors across 140 records resolve — 325 symbol (299 declaration, 26 literal), 1813 file-level, 27 cross-repo, 6 exempt, 9 continuation". - **`check:adr-anchors`:** "OK (56 anchored file(s), every governing ADR still referenced; … 40272 citation(s) across 4997 file(s) resolve …)". - **`check:doc-formula-expressions`.** I built `@objectstack/formula` and `@objectstack/lint` first, through the verify lock (`turbo run build`, 4 of 4 tasks, exit 0). The gate then printed "22 record-scoped formula example(s) across 458 files / 1381 TS blocks judged clean". - **The ADR-0087 link fragment.** `check-adr-links` strips `#fragment` and never checks it (its own header says so). So I computed the slug of `## Addendum (2026-08-01b) — flows reach the finish line too (objectstack-ai#4454)` with `github-slugger` 2.0.0: `addendum-2026-08-01b--flows-reach-the-finish-line-too-4454`, which is what the link uses. That heading occurs once in ADR-0087. - **Ablation** (one-off, no permanent file). - Setup: with the change committed, I ran `scripts/ablation-replace.mjs` in wrap mode. It renamed the new anchor `view.zod.ts#VIEW_ITEM_OPTIONS_REFUSED` to `…REFUSEZ`: anchor count 1 to 0, blob `62fe9587` to `af2e8fa2`. - The gate printed "❌ check-adr-symbol-anchors: 1 finding(s)" and "[unresolved-symbol] docs/adr/0005-metadata-customization-overlay.md:472". - Restore: the blob is back to `62fe9587` (the HEAD blob) and `git diff HEAD` is empty. - So the gate judges the new anchors, including a non-exported `const`. - **eslint (narrowed, measured).** - The population is read from `eslint.config.mjs`: `files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`. No config object matches `.md`. - `eslint --no-inline-config --format json` on the changed file: 1 file, 0 errors, 1 warning, "File ignored because no matching configuration was supplied". - The config enables no type-aware linting (its own comment at `:327`). So a markdown-only diff moves no eslint verdict on any file. - **No changeset.** `docs/adr/**` publishes nothing. Measured: 82 tracked `package.json` files, 69 not private (the root is private). None has a `files[]` entry that names `docs/adr` or climbs out of its package directory. So `skip-changeset` applies, and the seat applies the label. ## Acceptance notes These are observations, not filed. 1. **The anchor's "only" is narrower than the code.** The invariant in `scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.json` says every other type is stored "with only the operator and `groups` → `sections` grafts applied". - The same file's code also applies `withDeclaredPageTypeDefault` (objectstack-ai#20101) to every type. - A flow body is its canonicalized `storable` before the gate. - The `resolveOverlaySchema` docblock names the page default. The anchor text does not. - That file is outside this card's surface (dispatch item 2), so it is not touched here. Carrier: none. 2. **The 2026-05-16 test list stays as written.** The line "Tests added in `packages/objectql/src/protocol-meta.test.ts` … unknown extras preserved …" is the 2026-05-16 addendum's dated record of what was added then, so it stays. The new note's item 5 records that the case was re-judged at stage (iv). 3. **Two Status-line entries carry the same date, 2026-09-30.** The ruling 甲 entry keeps its own provenance and gains the in-force clause. The ruling B entry records the refinement. The file's convention is one entry per decision-changing amendment. ## 维护者速读(草稿) **改了什么**:只改了 ADR-0005 这一份文档,四处。 - 状态行:裁决甲那一条原来写着"存储切换尚未落地",现在改成"自 PR objectstack-ai#20868 起对 `view` 生效"。另外新加一条"已修订",记录裁决 B。 - 附录 (c) 的"持久化文档"条目:原来写的是"存解析结果 `parsed.data`,已裁定、尚未生效"。现在照已合并的代码写:视图保存时,只存请求体里带了的每个键的解析值。没声明的键丢掉,作者没写的 schema 默认值不落库,旧拼写的键按规范拼写存(`groups` → `sections`,`visibleOn` → `visibleWhen`)。其它类型仍然存请求体。存量行不迁移。 - 第 (iii) 步那段注记的第 5、6 项(过渡状态、第 (iv) 步计划)原文保留,上方加了"⚠️ 历史——已被取代"标记。 - 新加一段 2026-09-30 注记。它写了出处(裁决 B、维护者原话、PR 与合并 sha),并原文引用被取代的第 (iii) 步条目。它还写了视图保存具体存什么、`options` 按名拒收,以及对应的测试。 **为什么改**:裁决 `5910106571` 点名要求这一步。第 (iv) 步(PR objectstack-ai#20868,`9905e61ca2`)已经合并,所以这三处"尚未生效 / 存 `parsed.data`"的文字在 `main` 上已经是错的。 **风险与代价(含回滚)**: - 纯文档改动,不动代码、不动测试、不发布任何包。 - 附录 (c) 的标题一字未改,所以代码注释和锚点文件都不用跟着动。 - 回滚:revert 本 PR 的提交即可。 **席位意见**: **你要做的**:审阅后在本 PR 上给 APPROVED。这是 Tier H 治理面 `docs/adr/**`,之后由席位落地。 --- _Generated by [Claude Code](https://claude.ai/code/session_01KTZmMfzVzjNvyaLyQ8mHvg)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20051
Clause-②: no
Stage (iv), the last stage, of ruling 甲 (
5856781584, confirmed5856866699), carried out under letter B of ruling5910106571. Maintainer verbatim: 「同意 批次 #256」. The stored-row census stays waived: 「20051 不考虑现有的数据」 (5909085121). The at-tier contract review is thedomain:specseat's, and it is owed before this PR is enqueued. This PR carries no review record of its own.What changes
1. A saved view stores the parsed value of every key its body carried.
saveMetaItemnow storesprojectStorableViewBody(request.item, parsed.data, reparse)forview, where it used to store the request body with two grafts. The rules:VIEW_CONSOLE_ROUND_TRIP_KEYS), so nothing it needs is lost.groups→sections, andvisibleOn→visibleWhen(never grafted before).storablerule, the one flows follow.Keys that are moved and defaults look alike in the parse output, so the projection re-parses the body it built. A key the parse added is kept only when the re-parse does NOT reproduce it. The loop ends when the stored body re-parses to exactly the save's parse. If it does not converge, the fallback stores the whole parse output: no key is lost, and a warning is logged once per view. Nothing in today's
viewschema reaches that fallback.The projection is gated on
singularType === 'view', the same way the flow branch is. Every other type keeps its body verbatim with the two grafts. Both grafts still serve other types:ViewFilterRuleSchemais reused by page, component and skill schemas, andFormViewSchemaby component, sharing and field schemas. A page save stays pinned verbatim here as the control.2. A ViewItem record's top-level
optionsbag is refused by name (Q3 = A). It is pinned absent on bothViewItemWireSchemaarms. The response is422 INVALID_METADATAatoptions, with the prescription to writeconfig.KIND. It usesz.never(), not the form overlay'sz.undefined(), becauseViewItemWire.jsonis published on its own andundefinedhas no JSON Schema form. Withz.undefined()the build refused ("1 previously published schema(s) disappeared"). The ADR-0087 disposition isregistered view-item-options-bag-refused, a new step-18 semantic entry.3. ADR citation. The five "ADR-0005 §Validation" citations in
protocol.tsnow cite ADR-0005 appendix (c), the "Addendum — 2026-05-16 (c): spec validation on overlay save". The same goes for the one inview.zod.ts.scripts/adr-anchors/packages__metadata-protocol__src__protocol.ts.jsongains ADR-0005 and ADR-0087 with the invariant. Nodocs/adr/**file is edited.Measured before the change (the console's own writes at
.objectui-shadb11afd4967c, through the realsaveMetaItem)type: 'grid'. Now none is stored. W1 also stops storing its sort rowid.idandobjectNameare no longer stored.sharing.enabled: falseor the sectioncollapsible/collapsed/columnsdefaults.parsed.datawhole would have minted toolbar rows that FAIL their own re-save. A column-less patch carrying thegriddefault is refused as "setstypebut lists nocolumns".Pins
protocol.graft-folded-form-sections.test.ts: the two GUARDs and the coexistence case.VIEW_CONSOLE_ROUND_TRIP_KEYSmaps tolistOverlay.isPinned/sortOrder(undeclared there) and keepsisDefault.sectionsis stored as authored": no section default, nosharing.enabled, and no undeclaredisPinned.protocol-meta.test.ts: the declared round-trip keys are stored, andobjectNameis dropped (objectcarries the binding).protocol-publish-rollback.test.ts: the diff's added key is the declaredrowHeight, and the undeclaredextranever reaches a version.protocol-view-identity-overlay.test.ts: the sort rowidis not stored.view-union-retirement-prescription.test.ts: the never-leaf population admits a closed two-entry family of pinned-absent refusals. Each entry must be reached and must match its curated text.type.id,objectName, a form-onlylayout).exportOptions, operator).optionsis refused on both arms, asserting envelopecode+status+ path + message, with 0 rows written.view-overlay-options-bag.test.ts: the record refusal on both arms, plus a control.options-wrapped out-of-contract key get the same refusal (door-half block).options.mapround-trips.Evidence (head
41913270bf)src/ui+src/migrations: 97 files, 3560 passed. The whole speclocalproject also ran before theorigin/mainmerge, all green once the retirement-population pin was re-judged.check:scripts-typecheckandcheck:test-typecheck.dist/rebuilt andablation-dist-preflightmarkers present in both packages):dist/, andgit status --porcelainempty.parsed.datawhole, which is option A): 20 red of 49 in the metadata-protocol file, all five toolbar pins among them. Restored to the HEAD blob.dispatch-gates --commands --repo objectstack-ai/objectstackderived 125.check:engine-split-ratiofirst refused on the shallow clone and passed afterfetch --shallow-since.check:skill-examplesfirst refused on a missing client-reactdistand passed after building it.check:dual-build-cjs-loadsandcheck:type-check-debt, both exit 3 PREREQUISITE NOT MET (they need a whole-repo build; CI runs it).--ranreconciliation: 0 UNRUN.check:generated: all 15 artifacts current;content/docs/references/ui/view.mdxwas regenerated..tsfiles; each resolves undereslint --print-config.--no-inline-config --format json: 15 files, 0 errors, 0 warnings.eslint.config.mjsenables no type-aware linting (its own comment says so), so this diff cannot move a verdict on an untouched file.Docs follow-up (Tier H, separate PR for the maintainer's hand)
ADR-0005 must change in three places:
saveMetaItemstill stores the request body (request.item)."parsed.data) once every round-trip key is declared". Under letter B it becomes: the parsed value of every key the body carried, with no schema default materialised.Acceptance notes
resolveOverlaySchemavalidates every typegetMetadataTypeSchemaregisters, not onlyviewanddashboard, and all of them still store the request body. Widening the projection is its own decision.listViewsand keeps onlyconfigplusname/label/isDefault. A saved view's toolbar toggle writes that tab back whole, so it rewrites the row withoutisPinned/sortOrder/visibility/columnState. This is a code read at the pin, not measured in a browser. Its carrier is objectui#11013, the objectui card on aligning console reads.packages/metadata-core/src/injected-system-columns.ts:132and three objectql object-roundtrip test headers). Those stay for the next change that touches them.Generated by Claude Code