diff --git a/.changeset/28771-sealed-overlay-report-served-override.md b/.changeset/28771-sealed-overlay-report-served-override.md new file mode 100644 index 00000000000..92c100b1798 --- /dev/null +++ b/.changeset/28771-sealed-overlay-report-served-override.md @@ -0,0 +1,14 @@ +--- +'@objectstack/metadata-protocol': patch +--- + +fix(metadata-protocol): the boot's sealed-overlay report no longer counts a served `.custom` override or tells the operator to delete it (#28771) + +Clause-②: no +Metadata-protocol: none + +At boot, `[metadata_sealed_overlay_unserved]` lists the environment rows that overlay sealed managed content, says no read serves them, and prescribes deleting each stored row. Since #28412 the shipping package's declared override in its paired `.custom` package is served, and for a flow it is armed in place of the base. The report still counted that row, so it told the operator to delete the live customization. + +- **A served override is not counted.** The census skips a row that the one served-override predicate (`isServedOverrideRowOf`) admits, judged on the row's `package_id` column, never on a body's `_packageId`. +- **Every unserved row is still reported, with its remedies.** That includes the legacy package-less row a declared override leaves shadowed, the pair's undeclared row, a dependent package's declaration, and a package-less row whose body only names the pair. The count goes down by exactly the served rows. +- **Nothing else changes.** Which row is served, loaded or armed stays the same; only the report line changes. diff --git a/packages/metadata-protocol/src/protocol.reads-environment-only.test.ts b/packages/metadata-protocol/src/protocol.reads-environment-only.test.ts index 50c80ce19c7..43d80e3dfd9 100644 --- a/packages/metadata-protocol/src/protocol.reads-environment-only.test.ts +++ b/packages/metadata-protocol/src/protocol.reads-environment-only.test.ts @@ -310,6 +310,56 @@ describe('§3 the boot report names the sealed overlays, per type, with their re expect(sealed[0]).toContain('datasource×1 (pkg_ds)'); expect(sealed[0]).not.toContain('host_ds'); }); + + // [#28771, ADR-0146 D2] The shipping package's declared `.custom` override + // of a shipped flow is served (and armed) since #28412, so the census that + // says "no read serves them … delete the stored row" does not count it. The + // judgement is `isServedOverrideRowOf` on the row's `package_id` column. + describe('[#28771] a served .custom override is not counted as unserved', () => { + const FLOW = 'managed_flow'; + const PAIR = `${PKG}.custom`; + const DECLARED = { overrides: { package: PKG, basedOn: '1.0.0' } }; + const flowArtifact: Artifact = { type: 'flow', name: FLOW, body: { name: FLOW, _packageId: PKG } }; + const override = (over: Row = {}, body: Row = DECLARED) => + metaRow('flow', FLOW, { label: 'override', ...body }, { id: 'r_flow_override', package_id: PAIR, ...over }); + const legacy = () => metaRow('flow', FLOW, { label: 'legacy' }, { id: 'r_flow_legacy' }); + const sealedLines = (warns: string[]) => warns.filter((w) => w.includes('[metadata_sealed_overlay_unserved]')); + + it('a boot holding only the declared override prints no sealed-overlay line', async () => { + const { warns } = await boot({ sys_metadata: [override()] }, [flowArtifact]); + expect(sealedLines(warns)).toEqual([]); + }); + + it('a boot holding the override and a legacy package-less row reports the legacy row alone, with its remedies', async () => { + const { warns } = await boot({ sys_metadata: [override(), legacy()] }, [flowArtifact]); + const sealed = sealedLines(warns); + expect(sealed).toHaveLength(1); + expect(sealed[0]).toContain(`1 active environment sys_metadata row(s)`); + expect(sealed[0]).toContain(`flow×1 (${FLOW})`); + expect(sealed[0]).toContain('linkage-free clone'); + expect(sealed[0]).toContain('delete the stored row'); + }); + + it('control: a legacy row alone is reported as before', async () => { + const { warns } = await boot({ sys_metadata: [legacy()] }, [flowArtifact]); + const sealed = sealedLines(warns); + expect(sealed).toHaveLength(1); + expect(sealed[0]).toContain(`flow×1 (${FLOW})`); + }); + + for (const [label, row] of [ + ['the pair\'s undeclared row', () => override({}, {})], + ['a dependent package\'s declaration', () => override({ package_id: 'app.partner' })], + ['a package-less row whose body stamps the pair', () => metaRow('flow', FLOW, { label: 'stamped', _packageId: PAIR, ...DECLARED }, { id: 'r_flow_stamped' })], + ] as const) { + it(`control: ${label} is served by no rule, so it is still reported`, async () => { + const { warns } = await boot({ sys_metadata: [row()] }, [flowArtifact]); + const sealed = sealedLines(warns); + expect(sealed).toHaveLength(1); + expect(sealed[0]).toContain(`flow×1 (${FLOW})`); + }); + } + }); }); describe('§4 [ADR-0131 C7] the anonymous form doors\' legacy organization read is deleted', () => { diff --git a/packages/metadata-protocol/src/protocol.ts b/packages/metadata-protocol/src/protocol.ts index 3a2f380e0b0..3f9cd578af3 100644 --- a/packages/metadata-protocol/src/protocol.ts +++ b/packages/metadata-protocol/src/protocol.ts @@ -29382,7 +29382,21 @@ export class ObjectStackProtocolImplementation implements // defines in code is named once, by the // datasource-admin boot restore's warning, which // carries its own remedy — not here as well. - if (!this.isHostCodeDatasource(normalizedType, String(record.name))) { + // + // [#28771, ADR-0146 D2] …nor a row the served-override + // rule serves: the shipping package's declared `.custom` + // override IS served (and, for a flow, armed), so a line + // saying "no read serves them … delete the stored row" + // would prescribe deleting the live customization. The + // one predicate, on the row's `package_id` column. The + // census only: what this loop loads is unchanged. + if ( + !this.isHostCodeDatasource(normalizedType, String(record.name)) + && !isServedOverrideRowOf( + record as ServedOverrideRow, + this.packagedArtifactOwner({ type: normalizedType, name: String(record.name) }), + ) + ) { sealedOverlays.push({ type: normalizedType, name: String(record.name) }); } if (normalizedType === 'object') continue;