@homeflare/alchemy 0.60.0 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,9 +24,24 @@ export declare class DuplicateResourceIdError extends DuplicateResourceIdError_b
24
24
  }> {
25
25
  }
26
26
  /**
27
- * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against.
27
+ * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this
28
+ * package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.
28
29
  * `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.
30
+ *
31
+ * ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls
32
+ * `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)
33
+ * must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the
34
+ * list a caller walks to find ALL supported pins, this one constant is not retargeted to mean
35
+ * "whichever is newest."
29
36
  */
30
37
  export declare const PATCHED_ALCHEMY_VERSION: string;
38
+ /**
39
+ * Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`
40
+ * for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a
41
+ * consumer's ACTUAL installed version instead of only ever checking against
42
+ * {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test
43
+ * both assert every entry here has a real `patches/alchemy@<version>.patch` file.
44
+ */
45
+ export declare const PATCHED_ALCHEMY_VERSIONS: readonly string[];
31
46
  export {};
32
47
  //# sourceMappingURL=duplicate-id.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"duplicate-id.d.ts","sourceRoot":"","sources":["../../src/scope/duplicate-id.ts"],"names":[],"mappings":";;;AA4BA;;;;;;;;;;;;GAYG;AACH,qBAAa,wBAAyB,SAAQ,8BAA6C;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;CAAG;AAEL;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAwB,CAAC"}
1
+ {"version":3,"file":"duplicate-id.d.ts","sourceRoot":"","sources":["../../src/scope/duplicate-id.ts"],"names":[],"mappings":";;;AAqCA;;;;;;;;;;;;GAYG;AACH,qBAAa,wBAAyB,SAAQ,8BAA6C;IACzF,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;CAAG;AAEL;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAwB,CAAC;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAAuC,CAAC"}
@@ -2,8 +2,9 @@
2
2
  * `@homeflare/alchemy/scope` — a consuming stack's own check that it carries the
3
3
  * duplicate-resource-id patch, and the error shape that patch throws.
4
4
  *
5
- * The fix itself is a patch to `alchemy`'s own `Resource.ts`
6
- * (`../../patches/alchemy@2.0.0-beta.81.patch`), applied at EACH consuming stack's own root
5
+ * The fix itself is a patch to `alchemy`'s own `Resource.ts`, one per supported alchemy version
6
+ * (`../../patches/alchemy@<version>.patch`, listed in `PATCHED_ALCHEMY_VERSIONS`: beta.81 and
7
+ * beta.83), applied at EACH consuming stack's own root
7
8
  * (`alchemy` is a peer dependency here, so `patchedDependencies` in this package's manifest
8
9
  * only reaches this package's own install, never a consumer's). docs/duplicate-resource-id.md
9
10
  * has the full guide, the exact `bun patch` workflow, and what this does and does not cover.
@@ -18,6 +19,6 @@
18
19
  * }
19
20
  * ```
20
21
  */
21
- export { DuplicateResourceIdError, PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
22
+ export { DuplicateResourceIdError, PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS, } from './duplicate-id.ts';
22
23
  export { checkAlchemyPatch, type PatchProblem } from './patch-check.ts';
23
24
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scope/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACtF,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scope/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC"}
@@ -4,20 +4,26 @@ import * as Data from "effect/Data";
4
4
  class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError") {
5
5
  }
6
6
  var PATCHED_ALCHEMY_VERSION = "2.0.0-beta.81";
7
+ var PATCHED_ALCHEMY_VERSIONS = ["2.0.0-beta.81", "2.0.0-beta.83"];
7
8
  // src/scope/patch-check.ts
8
9
  var patchKey = (version) => `alchemy@${version}`;
9
10
  var shippedPatchPath = (version) => new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;
10
- var checkAlchemyPatch = async (projectDir, alchemyVersion = PATCHED_ALCHEMY_VERSION) => {
11
+ var checkAlchemyPatch = async (projectDir, alchemyVersion) => {
11
12
  const problems = [];
12
- const key = patchKey(alchemyVersion);
13
13
  const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);
14
14
  if (!await installedPkg.exists()) {
15
15
  problems.push("node_modules/alchemy: not installed — nothing to check the patch against");
16
16
  return problems;
17
17
  }
18
18
  const installedVersion = (await installedPkg.json()).version;
19
- if (installedVersion !== alchemyVersion) {
20
- problems.push(`node_modules/alchemy is ${installedVersion ?? "unknown"}, but the duplicate-resource-id ` + `patch targets ${alchemyVersion} — bump the patch (@homeflare/alchemy's own ` + "patches/) before bumping alchemy, or this install has no protection at all");
19
+ const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;
20
+ const key = patchKey(targetVersion);
21
+ if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {
22
+ problems.push(`alchemy ${targetVersion} (installed: ${installedVersion ?? "unknown"}) is a version ` + `@homeflare/alchemy has ` + `no duplicate-resource-id patch for — supported versions are ` + `${PATCHED_ALCHEMY_VERSIONS.join(", ")}; cut a new patches/${key}.patch ` + "(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no " + "protection at all");
23
+ return problems;
24
+ }
25
+ if (installedVersion !== targetVersion) {
26
+ problems.push(`node_modules/alchemy is ${installedVersion ?? "unknown"}, but the duplicate-resource-id ` + `check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` + "alchemyVersion to check against whatever is actually installed");
21
27
  return problems;
22
28
  }
23
29
  const pkgFile = Bun.file(`${projectDir}/package.json`);
@@ -36,7 +42,7 @@ var checkAlchemyPatch = async (projectDir, alchemyVersion = PATCHED_ALCHEMY_VERS
36
42
  problems.push(`package.json declares "${key}" at ${declaredPath}, but that file is missing`);
37
43
  return problems;
38
44
  }
39
- const ours = Bun.file(shippedPatchPath(alchemyVersion));
45
+ const ours = Bun.file(shippedPatchPath(targetVersion));
40
46
  if (!await ours.exists()) {
41
47
  problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);
42
48
  return problems;
@@ -59,8 +65,9 @@ var checkAlchemyPatch = async (projectDir, alchemyVersion = PATCHED_ALCHEMY_VERS
59
65
  export {
60
66
  DuplicateResourceIdError,
61
67
  PATCHED_ALCHEMY_VERSION,
68
+ PATCHED_ALCHEMY_VERSIONS,
62
69
  checkAlchemyPatch
63
70
  };
64
71
 
65
- //# debugId=BFEA50A1CBA14B6A64756E2164756E21
72
+ //# debugId=461D34CD68FB220064756E2164756E21
66
73
  //# sourceMappingURL=index.js.map
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/scope/duplicate-id.ts", "../src/scope/patch-check.ts"],
4
4
  "sourcesContent": [
5
- "/**\n * `DuplicateResourceIdError` and the pinned alchemy version the kit's patch targets.\n *\n * ★ THE FIX ITSELF IS NOT HERE. A second resource of a different type claiming an id already\n * taken in one stack scope is refused by a PATCH to `alchemy`'s own `Resource.ts` — see\n * `../../patches/alchemy@2.0.0-beta.81.patch` and docs/duplicate-resource-id.md for why a\n * kit-side `Proxy` wrapper (PR 468's first version, replaced by this PR) cannot actually\n * catch the incident it was built for: wrapping `@homeflare/alchemy`'s OWN resource exports\n * never reaches `NixJobRetirement`, a resource homeflare-mini declares directly against\n * `alchemy`'s `Resource()`, so the measured collision still succeeded silently in BOTH\n * orderings. This file now holds only:\n *\n * 1. `DuplicateResourceIdError` — the exact shape the patched `alchemy` throws, duplicated\n * here (not imported — `alchemy` cannot depend on `@homeflare/alchemy`) purely so a\n * consumer can write `Effect.catchTag(\"DuplicateResourceIdError\", …)` against a type this\n * package exports, without needing to know alchemy carries its own copy of the same tag.\n * Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class\n * works identically as a catch target.\n * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version the patch was cut against.\n * Hardcoded, like `ESTATE_VERSIONS` in `@homeflare/config/versions` (the same reasoning:\n * a second source of truth derived by import would cross `tsconfig.build.json`'s\n * `rootDir: \"src\"` boundary to reach `package.json`). `duplicate-id.test.ts` asserts it\n * still matches this package's own `peerDependencies.alchemy`, so a pin bump that forgets\n * to re-cut the patch fails a test here instead of silently shipping a patch filename that\n * no longer matches what consumers install.\n */\nimport * as Data from 'effect/Data';\n\n/**\n * Mirrors the error the patched `alchemy@2.0.0-beta.81` throws at registration — same `_tag`,\n * same fields — for a consumer that wants to `catchTag` it without importing from `alchemy`'s\n * own patched internals.\n *\n * ⚠️ ONLY `Type` IS COMPARED, NEVER `Props`. A platform-typed resource's tagged pattern\n * (`alchemy`'s own `Platform.ts` ~581) rewrites `Props` on a resource already registered, and a\n * bare-tag forward reference legitimately registers with `Props: undefined` until its\n * `.make(...)` Layer builds (`MissingImplementationError`'s own doc comment in `Resource.ts`).\n * Comparing props as well as type would make both of those die. `Type` is stable from a\n * resource's very first `yield*` and is exactly what distinguishes \"the same resource,\n * referenced twice\" (fine) from \"two different resources that picked the same id\" (refused).\n */\nexport class DuplicateResourceIdError extends Data.TaggedError('DuplicateResourceIdError')<{\n message: string;\n fqn: string;\n /** The type that first claimed `fqn`. */\n firstType: string;\n /** The different type that tried to claim it again. */\n secondType: string;\n}> {}\n\n/**\n * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against.\n * `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.\n */\nexport const PATCHED_ALCHEMY_VERSION: string = '2.0.0-beta.81';\n",
6
- "/**\n * A consuming stack's own `check` calls `checkAlchemyPatch` to assert it actually carries the\n * duplicate-resource-id fix — not just that SOME version of `alchemy` is installed, but that\n * THIS install carries the patch, for the alchemy version it was cut against.\n *\n * ★ WHY A BUMP CAN OTHERWISE DROP IT SILENTLY. `patchedDependencies` keys a patch to an EXACT\n * version string (`alchemy@2.0.0-beta.81`). `bun add alchemy@2.0.0-beta.82` leaves that key\n * pointing at a version nothing installs anymore — `bun install` neither errors nor warns,\n * it just stops applying the patch, and the stack is back to the original silent collision\n * with every other gate still green. This check is what turns that bump into a failing\n * `check`, the same way `tests/dist.test.ts` turns a build regression into a failing test\n * instead of a surprise three repos downstream (this package's own AGENTS.md, \"the smoke\n * test is not optional\").\n * ★ A STATIC CHECK, mirroring `@homeflare/config/check`'s `checkCopy`/`checkOxfmt` — not a\n * functional probe that spins up a `Stack`/`Resource`/`Effect` harness on every `check` run.\n * The functional proof that the patch's LOGIC works lives in this package's own\n * `duplicate-id.test.ts`, against the exact pinned version; a consumer's `check` only needs\n * to know whether ITS install carries that same patch, applied — three file reads, no\n * `effect` runtime pulled into every repo's check step.\n * ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this\n * never edits `package.json` or re-runs `bun install` for the caller.\n */\nimport { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';\n\n/** One thing wrong with a consuming stack's alchemy patch, in the imperative. */\nexport type PatchProblem = string;\n\nconst patchKey = (version: string) => `alchemy@${version}`;\n\n/** Where THIS package's own shipped copy of the patch lives, however it was installed. */\nconst shippedPatchPath = (version: string): string =>\n new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;\n\ntype PackageJson = {\n readonly patchedDependencies?: Readonly<Record<string, string>>;\n};\n\n/**\n * @param projectDir The consuming stack's OWN repo root (where its `package.json` and\n * `patches/` directory live) — never this kit's own.\n * @param alchemyVersion The version to check for — defaults to the version this package's\n * own shipped patch targets ({@link PATCHED_ALCHEMY_VERSION}); a caller only overrides it to\n * test a different pin.\n */\nexport const checkAlchemyPatch = async (\n projectDir: string,\n alchemyVersion: string = PATCHED_ALCHEMY_VERSION,\n): Promise<readonly PatchProblem[]> => {\n const problems: PatchProblem[] = [];\n const key = patchKey(alchemyVersion);\n\n const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);\n if (!(await installedPkg.exists())) {\n problems.push('node_modules/alchemy: not installed — nothing to check the patch against');\n return problems;\n }\n const installedVersion = ((await installedPkg.json()) as { version?: string }).version;\n if (installedVersion !== alchemyVersion) {\n problems.push(\n `node_modules/alchemy is ${installedVersion ?? 'unknown'}, but the duplicate-resource-id ` +\n `patch targets ${alchemyVersion} — bump the patch (@homeflare/alchemy's own ` +\n 'patches/) before bumping alchemy, or this install has no protection at all',\n );\n return problems;\n }\n\n const pkgFile = Bun.file(`${projectDir}/package.json`);\n if (!(await pkgFile.exists())) {\n problems.push('package.json: missing');\n return problems;\n }\n const pkg = (await pkgFile.json()) as PackageJson;\n const declaredPath = pkg.patchedDependencies?.[key];\n if (declaredPath === undefined) {\n problems.push(\n `package.json: patchedDependencies has no entry for \"${key}\" — this stack has no ` +\n 'duplicate-resource-id protection at all; see @homeflare/alchemy/docs/duplicate-resource-id.md',\n );\n return problems;\n }\n\n const theirs = Bun.file(`${projectDir}/${declaredPath}`);\n if (!(await theirs.exists())) {\n problems.push(`package.json declares \"${key}\" at ${declaredPath}, but that file is missing`);\n return problems;\n }\n const ours = Bun.file(shippedPatchPath(alchemyVersion));\n if (!(await ours.exists())) {\n // The kit's own tarball is missing its patch — a packaging bug, not the consumer's.\n problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);\n return problems;\n }\n if ((await theirs.text()) !== (await ours.text())) {\n problems.push(\n `${declaredPath}: differs from @homeflare/alchemy's shipped patch — re-copy it, ` +\n 'or say why it differs',\n );\n }\n\n // Declared and byte-identical is not yet APPLIED: a hand-edited patchedDependencies entry,\n // or an install under a package manager that does not support bun's patch format, leaves\n // node_modules unpatched with nothing above noticing. One cheap textual proof it landed.\n const resourceSrc = Bun.file(`${projectDir}/node_modules/alchemy/src/Resource.ts`);\n const resourceLib = Bun.file(`${projectDir}/node_modules/alchemy/lib/Resource.js`);\n const marker = 'DuplicateResourceIdError';\n const [srcText, libText] = await Promise.all([\n resourceSrc.exists().then((ok) => (ok ? resourceSrc.text() : '')),\n resourceLib.exists().then((ok) => (ok ? resourceLib.text() : '')),\n ]);\n if (!srcText.includes(marker) && !libText.includes(marker)) {\n problems.push(\n 'node_modules/alchemy/{src/Resource.ts,lib/Resource.js}: neither contains the patch — ' +\n 'declared in package.json but not applied; re-run `bun install`',\n );\n }\n\n return problems;\n};\n"
5
+ "/**\n * `DuplicateResourceIdError` and the pinned alchemy version the kit's patch targets.\n *\n * ★ THE FIX ITSELF IS NOT HERE. A second resource of a different type claiming an id already\n * taken in one stack scope is refused by a PATCH to `alchemy`'s own `Resource.ts` — see\n * `../../patches/alchemy@2.0.0-beta.81.patch` and docs/duplicate-resource-id.md for why a\n * kit-side `Proxy` wrapper (PR 468's first version, replaced by this PR) cannot actually\n * catch the incident it was built for: wrapping `@homeflare/alchemy`'s OWN resource exports\n * never reaches `NixJobRetirement`, a resource homeflare-mini declares directly against\n * `alchemy`'s `Resource()`, so the measured collision still succeeded silently in BOTH\n * orderings. This file now holds only:\n *\n * 1. `DuplicateResourceIdError` — the exact shape the patched `alchemy` throws, duplicated\n * here (not imported — `alchemy` cannot depend on `@homeflare/alchemy`) purely so a\n * consumer can write `Effect.catchTag(\"DuplicateResourceIdError\", …)` against a type this\n * package exports, without needing to know alchemy carries its own copy of the same tag.\n * Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class\n * works identically as a catch target.\n * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version THIS package's own peer pin and\n * devDependency sit at, and the one `duplicate-id.test.ts` drives its real `Stack` against.\n * Hardcoded, like `ESTATE_VERSIONS` in `@homeflare/config/versions` (the same reasoning:\n * a second source of truth derived by import would cross `tsconfig.build.json`'s\n * `rootDir: \"src\"` boundary to reach `package.json`). `duplicate-id.test.ts` asserts it\n * still matches this package's own `peerDependencies.alchemy`, so a pin bump that forgets\n * to re-cut the patch fails a test here instead of silently shipping a patch filename that\n * no longer matches what consumers install.\n * 3. `PATCHED_ALCHEMY_VERSIONS` — every alchemy version this package ships a CUT patch for\n * (currently beta.81 and beta.83, kit PR shipping beta.83's — a consumer on beta.82 or\n * beta.84 is NOT covered merely by being \"close\"; `bun`'s patch format keys to an exact\n * version string, and a patch cut against one version is not guaranteed to even apply to\n * another — see docs/duplicate-resource-id.md for why a diff was taken between the two\n * tarballs rather than assumed). `checkAlchemyPatch` (`patch-check.ts`) walks this list to\n * find the shipped patch matching a consumer's ACTUAL install, so a stack on beta.83 is not\n * forced to pin the single oldest supported version just to pass its own check.\n */\nimport * as Data from 'effect/Data';\n\n/**\n * Mirrors the error the patched `alchemy@2.0.0-beta.81` throws at registration — same `_tag`,\n * same fields — for a consumer that wants to `catchTag` it without importing from `alchemy`'s\n * own patched internals.\n *\n * ⚠️ ONLY `Type` IS COMPARED, NEVER `Props`. A platform-typed resource's tagged pattern\n * (`alchemy`'s own `Platform.ts` ~581) rewrites `Props` on a resource already registered, and a\n * bare-tag forward reference legitimately registers with `Props: undefined` until its\n * `.make(...)` Layer builds (`MissingImplementationError`'s own doc comment in `Resource.ts`).\n * Comparing props as well as type would make both of those die. `Type` is stable from a\n * resource's very first `yield*` and is exactly what distinguishes \"the same resource,\n * referenced twice\" (fine) from \"two different resources that picked the same id\" (refused).\n */\nexport class DuplicateResourceIdError extends Data.TaggedError('DuplicateResourceIdError')<{\n message: string;\n fqn: string;\n /** The type that first claimed `fqn`. */\n firstType: string;\n /** The different type that tried to claim it again. */\n secondType: string;\n}> {}\n\n/**\n * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this\n * package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.\n * `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.\n *\n * ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls\n * `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)\n * must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the\n * list a caller walks to find ALL supported pins, this one constant is not retargeted to mean\n * \"whichever is newest.\"\n */\nexport const PATCHED_ALCHEMY_VERSION: string = '2.0.0-beta.81';\n\n/**\n * Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`\n * for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a\n * consumer's ACTUAL installed version instead of only ever checking against\n * {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test\n * both assert every entry here has a real `patches/alchemy@<version>.patch` file.\n */\nexport const PATCHED_ALCHEMY_VERSIONS: readonly string[] = ['2.0.0-beta.81', '2.0.0-beta.83'];\n",
6
+ "/**\n * A consuming stack's own `check` calls `checkAlchemyPatch` to assert it actually carries the\n * duplicate-resource-id fix — not just that SOME version of `alchemy` is installed, but that\n * THIS install carries the patch, for the alchemy version it was cut against.\n *\n * ★ WHY A BUMP CAN OTHERWISE DROP IT SILENTLY. `patchedDependencies` keys a patch to an EXACT\n * version string (`alchemy@2.0.0-beta.81`). `bun add alchemy@2.0.0-beta.82` leaves that key\n * pointing at a version nothing installs anymore — `bun install` neither errors nor warns,\n * it just stops applying the patch, and the stack is back to the original silent collision\n * with every other gate still green. This check is what turns that bump into a failing\n * `check`, the same way `tests/dist.test.ts` turns a build regression into a failing test\n * instead of a surprise three repos downstream (this package's own AGENTS.md, \"the smoke\n * test is not optional\").\n * ★ A STATIC CHECK, mirroring `@homeflare/config/check`'s `checkCopy`/`checkOxfmt` — not a\n * functional probe that spins up a `Stack`/`Resource`/`Effect` harness on every `check` run.\n * The functional proof that the patch's LOGIC works lives in this package's own\n * `duplicate-id.test.ts`, against the exact pinned version; a consumer's `check` only needs\n * to know whether ITS install carries that same patch, applied — three file reads, no\n * `effect` runtime pulled into every repo's check step.\n * ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this\n * never edits `package.json` or re-runs `bun install` for the caller.\n */\nimport { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';\n\n/** One thing wrong with a consuming stack's alchemy patch, in the imperative. */\nexport type PatchProblem = string;\n\nconst patchKey = (version: string) => `alchemy@${version}`;\n\n/** Where THIS package's own shipped copy of the patch lives, however it was installed. */\nconst shippedPatchPath = (version: string): string =>\n new URL(`../../patches/${patchKey(version)}.patch`, import.meta.url).pathname;\n\ntype PackageJson = {\n readonly patchedDependencies?: Readonly<Record<string, string>>;\n};\n\n/**\n * @param projectDir The consuming stack's OWN repo root (where its `package.json` and\n * `patches/` directory live) — never this kit's own.\n * @param alchemyVersion The version to check for. Omitted (the normal case): the version\n * actually installed at `projectDir` is used, so a consumer on ANY version this package\n * ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin\n * {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the\n * beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly\n * only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).\n */\nexport const checkAlchemyPatch = async (\n projectDir: string,\n alchemyVersion?: string,\n): Promise<readonly PatchProblem[]> => {\n const problems: PatchProblem[] = [];\n\n const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);\n if (!(await installedPkg.exists())) {\n problems.push('node_modules/alchemy: not installed — nothing to check the patch against');\n return problems;\n }\n const installedVersion = ((await installedPkg.json()) as { version?: string }).version;\n\n // No explicit ask: check against whatever is ACTUALLY installed, not a single hardcoded\n // pin — that is what lets a beta.83 consumer pass without also being on beta.81.\n const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;\n const key = patchKey(targetVersion);\n\n if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {\n problems.push(\n // ★ Name the version being CHECKED: with an explicit `alchemyVersion` it can differ from\n // the installed one, and naming the installed version then blames a supported one.\n `alchemy ${targetVersion} (installed: ${installedVersion ?? 'unknown'}) is a version ` +\n `@homeflare/alchemy has ` +\n `no duplicate-resource-id patch for — supported versions are ` +\n `${PATCHED_ALCHEMY_VERSIONS.join(', ')}; cut a new patches/${key}.patch ` +\n \"(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no \" +\n 'protection at all',\n );\n return problems;\n }\n if (installedVersion !== targetVersion) {\n problems.push(\n `node_modules/alchemy is ${installedVersion ?? 'unknown'}, but the duplicate-resource-id ` +\n `check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` +\n 'alchemyVersion to check against whatever is actually installed',\n );\n return problems;\n }\n\n const pkgFile = Bun.file(`${projectDir}/package.json`);\n if (!(await pkgFile.exists())) {\n problems.push('package.json: missing');\n return problems;\n }\n const pkg = (await pkgFile.json()) as PackageJson;\n const declaredPath = pkg.patchedDependencies?.[key];\n if (declaredPath === undefined) {\n problems.push(\n `package.json: patchedDependencies has no entry for \"${key}\" — this stack has no ` +\n 'duplicate-resource-id protection at all; see @homeflare/alchemy/docs/duplicate-resource-id.md',\n );\n return problems;\n }\n\n const theirs = Bun.file(`${projectDir}/${declaredPath}`);\n if (!(await theirs.exists())) {\n problems.push(`package.json declares \"${key}\" at ${declaredPath}, but that file is missing`);\n return problems;\n }\n const ours = Bun.file(shippedPatchPath(targetVersion));\n if (!(await ours.exists())) {\n // The kit's own tarball is missing its patch — a packaging bug, not the consumer's.\n problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);\n return problems;\n }\n if ((await theirs.text()) !== (await ours.text())) {\n problems.push(\n `${declaredPath}: differs from @homeflare/alchemy's shipped patch — re-copy it, ` +\n 'or say why it differs',\n );\n }\n\n // Declared and byte-identical is not yet APPLIED: a hand-edited patchedDependencies entry,\n // or an install under a package manager that does not support bun's patch format, leaves\n // node_modules unpatched with nothing above noticing. One cheap textual proof it landed.\n const resourceSrc = Bun.file(`${projectDir}/node_modules/alchemy/src/Resource.ts`);\n const resourceLib = Bun.file(`${projectDir}/node_modules/alchemy/lib/Resource.js`);\n const marker = 'DuplicateResourceIdError';\n const [srcText, libText] = await Promise.all([\n resourceSrc.exists().then((ok) => (ok ? resourceSrc.text() : '')),\n resourceLib.exists().then((ok) => (ok ? resourceLib.text() : '')),\n ]);\n if (!srcText.includes(marker) && !libText.includes(marker)) {\n problems.push(\n 'node_modules/alchemy/{src/Resource.ts,lib/Resource.js}: neither contains the patch — ' +\n 'declared in package.json but not applied; re-run `bun install`',\n );\n }\n\n return problems;\n};\n"
7
7
  ],
8
- "mappings": ";AA0BA;AAAA;AAeO,MAAM,iCAAsC,iBAAY,0BAA0B,EAOtF;AAAC;AAMG,IAAM,0BAAkC;;AC3B/C,IAAM,WAAW,CAAC,YAAoB,WAAW;AAGjD,IAAM,mBAAmB,CAAC,YACxB,IAAI,IAAI,iBAAiB,SAAS,OAAO,WAAW,YAAY,GAAG,EAAE;AAahE,IAAM,oBAAoB,OAC/B,YACA,iBAAyB,4BACY;AAAA,EACrC,MAAM,WAA2B,CAAC;AAAA,EAClC,MAAM,MAAM,SAAS,cAAc;AAAA,EAEnC,MAAM,eAAe,IAAI,KAAK,GAAG,8CAA8C;AAAA,EAC/E,IAAI,CAAE,MAAM,aAAa,OAAO,GAAI;AAAA,IAClC,SAAS,KAAK,0EAA0E;AAAA,IACxF,OAAO;AAAA,EACT;AAAA,EACA,MAAM,oBAAqB,MAAM,aAAa,KAAK,GAA4B;AAAA,EAC/E,IAAI,qBAAqB,gBAAgB;AAAA,IACvC,SAAS,KACP,2BAA2B,oBAAoB,8CAC7C,iBAAiB,+DACjB,4EACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,IAAI,KAAK,GAAG,yBAAyB;AAAA,EACrD,IAAI,CAAE,MAAM,QAAQ,OAAO,GAAI;AAAA,IAC7B,SAAS,KAAK,uBAAuB;AAAA,IACrC,OAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAO,MAAM,QAAQ,KAAK;AAAA,EAChC,MAAM,eAAe,IAAI,sBAAsB;AAAA,EAC/C,IAAI,iBAAiB,WAAW;AAAA,IAC9B,SAAS,KACP,uDAAuD,8BACrD,+FACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,IAAI,KAAK,GAAG,cAAc,cAAc;AAAA,EACvD,IAAI,CAAE,MAAM,OAAO,OAAO,GAAI;AAAA,IAC5B,SAAS,KAAK,0BAA0B,WAAW,wCAAwC;AAAA,IAC3F,OAAO;AAAA,EACT;AAAA,EACA,MAAM,OAAO,IAAI,KAAK,iBAAiB,cAAc,CAAC;AAAA,EACtD,IAAI,CAAE,MAAM,KAAK,OAAO,GAAI;AAAA,IAE1B,SAAS,KAAK,uCAAuC,8BAA8B;AAAA,IACnF,OAAO;AAAA,EACT;AAAA,EACA,IAAK,MAAM,OAAO,KAAK,MAAQ,MAAM,KAAK,KAAK,GAAI;AAAA,IACjD,SAAS,KACP,GAAG,iFACD,uBACJ;AAAA,EACF;AAAA,EAKA,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,SAAS;AAAA,EACf,OAAO,SAAS,WAAW,MAAM,QAAQ,IAAI;AAAA,IAC3C,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,IAChE,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,EAClE,CAAC;AAAA,EACD,IAAI,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,SAAS,MAAM,GAAG;AAAA,IAC1D,SAAS,KACP,0FACE,gEACJ;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;",
9
- "debugId": "BFEA50A1CBA14B6A64756E2164756E21",
8
+ "mappings": ";AAmCA;AAAA;AAeO,MAAM,iCAAsC,iBAAY,0BAA0B,EAOtF;AAAC;AAaG,IAAM,0BAAkC;AASxC,IAAM,2BAA8C,CAAC,iBAAiB,eAAe;;ACpD5F,IAAM,WAAW,CAAC,YAAoB,WAAW;AAGjD,IAAM,mBAAmB,CAAC,YACxB,IAAI,IAAI,iBAAiB,SAAS,OAAO,WAAW,YAAY,GAAG,EAAE;AAgBhE,IAAM,oBAAoB,OAC/B,YACA,mBACqC;AAAA,EACrC,MAAM,WAA2B,CAAC;AAAA,EAElC,MAAM,eAAe,IAAI,KAAK,GAAG,8CAA8C;AAAA,EAC/E,IAAI,CAAE,MAAM,aAAa,OAAO,GAAI;AAAA,IAClC,SAAS,KAAK,0EAA0E;AAAA,IACxF,OAAO;AAAA,EACT;AAAA,EACA,MAAM,oBAAqB,MAAM,aAAa,KAAK,GAA4B;AAAA,EAI/E,MAAM,gBAAgB,kBAAkB,oBAAoB;AAAA,EAC5D,MAAM,MAAM,SAAS,aAAa;AAAA,EAElC,IAAI,CAAC,yBAAyB,SAAS,aAAa,GAAG;AAAA,IACrD,SAAS,KAGP,WAAW,6BAA6B,oBAAoB,6BAC1D,4BACA,iEACA,GAAG,yBAAyB,KAAK,IAAI,wBAAwB,eAC7D,wFACA,mBACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EACA,IAAI,qBAAqB,eAAe;AAAA,IACtC,SAAS,KACP,2BAA2B,oBAAoB,8CAC7C,uBAAuB,oEACvB,gEACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,UAAU,IAAI,KAAK,GAAG,yBAAyB;AAAA,EACrD,IAAI,CAAE,MAAM,QAAQ,OAAO,GAAI;AAAA,IAC7B,SAAS,KAAK,uBAAuB;AAAA,IACrC,OAAO;AAAA,EACT;AAAA,EACA,MAAM,MAAO,MAAM,QAAQ,KAAK;AAAA,EAChC,MAAM,eAAe,IAAI,sBAAsB;AAAA,EAC/C,IAAI,iBAAiB,WAAW;AAAA,IAC9B,SAAS,KACP,uDAAuD,8BACrD,+FACJ;AAAA,IACA,OAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAS,IAAI,KAAK,GAAG,cAAc,cAAc;AAAA,EACvD,IAAI,CAAE,MAAM,OAAO,OAAO,GAAI;AAAA,IAC5B,SAAS,KAAK,0BAA0B,WAAW,wCAAwC;AAAA,IAC3F,OAAO;AAAA,EACT;AAAA,EACA,MAAM,OAAO,IAAI,KAAK,iBAAiB,aAAa,CAAC;AAAA,EACrD,IAAI,CAAE,MAAM,KAAK,OAAO,GAAI;AAAA,IAE1B,SAAS,KAAK,uCAAuC,8BAA8B;AAAA,IACnF,OAAO;AAAA,EACT;AAAA,EACA,IAAK,MAAM,OAAO,KAAK,MAAQ,MAAM,KAAK,KAAK,GAAI;AAAA,IACjD,SAAS,KACP,GAAG,iFACD,uBACJ;AAAA,EACF;AAAA,EAKA,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,cAAc,IAAI,KAAK,GAAG,iDAAiD;AAAA,EACjF,MAAM,SAAS;AAAA,EACf,OAAO,SAAS,WAAW,MAAM,QAAQ,IAAI;AAAA,IAC3C,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,IAChE,YAAY,OAAO,EAAE,KAAK,CAAC,OAAQ,KAAK,YAAY,KAAK,IAAI,EAAG;AAAA,EAClE,CAAC;AAAA,EACD,IAAI,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,QAAQ,SAAS,MAAM,GAAG;AAAA,IAC1D,SAAS,KACP,0FACE,gEACJ;AAAA,EACF;AAAA,EAEA,OAAO;AAAA;",
9
+ "debugId": "461D34CD68FB220064756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -3,9 +3,12 @@ export type PatchProblem = string;
3
3
  /**
4
4
  * @param projectDir The consuming stack's OWN repo root (where its `package.json` and
5
5
  * `patches/` directory live) — never this kit's own.
6
- * @param alchemyVersion The version to check for — defaults to the version this package's
7
- * own shipped patch targets ({@link PATCHED_ALCHEMY_VERSION}); a caller only overrides it to
8
- * test a different pin.
6
+ * @param alchemyVersion The version to check for. Omitted (the normal case): the version
7
+ * actually installed at `projectDir` is used, so a consumer on ANY version this package
8
+ * ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin
9
+ * {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the
10
+ * beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly
11
+ * only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).
9
12
  */
10
13
  export declare const checkAlchemyPatch: (projectDir: string, alchemyVersion?: string) => Promise<readonly PatchProblem[]>;
11
14
  //# sourceMappingURL=patch-check.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"patch-check.d.ts","sourceRoot":"","sources":["../../src/scope/patch-check.ts"],"names":[],"mappings":"AAwBA,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAYlC;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,eAChB,MAAM,mBACF,MAAM,KACrB,OAAO,CAAC,SAAS,YAAY,EAAE,CAsEjC,CAAC"}
1
+ {"version":3,"file":"patch-check.d.ts","sourceRoot":"","sources":["../../src/scope/patch-check.ts"],"names":[],"mappings":"AAwBA,iFAAiF;AACjF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAYlC;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,eAChB,MAAM,mBACD,MAAM,KACtB,OAAO,CAAC,SAAS,YAAY,EAAE,CAwFjC,CAAC"}
@@ -66,6 +66,29 @@ It touches three files so every consumer resolution path is covered: `src/Resour
66
66
  registering the same collision under `bun test` and under plain `node` both die with the
67
67
  identical message.
68
68
 
69
+ ### Supported versions
70
+
71
+ `patchedDependencies` keys to an EXACT version string, and `bun`'s patch format is not
72
+ guaranteed to apply across versions even when the surrounding file is otherwise unchanged — so
73
+ each alchemy version this covers gets its OWN cut, byte-diffed against that version's real
74
+ tarball, not a renamed copy:
75
+
76
+ | alchemy version | patch file |
77
+ | --------------- | ------------------------------------- |
78
+ | `2.0.0-beta.81` | `patches/alchemy@2.0.0-beta.81.patch` |
79
+ | `2.0.0-beta.83` | `patches/alchemy@2.0.0-beta.83.patch` |
80
+
81
+ Between beta.81 and beta.83, upstream's only change touching these three files was an
82
+ unrelated, cosmetic rewrite of `isResource` (a function body collapsed to an arrow
83
+ expression) earlier in `src/Resource.ts` / `lib/Resource.js` — `lib/Resource.d.ts` is
84
+ byte-identical between the two. The patched spot itself (the `TODO(sam)` branch and its
85
+ surrounding context) is unchanged content, shifted by the line count that earlier rewrite
86
+ added or removed. `PATCHED_ALCHEMY_VERSIONS` (`@homeflare/alchemy/scope`) is the authoritative
87
+ list; `checkAlchemyPatch` walks it to find the entry matching what is actually installed.
88
+ A version not in that list — beta.82 included — has no patch cut for it; adopt the nearest
89
+ supported version's workflow below and open an issue, rather than assuming an untested patch
90
+ applies.
91
+
69
92
  **Only `Type` is compared, never `Props`.** Comparing props as well would be wrong: a
70
93
  platform-typed resource's tagged pattern (`alchemy`'s own `Platform.ts` ~581) rewrites `Props`
71
94
  on a resource already registered, and a bare-tag forward reference legitimately registers with
@@ -74,15 +97,18 @@ first `yield*`; `Props` is not.
74
97
 
75
98
  ## Adopting it in a consuming stack
76
99
 
100
+ Pick the file matching the stack's OWN installed `alchemy` version from the table above —
101
+ here shown for beta.83; beta.81 is identical with the version string swapped:
102
+
77
103
  ```sh
78
- cp node_modules/@homeflare/alchemy/patches/alchemy@2.0.0-beta.81.patch patches/
104
+ cp node_modules/@homeflare/alchemy/patches/alchemy@2.0.0-beta.83.patch patches/
79
105
  ```
80
106
 
81
107
  then add, in the stack's own root `package.json`:
82
108
 
83
109
  ```json
84
110
  "patchedDependencies": {
85
- "alchemy@2.0.0-beta.81": "patches/alchemy@2.0.0-beta.81.patch"
111
+ "alchemy@2.0.0-beta.83": "patches/alchemy@2.0.0-beta.83.patch"
86
112
  }
87
113
  ```
88
114
 
@@ -107,11 +133,18 @@ if (problems.length > 0) {
107
133
  }
108
134
  ```
109
135
 
136
+ With no second argument, it checks against whatever `alchemy` version is ACTUALLY installed
137
+ at that stack's root — a stack on beta.83 is checked against the beta.83 patch, one on beta.81
138
+ against the beta.81 patch, neither has to pin `PATCHED_ALCHEMY_VERSION` (which stays beta.81,
139
+ for the consumer that already reads it directly) to pass. Pass a version explicitly only to
140
+ assert a SPECIFIC pin regardless of what is installed.
141
+
110
142
  It is a static check — three file reads, no `effect` runtime spun up — covering: alchemy
111
- installed at the wrong version for the patch, no `patchedDependencies` entry at all, the
112
- referenced file missing or differing from this package's own shipped copy, and (the one failure
113
- mode none of those other checks would catch) declared and byte-identical but never actually
114
- applied.
143
+ installed at a version `@homeflare/alchemy` has no shipped patch for at all (`PATCHED_ALCHEMY_VERSIONS`
144
+ names every version it DOES cover), an explicit ask that disagrees with what is installed, no
145
+ `patchedDependencies` entry at all, the referenced file missing or differing from this
146
+ package's own shipped copy, and (the one failure mode none of those other checks would catch)
147
+ declared and byte-identical but never actually applied.
115
148
 
116
149
  ## Upstream
117
150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homeflare/alchemy",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
5
5
  "homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
6
6
  "bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
@@ -0,0 +1,149 @@
1
+ diff --git a/lib/Resource.d.ts b/lib/Resource.d.ts
2
+ index 07b92ae..bfd1a13 100644
3
+ --- a/lib/Resource.d.ts
4
+ +++ b/lib/Resource.d.ts
5
+ @@ -228,6 +228,24 @@ export declare class MissingImplementationError extends MissingImplementationErr
6
+ id: string;
7
+ }> {
8
+ }
9
+ +declare const DuplicateResourceIdError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
10
+ + readonly _tag: "DuplicateResourceIdError";
11
+ +} & Readonly<A>;
12
+ +/**
13
+ + * HOMEFLARE PATCH (homeflare-kit PR superseding #468; re-cut for alchemy@2.0.0-beta.83) — see
14
+ + * Resource.ts for the full comment.
15
+ + * A second resource of a DIFFERENT type registered at a logical id an earlier resource
16
+ + * already claimed in the same scope (namespace + id, i.e. the same FQN).
17
+ + */
18
+ +export declare class DuplicateResourceIdError extends DuplicateResourceIdError_base<{
19
+ + message: string;
20
+ + fqn: string;
21
+ + /** The type that first claimed `fqn`. */
22
+ + firstType: string;
23
+ + /** The different type that tried to claim it again. */
24
+ + secondType: string;
25
+ +}> {
26
+ +}
27
+ export declare const missingImplementation: (type: string, id: string) => MissingImplementationError;
28
+ /**
29
+ * Creates a resource constructor for a concrete resource type.
30
+ diff --git a/lib/Resource.js b/lib/Resource.js
31
+ index cbc4f09..b57d530 100644
32
+ --- a/lib/Resource.js
33
+ +++ b/lib/Resource.js
34
+ @@ -4,7 +4,7 @@ import * as Effectable from "effect/Effectable";
35
+ import * as Layer from "effect/Layer";
36
+ import * as Option from "effect/Option";
37
+ import { AdoptPolicy } from "./AdoptPolicy.js";
38
+ -import { toFqn } from "./FQN.js";
39
+ +import { toFqn, toPath } from "./FQN.js";
40
+ import { CurrentNamespace } from "./Namespace.js";
41
+ import * as Output from "./Output.js";
42
+ import { Provider } from "./Provider.js";
43
+ @@ -58,6 +58,14 @@ export const isResourceOfType = (value, type) => (typeof value === "object" || t
44
+ */
45
+ export class MissingImplementationError extends Data.TaggedError("MissingImplementationError") {
46
+ }
47
+ +/**
48
+ + * HOMEFLARE PATCH (homeflare-kit PR superseding #468; re-cut for alchemy@2.0.0-beta.83) — see
49
+ + * Resource.ts for the full comment.
50
+ + * A second resource of a DIFFERENT type registered at a logical id an earlier resource
51
+ + * already claimed in the same scope (namespace + id, i.e. the same FQN).
52
+ + */
53
+ +export class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError") {
54
+ +}
55
+ export const missingImplementation = (type, id) => new MissingImplementationError({
56
+ type,
57
+ id,
58
+ @@ -126,7 +134,17 @@ export function Resource(type, options) {
59
+ conflictingMode: ambientMode,
60
+ }));
61
+ }
62
+ - // // TODO(sam): check if props are different and die
63
+ + // HOMEFLARE PATCH: Type, not Props — see Resource.ts for why.
64
+ + if (existing.Type !== type) {
65
+ + return yield* Effect.die(new DuplicateResourceIdError({
66
+ + message: `REFUSING: duplicate resource id "${fqn}" in scope ` +
67
+ + `${toPath(namespace).join("/") || "(root)"}: first declared by ` +
68
+ + `${existing.Type}, again by ${type}`,
69
+ + fqn,
70
+ + firstType: existing.Type,
71
+ + secondType: type,
72
+ + }));
73
+ + }
74
+ return existing;
75
+ }
76
+ const bind = (...args) => typeof args[0] === "string"
77
+ diff --git a/src/Resource.ts b/src/Resource.ts
78
+ index c4d0a36..29e5ca7 100644
79
+ --- a/src/Resource.ts
80
+ +++ b/src/Resource.ts
81
+ @@ -5,7 +5,7 @@ import * as Layer from "effect/Layer";
82
+ import * as Option from "effect/Option";
83
+ import type { Pipeable } from "effect/Pipeable";
84
+ import { AdoptPolicy } from "./AdoptPolicy.ts";
85
+ -import { toFqn } from "./FQN.ts";
86
+ +import { toFqn, toPath } from "./FQN.ts";
87
+ import type { Input, InputProps, PropsInput } from "./Input.ts";
88
+ import { CurrentNamespace, type NamespaceNode } from "./Namespace.ts";
89
+ import * as Output from "./Output.ts";
90
+ @@ -292,6 +292,30 @@ export class MissingImplementationError extends Data.TaggedError("MissingImpleme
91
+ id: string;
92
+ }> {}
93
+
94
+ +/**
95
+ + * A second resource of a DIFFERENT type registered (`yield*`ed) at a logical id an earlier
96
+ + * resource already claimed in the same scope (namespace + id, i.e. the same FQN).
97
+ + *
98
+ + * ★ HOMEFLARE PATCH (homeflare-kit PR superseding #468, 2026-10-19; re-cut for
99
+ + * alchemy@2.0.0-beta.83). Upstream's own idempotent-registration branch below used to
100
+ + * return the FIRST-registered resource for ANY second `yield*` at an already-claimed FQN —
101
+ + * correct for "the same resource read from two call sites," but it never compared `Type`,
102
+ + * so a second, DIFFERENT resource that merely picked the same id silently lost: its props
103
+ + * were never written to `stack.resources`, so neither `Plan.make` nor any later inspection
104
+ + * could tell two different resources ever registered. Measured: `NixJobRetirement` and
105
+ + * `Caddy.Config` both declaring id `caddy` in one stack planned `[caddy] noop` and dropped
106
+ + * the retirement entirely. This error, and the `Type` check below, close that gap. Only
107
+ + * `Type` is compared, not `Props` — see the check's own comment for why.
108
+ + */
109
+ +export class DuplicateResourceIdError extends Data.TaggedError("DuplicateResourceIdError")<{
110
+ + message: string;
111
+ + fqn: string;
112
+ + /** The type that first claimed `fqn`. */
113
+ + firstType: string;
114
+ + /** The different type that tried to claim it again. */
115
+ + secondType: string;
116
+ +}> {}
117
+ +
118
+ export const missingImplementation = (type: string, id: string) =>
119
+ new MissingImplementationError({
120
+ type,
121
+ @@ -372,7 +396,27 @@ export function Resource<R extends ResourceLike>(
122
+ }),
123
+ );
124
+ }
125
+ - // // TODO(sam): check if props are different and die
126
+ + // HOMEFLARE PATCH: the TODO above asked whether PROPS differ; this checks TYPE
127
+ + // instead, which is the question that actually matters. Comparing props would be
128
+ + // wrong — `Platform.ts`'s own tagged resources rewrite Props after first registration
129
+ + // (Platform.ts ~581), and a bare-tag forward reference legitimately registers with
130
+ + // `Props: undefined` until its `.make(...)` Layer builds (MissingImplementationError's
131
+ + // own doc comment above). Type is stable from the resource's very first `yield*` and is
132
+ + // exactly what distinguishes "the same resource, referenced twice" from "two different
133
+ + // resources that picked the same id."
134
+ + if (existing.Type !== type) {
135
+ + return yield* Effect.die(
136
+ + new DuplicateResourceIdError({
137
+ + message:
138
+ + `REFUSING: duplicate resource id "${fqn}" in scope ` +
139
+ + `${toPath(namespace).join("/") || "(root)"}: first declared by ` +
140
+ + `${existing.Type}, again by ${type}`,
141
+ + fqn,
142
+ + firstType: existing.Type,
143
+ + secondType: type,
144
+ + }),
145
+ + );
146
+ + }
147
+ return existing;
148
+ }
149
+ const bind = (
@@ -16,13 +16,22 @@
16
16
  * package exports, without needing to know alchemy carries its own copy of the same tag.
17
17
  * Effect dispatches `catchTag` on the `_tag` STRING, not `instanceof`, so either class
18
18
  * works identically as a catch target.
19
- * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version the patch was cut against.
19
+ * 2. `PATCHED_ALCHEMY_VERSION` — the exact alchemy version THIS package's own peer pin and
20
+ * devDependency sit at, and the one `duplicate-id.test.ts` drives its real `Stack` against.
20
21
  * Hardcoded, like `ESTATE_VERSIONS` in `@homeflare/config/versions` (the same reasoning:
21
22
  * a second source of truth derived by import would cross `tsconfig.build.json`'s
22
23
  * `rootDir: "src"` boundary to reach `package.json`). `duplicate-id.test.ts` asserts it
23
24
  * still matches this package's own `peerDependencies.alchemy`, so a pin bump that forgets
24
25
  * to re-cut the patch fails a test here instead of silently shipping a patch filename that
25
26
  * no longer matches what consumers install.
27
+ * 3. `PATCHED_ALCHEMY_VERSIONS` — every alchemy version this package ships a CUT patch for
28
+ * (currently beta.81 and beta.83, kit PR shipping beta.83's — a consumer on beta.82 or
29
+ * beta.84 is NOT covered merely by being "close"; `bun`'s patch format keys to an exact
30
+ * version string, and a patch cut against one version is not guaranteed to even apply to
31
+ * another — see docs/duplicate-resource-id.md for why a diff was taken between the two
32
+ * tarballs rather than assumed). `checkAlchemyPatch` (`patch-check.ts`) walks this list to
33
+ * find the shipped patch matching a consumer's ACTUAL install, so a stack on beta.83 is not
34
+ * forced to pin the single oldest supported version just to pass its own check.
26
35
  */
27
36
  import * as Data from 'effect/Data';
28
37
 
@@ -49,7 +58,23 @@ export class DuplicateResourceIdError extends Data.TaggedError('DuplicateResourc
49
58
  }> {}
50
59
 
51
60
  /**
52
- * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against.
61
+ * The exact `alchemy` version `patches/alchemy@2.0.0-beta.81.patch` was cut against — this
62
+ * package's own peer pin, and the version `duplicate-id.test.ts` drives a real `Stack` over.
53
63
  * `duplicate-id.test.ts` keeps this honest against `package.json`'s own `peerDependencies`.
64
+ *
65
+ * ⛔ STAYS, MEANING BETA.81 — do not repoint this at beta.83. A consumer that calls
66
+ * `checkAlchemyPatch` with an explicit `alchemyVersion` (or reads this constant directly)
67
+ * must keep getting the same answer it always has; {@link PATCHED_ALCHEMY_VERSIONS} is the
68
+ * list a caller walks to find ALL supported pins, this one constant is not retargeted to mean
69
+ * "whichever is newest."
54
70
  */
55
71
  export const PATCHED_ALCHEMY_VERSION: string = '2.0.0-beta.81';
72
+
73
+ /**
74
+ * Every alchemy version `@homeflare/alchemy` ships a cut `patches/alchemy@<version>.patch`
75
+ * for, oldest first. `checkAlchemyPatch` uses this to pick the shipped patch matching a
76
+ * consumer's ACTUAL installed version instead of only ever checking against
77
+ * {@link PATCHED_ALCHEMY_VERSION}. `patch-check.test.ts` and the static patch-coverage test
78
+ * both assert every entry here has a real `patches/alchemy@<version>.patch` file.
79
+ */
80
+ export const PATCHED_ALCHEMY_VERSIONS: readonly string[] = ['2.0.0-beta.81', '2.0.0-beta.83'];
@@ -2,8 +2,9 @@
2
2
  * `@homeflare/alchemy/scope` — a consuming stack's own check that it carries the
3
3
  * duplicate-resource-id patch, and the error shape that patch throws.
4
4
  *
5
- * The fix itself is a patch to `alchemy`'s own `Resource.ts`
6
- * (`../../patches/alchemy@2.0.0-beta.81.patch`), applied at EACH consuming stack's own root
5
+ * The fix itself is a patch to `alchemy`'s own `Resource.ts`, one per supported alchemy version
6
+ * (`../../patches/alchemy@<version>.patch`, listed in `PATCHED_ALCHEMY_VERSIONS`: beta.81 and
7
+ * beta.83), applied at EACH consuming stack's own root
7
8
  * (`alchemy` is a peer dependency here, so `patchedDependencies` in this package's manifest
8
9
  * only reaches this package's own install, never a consumer's). docs/duplicate-resource-id.md
9
10
  * has the full guide, the exact `bun patch` workflow, and what this does and does not cover.
@@ -18,5 +19,9 @@
18
19
  * }
19
20
  * ```
20
21
  */
21
- export { DuplicateResourceIdError, PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
22
+ export {
23
+ DuplicateResourceIdError,
24
+ PATCHED_ALCHEMY_VERSION,
25
+ PATCHED_ALCHEMY_VERSIONS,
26
+ } from './duplicate-id.ts';
22
27
  export { checkAlchemyPatch, type PatchProblem } from './patch-check.ts';
@@ -2,18 +2,27 @@
2
2
  * `checkAlchemyPatch` against a fixture consumer directory — every failure mode a consuming
3
3
  * stack's own `check` should catch, built one `mkdtemp` at a time rather than against this
4
4
  * repo's own tree, so each case controls exactly what is "installed".
5
+ *
6
+ * ★ TWO SUPPORTED VERSIONS, NOT ONE. `@homeflare/alchemy` ships a cut patch for both
7
+ * `2.0.0-beta.81` ({@link PATCHED_ALCHEMY_VERSION}) and `2.0.0-beta.83` — a consumer on
8
+ * either passes `checkAlchemyPatch` without pinning a specific version; one on neither is
9
+ * the "unlisted version" case below.
5
10
  */
6
11
  import { mkdtempSync, rmSync } from 'node:fs';
7
12
  import { mkdir, writeFile } from 'node:fs/promises';
8
13
  import { tmpdir } from 'node:os';
9
14
  import { join } from 'node:path';
10
15
  import { afterEach, describe, expect, test } from 'bun:test';
11
- import { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
16
+ import { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
12
17
  import { checkAlchemyPatch } from './patch-check.ts';
13
18
 
14
- const OUR_PATCH = await Bun.file(
15
- new URL(`../../patches/alchemy@${PATCHED_ALCHEMY_VERSION}.patch`, import.meta.url),
16
- ).text();
19
+ const BETA_83 = '2.0.0-beta.83';
20
+
21
+ const shippedPatch = (version: string) =>
22
+ Bun.file(new URL(`../../patches/alchemy@${version}.patch`, import.meta.url)).text();
23
+
24
+ const OUR_PATCH_81 = await shippedPatch(PATCHED_ALCHEMY_VERSION);
25
+ const OUR_PATCH_83 = await shippedPatch(BETA_83);
17
26
 
18
27
  let dir: string | undefined;
19
28
  afterEach(() => {
@@ -44,19 +53,31 @@ const installAlchemy = async (
44
53
  await writeFile(join(alchemyDir, 'lib', 'Resource.js'), marker);
45
54
  };
46
55
 
56
+ /** Declares `patchedDependencies` for `version`, pointing at a copy of `patchContent`. */
57
+ const declarePatch = async (root: string, version: string, patchContent: string) => {
58
+ await mkdir(join(root, 'patches'), { recursive: true });
59
+ await writeFile(join(root, 'patches', 'alchemy.patch'), patchContent);
60
+ await writePkg(root, {
61
+ patchedDependencies: { [`alchemy@${version}`]: 'patches/alchemy.patch' },
62
+ });
63
+ };
64
+
47
65
  describe('checkAlchemyPatch', () => {
48
66
  test('alchemy not installed at all', async () => {
49
67
  const root = scratch();
50
68
  expect(await checkAlchemyPatch(root)).toEqual([expect.stringContaining('not installed')]);
51
69
  });
52
70
 
53
- test('installed alchemy is a different version than the patch targets', async () => {
71
+ test('installed alchemy is a version this package has no shipped patch for', async () => {
54
72
  const root = scratch();
55
73
  await installAlchemy(root, { version: '2.0.0-beta.999' });
56
74
  const problems = await checkAlchemyPatch(root);
57
75
  expect(problems).toHaveLength(1);
58
76
  expect(problems[0]).toContain('2.0.0-beta.999');
59
- expect(problems[0]).toContain(PATCHED_ALCHEMY_VERSION);
77
+ // Every supported version is named, so the caller knows what to re-cut against.
78
+ for (const supported of PATCHED_ALCHEMY_VERSIONS) {
79
+ expect(problems[0]).toContain(supported);
80
+ }
60
81
  });
61
82
 
62
83
  test('right version installed, but package.json has no patchedDependencies entry', async () => {
@@ -80,11 +101,7 @@ describe('checkAlchemyPatch', () => {
80
101
  test('the referenced patch file differs from what @homeflare/alchemy ships', async () => {
81
102
  const root = scratch();
82
103
  await installAlchemy(root);
83
- await mkdir(join(root, 'patches'), { recursive: true });
84
- await writeFile(join(root, 'patches', 'alchemy.patch'), 'not the real patch');
85
- await writePkg(root, {
86
- patchedDependencies: { [`alchemy@${PATCHED_ALCHEMY_VERSION}`]: 'patches/alchemy.patch' },
87
- });
104
+ await declarePatch(root, PATCHED_ALCHEMY_VERSION, 'not the real patch');
88
105
  const problems = await checkAlchemyPatch(root);
89
106
  expect(problems).toEqual([expect.stringContaining('differs')]);
90
107
  });
@@ -92,23 +109,34 @@ describe('checkAlchemyPatch', () => {
92
109
  test('declared and byte-identical, but never actually applied', async () => {
93
110
  const root = scratch();
94
111
  await installAlchemy(root, { applied: false });
95
- await mkdir(join(root, 'patches'), { recursive: true });
96
- await writeFile(join(root, 'patches', 'alchemy.patch'), OUR_PATCH);
97
- await writePkg(root, {
98
- patchedDependencies: { [`alchemy@${PATCHED_ALCHEMY_VERSION}`]: 'patches/alchemy.patch' },
99
- });
112
+ await declarePatch(root, PATCHED_ALCHEMY_VERSION, OUR_PATCH_81);
100
113
  const problems = await checkAlchemyPatch(root);
101
114
  expect(problems).toEqual([expect.stringContaining('not applied')]);
102
115
  });
103
116
 
104
- test('declared, byte-identical, and applied: no problems', async () => {
117
+ test('beta.81, declared, byte-identical, and applied: no problems', async () => {
105
118
  const root = scratch();
106
119
  await installAlchemy(root);
107
- await mkdir(join(root, 'patches'), { recursive: true });
108
- await writeFile(join(root, 'patches', 'alchemy.patch'), OUR_PATCH);
109
- await writePkg(root, {
110
- patchedDependencies: { [`alchemy@${PATCHED_ALCHEMY_VERSION}`]: 'patches/alchemy.patch' },
111
- });
120
+ await declarePatch(root, PATCHED_ALCHEMY_VERSION, OUR_PATCH_81);
121
+ expect(await checkAlchemyPatch(root)).toEqual([]);
122
+ });
123
+
124
+ test('beta.83, declared, byte-identical, and applied: no problems, no version argument needed', async () => {
125
+ const root = scratch();
126
+ await installAlchemy(root, { version: BETA_83 });
127
+ await declarePatch(root, BETA_83, OUR_PATCH_83);
128
+ // No explicit alchemyVersion — a beta.83 consumer must pass by what is ACTUALLY
129
+ // installed, not by also being pinned to PATCHED_ALCHEMY_VERSION (beta.81).
112
130
  expect(await checkAlchemyPatch(root)).toEqual([]);
113
131
  });
132
+
133
+ test('an explicit alchemyVersion that does not match what is installed is still a problem', async () => {
134
+ const root = scratch();
135
+ await installAlchemy(root, { version: BETA_83 });
136
+ await declarePatch(root, BETA_83, OUR_PATCH_83);
137
+ const problems = await checkAlchemyPatch(root, PATCHED_ALCHEMY_VERSION);
138
+ expect(problems).toHaveLength(1);
139
+ expect(problems[0]).toContain(BETA_83);
140
+ expect(problems[0]).toContain(PATCHED_ALCHEMY_VERSION);
141
+ });
114
142
  });
@@ -20,7 +20,7 @@
20
20
  * ⛔ IT REPORTS, IT DOES NOT REPAIR, same as `checkProject` in `@homeflare/config/check` — this
21
21
  * never edits `package.json` or re-runs `bun install` for the caller.
22
22
  */
23
- import { PATCHED_ALCHEMY_VERSION } from './duplicate-id.ts';
23
+ import { PATCHED_ALCHEMY_VERSION, PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
24
24
 
25
25
  /** One thing wrong with a consuming stack's alchemy patch, in the imperative. */
26
26
  export type PatchProblem = string;
@@ -38,16 +38,18 @@ type PackageJson = {
38
38
  /**
39
39
  * @param projectDir The consuming stack's OWN repo root (where its `package.json` and
40
40
  * `patches/` directory live) — never this kit's own.
41
- * @param alchemyVersion The version to check for — defaults to the version this package's
42
- * own shipped patch targets ({@link PATCHED_ALCHEMY_VERSION}); a caller only overrides it to
43
- * test a different pin.
41
+ * @param alchemyVersion The version to check for. Omitted (the normal case): the version
42
+ * actually installed at `projectDir` is used, so a consumer on ANY version this package
43
+ * ships a patch for ({@link PATCHED_ALCHEMY_VERSIONS}) passes without having to pin
44
+ * {@link PATCHED_ALCHEMY_VERSION} specifically — a stack on beta.83 is checked against the
45
+ * beta.83 patch, one on beta.81 against the beta.81 patch. A caller passes this explicitly
46
+ * only to assert a SPECIFIC pin regardless of what happens to be installed (as the tests do).
44
47
  */
45
48
  export const checkAlchemyPatch = async (
46
49
  projectDir: string,
47
- alchemyVersion: string = PATCHED_ALCHEMY_VERSION,
50
+ alchemyVersion?: string,
48
51
  ): Promise<readonly PatchProblem[]> => {
49
52
  const problems: PatchProblem[] = [];
50
- const key = patchKey(alchemyVersion);
51
53
 
52
54
  const installedPkg = Bun.file(`${projectDir}/node_modules/alchemy/package.json`);
53
55
  if (!(await installedPkg.exists())) {
@@ -55,11 +57,30 @@ export const checkAlchemyPatch = async (
55
57
  return problems;
56
58
  }
57
59
  const installedVersion = ((await installedPkg.json()) as { version?: string }).version;
58
- if (installedVersion !== alchemyVersion) {
60
+
61
+ // No explicit ask: check against whatever is ACTUALLY installed, not a single hardcoded
62
+ // pin — that is what lets a beta.83 consumer pass without also being on beta.81.
63
+ const targetVersion = alchemyVersion ?? installedVersion ?? PATCHED_ALCHEMY_VERSION;
64
+ const key = patchKey(targetVersion);
65
+
66
+ if (!PATCHED_ALCHEMY_VERSIONS.includes(targetVersion)) {
67
+ problems.push(
68
+ // ★ Name the version being CHECKED: with an explicit `alchemyVersion` it can differ from
69
+ // the installed one, and naming the installed version then blames a supported one.
70
+ `alchemy ${targetVersion} (installed: ${installedVersion ?? 'unknown'}) is a version ` +
71
+ `@homeflare/alchemy has ` +
72
+ `no duplicate-resource-id patch for — supported versions are ` +
73
+ `${PATCHED_ALCHEMY_VERSIONS.join(', ')}; cut a new patches/${key}.patch ` +
74
+ "(@homeflare/alchemy's own patches/) before bumping alchemy, or this install has no " +
75
+ 'protection at all',
76
+ );
77
+ return problems;
78
+ }
79
+ if (installedVersion !== targetVersion) {
59
80
  problems.push(
60
81
  `node_modules/alchemy is ${installedVersion ?? 'unknown'}, but the duplicate-resource-id ` +
61
- `patch targets ${alchemyVersion} — bump the patch (@homeflare/alchemy's own ` +
62
- 'patches/) before bumping alchemy, or this install has no protection at all',
82
+ `check was asked for ${targetVersion} — pass the installed version explicitly, or omit ` +
83
+ 'alchemyVersion to check against whatever is actually installed',
63
84
  );
64
85
  return problems;
65
86
  }
@@ -84,7 +105,7 @@ export const checkAlchemyPatch = async (
84
105
  problems.push(`package.json declares "${key}" at ${declaredPath}, but that file is missing`);
85
106
  return problems;
86
107
  }
87
- const ours = Bun.file(shippedPatchPath(alchemyVersion));
108
+ const ours = Bun.file(shippedPatchPath(targetVersion));
88
109
  if (!(await ours.exists())) {
89
110
  // The kit's own tarball is missing its patch — a packaging bug, not the consumer's.
90
111
  problems.push(`@homeflare/alchemy ships no patches/${key}.patch to compare against`);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Static coverage: every version `PATCHED_ALCHEMY_VERSIONS` claims to support actually has a
3
+ * shipped `patches/alchemy@<version>.patch` carrying the `DuplicateResourceIdError` fix.
4
+ *
5
+ * ★ STATIC, NOT FUNCTIONAL — no network. Proving a patch APPLIES to the real upstream tarball
6
+ * needs `npm pack alchemy@<version>` pulled live and diffed, which this suite's own CI lane
7
+ * cannot assume has network access (and must not: patch-check.ts's own header explains why a
8
+ * consumer's `check` stays a static file read rather than a network-dependent probe). The
9
+ * patch for {@link PATCHED_ALCHEMY_VERSION} (beta.81) already has that proof the hard way —
10
+ * `duplicate-id.test.ts` drives a REAL patched `Stack` over the devDependency `alchemy`
11
+ * install, which only passes because `bun install`'s `patchedDependencies` actually applied
12
+ * it. This test is the one that would catch a version ADDED to the list here without a
13
+ * patch file ever being cut for it, or a patch file that silently regressed back to the
14
+ * upstream TODO.
15
+ */
16
+ import { describe, expect, test } from 'bun:test';
17
+ import { PATCHED_ALCHEMY_VERSIONS } from './duplicate-id.ts';
18
+
19
+ /** `test.each` wants a plain mutable array of a known element type, not `readonly string[]`. */
20
+ const versions: string[] = [...PATCHED_ALCHEMY_VERSIONS];
21
+
22
+ const patchFile = (version: string) =>
23
+ Bun.file(new URL(`../../patches/alchemy@${version}.patch`, import.meta.url));
24
+
25
+ describe('every supported alchemy version ships a cut patch', () => {
26
+ test.each(versions)('alchemy@%s has a patches/ file', async (version: string) => {
27
+ expect(await patchFile(version).exists()).toBe(true);
28
+ });
29
+
30
+ test.each(versions)(
31
+ 'alchemy@%s patch carries the DuplicateResourceIdError fix',
32
+ async (version: string) => {
33
+ const text = await patchFile(version).text();
34
+ expect(text).toContain('DuplicateResourceIdError');
35
+ // The TODO it replaces must be gone from the ADDED ("+") side of the diff — present
36
+ // only as part of a removed ("-") line, never re-introduced unpatched.
37
+ const addedLines = text.split('\n').filter((line) => line.startsWith('+'));
38
+ expect(addedLines.some((line) => line.includes('TODO(sam)'))).toBe(false);
39
+ },
40
+ );
41
+
42
+ test('touches the three files every consumer resolution path needs', async () => {
43
+ for (const version of PATCHED_ALCHEMY_VERSIONS) {
44
+ const text = await patchFile(version).text();
45
+ expect(text).toContain('a/src/Resource.ts');
46
+ expect(text).toContain('a/lib/Resource.js');
47
+ expect(text).toContain('a/lib/Resource.d.ts');
48
+ }
49
+ });
50
+ });