Skip to content

Commit a0e117c

Browse files
committed
fix(runtime): the read-only package refusals name the customisation that works, not the retired org overlay
WRITABLE_PACKAGE_REQUIRED and DUPLICATE_SOURCE_NOT_A_BASE prescribed "an ADR-0005 org overlay". ADR-0131 D6 retired that axis: a write naming an organization answers 403 NOT_OVERRIDABLE. Both now name customising beside the package as environment-level state, one clause shared by the two doors; the duplicate refusal also names the unmanaged install for an editable copy. Text only: no code, status, condition or order changes. Claude-Session: https://claude.ai/code/session_019G5ZnRrTMiwcHG6iZM19zf Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2d21b9c commit a0e117c

2 files changed

Lines changed: 60 additions & 23 deletions

File tree

‎packages/runtime/src/domains/packages-readonly-gate.test.ts‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,14 @@ describe('/packages duplicate — a source that is not a base is refused (#14451
393393
expect(err?.httpStatus).toBe(422);
394394
expect(err?.details?.packageId).toBe(CODE_LOADED);
395395
expect(err?.details?.docs).toBe('docs/adr/0070-package-first-authoring.md');
396-
// The remedy is the one that EXISTS for a code package (ADR-0005
397-
// overlay), and the message says why the copy would be empty rather
398-
// than only that it is refused.
399-
expect(err?.message).toContain('ADR-0005');
396+
// The remedies are the ones that EXIST for a code package (ADR-0131 D6:
397+
// customise beside it, or take an unmanaged copy), and the message says
398+
// why the copy would be empty rather than only that it is refused. The
399+
// retired ADR-0005 organization overlay (a write naming an organization
400+
// answers 403 NOT_OVERRIDABLE) is no longer prescribed.
401+
expect(err?.message).toContain('ADR-0131 D6');
402+
expect(err?.message).toContain("installMode 'unmanaged'");
403+
expect(err?.message).not.toContain('ADR-0005');
400404
expect(err?.message).toContain('read-only');
401405
});
402406

@@ -481,7 +485,7 @@ describe('/packages lifecycle — the read-only refusal no longer sends callers
481485
{ label: 'DELETE', path: (id: string) => `/${id}`, method: 'DELETE' },
482486
{ label: 'disable', path: (id: string) => `/${id}/disable`, method: 'PATCH' },
483487
]) {
484-
it(`${verb.label} prescribes the ADR-0005 overlay, not POST /:id/duplicate`, async () => {
488+
it(`${verb.label} prescribes customising beside the package (ADR-0131 D6), not POST /:id/duplicate`, async () => {
485489
const { dispatcher } = make();
486490
const r = await dispatcher.handlePackages(verb.path(CODE_LOADED), verb.method, {}, {}, admin());
487491
const message = String(r.response?.body?.error?.message ?? '');
@@ -491,9 +495,12 @@ describe('/packages lifecycle — the read-only refusal no longer sends callers
491495
// (POST /packages/<id>/duplicate) and change that" — a remedy that,
492496
// for exactly the packages this refusal fires on, now answers 422
493497
// and before that answered an empty 200. A refusal that prescribes a
494-
// dead end is worse than one that prescribes nothing.
498+
// dead end is worse than one that prescribes nothing. Its next
499+
// remedy, the ADR-0005 organization overlay, is retired too (a write
500+
// naming an organization answers 403 NOT_OVERRIDABLE).
495501
expect(message).not.toContain('/duplicate');
496-
expect(message).toContain('ADR-0005');
502+
expect(message).not.toContain('ADR-0005');
503+
expect(message).toContain('ADR-0131 D6');
497504
});
498505
}
499506
});

‎packages/runtime/src/domains/packages.ts‎

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -386,14 +386,20 @@ function requireReadCapability(deps: DomainHandlerDeps, context: HttpProtocolCon
386386
* (ADR-0070 D2). Revert asks it only after the protocol's stored-row answer, so
387387
* a `sys_metadata` row bound to the package keeps its own answer.
388388
*
389-
* [#14451] The remedy it names is ADR-0005 org overlay, and it used to name
390-
* `POST /packages/:id/duplicate` instead. That sentence sent a caller holding a
391-
* read-only package at a route which — for exactly the packages this refusal
392-
* fires on — cannot help: duplicate clones a base's `sys_metadata` rows, a code
393-
* package owns none, and until {@link requireDuplicableSource} the trip ended
394-
* in a 200 reporting `copiedCount: 0` (the #14451 measurement). A refusal that
395-
* prescribes a dead end is worse than one that prescribes nothing, so the two
396-
* doors now agree: overlay is how a code package is customised.
389+
* [#14451] It used to name `POST /packages/:id/duplicate` as the remedy. That
390+
* sentence sent a caller holding a read-only package at a route which — for
391+
* exactly the packages this refusal fires on — cannot help: duplicate clones a
392+
* base's `sys_metadata` rows, a code package owns none, and until {@link
393+
* requireDuplicableSource} the trip ended in a 200 reporting `copiedCount: 0`
394+
* (the #14451 measurement). A refusal that prescribes a dead end is worse than
395+
* one that prescribes nothing.
396+
*
397+
* [ADR-0131 D6] For the same reason it no longer names the ADR-0005
398+
* organization overlay, its remedy after that: the per-organization overlay
399+
* axis is retired, and a write naming an organization answers `403
400+
* NOT_OVERRIDABLE`. What a managed package provides is customised BESIDE it,
401+
* as environment-level state, and the two doors name that remedy in one
402+
* clause ({@link CUSTOMISE_BESIDE_REMEDY}).
397403
*
398404
* ⛔ Deliberately NOT caller-sensitive — there is no `isSystem` bypass, unlike
399405
* {@link requireManageMetadata}. Read-only is a property of the PACKAGE. Engine
@@ -521,6 +527,24 @@ function commitItemTypes(items: unknown): unknown[] {
521527
return Array.isArray(list) ? list.map((item) => (item as { type?: unknown } | null)?.type) : [];
522528
}
523529

530+
/**
531+
* [ADR-0131 D6] The customisation remedy both read-only refusals below name
532+
* ({@link requireWritablePackage}, {@link requireDuplicableSource}), in one
533+
* spelling. A managed package's items are sealed, and what a customer changes
534+
* lives beside them as environment-level state: a save under an item's own
535+
* name is an environment overlay on a type whose registry entry takes one
536+
* (`allowEnvironmentOverlay`) unless the package's lock refuses it — that save
537+
* is refused `ITEM_LOCKED`, naming the lock and its reason (ADR-0010 §3.3) —
538+
* and on every other type the save door's own refusal names the route that
539+
* type has: a flow's clone and switch, an action's switch, a permission set's
540+
* clone, else the source. The sentence names that door rather than a list of
541+
* types, so it cannot drift from the regime table the door reads.
542+
*/
543+
const CUSTOMISE_BESIDE_REMEDY =
544+
'customise what this package provides beside it, as environment-level state (ADR-0131 D6): save an item '
545+
+ 'under its own name — that overlays it where the item\'s type takes an environment overlay and the package\'s '
546+
+ 'lock does not refuse it, and is otherwise refused naming the route that type has, or the lock.';
547+
524548
function requireWritablePackage(
525549
deps: DomainHandlerDeps,
526550
engine: unknown,
@@ -534,8 +558,8 @@ function requireWritablePackage(
534558
response: deps.error(
535559
`[writable_package_required] Cannot ${action} package '${id}': it is read-only `
536560
+ `(provided by code or an installed app). Packages the deployment ships are managed by `
537-
+ `the deployment, not over this API — ${action} a package you own, or customise what `
538-
+ `this one provides in place, with an ADR-0005 org overlay.`,
561+
+ `the deployment, not over this API — ${action} a package you own, or `
562+
+ CUSTOMISE_BESIDE_REMEDY,
539563
422,
540564
{
541565
code: 'WRITABLE_PACKAGE_REQUIRED',
@@ -591,9 +615,13 @@ function requireWritablePackage(
591615
* Making it clone a code package's items would extend D4 from bases to code
592616
* packages — a NEW decision, and one the ADR itself still lists as open
593617
* ("should customising a code item also fork it into a writable base?
594-
* *Leaning: keep overlay for surgical tweaks*"). ADR-0005 overlay is the built,
595-
* shipped answer for customising what a code package provides, so the honest
596-
* behaviour of an unbuilt gesture is to say so.
618+
* *Leaning: keep overlay for surgical tweaks*"). Two built answers exist
619+
* instead (ADR-0131 D6): customising BESIDE the package, as environment-level
620+
* state ({@link CUSTOMISE_BESIDE_REMEDY}), and, for an editable copy of the
621+
* whole package, its `unmanaged` install, which the install door judges (the
622+
* package must permit it, and the copy cannot sit beside a managed install of
623+
* the same package). So the honest behaviour of an unbuilt gesture is to say
624+
* so and name both.
597625
*
598626
* ## Shape
599627
*
@@ -603,7 +631,8 @@ function requireWritablePackage(
603631
* a lie by implicature here: nothing is being written to the source, and its
604632
* remedy ("use a writable package") reads as *make the source writable*, which
605633
* is neither possible nor the point. `DUPLICATE_SOURCE_NOT_A_BASE` names the
606-
* one thing the caller can act on — pick a base, or overlay instead.
634+
* one thing the caller can act on — pick a base, customise beside the package,
635+
* or install it unmanaged where it permits that.
607636
*
608637
* ⛔ Deliberately NOT a check on emptiness. A WRITABLE base holding no active
609638
* rows still duplicates to `copiedCount: 0`, and that answer stays exactly as
@@ -637,8 +666,9 @@ function requireDuplicableSource(
637666
+ `(provided by code or an installed app), and duplicate clones a writable base's `
638667
+ `stored metadata rows. A code package's metadata is delivered as code, so there are `
639668
+ `no rows to clone and the copy would be empty (ADR-0070 D4 duplicates a BASE). `
640-
+ `Duplicate a base you own, or customise what this package provides in place, with an `
641-
+ `ADR-0005 org overlay.`,
669+
+ `Duplicate a base you own, or ${CUSTOMISE_BESIDE_REMEDY} `
670+
+ `For an editable copy of the whole package, install it unmanaged instead `
671+
+ `(installMode 'unmanaged'), where its manifest's installModes permits that.`,
642672
422,
643673
{
644674
code: 'DUPLICATE_SOURCE_NOT_A_BASE',

0 commit comments

Comments
 (0)