Skip to content

Commit cdecf0f

Browse files
committed
wip: re-judge objectql email-locale, dependent-package and runtime reload pins into the pair
Claude-Session: https://claude.ai/code/session_01FEkQ5ni5yjiTswP6crgAkX Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ddbdb76 commit cdecf0f

3 files changed

Lines changed: 106 additions & 29 deletions

File tree

‎packages/objectql/src/environment-overlay-email-locale-restart.test.ts‎

Lines changed: 82 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@
2626
* - a row stored before the member column existed (a second locale's body,
2727
* no member) is backfilled at boot, so it is answered as its own member.
2828
*
29+
* [#28576, ADR-0146 D11] A write naming no package onto the package's
30+
* template is closed, so the administrator writes here go into the base's
31+
* pair (`inPair`, below), where the member semantics are the same row for
32+
* row; a package-less row is laid down as a release before the close stored
33+
* it (`seedPackagelessRow`), served until the ceremony moves it, and its
34+
* package-less write is refused.
35+
*
2936
* ## The harness
3037
*
3138
* `environment-overlay-five-types.test.ts`'s: the real `ObjectQL` engine and
@@ -70,6 +77,8 @@ function mailManifest(): Record<string, unknown> {
7077
// package's protection envelope (`_packageId`, `_provenance`), which a
7178
// hydrated overlay of them is grafted with.
7279
version: '1.0.0',
80+
// [#28576] A pair is formed from the base's namespace (ADR-0146 D3).
81+
namespace: 'acmemail',
7382
emailTemplates: [
7483
template('en-US', 'PKG en'), template('zh-CN', 'PKG zh'),
7584
// [#22877 stage 2] A second template the package ships in `en-US`
@@ -182,7 +191,51 @@ async function boot(
182191
engine, () => new Map(), kernel === 'environment' ? 'env_prod' : undefined,
183192
);
184193
const load = await protocol.loadMetaFromDb();
185-
return { engine, protocol, load };
194+
await protocol.ensureCustomizationPackage({ basePackageId: PKG } as any);
195+
return { engine, protocol: inPair(protocol), raw: protocol, load };
196+
}
197+
198+
/**
199+
* [#28576, ADR-0146 D11] A write naming no package onto the package's
200+
* template is closed, so every administrator write in this file is the one
201+
* that stays open: into the base's pair, `com.acme.mail.custom`, declaring
202+
* what it overrides. This wrapper names the pair on each save, publish and
203+
* delete that names no package, and adds the declaration to a saved body —
204+
* the member semantics pinned below are the pair's, row for row, as they were
205+
* the package-less row's. A write that names a package (the drafts into
206+
* `PKG` below) is passed through untouched.
207+
*/
208+
const PAIR = 'com.acme.mail.custom';
209+
const DECLARATION = { package: PKG, basedOn: '1.0.0' };
210+
function inPair(protocol: ObjectStackProtocolImplementation): ObjectStackProtocolImplementation {
211+
const named = (request: any) => request?.packageId !== undefined;
212+
return new Proxy(protocol, {
213+
get(target, key, receiver) {
214+
const value = Reflect.get(target, key, receiver);
215+
if (key === 'saveMetaItem') {
216+
return (request: any) => (named(request) ? value.call(target, request) : value.call(target, {
217+
...request, packageId: PAIR, item: { ...request.item, overrides: DECLARATION },
218+
}));
219+
}
220+
if (key === 'deleteMetaItem' || key === 'publishMetaItem') {
221+
return (request: any) => value.call(target, named(request) ? request : { ...request, packageId: PAIR });
222+
}
223+
return typeof value === 'function' ? value.bind(target) : value;
224+
},
225+
});
226+
}
227+
228+
/**
229+
* [#28576] A package-less row of the package's template as a release before
230+
* the close stored it, written straight to the overlay store (the write door
231+
* refuses that write now): such a row is still served by every read until
232+
* `os migrate packageless-overlays` moves it (ADR-0146 D11).
233+
*/
234+
async function seedPackagelessRow(raw: ObjectStackProtocolImplementation, name: string, body: Record<string, unknown>) {
235+
const repo = (raw as any).getOverlayRepo();
236+
await repo.put({ type: 'email_template', name, org: 'env' }, body, {
237+
parentVersion: null, actor: null, source: 'older-release', intent: 'override-artifact', packageId: null,
238+
});
186239
}
187240

188241
const overlayRows = (stores: Map<string, Map<string, Record<string, unknown>>>) =>
@@ -308,10 +361,15 @@ describe('two locales of one email template survive a restart (#22877)', () => {
308361
expect(await bareNameSubject(second.protocol)).toBe('PKG en');
309362
expect(await servedSubjects(second.protocol)).toEqual({ 'en-US': 'PKG en', 'zh-CN': 'LEGACY zh' });
310363

311-
// That locale's next save updates the backfilled row — no second row.
312-
await second.protocol.saveMetaItem({ type: 'email_template', name: NAME, item: template('zh-CN', 'ADMIN zh') });
364+
// [#28576, ADR-0146 D11] That locale's next save naming no package is
365+
// refused, naming the ceremony that moves the row; the backfilled row
366+
// stays the one served — no second row.
367+
const refused: any = await second.raw.saveMetaItem({ type: 'email_template', name: NAME, item: template('zh-CN', 'ADMIN zh') })
368+
.then(() => undefined, (e: unknown) => e);
369+
expect(refused).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
370+
expect(String(refused?.message)).toContain('os migrate packageless-overlays');
313371
expect(overlayRows(stores).map((r) => r.id)).toEqual(['legacy_zh']);
314-
expect(await servedSubjects(second.protocol)).toEqual({ 'en-US': 'PKG en', 'zh-CN': 'ADMIN zh' });
372+
expect(await servedSubjects(second.protocol)).toEqual({ 'en-US': 'PKG en', 'zh-CN': 'LEGACY zh' });
315373
});
316374
});
317375

@@ -704,9 +762,10 @@ async function bootWithOverlays(
704762
beforeHydration?: (engine: ObjectQL) => void,
705763
) {
706764
const first = await boot(stores);
765+
// [#28576] Package-less rows a release before the close stored: the
766+
// hydration of an unmigrated row is what this section pins.
707767
for (const locale of ['en-US', 'zh-CN']) {
708-
expect(await first.protocol.saveMetaItem({ type: T, name, item: { ...template(locale, `ADMIN ${name} ${locale}`), name } }))
709-
.toMatchObject({ success: true });
768+
await seedPackagelessRow(first.raw, name, { ...template(locale, `ADMIN ${name} ${locale}`), name });
710769
}
711770
return boot(stores, 'environment', beforeHydration);
712771
}
@@ -794,32 +853,39 @@ describe('[#23074] the artifact envelope is stamped only on the member the packa
794853
* (`restoreArtifactRegistryView`) calls is asked about the hydrated entry,
795854
* on a boot of its own.
796855
*/
797-
async function resetZh(name: string, verbs: { shadow: boolean; overlayEntry: boolean }, after: string | undefined) {
856+
async function resetZh(name: string, verbs: { shadow: boolean; overlayEntry: boolean }) {
798857
const shadowBoot = (await bootWithOverlays(new Map(), name)).engine;
799858
expect(shadowBoot.registry.removeRuntimeShadow(T, name, 'zh-CN'), 'removeRuntimeShadow').toBe(verbs.shadow);
800859
const overlayEntryBoot = (await bootWithOverlays(new Map(), name)).engine;
801860
expect(overlayEntryBoot.registry.removeOverlayEntry(T, name, 'zh-CN'), 'removeOverlayEntry').toBe(verbs.overlayEntry);
802861

862+
// [#28576, ADR-0146 D11] The reset of that unmigrated row naming no
863+
// package is refused, naming the ceremony that moves it, and the row
864+
// keeps serving its member — before and after a restart. The registry
865+
// verbs above are what the ceremony's move leaves to the next boot.
803866
const stores = new Map<string, Map<string, Record<string, unknown>>>();
804-
const { engine, protocol } = await bootWithOverlays(stores, name);
805-
expect(await protocol.deleteMetaItem({ type: T, name, member: 'zh-CN' })).toMatchObject({ success: true });
806-
expect(engine.registry.getItem<any>(T, name, undefined, 'zh-CN')?.subject).toBe(after);
867+
const { engine, protocol, raw } = await bootWithOverlays(stores, name);
868+
const refused: any = await raw.deleteMetaItem({ type: T, name, member: 'zh-CN' }).then(() => undefined, (e: unknown) => e);
869+
expect(refused).toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403 });
870+
expect(String(refused?.message)).toContain('os migrate packageless-overlays');
871+
const kept = `ADMIN ${name} zh-CN`;
872+
expect(engine.registry.getItem<any>(T, name, undefined, 'zh-CN')?.subject).toBe(kept);
807873
expect(engine.registry.getItem<any>(T, name, undefined, 'en-US')?.subject).toBe(`ADMIN ${name} en-US`);
808-
expect(((await protocol.getMetaItem({ type: T, name, member: 'zh-CN' })) as any)?.item?.subject).toBe(after);
874+
expect(((await protocol.getMetaItem({ type: T, name, member: 'zh-CN' })) as any)?.item?.subject).toBe(kept);
809875
const restarted = await boot(stores);
810-
expect(((await restarted.protocol.getMetaItem({ type: T, name, member: 'zh-CN' })) as any)?.item?.subject).toBe(after);
876+
expect(((await restarted.protocol.getMetaItem({ type: T, name, member: 'zh-CN' })) as any)?.item?.subject).toBe(kept);
811877
}
812878

813-
it('the reset (?member=): an unshipped member is retired by the overlay-entry tier, and nothing is left serving it', async () => {
879+
it('the reset (?member=): an unshipped member is retired by the overlay-entry tier; its package-less reset door is closed', async () => {
814880
// No `zh-CN` artifact takes its place, so the shadow heal declines and
815881
// the overlay-entry retirement takes the tenant row.
816-
await resetZh(DIGEST, { shadow: false, overlayEntry: true }, undefined);
882+
await resetZh(DIGEST, { shadow: false, overlayEntry: true });
817883
});
818884

819-
it('CONTROL the reset (?member=): a shipped member is un-shadowed onto its artifact', async () => {
885+
it('CONTROL the reset (?member=): a shipped member is un-shadowed onto its artifact; its package-less reset door is closed', async () => {
820886
// The shadow heal lifts the entry onto the `zh-CN` artifact, and the
821887
// retirement refuses it as packaged code.
822-
await resetZh(NAME, { shadow: true, overlayEntry: false }, 'PKG zh');
888+
await resetZh(NAME, { shadow: true, overlayEntry: false });
823889
});
824890

825891
it('a registry that answers another member\'s artifact: the graft keeps it off the body', async () => {

‎packages/objectql/src/protocol-packageless-read-dependent-package.test.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
*
2323
* - the card's three reads answer base / dependent / base, and the layered
2424
* read's `effective` and the list scoped to the base agree with them;
25-
* - the write address does not move: a save naming no package still
26-
* addresses the dependent package's row, as {@link packagelessAddress}
27-
* resolves it — and since #28410 that write is refused (ADR-0146 D2);
25+
* - the write address does not move to the dependent package: a save naming
26+
* no package is refused before any address is resolved — since #28576 a
27+
* write naming no package onto a name the base ships is closed, naming the
28+
* base's pair (ADR-0146 D11; #28410 refused it as the dependent's
29+
* same-named item before that);
2830
* - CONTROL: a writable package's OWN item, a name no code package ships,
2931
* is still served to the read naming no package (#23291);
3032
* - CONTROL: a package-unbound row (the ADR-0005 overlay Setup writes) still
@@ -275,19 +277,19 @@ describe('[#28238] a dependent package\'s row under a name the base ships is not
275277
expect(listed.items.filter((i: any) => i.name === VIEW).map((i: any) => columnsOf(i))).toEqual([BASE_COLUMNS]);
276278
});
277279

278-
it('the write address does not move: a save naming no package still addresses the dependent package\'s row, which the override gate refuses', async () => {
280+
it('the write address does not move: a save naming no package is refused, naming the base\'s pair, and the dependent package\'s row is untouched', async () => {
279281
const { protocol, rowsOf } = await boot();
280282
await installDependent(protocol);
281283
await saveIntoDependentBeforeTheGate(protocol, DEPENDENT_COLUMNS);
282284
expect((await (protocol as any).packagelessAddress('view', VIEW)).packageId).toBe(DEPENDENT);
283285

284-
// [#28410, ADR-0146 D2, ruling A on #28390] The save naming no package
285-
// resolves to the dependent package's row — the address does not move
286-
// — and that write is a dependent's same-named item, which the
287-
// override write gate refuses (rename): the refusal names the
288-
// dependent package, and the stored row is untouched.
286+
// [#28576, ADR-0146 D11] The save naming no package is a write onto a
287+
// name the base ships, which is closed: the refusal names the base's
288+
// pair and the ceremony, ahead of the override write gate that refused
289+
// it as the dependent's same-named item (#28410). The stored row is
290+
// untouched.
289291
await expect(protocol.saveMetaItem({ type: 'view', name: VIEW, item: listBody(['title']) } as any))
290-
.rejects.toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403, message: expect.stringContaining(`'${DEPENDENT}'`) });
292+
.rejects.toMatchObject({ code: 'NOT_OVERRIDABLE', status: 403, message: expect.stringContaining(`'${BASE}.custom' (?package=${BASE}.custom)`) });
291293

292294
const rows = rowsOf(VIEW).filter((r) => r.state === 'active');
293295
expect(rows.map((r) => r.package_id)).toEqual([DEPENDENT]);

‎packages/runtime/src/standalone-stack-artifact-reload-registry-refresh.test.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ const PACKAGE_ID = 'com.test.artifact-reload';
4949

5050
/** One artifact, every display string carrying `v`. */
5151
const artifact = (v: string) => ({
52-
manifest: { id: PACKAGE_ID, name: 'artifact_reload', type: 'app', version: '1.0.0' },
52+
// [#28576] `namespace`: the base's `.custom` pair is formed from it (ADR-0146 D3).
53+
manifest: { id: PACKAGE_ID, name: 'artifact_reload', type: 'app', version: '1.0.0', namespace: 'arl' },
5354
objects: [{
5455
name: 'arl_note',
5556
label: `Note ${v}`,
@@ -208,7 +209,10 @@ describe('an artifact hot reload refreshes every read of every item the boot reg
208209
expect(await read()).toEqual(boot);
209210
}, BOOT_TIMEOUT);
210211

211-
it('an environment save of the template still wins on every read after a reload re-registers the package', async () => {
212+
// [#28576, ADR-0146 D11] The environment's customization of a packaged
213+
// template is written into the base's pair, declaring what it overrides: the
214+
// package-less save onto it is closed.
215+
it('a customization of the template saved into its pair still wins on every read after a reload re-registers the package', async () => {
212216
const { protocol, reload } = await bootOverArtifact();
213217
const subjects = async () => {
214218
const pick = (res: any) => (res?.items ?? []).filter((i: any) => i?.name === 'arl_mail').map((i: any) => i.subject);
@@ -218,10 +222,15 @@ describe('an artifact hot reload refreshes every read of every item the boot reg
218222
byName: (await protocol.getMetaItem({ type: 'email_template', name: 'arl_mail' }))?.item?.subject,
219223
};
220224
};
225+
await protocol.ensureCustomizationPackage({ basePackageId: PACKAGE_ID });
221226
await protocol.saveMetaItem({
222227
type: 'email_template',
223228
name: 'arl_mail',
224-
item: { name: 'arl_mail', label: 'Mail', locale: 'en-US', subject: 'Saved subject', bodyHtml: '<p>saved</p>' },
229+
packageId: `${PACKAGE_ID}.custom`,
230+
item: {
231+
name: 'arl_mail', label: 'Mail', locale: 'en-US', subject: 'Saved subject', bodyHtml: '<p>saved</p>',
232+
overrides: { package: PACKAGE_ID, basedOn: '1.0.0' },
233+
},
225234
});
226235
const saved = { list: ['Saved subject'], execution: ['Saved subject'], byName: 'Saved subject' };
227236
// Positive control: the save is live before any reload.

0 commit comments

Comments
 (0)