Skip to content

Commit 4cb0f25

Browse files
committed
test(spec): pin the view item owner/hidden retirement at every record door
Adds the tombstone, conversion, registration and tree-scoped structural absence pins, and extends the D2 conversion to the assembled-manifest viewItems channel so an artifact assembled before the retirement still registers. Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
1 parent d37f9f0 commit 4cb0f25

3 files changed

Lines changed: 599 additions & 6 deletions

File tree

‎packages/spec/src/conversions/registry.ts‎

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
normalizeFilterOperator,
4343
type ViewFilterOperator,
4444
} from '../ui/view.zod.js';
45+
import { ASSEMBLED_VIEW_ITEMS_KEY } from '../ui/assembled-views.zod.js';
4546

4647
/**
4748
* Flow callout node type rename (protocol 11.0).
@@ -10318,6 +10319,17 @@ const objectTenancyOrganizationFieldRemoved: MetadataConversion = {
1031810319
* forever over two keys that never had an effect. It also lets
1031910320
* `os migrate meta --from 17` list the mechanical edits for existing sources.
1032010321
*
10322+
* **Two collections, because a ViewItem record travels in two.** `views` is the
10323+
* stack collection and the stored-row seam's `{ views: [row] }` wrapping.
10324+
* `viewItems` ({@link ASSEMBLED_VIEW_ITEMS_KEY}) is the assembled-manifest
10325+
* channel — package export and environment artifacts carry tenant-authored
10326+
* standalone ViewItems there, `applyArtifactForwardConversions` replays the
10327+
* chain over it, and the registration loop then parses each entry against
10328+
* `AssembledViewArtifactSchema`, whose ViewItem member carries these tombstones.
10329+
* Walking `views` alone would leave an artifact assembled before this release
10330+
* refused at registration (`INVALID_METADATA`, 422) over two keys that never
10331+
* had an effect.
10332+
*
1032110333
* ⚠️ Scoped to the ViewItem RECORD spelling — `viewKind` names the family and
1032210334
* `config` holds the payload, the discriminator {@link mapViewPayloads} uses for
1032310335
* its case 1 — and deliberately NOT walked through `mapViewPayloads`, whose
@@ -10339,14 +10351,30 @@ const viewItemOwnerHiddenRemoved: MetadataConversion = {
1033910351
+ 'and stored verbatim, read by nothing: no view switcher ever filtered on `hidden`, and no '
1034010352
+ 'per-user scope ever read `owner`, so a view marked as one user\'s was listed for everyone)',
1034110353
apply(stack, emit) {
10342-
return mapCollection(stack, 'views', (view, path) => {
10354+
const stripFromRecord = (view: Dict, path: string): Dict => {
1034310355
const kind = view.viewKind;
1034410356
if ((kind !== 'list' && kind !== 'form') || !isDict(view.config)) return view;
1034510357
return stripKeys(view, ['owner', 'hidden'], emit, path);
10346-
});
10358+
};
10359+
return mapCollection(
10360+
mapCollection(stack, 'views', stripFromRecord),
10361+
ASSEMBLED_VIEW_ITEMS_KEY,
10362+
stripFromRecord,
10363+
);
1034710364
},
1034810365
fixture: {
1034910366
before: {
10367+
// The assembled-manifest channel: a standalone record a package export
10368+
// carried before this release.
10369+
viewItems: [
10370+
{
10371+
name: 'crm_lead.escalations',
10372+
object: 'crm_lead',
10373+
viewKind: 'list',
10374+
hidden: false,
10375+
config: { type: 'grid', columns: ['name'] },
10376+
},
10377+
],
1035010378
views: [
1035110379
// A record carrying both keys: both go, and the live identity keys
1035210380
// beside them (`scope`, `label`) are untouched.
@@ -10368,6 +10396,14 @@ const viewItemOwnerHiddenRemoved: MetadataConversion = {
1036810396
],
1036910397
},
1037010398
after: {
10399+
viewItems: [
10400+
{
10401+
name: 'crm_lead.escalations',
10402+
object: 'crm_lead',
10403+
viewKind: 'list',
10404+
config: { type: 'grid', columns: ['name'] },
10405+
},
10406+
],
1037110407
views: [
1037210408
{
1037310409
name: 'crm_lead.my_hot_leads',
@@ -10381,8 +10417,9 @@ const viewItemOwnerHiddenRemoved: MetadataConversion = {
1038110417
{ name: 'crm_lead.pipeline', object: 'crm_lead', viewKind: 'list', hidden: true },
1038210418
],
1038310419
},
10384-
// `stripKeys` emits one notice per KEY removed: two keys on one record.
10385-
expectedNotices: 2,
10420+
// `stripKeys` emits one notice per KEY removed: two on the `views` record,
10421+
// one on the `viewItems` record.
10422+
expectedNotices: 3,
1038610423
},
1038710424
};
1038810425

‎packages/spec/src/migrations/registry.ts‎

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5303,8 +5303,11 @@ const step18: MigrationStep = {
53035303
+ 'tombstones on the SHARED shape, because that shape also feeds the `.strip()` wire '
53045304
+ 'member, where a bare deletion would be a silent strip. The D2 conversion '
53055305
+ '`view-item-owner-hidden-removed` strips them from the view item RECORD spelling only, '
5306-
+ 'as a lossless delete; a flattened overlay keeps its own `owner` / `hidden`, which are '
5307-
+ 'declared on a different door this retirement does not touch.',
5306+
+ 'as a lossless delete, in both collections a record travels in — `views` (stack sources '
5307+
+ 'and stored rows) and the assembled-manifest `viewItems` channel (package export, '
5308+
+ 'environment artifacts), whose registration parse would otherwise refuse an artifact '
5309+
+ 'assembled before this release; a flattened overlay keeps its own `owner` / `hidden`, '
5310+
+ 'which are declared on a different door this retirement does not touch.',
53085311
conversionIds: [
53095312
'field-malformed-scale-precision-removed',
53105313
'record-chatter-position-vocabulary',
@@ -18258,6 +18261,56 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
1825818261
// carrying both — and the D2 conversion `chart-config-aria-removed` strips all
1825918262
// of them together with the dashboard site.
1826018263
'ui/ReportChart:aria',
18264+
// #20085 (ADR-0049 enforce-or-remove; triage direction 「retire both keys」).
18265+
// `ViewItem.hidden` promised to hide a view item from the switcher, and nothing
18266+
// ever read it: no writer and no reader of the view-item key in the framework,
18267+
// in objectui at its pin and at `main`, or in cloud, and both switcher read
18268+
// paths filter on `viewKind` + `object` only — `hidden: true` hid nothing.
18269+
// Tombstoned with `retiredKey()` on the shared `viewItemBaseShape()`, because
18270+
// that shape also feeds the `.strip()` wire member (`ui/ViewItemWire`,
18271+
// registered beside this row), where a bare deletion would strip in silence.
18272+
// The flattened-overlay members declare their own `hidden` on a different door,
18273+
// untouched. ⚠️ No gate below can JUDGE this row: `ui/ViewItem` is a
18274+
// discriminated union, whose emitted JSON Schema has no top-level
18275+
// `properties`, so `authorable-surface/` carries no `ui/ViewItem:*` line and
18276+
// check (b) never sees the tombstone — the row is declared, not checked.
18277+
// D2: `view-item-owner-hidden-removed`.
18278+
'ui/ViewItem:hidden',
18279+
// #20085 (ADR-0049 enforce-or-remove; triage direction 「retire both keys」).
18280+
// `ViewItem.owner` named the user a `personal` view item belonged to, and
18281+
// nothing ever read it: no writer and no reader of the view-item key in the
18282+
// framework, in objectui at its pin and at `main`, or in cloud, and both
18283+
// switcher read paths filter on `viewKind` + `object` only — so a view marked
18284+
// as one user's was listed for everyone who can read the object. Per-user view
18285+
// scoping is a parked direction (ADR-0017, amended 2026-09-04). Tombstoned with
18286+
// `retiredKey()` on the shared `viewItemBaseShape()`, because that shape also
18287+
// feeds the `.strip()` wire member (`ui/ViewItemWire`, registered beside this
18288+
// row), where a bare deletion would strip in silence. ⚠️ No gate below can
18289+
// JUDGE this row: `ui/ViewItem` is a discriminated union, whose emitted JSON
18290+
// Schema has no top-level `properties`, so `authorable-surface/` carries no
18291+
// `ui/ViewItem:*` line and check (b) never sees the tombstone — the row is
18292+
// declared, not checked. D2: `view-item-owner-hidden-removed`.
18293+
'ui/ViewItem:owner',
18294+
// #20085 — the wire carrier of `ui/ViewItem:hidden` (see that row for the
18295+
// measurement). `ViewItemWireSchema` is member 1 of the `view` union
18296+
// `saveMetaItem` validates; it is built from the same `viewItemBaseShape()`, so
18297+
// the one tombstone refuses the key there too instead of letting `.strip()`
18298+
// drop it in silence — registered under both def keys, the
18299+
// `integration/DeclarativeConnectorEntry:connectionTimeoutMs` precedent. Same
18300+
// blind spot as its sibling: a discriminated-union def emits no top-level
18301+
// `properties`, so no gate judges this row.
18302+
// D2: `view-item-owner-hidden-removed`.
18303+
'ui/ViewItemWire:hidden',
18304+
// #20085 — the wire carrier of `ui/ViewItem:owner` (see that row for the
18305+
// measurement). `ViewItemWireSchema` is member 1 of the `view` union
18306+
// `saveMetaItem` validates; it is built from the same `viewItemBaseShape()`, so
18307+
// the one tombstone refuses the key there too instead of letting `.strip()`
18308+
// drop it in silence — registered under both def keys, the
18309+
// `integration/DeclarativeConnectorEntry:connectionTimeoutMs` precedent. Same
18310+
// blind spot as its sibling: a discriminated-union def emits no top-level
18311+
// `properties`, so no gate judges this row.
18312+
// D2: `view-item-owner-hidden-removed`.
18313+
'ui/ViewItemWire:owner',
1826118314
// </os-generated retired-key:18>
1826218315
],
1826318316
};

0 commit comments

Comments
 (0)