@homeflare/alchemy 0.8.0 → 0.10.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.
Files changed (240) hide show
  1. package/README.md +22 -11
  2. package/dist/caddy/config-lifecycle.d.ts +1 -1
  3. package/dist/caddy/config.d.ts.map +1 -1
  4. package/dist/caddy/index.js +221 -53
  5. package/dist/caddy/index.js.map +13 -9
  6. package/dist/launchd/fake-runner.d.ts +1 -0
  7. package/dist/launchd/fake-runner.d.ts.map +1 -1
  8. package/dist/launchd/fake-sudo.d.ts +1 -0
  9. package/dist/launchd/fake-sudo.d.ts.map +1 -1
  10. package/dist/launchd/host-effect.d.ts +9 -18
  11. package/dist/launchd/host-effect.d.ts.map +1 -1
  12. package/dist/launchd/host-file-lifecycle.d.ts +14 -1
  13. package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
  14. package/dist/launchd/host-file.d.ts +2 -1
  15. package/dist/launchd/host-file.d.ts.map +1 -1
  16. package/dist/launchd/index.js +345 -86
  17. package/dist/launchd/index.js.map +18 -12
  18. package/dist/launchd/job-lifecycle.d.ts +8 -1
  19. package/dist/launchd/job-lifecycle.d.ts.map +1 -1
  20. package/dist/launchd/job-preflight.d.ts +13 -1
  21. package/dist/launchd/job-preflight.d.ts.map +1 -1
  22. package/dist/launchd/job.d.ts +2 -1
  23. package/dist/launchd/job.d.ts.map +1 -1
  24. package/dist/launchd/runner.d.ts +7 -0
  25. package/dist/launchd/runner.d.ts.map +1 -1
  26. package/dist/launchd/sudo-acl.d.ts +25 -0
  27. package/dist/launchd/sudo-acl.d.ts.map +1 -0
  28. package/dist/launchd/sudo-guard.d.ts +31 -4
  29. package/dist/launchd/sudo-guard.d.ts.map +1 -1
  30. package/dist/launchd/sudo-runner.d.ts +3 -3
  31. package/dist/launchd/sudo-runner.d.ts.map +1 -1
  32. package/dist/launchd/sudo-said.d.ts +10 -0
  33. package/dist/launchd/sudo-said.d.ts.map +1 -0
  34. package/dist/openbao/auth-method-reconcile.d.ts +6 -2
  35. package/dist/openbao/auth-method-reconcile.d.ts.map +1 -1
  36. package/dist/openbao/auth-method.d.ts.map +1 -1
  37. package/dist/openbao/auth-role.d.ts.map +1 -1
  38. package/dist/openbao/cloudflare-role.d.ts.map +1 -1
  39. package/dist/openbao/fake-bao.d.ts +18 -0
  40. package/dist/openbao/fake-bao.d.ts.map +1 -1
  41. package/dist/openbao/fake-core.d.ts +39 -0
  42. package/dist/openbao/fake-core.d.ts.map +1 -0
  43. package/dist/openbao/fake-families.d.ts +6 -3
  44. package/dist/openbao/fake-families.d.ts.map +1 -1
  45. package/dist/openbao/fake-mounts.d.ts +16 -0
  46. package/dist/openbao/fake-mounts.d.ts.map +1 -0
  47. package/dist/openbao/fake-stack.d.ts +5 -1
  48. package/dist/openbao/fake-stack.d.ts.map +1 -1
  49. package/dist/openbao/index.js +541 -270
  50. package/dist/openbao/index.js.map +26 -21
  51. package/dist/openbao/jwt-config.d.ts.map +1 -1
  52. package/dist/openbao/jwt-role.d.ts.map +1 -1
  53. package/dist/openbao/kubernetes-role.d.ts.map +1 -1
  54. package/dist/openbao/mfa-enforcement.d.ts +6 -2
  55. package/dist/openbao/mfa-enforcement.d.ts.map +1 -1
  56. package/dist/openbao/mfa-totp.d.ts +6 -2
  57. package/dist/openbao/mfa-totp.d.ts.map +1 -1
  58. package/dist/openbao/mount-reconcile.d.ts +5 -1
  59. package/dist/openbao/mount-reconcile.d.ts.map +1 -1
  60. package/dist/openbao/mount.d.ts.map +1 -1
  61. package/dist/openbao/pki-role.d.ts.map +1 -1
  62. package/dist/openbao/plugin-reconcile.d.ts +6 -2
  63. package/dist/openbao/plugin-reconcile.d.ts.map +1 -1
  64. package/dist/openbao/plugin.d.ts.map +1 -1
  65. package/dist/openbao/policy.d.ts.map +1 -1
  66. package/dist/openbao/proxmox-role.d.ts.map +1 -1
  67. package/dist/openbao/role-reconcile.d.ts +11 -1
  68. package/dist/openbao/role-reconcile.d.ts.map +1 -1
  69. package/dist/openbao/ssh-role.d.ts.map +1 -1
  70. package/dist/ownership/adopt.d.ts +52 -0
  71. package/dist/ownership/adopt.d.ts.map +1 -0
  72. package/dist/ownership/probe.d.ts +13 -0
  73. package/dist/ownership/probe.d.ts.map +1 -0
  74. package/dist/ownership/resume.d.ts +40 -0
  75. package/dist/ownership/resume.d.ts.map +1 -0
  76. package/dist/ownership/rows.d.ts +33 -0
  77. package/dist/ownership/rows.d.ts.map +1 -0
  78. package/dist/ownership/whole.d.ts +14 -0
  79. package/dist/ownership/whole.d.ts.map +1 -0
  80. package/dist/proxmox/ceph-pool-form.d.ts +3 -3
  81. package/dist/proxmox/ceph-pool.d.ts.map +1 -1
  82. package/dist/proxmox/fake-bao-env.d.ts +3 -0
  83. package/dist/proxmox/fake-bao-env.d.ts.map +1 -0
  84. package/dist/proxmox/fake-pve-lxc.d.ts +42 -0
  85. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -0
  86. package/dist/proxmox/fake-pve.d.ts +38 -0
  87. package/dist/proxmox/fake-pve.d.ts.map +1 -0
  88. package/dist/proxmox/index.d.ts +6 -2
  89. package/dist/proxmox/index.d.ts.map +1 -1
  90. package/dist/proxmox/index.js +970 -186
  91. package/dist/proxmox/index.js.map +22 -9
  92. package/dist/proxmox/lxc-create-form.d.ts +6 -0
  93. package/dist/proxmox/lxc-create-form.d.ts.map +1 -0
  94. package/dist/proxmox/lxc-harness.d.ts +34 -0
  95. package/dist/proxmox/lxc-harness.d.ts.map +1 -0
  96. package/dist/proxmox/lxc-identity.d.ts +18 -0
  97. package/dist/proxmox/lxc-identity.d.ts.map +1 -0
  98. package/dist/proxmox/lxc-judge.d.ts +46 -0
  99. package/dist/proxmox/lxc-judge.d.ts.map +1 -0
  100. package/dist/proxmox/lxc-lifecycle.d.ts +68 -0
  101. package/dist/proxmox/lxc-lifecycle.d.ts.map +1 -0
  102. package/dist/proxmox/lxc-props.d.ts +139 -0
  103. package/dist/proxmox/lxc-props.d.ts.map +1 -0
  104. package/dist/proxmox/lxc-volume.d.ts +29 -0
  105. package/dist/proxmox/lxc-volume.d.ts.map +1 -0
  106. package/dist/proxmox/lxc-wire.d.ts +51 -0
  107. package/dist/proxmox/lxc-wire.d.ts.map +1 -0
  108. package/dist/proxmox/lxc.d.ts +27 -47
  109. package/dist/proxmox/lxc.d.ts.map +1 -1
  110. package/dist/proxmox/network-apply-read.d.ts +1 -1
  111. package/dist/proxmox/network-apply-read.d.ts.map +1 -1
  112. package/dist/proxmox/pbs-datastore.d.ts.map +1 -1
  113. package/dist/proxmox/resource.d.ts.map +1 -1
  114. package/dist/proxmox/update-guard.d.ts +27 -0
  115. package/dist/proxmox/update-guard.d.ts.map +1 -0
  116. package/dist/verify/args.d.ts +16 -0
  117. package/dist/verify/args.d.ts.map +1 -0
  118. package/dist/verify/cli.d.ts +3 -0
  119. package/dist/verify/cli.d.ts.map +1 -0
  120. package/dist/verify/cli.js +320 -0
  121. package/dist/verify/cli.js.map +15 -0
  122. package/dist/verify/fake-engine.d.ts +57 -0
  123. package/dist/verify/fake-engine.d.ts.map +1 -0
  124. package/dist/verify/index.d.ts +16 -0
  125. package/dist/verify/index.d.ts.map +1 -0
  126. package/dist/verify/index.js +297 -0
  127. package/dist/verify/index.js.map +14 -0
  128. package/dist/verify/report.d.ts +6 -0
  129. package/dist/verify/report.d.ts.map +1 -0
  130. package/dist/verify/rows.d.ts +57 -0
  131. package/dist/verify/rows.d.ts.map +1 -0
  132. package/dist/verify/spy.d.ts +31 -0
  133. package/dist/verify/spy.d.ts.map +1 -0
  134. package/dist/verify/verify.d.ts +43 -0
  135. package/dist/verify/verify.d.ts.map +1 -0
  136. package/docs/adopt-verify.md +108 -0
  137. package/docs/adopted-deploys.md +85 -0
  138. package/docs/credentials.md +15 -0
  139. package/docs/launchd-sudo.md +31 -6
  140. package/docs/launchd.md +7 -3
  141. package/docs/ownership.md +113 -0
  142. package/docs/proxmox-lxc.md +196 -0
  143. package/docs/proxmox.md +12 -0
  144. package/package.json +9 -2
  145. package/src/caddy/adopt-scope.test.ts +1 -1
  146. package/src/caddy/config-lifecycle.ts +1 -1
  147. package/src/caddy/config.ts +2 -1
  148. package/src/launchd/adopt-apply.test.ts +166 -0
  149. package/src/launchd/fake-runner.ts +26 -4
  150. package/src/launchd/host-effect.ts +1 -33
  151. package/src/launchd/host-file-lifecycle.ts +39 -14
  152. package/src/launchd/host-file.ts +13 -4
  153. package/src/launchd/job-lifecycle.ts +22 -14
  154. package/src/launchd/job-preflight.ts +18 -1
  155. package/src/launchd/job.ts +13 -4
  156. package/src/launchd/runner.ts +7 -0
  157. package/src/launchd/sudo-acl.ts +68 -0
  158. package/src/launchd/sudo-chain.test.ts +121 -0
  159. package/src/launchd/sudo-guard.ts +89 -10
  160. package/src/launchd/sudo-modes.test.ts +135 -0
  161. package/src/launchd/sudo-runner.ts +40 -59
  162. package/src/launchd/sudo-said.ts +58 -0
  163. package/src/openbao/README.md +5 -2
  164. package/src/openbao/REPLACE.md +13 -11
  165. package/src/openbao/adopt-core.test.ts +119 -0
  166. package/src/openbao/adopt-families.test.ts +97 -0
  167. package/src/openbao/adopt-holes.test.ts +168 -0
  168. package/src/openbao/adopt-recovery.test.ts +71 -0
  169. package/src/openbao/auth-method-reconcile.ts +15 -3
  170. package/src/openbao/auth-method.ts +15 -6
  171. package/src/openbao/auth-role.ts +14 -4
  172. package/src/openbao/cloudflare-role.ts +17 -4
  173. package/src/openbao/fake-bao.ts +52 -0
  174. package/src/openbao/fake-core.ts +141 -0
  175. package/src/openbao/fake-families.ts +11 -5
  176. package/src/openbao/fake-mounts.ts +74 -0
  177. package/src/openbao/fake-stack.ts +17 -4
  178. package/src/openbao/jwt-config.ts +10 -7
  179. package/src/openbao/jwt-role.ts +10 -7
  180. package/src/openbao/kubernetes-role.ts +13 -7
  181. package/src/openbao/mfa-enforcement.ts +20 -6
  182. package/src/openbao/mfa-totp.ts +22 -7
  183. package/src/openbao/mount-move.test.ts +2 -35
  184. package/src/openbao/mount-reconcile.ts +13 -2
  185. package/src/openbao/mount.ts +15 -6
  186. package/src/openbao/pki-role.ts +15 -4
  187. package/src/openbao/plugin-reconcile.ts +16 -5
  188. package/src/openbao/plugin.ts +17 -6
  189. package/src/openbao/policy.ts +17 -4
  190. package/src/openbao/proxmox-role.ts +13 -4
  191. package/src/openbao/rename-adoption.test.ts +32 -19
  192. package/src/openbao/role-reconcile.ts +23 -0
  193. package/src/openbao/ssh-role.ts +14 -4
  194. package/src/ownership/adopt.ts +122 -0
  195. package/src/ownership/ownership.test.ts +200 -0
  196. package/src/ownership/probe.ts +65 -0
  197. package/src/ownership/resume.ts +118 -0
  198. package/src/ownership/rows.ts +117 -0
  199. package/src/ownership/whole.test.ts +39 -0
  200. package/src/ownership/whole.ts +60 -0
  201. package/src/proxmox/adopt-noop.test.ts +112 -0
  202. package/src/proxmox/ceph-pool-adopt.test.ts +88 -0
  203. package/src/proxmox/ceph-pool-form.ts +3 -3
  204. package/src/proxmox/ceph-pool.ts +10 -3
  205. package/src/proxmox/fake-bao-env.ts +28 -0
  206. package/src/proxmox/fake-pve-lxc.ts +210 -0
  207. package/src/proxmox/fake-pve.ts +75 -0
  208. package/src/proxmox/index.test.ts +6 -6
  209. package/src/proxmox/index.ts +6 -2
  210. package/src/proxmox/lxc-adopt.test.ts +170 -0
  211. package/src/proxmox/lxc-create-form.ts +76 -0
  212. package/src/proxmox/lxc-create.test.ts +126 -0
  213. package/src/proxmox/lxc-guards.test.ts +158 -0
  214. package/src/proxmox/lxc-harness.ts +150 -0
  215. package/src/proxmox/lxc-identity.ts +43 -0
  216. package/src/proxmox/lxc-judge.test.ts +175 -0
  217. package/src/proxmox/lxc-judge.ts +196 -0
  218. package/src/proxmox/lxc-lifecycle.ts +200 -0
  219. package/src/proxmox/lxc-ownership.test.ts +159 -0
  220. package/src/proxmox/lxc-props.ts +215 -0
  221. package/src/proxmox/lxc-read.test.ts +60 -0
  222. package/src/proxmox/lxc-volume.test.ts +96 -0
  223. package/src/proxmox/lxc-volume.ts +153 -0
  224. package/src/proxmox/lxc-wire.ts +210 -0
  225. package/src/proxmox/lxc.ts +216 -93
  226. package/src/proxmox/network-apply-read.ts +3 -1
  227. package/src/proxmox/pbs-datastore.ts +4 -3
  228. package/src/proxmox/resource.ts +7 -7
  229. package/src/proxmox/update-guard.ts +31 -0
  230. package/src/verify/args.ts +82 -0
  231. package/src/verify/cli.test.ts +86 -0
  232. package/src/verify/cli.ts +49 -0
  233. package/src/verify/fake-engine.ts +158 -0
  234. package/src/verify/index.ts +36 -0
  235. package/src/verify/report.ts +52 -0
  236. package/src/verify/rows.ts +169 -0
  237. package/src/verify/spy.test.ts +141 -0
  238. package/src/verify/spy.ts +149 -0
  239. package/src/verify/verify.test.ts +124 -0
  240. package/src/verify/verify.ts +172 -0
@@ -0,0 +1,16 @@
1
+ import type { VerifyOptions, VerifyTarget } from './verify.ts';
2
+ export declare const USAGE = "Usage: hf-adopt-verify --stage <stage> [--config alchemy.run.ts] [options]\n\nPlans the stack with Alchemy's own planner, never writes, and reports each row\nwithout a state row: what the engine planned, the provider's read and its own\ndiff (before Alchemy forces adopted rows to update), and the declared fields\nthat differ from the live object. Exits 0 when every row is a no-op, 1 when\nany is not, 2 when it could not tell.\n\n -c, --config <file> stack entrypoint (default alchemy.run.ts)\n --stage <stage> stage to verify (default $ALCHEMY_STAGE; required)\n --profile <name> Alchemy auth profile\n --env-file <file> .env file layered under the environment\n --all report every declared row and pending deletion\n --json print the report as JSON\n -h, --help this text\n";
3
+ export type Parsed = {
4
+ readonly kind: 'help';
5
+ } | {
6
+ readonly kind: 'error';
7
+ readonly message: string;
8
+ } | {
9
+ readonly kind: 'run';
10
+ readonly target: VerifyTarget;
11
+ readonly options: VerifyOptions;
12
+ readonly json: boolean;
13
+ };
14
+ /** Parse argv (without the runtime and script) against the environment. Never throws. */
15
+ export declare const parseVerifyArgs: (argv: readonly string[], env: Readonly<Record<string, string | undefined>>) => Parsed;
16
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/verify/args.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE/D,eAAO,MAAM,KAAK,i1BAejB,CAAC;AAEF,MAAM,MAAM,MAAM,GACd;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpD;IACE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB,CAAC;AAkBN,yFAAyF;AACzF,eAAO,MAAM,eAAe,SACpB,SAAS,MAAM,EAAE,OAClB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,KAChD,MAoBF,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/verify/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/verify/cli.ts
4
+ import { runMain } from "alchemy/Util/PlatformServices";
5
+ import * as Cause from "effect/Cause";
6
+ import * as Console from "effect/Console";
7
+ import * as Effect3 from "effect/Effect";
8
+
9
+ // src/verify/args.ts
10
+ import { parseArgs } from "node:util";
11
+ var USAGE = `Usage: hf-adopt-verify --stage <stage> [--config alchemy.run.ts] [options]
12
+
13
+ Plans the stack with Alchemy's own planner, never writes, and reports each row
14
+ without a state row: what the engine planned, the provider's read and its own
15
+ diff (before Alchemy forces adopted rows to update), and the declared fields
16
+ that differ from the live object. Exits 0 when every row is a no-op, 1 when
17
+ any is not, 2 when it could not tell.
18
+
19
+ -c, --config <file> stack entrypoint (default alchemy.run.ts)
20
+ --stage <stage> stage to verify (default $ALCHEMY_STAGE; required)
21
+ --profile <name> Alchemy auth profile
22
+ --env-file <file> .env file layered under the environment
23
+ --all report every declared row and pending deletion
24
+ --json print the report as JSON
25
+ -h, --help this text
26
+ `;
27
+ var parse = (argv) => parseArgs({
28
+ args: [...argv],
29
+ allowPositionals: false,
30
+ options: {
31
+ all: { type: "boolean", default: false },
32
+ config: { type: "string", short: "c", default: "alchemy.run.ts" },
33
+ "env-file": { type: "string" },
34
+ help: { type: "boolean", short: "h", default: false },
35
+ json: { type: "boolean", default: false },
36
+ profile: { type: "string" },
37
+ stage: { type: "string" }
38
+ },
39
+ strict: true
40
+ });
41
+ var parseVerifyArgs = (argv, env) => {
42
+ let parsed;
43
+ try {
44
+ parsed = parse(argv);
45
+ } catch (error) {
46
+ return { kind: "error", message: error instanceof Error ? error.message : String(error) };
47
+ }
48
+ const { values } = parsed;
49
+ if (values.help)
50
+ return { kind: "help" };
51
+ const stage = values.stage ?? env["ALCHEMY_STAGE"];
52
+ if (stage === undefined || stage === "") {
53
+ return { kind: "error", message: "pass --stage (or set ALCHEMY_STAGE): no stage is guessed" };
54
+ }
55
+ const target = {
56
+ entrypoint: values.config,
57
+ stage,
58
+ ...values.profile === undefined ? {} : { profile: values.profile },
59
+ ...values["env-file"] === undefined ? {} : { envFile: values["env-file"] }
60
+ };
61
+ return { json: values.json, kind: "run", options: { all: values.all }, target };
62
+ };
63
+
64
+ // src/verify/report.ts
65
+ var exitCodeOf = (report) => report.rows.every((row) => row.ok) ? 0 : 1;
66
+ var pad = (text, width) => text.padEnd(width);
67
+ var line = (row, widths) => {
68
+ const head = [
69
+ row.ok ? "ok " : "FAIL",
70
+ pad(row.planned, widths.planned),
71
+ pad(row.diff, 8),
72
+ pad(row.read, 9),
73
+ pad(row.type, widths.type),
74
+ row.fqn
75
+ ].join(" ");
76
+ const changed = row.changed.length > 0 ? ` changed: ${row.changed.join(", ")}` : "";
77
+ return row.why === "" ? `${head}${changed}` : `${head}${changed}
78
+ ${row.why}`;
79
+ };
80
+ var formatReport = (report, all) => {
81
+ const scope = all ? "every row" : "rows without state";
82
+ const title = `hf-adopt-verify ${report.stack}/${report.stage}: ${String(report.rows.length)} ${scope} (of ${String(report.declared)} declared)`;
83
+ if (report.rows.length === 0) {
84
+ return `${title}
85
+ nothing to verify — every declared row already has state. --all checks those too.
86
+ `;
87
+ }
88
+ const widths = {
89
+ planned: Math.max(7, ...report.rows.map((row) => row.planned.length)),
90
+ type: Math.max(4, ...report.rows.map((row) => row.type.length))
91
+ };
92
+ const header = [" ", pad("planned", widths.planned), pad("diff", 8), pad("read", 9)].concat([pad("type", widths.type), "fqn"]).join(" ");
93
+ const failed = report.rows.filter((row) => !row.ok).length;
94
+ const verdict = failed === 0 ? "every row is a no-op: the deploy writes nothing to them" : `${String(failed)} row(s) are NOT a no-op: the deploy would write, create or delete`;
95
+ return [title, "", header, ...report.rows.map((row) => line(row, widths)), "", verdict, ""].join(`
96
+ `);
97
+ };
98
+
99
+ // src/verify/verify.ts
100
+ import * as Alchemist from "alchemy/Alchemist";
101
+ import {
102
+ ArtifactStore,
103
+ Artifacts,
104
+ createArtifactStore,
105
+ makeScopedArtifacts
106
+ } from "alchemy/Artifacts";
107
+ import { InstanceId } from "alchemy/InstanceId";
108
+ import * as Plan from "alchemy/Plan";
109
+ import { State, isActionState } from "alchemy/State";
110
+ import * as Effect2 from "effect/Effect";
111
+ import * as Option from "effect/Option";
112
+
113
+ // src/verify/rows.ts
114
+ import { deepEqual, isResolved } from "alchemy/Diff";
115
+ var changedFields = (declared, live) => {
116
+ if (typeof declared !== "object" || declared === null)
117
+ return [];
118
+ if (typeof live !== "object" || live === null)
119
+ return [];
120
+ const actual = live;
121
+ return Object.entries(declared).filter(([key, value]) => value !== undefined && (key in actual) && isResolved(value)).filter(([key, value]) => !deepEqual(value, actual[key])).map(([key]) => key).sort();
122
+ };
123
+ var STILL_LIVE = new Set(["adopted", "noop"]);
124
+ var isNoop = (planned, diff, stateRow) => STILL_LIVE.has(planned) && (diff === "noop" || diff === "none" && stateRow && planned === "noop");
125
+ var reason = (row, ok) => {
126
+ if (ok)
127
+ return row.read === "unowned" ? "unowned: the deploy needs --adopt or adopt(true)" : "";
128
+ if (isNoop(row.planned, row.diff, row.stateRow)) {
129
+ return `${String(row.bindings)} binding(s) change: the deploy reconciles them though the diff is noop`;
130
+ }
131
+ if (row.planned === "delete")
132
+ return "no longer declared: the deploy deletes it";
133
+ if (row.planned === "orphaned")
134
+ return "no longer declared: the deploy drops its state (retain)";
135
+ if (row.read === "failed")
136
+ return "the verifier's own read of it failed";
137
+ if (row.planned === "create") {
138
+ if (row.stateRow)
139
+ return "an interrupted create: the deploy finishes creating it";
140
+ return row.read === "absent" ? "the read found nothing, so the deploy CREATES it (a lane that cannot see it reads the same)" : "never probed (a prop is an Output of another resource, or the provider has no read): " + "the deploy CREATES it";
141
+ }
142
+ if (row.diff === "update")
143
+ return "the provider's diff says update: the deploy writes";
144
+ if (row.diff === "replace")
145
+ return "the provider's diff says replace: a new one, then a delete";
146
+ if (row.diff === "none") {
147
+ return "the provider has no diff, so the engine compared the declaration with itself";
148
+ }
149
+ return `the engine plans ${row.planned} although the diff says ${row.diff} (a renamed row, changed bindings or --force)`;
150
+ };
151
+ var rowFor = (fqn, node, stateRow, seen) => {
152
+ const observed = seen.get(fqn);
153
+ const base = {
154
+ bindings: (node.bindings ?? []).filter((binding) => binding.action !== "noop").length,
155
+ changed: changedFields(observed?.diff?.news, observed?.read?.attributes),
156
+ diff: observed?.diff?.answer ?? "not-run",
157
+ fqn,
158
+ planned: node.action,
159
+ read: observed?.read?.answer ?? "not-read",
160
+ stateRow,
161
+ type: node.resource.Type
162
+ };
163
+ const ok = isNoop(base.planned, base.diff, stateRow) && base.bindings === 0;
164
+ return { ...base, ok, why: reason(base, ok) };
165
+ };
166
+ var taskRow = (fqn, task) => ({
167
+ bindings: 0,
168
+ changed: [],
169
+ diff: "not-run",
170
+ fqn,
171
+ ok: task.action === "noop",
172
+ planned: task.action,
173
+ read: "not-read",
174
+ stateRow: task.action !== "run",
175
+ type: task.def.Type,
176
+ why: task.action === "noop" ? "" : `a stack task: the deploy will ${task.action} it`
177
+ });
178
+ var rowsOf = (plan, stateRows, seen, all) => {
179
+ const declared = Object.entries(plan.resources).filter(([fqn]) => all || !stateRows.has(fqn)).map(([fqn, node]) => rowFor(fqn, node, stateRows.has(fqn), seen));
180
+ const removed = all ? Object.entries(plan.deletions).flatMap(([fqn, node]) => node === undefined ? [] : [rowFor(fqn, node, true, seen)]) : [];
181
+ const tasks = Object.entries(plan.actions ?? {}).flatMap(([fqn, task]) => task === undefined || !all && task.action === "noop" ? [] : [taskRow(fqn, task)]);
182
+ return [...declared, ...removed, ...tasks].sort((a, b) => a.fqn.localeCompare(b.fqn));
183
+ };
184
+
185
+ // src/verify/spy.ts
186
+ import { Unowned, stripUnowned } from "alchemy/AdoptPolicy";
187
+ import {
188
+ isProviderCollectionService,
189
+ isProviderService
190
+ } from "alchemy/Provider";
191
+ import * as Context from "effect/Context";
192
+ import * as Effect from "effect/Effect";
193
+ var seenFor = (seen, fqn) => {
194
+ const existing = seen.get(fqn);
195
+ if (existing !== undefined)
196
+ return existing;
197
+ const fresh = {};
198
+ seen.set(fqn, fresh);
199
+ return fresh;
200
+ };
201
+ var WRITE_REFUSED = "hf-adopt-verify never writes";
202
+ var refuse = (type, operation) => () => Effect.die(new Error(`${WRITE_REFUSED}: ${type}.${operation} was called during a verification. Alchemy's planner ` + "does not call it, so something other than Plan.make is driving these providers."));
203
+ var readAnswer = (attributes) => attributes === undefined ? "absent" : Unowned.is(attributes) ? "unowned" : "found";
204
+ var diffAnswer = (diff) => {
205
+ const action = diff?.action;
206
+ return action === "noop" || action === "update" || action === "replace" ? action : "none";
207
+ };
208
+ var spyService = (type, service, seen) => {
209
+ const read = service.read;
210
+ const diff = service.diff;
211
+ const modes = service.modes;
212
+ const watched = {
213
+ ...service,
214
+ diff: (input) => (diff === undefined ? Effect.succeed(undefined) : diff(input)).pipe(Effect.tap((answer) => Effect.sync(() => {
215
+ seenFor(seen, input.fqn).diff = { answer: diffAnswer(answer), news: input.news };
216
+ }))),
217
+ reconcile: refuse(type, "reconcile"),
218
+ delete: refuse(type, "delete")
219
+ };
220
+ if (read !== undefined) {
221
+ watched.read = (input) => read(input).pipe(Effect.tap((answer) => Effect.sync(() => {
222
+ const attributes = answer === undefined ? undefined : stripUnowned(answer);
223
+ seenFor(seen, input.fqn).read = { answer: readAnswer(answer), attributes };
224
+ })));
225
+ }
226
+ if (service.precreate !== undefined)
227
+ watched.precreate = refuse(type, "precreate");
228
+ if (modes !== undefined) {
229
+ watched.modes = {
230
+ live: Effect.map(modes.live, (variant) => spyService(type, variant, seen)),
231
+ local: Effect.map(modes.local, (variant) => spyService(type, variant, seen))
232
+ };
233
+ }
234
+ return watched;
235
+ };
236
+ var spyCollection = (collection, seen) => {
237
+ const providers = Object.fromEntries(Object.entries(collection.providers).map(([type, service]) => [
238
+ type,
239
+ spyService(type, service, seen)
240
+ ]));
241
+ return { ...collection, get: (type) => providers[type], providers };
242
+ };
243
+ var spyContext = (context, seen) => {
244
+ const services = new Map;
245
+ for (const [key, value] of context.mapUnsafe) {
246
+ services.set(key, isProviderService(value) ? spyService(key, value, seen) : isProviderCollectionService(value) ? spyCollection(value, seen) : value);
247
+ }
248
+ return Context.makeUnsafe(services);
249
+ };
250
+
251
+ // src/verify/verify.ts
252
+ var readWithState = (fqn, node) => Effect2.gen(function* () {
253
+ const read = node.provider?.read;
254
+ if (read === undefined || node.state === undefined)
255
+ return;
256
+ const store = Option.getOrElse(yield* Effect2.serviceOption(ArtifactStore), createArtifactStore);
257
+ yield* read({
258
+ fqn,
259
+ id: node.resource.LogicalId,
260
+ instanceId: node.state.instanceId,
261
+ olds: node.state.props,
262
+ output: node.state.attr
263
+ }).pipe(Effect2.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect2.provideService(InstanceId, node.state.instanceId));
264
+ });
265
+ var withState = (stack, fqns) => Effect2.gen(function* () {
266
+ const store = yield* yield* State;
267
+ const found = yield* Effect2.all(fqns.map((fqn) => store.get({ fqn, stack: stack.name, stage: stack.stage }).pipe(Effect2.map((row) => row === undefined || isActionState(row) ? [] : [fqn]))), { concurrency: 8 });
268
+ return new Set(found.flat());
269
+ });
270
+ var verifySession = (session, options = {}) => {
271
+ const seen = new Map;
272
+ const all2 = options.all === true;
273
+ const plan = Plan.make(session.stack);
274
+ return Effect2.gen(function* () {
275
+ const planned = yield* plan;
276
+ const stateRows = yield* withState(session.stack, Object.keys(planned.resources));
277
+ if (all2) {
278
+ const stateful = Object.entries(planned.resources).filter(([fqn]) => stateRows.has(fqn));
279
+ yield* Effect2.all(stateful.map(([fqn, node]) => readWithState(fqn, node).pipe(Effect2.catchCause(() => Effect2.sync(() => {
280
+ const entry = seen.get(fqn) ?? {};
281
+ entry.read = { answer: "failed", attributes: undefined };
282
+ seen.set(fqn, entry);
283
+ })))), { concurrency: 8 });
284
+ }
285
+ return {
286
+ declared: Object.keys(planned.resources).length,
287
+ rows: rowsOf(planned, stateRows, seen, all2),
288
+ stack: session.stack.name,
289
+ stage: session.stack.stage
290
+ };
291
+ }).pipe(Effect2.provide(spyContext(session.context, seen)), Effect2.scoped);
292
+ };
293
+ var verifyStack = (target, options = {}) => Effect2.gen(function* () {
294
+ const session = yield* Alchemist.open(target, { adopt: true });
295
+ return yield* verifySession(session, options);
296
+ }).pipe(Effect2.scoped, Effect2.provide(Alchemist.layer()));
297
+
298
+ // src/verify/cli.ts
299
+ var setExit = (code) => Effect3.sync(() => {
300
+ process.exitCode = code;
301
+ });
302
+ var program = Effect3.gen(function* () {
303
+ const parsed = parseVerifyArgs(process.argv.slice(2), process.env);
304
+ if (parsed.kind === "help")
305
+ return yield* Console.log(USAGE);
306
+ if (parsed.kind === "error") {
307
+ yield* Console.error(`hf-adopt-verify: ${parsed.message}
308
+
309
+ ${USAGE}`);
310
+ return yield* setExit(2);
311
+ }
312
+ const report = yield* verifyStack(parsed.target, parsed.options);
313
+ yield* Console.log(parsed.json ? JSON.stringify(report, null, 2) : formatReport(report, parsed.options.all === true));
314
+ yield* setExit(exitCodeOf(report));
315
+ }).pipe(Effect3.catchCause((cause) => Console.error(`hf-adopt-verify: the plan could not be verified
316
+ ${Cause.pretty(cause)}`).pipe(Effect3.andThen(setExit(2)))));
317
+ runMain(program);
318
+
319
+ //# debugId=B36621FDE54AA90464756E2164756E21
320
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,15 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/verify/cli.ts", "../src/verify/args.ts", "../src/verify/report.ts", "../src/verify/verify.ts", "../src/verify/rows.ts", "../src/verify/spy.ts"],
4
+ "sourcesContent": [
5
+ "#!/usr/bin/env node\n/**\n * `hf-adopt-verify` — the bin. Parse, verify, print, exit 0 / 1 / 2.\n *\n * ★ `runMain` IS ALCHEMY'S (alchemy/Util/PlatformServices), so this runs under bun or node the way\n * `alchemy` itself does, and exits once the report is out rather than lingering on keep-alive\n * sockets. Run it where the stack's `alchemy` runs: `bunx --bun hf-adopt-verify …` next to\n * `bun ./node_modules/alchemy/bin/cli.js plan …`, because the entrypoint it imports is\n * TypeScript.\n * ⛔ EXIT 2 IS \"COULD NOT TELL\", NEVER A PASS. A plan that fails — a provider that dies, an\n * `adopt(false)` row that is not ours, a state store it cannot read — prints the cause and exits\n * 2, so a gate reading `!= 0` stops either way.\n */\nimport { runMain } from 'alchemy/Util/PlatformServices';\nimport * as Cause from 'effect/Cause';\nimport * as Console from 'effect/Console';\nimport * as Effect from 'effect/Effect';\nimport { USAGE, parseVerifyArgs } from './args.ts';\nimport { exitCodeOf, formatReport } from './report.ts';\nimport { verifyStack } from './verify.ts';\n\nconst setExit = (code: number) =>\n Effect.sync(() => {\n process.exitCode = code;\n });\n\nconst program = Effect.gen(function* () {\n const parsed = parseVerifyArgs(process.argv.slice(2), process.env);\n if (parsed.kind === 'help') return yield* Console.log(USAGE);\n if (parsed.kind === 'error') {\n yield* Console.error(`hf-adopt-verify: ${parsed.message}\\n\\n${USAGE}`);\n return yield* setExit(2);\n }\n const report = yield* verifyStack(parsed.target, parsed.options);\n yield* Console.log(\n parsed.json\n ? JSON.stringify(report, null, 2)\n : formatReport(report, parsed.options.all === true),\n );\n yield* setExit(exitCodeOf(report));\n}).pipe(\n Effect.catchCause((cause) =>\n Console.error(`hf-adopt-verify: the plan could not be verified\\n${Cause.pretty(cause)}`).pipe(\n Effect.andThen(setExit(2)),\n ),\n ),\n);\n\nrunMain(program);\n",
6
+ "/**\n * `hf-adopt-verify`'s flags — the ones `alchemy plan` takes, so a stack script can swap one\n * command for the other.\n *\n * ★ `node:util` parseArgs, NOT effect's CLI module. Alchemy builds its CLI on\n * `effect/unstable/cli`, but that needs a CliConfig and a Terminal layer to parse six flags; the\n * standard library parses them with no service at all, which keeps this pure and testable.\n * ⛔ `--stage` HAS NO GUESSED DEFAULT. Alchemy falls back to `live_$USER`; a gate that silently\n * verified the wrong stage would pass for the wrong reason. It comes from the flag or from\n * `$ALCHEMY_STAGE`, or the run stops.\n */\nimport { parseArgs } from 'node:util';\nimport type { VerifyOptions, VerifyTarget } from './verify.ts';\n\nexport const USAGE = `Usage: hf-adopt-verify --stage <stage> [--config alchemy.run.ts] [options]\n\nPlans the stack with Alchemy's own planner, never writes, and reports each row\nwithout a state row: what the engine planned, the provider's read and its own\ndiff (before Alchemy forces adopted rows to update), and the declared fields\nthat differ from the live object. Exits 0 when every row is a no-op, 1 when\nany is not, 2 when it could not tell.\n\n -c, --config <file> stack entrypoint (default alchemy.run.ts)\n --stage <stage> stage to verify (default $ALCHEMY_STAGE; required)\n --profile <name> Alchemy auth profile\n --env-file <file> .env file layered under the environment\n --all report every declared row and pending deletion\n --json print the report as JSON\n -h, --help this text\n`;\n\nexport type Parsed =\n | { readonly kind: 'help' }\n | { readonly kind: 'error'; readonly message: string }\n | {\n readonly kind: 'run';\n readonly target: VerifyTarget;\n readonly options: VerifyOptions;\n readonly json: boolean;\n };\n\nconst parse = (argv: readonly string[]) =>\n parseArgs({\n args: [...argv],\n allowPositionals: false,\n options: {\n all: { type: 'boolean', default: false },\n config: { type: 'string', short: 'c', default: 'alchemy.run.ts' },\n 'env-file': { type: 'string' },\n help: { type: 'boolean', short: 'h', default: false },\n json: { type: 'boolean', default: false },\n profile: { type: 'string' },\n stage: { type: 'string' },\n },\n strict: true,\n });\n\n/** Parse argv (without the runtime and script) against the environment. Never throws. */\nexport const parseVerifyArgs = (\n argv: readonly string[],\n env: Readonly<Record<string, string | undefined>>,\n): Parsed => {\n let parsed: ReturnType<typeof parse>;\n try {\n parsed = parse(argv);\n } catch (error) {\n return { kind: 'error', message: error instanceof Error ? error.message : String(error) };\n }\n const { values } = parsed;\n if (values.help) return { kind: 'help' };\n const stage = values.stage ?? env['ALCHEMY_STAGE'];\n if (stage === undefined || stage === '') {\n return { kind: 'error', message: 'pass --stage (or set ALCHEMY_STAGE): no stage is guessed' };\n }\n const target: VerifyTarget = {\n entrypoint: values.config,\n stage,\n ...(values.profile === undefined ? {} : { profile: values.profile }),\n ...(values['env-file'] === undefined ? {} : { envFile: values['env-file'] }),\n };\n return { json: values.json, kind: 'run', options: { all: values.all }, target };\n};\n",
7
+ "/**\n * The verifier's answer, for a terminal and for a gate.\n *\n * ⛔ NAMES, NEVER VALUES. A row lists which declared fields differ from the live object, not what\n * either side holds: the tool runs over any stack, and a prop or an attribute can be a secret.\n * `alchemy plan --detailed` shows declared values; the family's own read shows live ones.\n */\nimport type { AdoptRow } from './rows.ts';\nimport type { AdoptReport } from './verify.ts';\n\n/** 0 when every reported row is a no-op, 1 when any is not. (2 is the CLI's: it could not tell.) */\nexport const exitCodeOf = (report: AdoptReport): 0 | 1 =>\n report.rows.every((row) => row.ok) ? 0 : 1;\n\nconst pad = (text: string, width: number) => text.padEnd(width);\n\nconst line = (row: AdoptRow, widths: { planned: number; type: number }) => {\n const head = [\n row.ok ? 'ok ' : 'FAIL',\n pad(row.planned, widths.planned),\n pad(row.diff, 8),\n pad(row.read, 9),\n pad(row.type, widths.type),\n row.fqn,\n ].join(' ');\n const changed = row.changed.length > 0 ? ` changed: ${row.changed.join(', ')}` : '';\n return row.why === '' ? `${head}${changed}` : `${head}${changed}\\n ${row.why}`;\n};\n\n/** Plain text, one row per line, with the reason under any row that is not a no-op. */\nexport const formatReport = (report: AdoptReport, all: boolean): string => {\n const scope = all ? 'every row' : 'rows without state';\n const title = `hf-adopt-verify ${report.stack}/${report.stage}: ${String(report.rows.length)} ${scope} (of ${String(report.declared)} declared)`;\n if (report.rows.length === 0) {\n return `${title}\\nnothing to verify — every declared row already has state. --all checks those too.\\n`;\n }\n const widths = {\n planned: Math.max(7, ...report.rows.map((row) => row.planned.length)),\n type: Math.max(4, ...report.rows.map((row) => row.type.length)),\n };\n const header = [' ', pad('planned', widths.planned), pad('diff', 8), pad('read', 9)]\n .concat([pad('type', widths.type), 'fqn'])\n .join(' ');\n const failed = report.rows.filter((row) => !row.ok).length;\n const verdict =\n failed === 0\n ? 'every row is a no-op: the deploy writes nothing to them'\n : `${String(failed)} row(s) are NOT a no-op: the deploy would write, create or delete`;\n return [title, '', header, ...report.rows.map((row) => line(row, widths)), '', verdict, ''].join(\n '\\n',\n );\n};\n",
8
+ "/**\n * Prove a deploy's adoptions are no-ops — with Alchemy's own planner, before anything is written.\n *\n * ★ THE ENGINE PLANS, THIS ONLY WATCHES. `verifySession` runs `Plan.make` — the same call\n * `alchemy plan` makes (Alchemist routes/stack.ts) — over the session's context with every\n * provider swapped for a watched copy (spy.ts). Output resolution, the state lookup, rename\n * migrations, the adoption probe and `adopt(…)` all stay the engine's; nothing here re-derives\n * them. What it adds is the provider's own `diff` answer, which Plan.ts overwrites for every\n * adopted row (`forceUpdateAfterAdoption`), and the state-row check that says which rows ARE\n * adoptions.\n *\n * ⛔ READ-ONLY BY CONSTRUCTION. The planner calls `read` and `diff`; spy.ts replaces `reconcile`,\n * `delete` and `precreate` with refusals; nothing here calls `apply`. `--all` adds one `read` per\n * row with state — still a read by Alchemy's provider contract.\n * ⚠️ READS ARE NOT FREE FOR EVERY FAMILY. A kit PVE/PBS read mints a lease through OpenBao, which\n * creates a short-lived API token on the cluster, exactly as `alchemy plan` does.\n */\nimport * as Alchemist from 'alchemy/Alchemist';\nimport {\n ArtifactStore,\n Artifacts,\n createArtifactStore,\n makeScopedArtifacts,\n} from 'alchemy/Artifacts';\nimport { InstanceId } from 'alchemy/InstanceId';\nimport * as Plan from 'alchemy/Plan';\nimport type { ProviderService } from 'alchemy/Provider';\nimport type { CompiledStack } from 'alchemy/Stack';\nimport { State, isActionState } from 'alchemy/State';\nimport type * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\nimport { type AdoptRow, type PlanView, rowsOf } from './rows.ts';\nimport { type Observations, spyContext } from './spy.ts';\n\nexport interface VerifyOptions {\n /** Report every declared row and every pending deletion, not only rows without state. */\n readonly all?: boolean;\n}\n\nexport interface AdoptReport {\n readonly stack: string;\n readonly stage: string;\n /** Resources the stack declares. */\n readonly declared: number;\n readonly rows: readonly AdoptRow[];\n}\n\n/** An imported stack and the context it plans under — what `Alchemist.open` returns. */\nexport interface VerifySession {\n readonly stack: CompiledStack<unknown, unknown>;\n readonly context: Context.Context<unknown>;\n}\n\n/** The plan-node fields `--all` needs to read a row that has state. */\ntype StatefulNode = {\n readonly provider?: ProviderService;\n readonly resource: { readonly LogicalId: string };\n readonly state?: {\n readonly instanceId: string;\n readonly props: object;\n readonly attr: object | undefined;\n };\n};\n\n/**\n * ⚠️ THE SAME SCOPE Plan.ts GIVES ITS OWN READS (`providePlanScope`, not exported): a scoped\n * `Artifacts` bag and the row's `InstanceId`. A kit family that consults either — the ownership\n * helpers do — sees what it would see under `alchemy plan`.\n */\nconst readWithState = (fqn: string, node: StatefulNode) =>\n Effect.gen(function* () {\n const read = node.provider?.read;\n if (read === undefined || node.state === undefined) return;\n const store = Option.getOrElse(yield* Effect.serviceOption(ArtifactStore), createArtifactStore);\n yield* read({\n fqn,\n id: node.resource.LogicalId,\n instanceId: node.state.instanceId,\n olds: node.state.props,\n output: node.state.attr,\n }).pipe(\n Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)),\n Effect.provideService(InstanceId, node.state.instanceId),\n );\n });\n\n/** Which of `fqns` the stage's state store holds a resource row for. */\nconst withState = (stack: CompiledStack<unknown, unknown>, fqns: readonly string[]) =>\n Effect.gen(function* () {\n const store = yield* yield* State;\n const found = yield* Effect.all(\n fqns.map((fqn) =>\n store\n .get({ fqn, stack: stack.name, stage: stack.stage })\n .pipe(Effect.map((row) => (row === undefined || isActionState(row) ? [] : [fqn]))),\n ),\n { concurrency: 8 },\n );\n return new Set(found.flat());\n });\n\n/**\n * Plan `session.stack` with watched providers and report each row without state (or, with\n * `all`, every row). Never writes.\n */\nexport const verifySession = (\n session: VerifySession,\n options: VerifyOptions = {},\n): Effect.Effect<AdoptReport, unknown> => {\n const seen: Observations = new Map();\n const all = options.all === true;\n // ⚠️ THE CAST IS AT THE ENGINE'S TYPED BOUNDARY. `Plan.make` declares only `State` and hides the\n // rest behind a ts-expect-error; the session context carries all of it, as in routes/stack.ts.\n const plan = Plan.make(session.stack) as unknown as Effect.Effect<\n PlanView & { readonly resources: Readonly<Record<string, StatefulNode>> },\n unknown\n >;\n return Effect.gen(function* () {\n const planned = yield* plan;\n const stateRows = yield* withState(session.stack, Object.keys(planned.resources));\n if (all) {\n const stateful = Object.entries(planned.resources).filter(([fqn]) => stateRows.has(fqn));\n yield* Effect.all(\n stateful.map(([fqn, node]) =>\n readWithState(fqn, node).pipe(\n Effect.catchCause(() =>\n Effect.sync(() => {\n const entry = seen.get(fqn) ?? {};\n entry.read = { answer: 'failed', attributes: undefined };\n seen.set(fqn, entry);\n }),\n ),\n ),\n ),\n { concurrency: 8 },\n );\n }\n return {\n declared: Object.keys(planned.resources).length,\n rows: rowsOf(planned, stateRows, seen, all),\n stack: session.stack.name,\n stage: session.stack.stage,\n } satisfies AdoptReport;\n }).pipe(Effect.provide(spyContext(session.context, seen)), Effect.scoped);\n};\n\n/** Where the stack lives — the fields `alchemy plan` takes as flags. */\nexport interface VerifyTarget {\n /** The stack entrypoint, e.g. `alchemy.run.ts`. */\n readonly entrypoint: string;\n readonly stage: string;\n readonly profile?: string;\n readonly envFile?: string;\n}\n\n/**\n * Import the stack the way the Alchemy CLI does and verify it.\n *\n * ★ `adopt: true`, ALWAYS, AND IT CANNOT WRITE. Planning is side-effect free (Plan.ts keeps the\n * adopted state in memory, alchemy#793), so the flag only lets an `Unowned` row reach its\n * `diff` instead of failing the plan — the report marks it, because the real deploy then needs\n * `--adopt`. A resource declared `.pipe(adopt(false))` still fails the plan, as it would deploy.\n */\nexport const verifyStack = (\n target: VerifyTarget,\n options: VerifyOptions = {},\n): Effect.Effect<AdoptReport, unknown> =>\n Effect.gen(function* () {\n const session = yield* Alchemist.open(target, { adopt: true });\n return yield* verifySession(session, options);\n }).pipe(Effect.scoped, Effect.provide(Alchemist.layer()));\n",
9
+ "/**\n * One report row per planned resource: what the engine planned, what the provider said, and\n * whether that adds up to \"this deploy leaves the object alone\".\n *\n * ★ PURE, SO THE VERDICT IS TESTABLE WITHOUT AN ENGINE. verify.ts gathers the three inputs — the\n * plan, which FQNs have a state row, and what the watched providers answered (spy.ts) — and\n * this file only decides.\n */\nimport { deepEqual, isResolved } from 'alchemy/Diff';\nimport type { DiffAnswer, Observations, ReadAnswer } from './spy.ts';\n\nexport interface AdoptRow {\n readonly fqn: string;\n readonly type: string;\n /** Whether the state store holds a row for this FQN. An adoption is a row without one. */\n readonly stateRow: boolean;\n /** The engine's action — what `alchemy plan` prints (`adopted`, `create`, `noop`, …). */\n readonly planned: string;\n /** What the provider's `read` answered, or `not-read` when nothing asked it. */\n readonly read: ReadAnswer;\n /** What the provider's own `diff` answered BEFORE Plan.ts forced it (spy.ts). */\n readonly diff: DiffAnswer;\n /**\n * Declared fields whose live value differs, by NAME only — values can be secrets. Compared on\n * same-named keys of the declaration and the read's attributes, so a family whose attributes\n * rename a field cannot show it here; the diff answer, not this list, is the verdict.\n */\n readonly changed: readonly string[];\n /** Bindings the engine will create, update or delete — reconcile applies them even on `noop`. */\n readonly bindings: number;\n readonly ok: boolean;\n /** Why the row is not a no-op, or a note on one that is; empty when there is nothing to say. */\n readonly why: string;\n}\n\n/** The slice of an Alchemy plan node this file reads. */\nexport interface NodeView {\n readonly action: string;\n readonly resource: { readonly Type: string };\n readonly bindings?: readonly { readonly action: string }[];\n}\n\n/** A stack task (an Alchemy action): `run`, `noop` or `delete`. It has no provider to ask. */\nexport interface TaskView {\n readonly action: string;\n readonly def: { readonly Type: string };\n}\n\n/** The slice of an Alchemy `Plan` this file reads. */\nexport interface PlanView {\n readonly resources: Readonly<Record<string, NodeView>>;\n readonly deletions: Readonly<Record<string, NodeView | undefined>>;\n readonly actions?: Readonly<Record<string, TaskView | undefined>>;\n}\n\n/** Same-named, declared, resolved fields where the live value differs. */\nexport const changedFields = (declared: unknown, live: unknown): string[] => {\n if (typeof declared !== 'object' || declared === null) return [];\n if (typeof live !== 'object' || live === null) return [];\n const actual = live as Record<string, unknown>;\n return Object.entries(declared as Record<string, unknown>)\n .filter(([key, value]) => value !== undefined && key in actual && isResolved(value))\n .filter(([key, value]) => !deepEqual(value, actual[key]))\n .map(([key]) => key)\n .sort();\n};\n\nconst STILL_LIVE = new Set(['adopted', 'noop']);\n\n/**\n * ⛔ `adopted` NEEDS THE PROVIDER'S OWN `noop`. With no diff (`none`) the engine compared the\n * declaration with the declaration — an adopted row's state carries `props: news` (Plan.ts) — so\n * its noop proves nothing. A row with state is different: there the engine compared against the\n * props it last deployed, which is the same check `alchemy plan` makes.\n */\nconst isNoop = (planned: string, diff: DiffAnswer, stateRow: boolean) =>\n STILL_LIVE.has(planned) &&\n (diff === 'noop' || (diff === 'none' && stateRow && planned === 'noop'));\n\nconst reason = (row: Omit<AdoptRow, 'ok' | 'why'>, ok: boolean): string => {\n if (ok) return row.read === 'unowned' ? 'unowned: the deploy needs --adopt or adopt(true)' : '';\n if (isNoop(row.planned, row.diff, row.stateRow)) {\n return `${String(row.bindings)} binding(s) change: the deploy reconciles them though the diff is noop`;\n }\n if (row.planned === 'delete') return 'no longer declared: the deploy deletes it';\n if (row.planned === 'orphaned') return 'no longer declared: the deploy drops its state (retain)';\n if (row.read === 'failed') return \"the verifier's own read of it failed\";\n if (row.planned === 'create') {\n if (row.stateRow) return 'an interrupted create: the deploy finishes creating it';\n return row.read === 'absent'\n ? 'the read found nothing, so the deploy CREATES it (a lane that cannot see it reads the same)'\n : 'never probed (a prop is an Output of another resource, or the provider has no read): ' +\n 'the deploy CREATES it';\n }\n if (row.diff === 'update') return \"the provider's diff says update: the deploy writes\";\n if (row.diff === 'replace') return \"the provider's diff says replace: a new one, then a delete\";\n if (row.diff === 'none') {\n return 'the provider has no diff, so the engine compared the declaration with itself';\n }\n return `the engine plans ${row.planned} although the diff says ${row.diff} (a renamed row, changed bindings or --force)`;\n};\n\nexport const rowFor = (\n fqn: string,\n node: NodeView,\n stateRow: boolean,\n seen: Observations,\n): AdoptRow => {\n const observed = seen.get(fqn);\n const base = {\n bindings: (node.bindings ?? []).filter((binding) => binding.action !== 'noop').length,\n changed: changedFields(observed?.diff?.news, observed?.read?.attributes),\n diff: observed?.diff?.answer ?? 'not-run',\n fqn,\n planned: node.action,\n read: observed?.read?.answer ?? 'not-read',\n stateRow,\n type: node.resource.Type,\n } satisfies Omit<AdoptRow, 'ok' | 'why'>;\n /**\n * ⚠️ A NOOP DIFF DOES NOT COVER BINDINGS. Plan.ts diffs them separately and hands every\n * non-noop binding to `reconcile`, which an adopted row reaches anyway — a Worker adopted with\n * a new binding writes although its own diff said nothing changed.\n */\n const ok = isNoop(base.planned, base.diff, stateRow) && base.bindings === 0;\n return { ...base, ok, why: reason(base, ok) };\n};\n\n/**\n * ⚠️ A TASK THAT RUNS IS A WRITE THE PROVIDERS NEVER SEE, so it is reported in both modes; a task\n * that would not run appears only with `all`.\n */\nconst taskRow = (fqn: string, task: TaskView): AdoptRow => ({\n bindings: 0,\n changed: [],\n diff: 'not-run',\n fqn,\n ok: task.action === 'noop',\n planned: task.action,\n read: 'not-read',\n stateRow: task.action !== 'run',\n type: task.def.Type,\n why: task.action === 'noop' ? '' : `a stack task: the deploy will ${task.action} it`,\n});\n\n/**\n * The rows to report. Without `all`, only rows WITHOUT a state row — the adoptions and creates a\n * deploy would perform — which is the question this tool exists for, plus any task that runs. With\n * `all`, every declared row, every pending deletion and every task.\n */\nexport const rowsOf = (\n plan: PlanView,\n stateRows: ReadonlySet<string>,\n seen: Observations,\n all: boolean,\n): AdoptRow[] => {\n const declared = Object.entries(plan.resources)\n .filter(([fqn]) => all || !stateRows.has(fqn))\n .map(([fqn, node]) => rowFor(fqn, node, stateRows.has(fqn), seen));\n const removed = all\n ? Object.entries(plan.deletions).flatMap(([fqn, node]) =>\n node === undefined ? [] : [rowFor(fqn, node, true, seen)],\n )\n : [];\n const tasks = Object.entries(plan.actions ?? {}).flatMap(([fqn, task]) =>\n task === undefined || (!all && task.action === 'noop') ? [] : [taskRow(fqn, task)],\n );\n return [...declared, ...removed, ...tasks].sort((a, b) => a.fqn.localeCompare(b.fqn));\n};\n",
10
+ "/**\n * The providers a stack registered, wrapped so the plan can be watched and can never write.\n *\n * ★ WHY A WRAPPER AND NOT A LOG LINE. alchemy beta.79 Plan.ts loses the provider's answer on the one\n * path that matters: after the cold-start adoption probe it sets `forceUpdateAfterAdoption` and\n * maps a `noop` diff to `update`, and the node says `adopted` either way. Nothing is logged, and\n * `alchemy plan` has no JSON output — `describePlan` carries only the forced action. The engine\n * resolves every provider from the Effect context by resource type (Provider.ts\n * `tryFindProviderRegistrationByType`), so the one honest place to hear the raw answer is the\n * provider service itself. This hands Alchemy's own `Plan.make` a context whose providers record\n * what `read` and `diff` answered, per FQN, and otherwise behave exactly as registered.\n *\n * ⛔ EVERY WRITE PATH DIES. `reconcile`, `delete` and `precreate` are replaced, not wrapped: the\n * planner never calls them, and a verifier that could reach one by accident is worse than none.\n * `read`, `diff`, `list`, `tail` and `logs` pass through — each is a read by Alchemy's contract.\n *\n * ⚠️ A MISSING `diff` IS WRAPPED AS ONE ANSWERING `undefined`, which is exactly what the planner\n * does without one (`provider?.diff?.(…)` then its own props compare), so behaviour is unchanged\n * and the report can say `none` instead of guessing. A missing `read` stays missing: adding one\n * would make the planner probe where it otherwise would not.\n */\nimport { Unowned, stripUnowned } from 'alchemy/AdoptPolicy';\nimport {\n type ProviderCollectionService,\n type ProviderService,\n isProviderCollectionService,\n isProviderService,\n} from 'alchemy/Provider';\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\n\nexport type ReadAnswer = 'found' | 'unowned' | 'absent' | 'failed' | 'not-read';\nexport type DiffAnswer = 'noop' | 'update' | 'replace' | 'none' | 'not-run';\n\n/** What one FQN's provider answered during the plan — the LAST answer of each kind. */\nexport interface Seen {\n read?: { readonly answer: ReadAnswer; readonly attributes: unknown };\n diff?: { readonly answer: DiffAnswer; readonly news: unknown };\n}\n\n/** Keyed by FQN. Filled while the plan runs; read once it is done. */\nexport type Observations = Map<string, Seen>;\n\nconst seenFor = (seen: Observations, fqn: string): Seen => {\n const existing = seen.get(fqn);\n if (existing !== undefined) return existing;\n const fresh: Seen = {};\n seen.set(fqn, fresh);\n return fresh;\n};\n\n/** The refusal every write path answers with. Exported so a test can match it. */\nexport const WRITE_REFUSED = 'hf-adopt-verify never writes';\n\nconst refuse = (type: string, operation: string) => () =>\n Effect.die(\n new Error(\n `${WRITE_REFUSED}: ${type}.${operation} was called during a verification. Alchemy's planner ` +\n 'does not call it, so something other than Plan.make is driving these providers.',\n ),\n );\n\nconst readAnswer = (attributes: unknown): ReadAnswer =>\n attributes === undefined ? 'absent' : Unowned.is(attributes) ? 'unowned' : 'found';\n\nconst diffAnswer = (diff: unknown): DiffAnswer => {\n const action = (diff as { action?: unknown } | undefined)?.action;\n return action === 'noop' || action === 'update' || action === 'replace' ? action : 'none';\n};\n\n/**\n * One provider service, watched. ⚠️ The spread keeps `version`, `aliases`, `mode`, `stables`,\n * `list` and the rest exactly as registered — the planner reads several of them.\n */\nexport const spyService = (type: string, service: ProviderService, seen: Observations) => {\n const read = service.read;\n const diff = service.diff;\n const modes = service.modes;\n const watched: ProviderService = {\n ...service,\n diff: (input) =>\n (diff === undefined ? Effect.succeed(undefined) : diff(input)).pipe(\n Effect.tap((answer) =>\n Effect.sync(() => {\n seenFor(seen, input.fqn).diff = { answer: diffAnswer(answer), news: input.news };\n }),\n ),\n ),\n reconcile: refuse(type, 'reconcile'),\n delete: refuse(type, 'delete'),\n };\n if (read !== undefined) {\n watched.read = (input) =>\n read(input).pipe(\n Effect.tap((answer) =>\n Effect.sync(() => {\n const attributes = answer === undefined ? undefined : stripUnowned(answer);\n seenFor(seen, input.fqn).read = { answer: readAnswer(answer), attributes };\n }),\n ),\n );\n }\n if (service.precreate !== undefined) watched.precreate = refuse(type, 'precreate');\n // ⚠️ A `ProviderLayer.dual` registration resolves a per-mode variant at plan time\n // (Provider.ts `providerForMode`); wrapping only the registration would miss it.\n if (modes !== undefined) {\n watched.modes = {\n live: Effect.map(modes.live, (variant) => spyService(type, variant, seen)),\n local: Effect.map(modes.local, (variant) => spyService(type, variant, seen)),\n };\n }\n return watched;\n};\n\nconst spyCollection = (\n collection: ProviderCollectionService,\n seen: Observations,\n): ProviderCollectionService => {\n const providers = Object.fromEntries(\n Object.entries(collection.providers).map(([type, service]) => [\n type,\n spyService(type, service, seen),\n ]),\n );\n return { ...collection, get: (type: string) => providers[type] as never, providers };\n};\n\n/**\n * The same context with every provider service — direct or inside a `Provider.collection` —\n * replaced by its watched copy. Everything else (the state store, the stack, credentials) is the\n * very object the session built.\n */\nexport const spyContext = <R>(\n context: Context.Context<R>,\n seen: Observations,\n): Context.Context<R> => {\n const services = new Map<string, unknown>();\n for (const [key, value] of context.mapUnsafe) {\n services.set(\n key,\n isProviderService(value)\n ? spyService(key, value, seen)\n : isProviderCollectionService(value)\n ? spyCollection(value, seen)\n : value,\n );\n }\n return Context.makeUnsafe<R>(services);\n};\n"
11
+ ],
12
+ "mappings": "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",
13
+ "debugId": "B36621FDE54AA90464756E2164756E21",
14
+ "names": []
15
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * A test-only stack: Alchemy's real Plan and Apply over an in-memory state store, and a fake family
3
+ * whose "cloud" is a Map — so the verifier runs against the engine's own plans with no network.
4
+ *
5
+ * ★ THE ENGINE, NOT A HAND-BUILT PLAN. What the verifier claims is about alchemy beta.79: that an
6
+ * adopted row's `diff` answer is lost to `forceUpdateAfterAdoption`, that Apply then reconciles
7
+ * it, and that a watched plan never writes. openbao/fake-stack.ts measured the same engine for
8
+ * the Bao families; this one is family-neutral and records every provider call in order.
9
+ * ⛔ TEST-ONLY. No provider imports this file.
10
+ */
11
+ import * as Alchemy from 'alchemy';
12
+ import * as Effect from 'effect/Effect';
13
+ import * as Layer from 'effect/Layer';
14
+ import { type AdoptReport, type VerifyOptions } from './verify.ts';
15
+ export interface ThingProps {
16
+ name: string;
17
+ comment?: string;
18
+ }
19
+ export interface ThingAttributes {
20
+ name: string;
21
+ comment: string;
22
+ }
23
+ export interface Thing extends Alchemy.Resource<'Test.Thing', ThingProps, ThingAttributes> {
24
+ }
25
+ export declare const Thing: Alchemy.ResourceClass<Thing>;
26
+ /** The same object with no `diff`, like a provider that never wrote one. */
27
+ export interface Blind extends Alchemy.Resource<'Test.Blind', ThingProps, ThingAttributes> {
28
+ }
29
+ export declare const Blind: Alchemy.ResourceClass<Blind>;
30
+ /** The fake cloud: what exists, what is someone else's, and every call made to it, in order. */
31
+ export interface Cloud {
32
+ readonly live: Map<string, string>;
33
+ readonly unowned: Set<string>;
34
+ readonly calls: string[];
35
+ }
36
+ export declare const cloudOf: (live?: Record<string, string>) => Cloud;
37
+ /** Every call that would change the cloud. */
38
+ export declare const writesOf: (cloud: Cloud) => string[];
39
+ /** Both fake families over `cloud` — also what tests/fixtures/verify-stack.ts registers. */
40
+ export declare const thingProviders: (cloud: Cloud) => Layer.Layer<Alchemy.Provider<Blind> | Alchemy.Provider<Thing>, never, never>;
41
+ /** A stack body: resource declarations, as an `alchemy.run.ts` would write them. */
42
+ export type Body = Effect.Effect<unknown, unknown, unknown>;
43
+ export interface FakeEngine {
44
+ /** Plan and apply `body`, as `alchemy deploy --adopt` would; resolves to the planned actions. */
45
+ readonly deploy: (body: Body) => Promise<Record<string, string>>;
46
+ /** Verify `body` against what the earlier deploys recorded. */
47
+ readonly verify: (body: Body, options?: VerifyOptions) => Promise<AdoptReport>;
48
+ }
49
+ /**
50
+ * One stack over one state store and any providers, shared by every call — a family's own test
51
+ * hands in its real provider over a fake transport (proxmox/ceph-pool-adopt.test.ts).
52
+ * ⚠️ THE CASTS ARE AT THE ENGINE'S TYPED BOUNDARY — openbao/fake-stack.ts has the reasoning.
53
+ */
54
+ export declare const engineOver: <ROut, E, RIn>(layer: Layer.Layer<ROut, E, RIn>) => FakeEngine;
55
+ /** The fake family over `cloud`. */
56
+ export declare const fakeEngine: (cloud: Cloud) => FakeEngine;
57
+ //# sourceMappingURL=fake-engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-engine.d.ts","sourceRoot":"","sources":["../../src/verify/fake-engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAKnC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,aAAa,EAAiB,MAAM,aAAa,CAAC;AAElF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,KAAM,SAAQ,OAAO,CAAC,QAAQ,CAAC,YAAY,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AAC7F,eAAO,MAAM,KAAK,8BAAwC,CAAC;AAE3D,gGAAgG;AAChG,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,eAAO,MAAM,OAAO,UAAU,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,KAI1D,CAAC;AAEH,8CAA8C;AAC9C,eAAO,MAAM,QAAQ,UAAW,KAAK,KAAG,MAAM,EAC2C,CAAC;AAyB1F,4FAA4F;AAC5F,eAAO,MAAM,cAAc,UAAW,KAAK,iFAmBxC,CAAC;AAEJ,oFAAoF;AACpF,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAK5D,MAAM,WAAW,UAAU;IACzB,iGAAiG;IACjG,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACjE,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,KAAK,OAAO,CAAC,WAAW,CAAC,CAAC;CAChF;AAED;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,IAAI,EAAE,CAAC,EAAE,GAAG,SAAS,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAG,UAuC3E,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,UAAU,UAAW,KAAK,KAAG,UAA+C,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@homeflare/alchemy/verify` — prove a deploy's adoptions are no-ops before it runs.
3
+ *
4
+ * ★ ONE TOOL FOR EVERY STACK, NOT A TEST PER REPO. A stack runs `hf-adopt-verify --config
5
+ * alchemy.run.ts --stage live` before a gated deploy, or calls `verifyStack` from its own
6
+ * script; both plan with Alchemy's planner and never write. docs/adopt-verify.md has the why,
7
+ * the report's columns, and what each kit family does on an adopted deploy.
8
+ * ⛔ A SUBPATH OF ITS OWN: it imports the stack module from disk and reads the state store, which
9
+ * is host access, so it never rides along with the runtime-neutral root entry.
10
+ */
11
+ export { type Parsed, USAGE, parseVerifyArgs } from './args.ts';
12
+ export { exitCodeOf, formatReport } from './report.ts';
13
+ export { type AdoptRow, type NodeView, type PlanView, type TaskView, changedFields, rowsOf, } from './rows.ts';
14
+ export { type DiffAnswer, type Observations, type ReadAnswer, type Seen, WRITE_REFUSED, spyContext, } from './spy.ts';
15
+ export { type AdoptReport, type VerifyOptions, type VerifySession, type VerifyTarget, verifySession, verifyStack, } from './verify.ts';
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/verify/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAChE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACvD,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,KAAK,QAAQ,EACb,aAAa,EACb,MAAM,GACP,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,IAAI,EACT,aAAa,EACb,UAAU,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,aAAa,EACb,WAAW,GACZ,MAAM,aAAa,CAAC"}