@homeflare/alchemy 0.10.0 → 0.11.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 (70) hide show
  1. package/dist/caddy/index.js.map +2 -2
  2. package/dist/launchd/index.js.map +2 -2
  3. package/dist/openbao/index.js +10 -8
  4. package/dist/openbao/index.js.map +4 -4
  5. package/dist/ownership/adopting.d.ts +34 -0
  6. package/dist/ownership/adopting.d.ts.map +1 -0
  7. package/dist/ownership/probe.d.ts +15 -5
  8. package/dist/ownership/probe.d.ts.map +1 -1
  9. package/dist/ownership/rows.d.ts +12 -0
  10. package/dist/ownership/rows.d.ts.map +1 -1
  11. package/dist/ownership/store-fixture.d.ts +10 -0
  12. package/dist/ownership/store-fixture.d.ts.map +1 -0
  13. package/dist/proxmox/fake-pve-lxc.d.ts +8 -0
  14. package/dist/proxmox/fake-pve-lxc.d.ts.map +1 -1
  15. package/dist/proxmox/index.js +147 -113
  16. package/dist/proxmox/index.js.map +8 -6
  17. package/dist/proxmox/lxc-adoption.d.ts +32 -0
  18. package/dist/proxmox/lxc-adoption.d.ts.map +1 -0
  19. package/dist/proxmox/lxc-harness.d.ts +12 -0
  20. package/dist/proxmox/lxc-harness.d.ts.map +1 -1
  21. package/dist/proxmox/lxc.d.ts +5 -5
  22. package/dist/proxmox/lxc.d.ts.map +1 -1
  23. package/dist/verify/cli.js +79 -29
  24. package/dist/verify/cli.js.map +8 -6
  25. package/dist/verify/fake-engine.d.ts +9 -1
  26. package/dist/verify/fake-engine.d.ts.map +1 -1
  27. package/dist/verify/index.d.ts +1 -1
  28. package/dist/verify/index.d.ts.map +1 -1
  29. package/dist/verify/index.js +67 -17
  30. package/dist/verify/index.js.map +8 -6
  31. package/dist/verify/plan-scope.d.ts +14 -0
  32. package/dist/verify/plan-scope.d.ts.map +1 -0
  33. package/dist/verify/recheck.d.ts +42 -0
  34. package/dist/verify/recheck.d.ts.map +1 -0
  35. package/dist/verify/rows.d.ts +13 -3
  36. package/dist/verify/rows.d.ts.map +1 -1
  37. package/dist/verify/spy.d.ts +12 -0
  38. package/dist/verify/spy.d.ts.map +1 -1
  39. package/dist/verify/verify.d.ts.map +1 -1
  40. package/docs/adopt-verify.md +13 -2
  41. package/docs/adopted-deploys.md +4 -2
  42. package/docs/ownership.md +1 -1
  43. package/docs/proxmox-lxc-adopt.md +83 -0
  44. package/docs/proxmox-lxc.md +7 -45
  45. package/docs/proxmox.md +2 -0
  46. package/package.json +1 -1
  47. package/src/ownership/adopting.test.ts +63 -0
  48. package/src/ownership/adopting.ts +46 -0
  49. package/src/ownership/ownership.test.ts +1 -34
  50. package/src/ownership/probe.ts +27 -14
  51. package/src/ownership/rows.ts +8 -2
  52. package/src/ownership/store-fixture.ts +50 -0
  53. package/src/proxmox/ceph-pool-adopt.test.ts +32 -0
  54. package/src/proxmox/fake-pve-lxc.ts +13 -1
  55. package/src/proxmox/lxc-adopt.test.ts +13 -20
  56. package/src/proxmox/lxc-adoption.ts +50 -0
  57. package/src/proxmox/lxc-guards.test.ts +6 -13
  58. package/src/proxmox/lxc-harness.ts +25 -2
  59. package/src/proxmox/lxc-ownership.test.ts +19 -14
  60. package/src/proxmox/lxc-strict-adopt.test.ts +104 -0
  61. package/src/proxmox/lxc.ts +16 -12
  62. package/src/verify/fake-engine.ts +24 -0
  63. package/src/verify/index.ts +1 -0
  64. package/src/verify/plan-scope.ts +34 -0
  65. package/src/verify/recheck.test.ts +56 -0
  66. package/src/verify/recheck.ts +71 -0
  67. package/src/verify/rows.ts +46 -5
  68. package/src/verify/spy.ts +14 -3
  69. package/src/verify/verify.test.ts +22 -0
  70. package/src/verify/verify.ts +38 -25
@@ -1 +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"}
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;;;;GAIG;AACH,MAAM,WAAW,QAAS,SAAQ,OAAO,CAAC,QAAQ,CAAC,eAAe,EAAE,UAAU,EAAE,eAAe,CAAC;CAAG;AACnG,eAAO,MAAM,QAAQ,iCAA8C,CAAC;AAEpE,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,8GAmCxC,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"}
@@ -11,6 +11,6 @@
11
11
  export { type Parsed, USAGE, parseVerifyArgs } from './args.ts';
12
12
  export { exitCodeOf, formatReport } from './report.ts';
13
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';
14
+ export { type DiffAnswer, type Observations, type ReadAnswer, type RecheckAnswer, type Seen, WRITE_REFUSED, spyContext, } from './spy.ts';
15
15
  export { type AdoptReport, type VerifyOptions, type VerifySession, type VerifyTarget, verifySession, verifyStack, } from './verify.ts';
16
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +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"}
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,aAAa,EAClB,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"}
@@ -98,10 +98,22 @@ var changedFields = (declared, live) => {
98
98
  };
99
99
  var STILL_LIVE = new Set(["adopted", "noop"]);
100
100
  var isNoop = (planned, diff, stateRow) => STILL_LIVE.has(planned) && (diff === "noop" || diff === "none" && stateRow && planned === "noop");
101
+ var proven = (recheck) => recheck === undefined || recheck === "noop";
102
+ var notes = (row) => [
103
+ row.read === "unowned" ? "unowned: the deploy needs --adopt or adopt(true)" : "",
104
+ row.recheck === "noop" ? `${row.changed.join(", ")} differ, but the diff says noop even given the live values: ` + "the family does not manage them, and the deploy leaves them alone" : ""
105
+ ].filter((note) => note !== "").join("; ");
101
106
  var reason = (row, ok) => {
102
107
  if (ok)
103
- return row.read === "unowned" ? "unowned: the deploy needs --adopt or adopt(true)" : "";
108
+ return notes(row);
104
109
  if (isNoop(row.planned, row.diff, row.stateRow)) {
110
+ const live = `the live ${row.changed.join(", ")}`;
111
+ if (row.recheck === "failed") {
112
+ return `asked again with ${live} as recorded props, the provider's diff failed: not proven`;
113
+ }
114
+ if (!proven(row.recheck)) {
115
+ return `the provider's diff compares recorded props, not the live object: given ${live} it ` + `says ${String(row.recheck)}, and the deploy's forced reconcile writes`;
116
+ }
105
117
  return `${String(row.bindings)} binding(s) change: the deploy reconciles them though the diff is noop`;
106
118
  }
107
119
  if (row.planned === "delete")
@@ -126,6 +138,7 @@ var reason = (row, ok) => {
126
138
  };
127
139
  var rowFor = (fqn, node, stateRow, seen) => {
128
140
  const observed = seen.get(fqn);
141
+ const recheck = observed?.recheck;
129
142
  const base = {
130
143
  bindings: (node.bindings ?? []).filter((binding) => binding.action !== "noop").length,
131
144
  changed: changedFields(observed?.diff?.news, observed?.read?.attributes),
@@ -133,10 +146,11 @@ var rowFor = (fqn, node, stateRow, seen) => {
133
146
  fqn,
134
147
  planned: node.action,
135
148
  read: observed?.read?.answer ?? "not-read",
149
+ ...recheck === undefined ? {} : { recheck },
136
150
  stateRow,
137
151
  type: node.resource.Type
138
152
  };
139
- const ok = isNoop(base.planned, base.diff, stateRow) && base.bindings === 0;
153
+ const ok = isNoop(base.planned, base.diff, stateRow) && proven(recheck) && base.bindings === 0;
140
154
  return { ...base, ok, why: reason(base, ok) };
141
155
  };
142
156
  var taskRow = (fqn, task) => ({
@@ -187,7 +201,7 @@ var spyService = (type, service, seen) => {
187
201
  const watched = {
188
202
  ...service,
189
203
  diff: (input) => (diff === undefined ? Effect.succeed(undefined) : diff(input)).pipe(Effect.tap((answer) => Effect.sync(() => {
190
- seenFor(seen, input.fqn).diff = { answer: diffAnswer(answer), news: input.news };
204
+ seenFor(seen, input.fqn).diff = { answer: diffAnswer(answer), input, news: input.news };
191
205
  }))),
192
206
  reconcile: refuse(type, "reconcile"),
193
207
  delete: refuse(type, "delete")
@@ -224,6 +238,11 @@ var spyContext = (context, seen) => {
224
238
  };
225
239
  // src/verify/verify.ts
226
240
  import * as Alchemist from "alchemy/Alchemist";
241
+ import * as Plan from "alchemy/Plan";
242
+ import { State, isActionState } from "alchemy/State";
243
+ import * as Effect4 from "effect/Effect";
244
+
245
+ // src/verify/plan-scope.ts
227
246
  import {
228
247
  ArtifactStore,
229
248
  Artifacts,
@@ -231,38 +250,69 @@ import {
231
250
  makeScopedArtifacts
232
251
  } from "alchemy/Artifacts";
233
252
  import { InstanceId } from "alchemy/InstanceId";
234
- import * as Plan from "alchemy/Plan";
235
- import { State, isActionState } from "alchemy/State";
236
253
  import * as Effect2 from "effect/Effect";
237
254
  import * as Option from "effect/Option";
238
- var readWithState = (fqn, node) => Effect2.gen(function* () {
255
+ var inPlanScope = (fqn, instanceId) => (effect) => Effect2.gen(function* () {
256
+ const store = Option.getOrElse(yield* Effect2.serviceOption(ArtifactStore), createArtifactStore);
257
+ return yield* effect.pipe(Effect2.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect2.provideService(InstanceId, instanceId));
258
+ });
259
+
260
+ // src/verify/recheck.ts
261
+ import * as Effect3 from "effect/Effect";
262
+ var recheck = (fqn, node, seen) => Effect3.gen(function* () {
263
+ const observed = seen.get(fqn);
264
+ const first = observed?.diff;
265
+ const input = first?.input;
266
+ const diff = node.provider?.diff;
267
+ if (node.action !== "adopted" || first?.answer !== "noop" || input === undefined)
268
+ return;
269
+ const live = observed?.read?.attributes;
270
+ const changed = changedFields(input.news, live);
271
+ if (live === undefined || changed.length === 0)
272
+ return;
273
+ if (diff === undefined || node.state === undefined)
274
+ return;
275
+ const olds = {
276
+ ...input.olds,
277
+ ...Object.fromEntries(changed.map((key) => [key, live[key]]))
278
+ };
279
+ const asked = diff({ ...input, olds });
280
+ const answer = yield* asked.pipe(inPlanScope(fqn, node.state.instanceId), Effect3.map(diffAnswer), Effect3.catchCause(() => Effect3.succeed("failed")));
281
+ if (observed !== undefined) {
282
+ observed.diff = first;
283
+ observed.recheck = answer;
284
+ }
285
+ });
286
+
287
+ // src/verify/verify.ts
288
+ var readWithState = (fqn, node) => Effect4.gen(function* () {
239
289
  const read = node.provider?.read;
240
290
  if (read === undefined || node.state === undefined)
241
291
  return;
242
- const store = Option.getOrElse(yield* Effect2.serviceOption(ArtifactStore), createArtifactStore);
243
292
  yield* read({
244
293
  fqn,
245
294
  id: node.resource.LogicalId,
246
295
  instanceId: node.state.instanceId,
247
296
  olds: node.state.props,
248
297
  output: node.state.attr
249
- }).pipe(Effect2.provideService(Artifacts, makeScopedArtifacts(store, fqn)), Effect2.provideService(InstanceId, node.state.instanceId));
298
+ }).pipe(inPlanScope(fqn, node.state.instanceId));
250
299
  });
251
- var withState = (stack, fqns) => Effect2.gen(function* () {
300
+ var withState = (stack, nodes) => Effect4.gen(function* () {
252
301
  const store = yield* yield* State;
253
- 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 });
302
+ const found = yield* Effect4.all(Object.entries(nodes).map(([fqn, node]) => store.get({ fqn, stack: stack.name, stage: stack.stage }).pipe(Effect4.map((row) => row === undefined || isActionState(row) || row.instanceId !== node.state?.instanceId ? [] : [fqn]))), { concurrency: 8 });
254
303
  return new Set(found.flat());
255
304
  });
256
305
  var verifySession = (session, options = {}) => {
257
306
  const seen = new Map;
258
307
  const all2 = options.all === true;
259
308
  const plan = Plan.make(session.stack);
260
- return Effect2.gen(function* () {
309
+ return Effect4.gen(function* () {
261
310
  const planned = yield* plan;
262
- const stateRows = yield* withState(session.stack, Object.keys(planned.resources));
311
+ const stateRows = yield* withState(session.stack, planned.resources);
312
+ yield* Effect4.all(Object.entries(planned.resources).map(([fqn, node]) => recheck(fqn, node, seen)), { concurrency: 8 });
263
313
  if (all2) {
264
314
  const stateful = Object.entries(planned.resources).filter(([fqn]) => stateRows.has(fqn));
265
- yield* Effect2.all(stateful.map(([fqn, node]) => readWithState(fqn, node).pipe(Effect2.catchCause(() => Effect2.sync(() => {
315
+ yield* Effect4.all(stateful.map(([fqn, node]) => readWithState(fqn, node).pipe(Effect4.catchCause(() => Effect4.sync(() => {
266
316
  const entry = seen.get(fqn) ?? {};
267
317
  entry.read = { answer: "failed", attributes: undefined };
268
318
  seen.set(fqn, entry);
@@ -274,12 +324,12 @@ var verifySession = (session, options = {}) => {
274
324
  stack: session.stack.name,
275
325
  stage: session.stack.stage
276
326
  };
277
- }).pipe(Effect2.provide(spyContext(session.context, seen)), Effect2.scoped);
327
+ }).pipe(Effect4.provide(spyContext(session.context, seen)), Effect4.scoped);
278
328
  };
279
- var verifyStack = (target, options = {}) => Effect2.gen(function* () {
329
+ var verifyStack = (target, options = {}) => Effect4.gen(function* () {
280
330
  const session = yield* Alchemist.open(target, { adopt: true });
281
331
  return yield* verifySession(session, options);
282
- }).pipe(Effect2.scoped, Effect2.provide(Alchemist.layer()));
332
+ }).pipe(Effect4.scoped, Effect4.provide(Alchemist.layer()));
283
333
  export {
284
334
  USAGE,
285
335
  WRITE_REFUSED,
@@ -293,5 +343,5 @@ export {
293
343
  verifyStack
294
344
  };
295
345
 
296
- //# debugId=903C6B880AA12D2264756E2164756E21
346
+ //# debugId=495B7940B300E88664756E2164756E21
297
347
  //# sourceMappingURL=index.js.map
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../src/verify/args.ts", "../src/verify/report.ts", "../src/verify/rows.ts", "../src/verify/spy.ts", "../src/verify/verify.ts"],
3
+ "sources": ["../src/verify/args.ts", "../src/verify/report.ts", "../src/verify/rows.ts", "../src/verify/spy.ts", "../src/verify/verify.ts", "../src/verify/plan-scope.ts", "../src/verify/recheck.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\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",
6
6
  "/**\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",
7
- "/**\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",
8
- "/**\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",
9
- "/**\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"
7
+ "/**\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, RecheckAnswer } from './spy.ts';\n\nexport interface AdoptRow {\n readonly fqn: string;\n readonly type: string;\n /**\n * Whether the row plans from a row the state store holds for it (verify.ts `withState`). An\n * adoption is a row without one.\n */\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 is the verdict; a name here beside an\n * adopted `noop` only makes recheck.ts ask that diff again.\n */\n readonly changed: readonly string[];\n /**\n * What the provider's `diff` answered when asked again with the live values of `changed` as its\n * recorded props (recheck.ts). Absent when it was not asked: only an adopted `noop` with\n * something in `changed` is.\n */\n readonly recheck?: RecheckAnswer;\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\n/**\n * ⛔ AND A `noop` MUST SURVIVE BEING ASKED WITH THE LIVE VALUES. A diff that compares recorded props\n * answers an adopted row `noop` whatever the cloud holds; recheck.ts asks it again, and only a\n * second `noop` (or no need to ask) proves anything.\n */\nconst proven = (recheck: RecheckAnswer | undefined) => recheck === undefined || recheck === 'noop';\n\n/** Notes on a row that passes: nothing to say is the common case. */\nconst notes = (row: Omit<AdoptRow, 'ok' | 'why'>): string =>\n [\n row.read === 'unowned' ? 'unowned: the deploy needs --adopt or adopt(true)' : '',\n row.recheck === 'noop'\n ? `${row.changed.join(', ')} differ, but the diff says noop even given the live values: ` +\n 'the family does not manage them, and the deploy leaves them alone'\n : '',\n ]\n .filter((note) => note !== '')\n .join('; ');\n\nconst reason = (row: Omit<AdoptRow, 'ok' | 'why'>, ok: boolean): string => {\n if (ok) return notes(row);\n if (isNoop(row.planned, row.diff, row.stateRow)) {\n const live = `the live ${row.changed.join(', ')}`;\n if (row.recheck === 'failed') {\n return `asked again with ${live} as recorded props, the provider's diff failed: not proven`;\n }\n if (!proven(row.recheck)) {\n return (\n `the provider's diff compares recorded props, not the live object: given ${live} it ` +\n `says ${String(row.recheck)}, and the deploy's forced reconcile writes`\n );\n }\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 recheck = observed?.recheck;\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 ...(recheck === undefined ? {} : { recheck }),\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) && proven(recheck) && 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",
8
+ "/**\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/** What the diff answered when asked again with the live values as its recorded props (recheck.ts). */\nexport type RecheckAnswer = DiffAnswer | 'failed';\n\n/** Exactly what the planner handed a provider's `diff`. */\nexport type DiffInput = Parameters<NonNullable<ProviderService['diff']>>[0];\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 /**\n * ★ `input` IS THE WHOLE CALL, NOT ONLY `news`, so recheck.ts can ask the same question again\n * with one field changed. In memory only; the report carries field names, never values. An\n * observation built by hand without it (rows.ts's tests) is simply never rechecked.\n */\n diff?: { readonly answer: DiffAnswer; readonly news: unknown; readonly input?: DiffInput };\n recheck?: RecheckAnswer;\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\nexport const 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), input, 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",
9
+ "/**\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, and recheck.ts one more `diff` for an adopted `noop` whose read differs from\n * the declaration — both still reads 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 * 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 { inPlanScope } from './plan-scope.ts';\nimport { recheck } from './recheck.ts';\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, and the recheck to replay a diff. */\ntype StatefulNode = {\n readonly action: string;\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/** ⚠️ In the scope Plan.ts gives its own reads (plan-scope.ts). */\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 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(inPlanScope(fqn, node.state.instanceId));\n });\n\n/**\n * Which planned rows plan FROM a resource row the stage's state store holds for them.\n *\n * ⚠️ THE ROW MUST BE THE ONE THE NODE PLANS FROM, NOT MERELY A ROW AT THE SAME FQN. Alchemy lets a\n * rename hand its old id to a new resource in the same deploy (Rename.ts: `Bucket(\"Assets\")\n * .pipe(renamedFrom(\"Bucket\"))` beside a fresh `Bucket(\"Bucket\")`). The store still holds the\n * renamer's row at `Bucket`, so a check by FQN alone called the reuser stateful and the default\n * report hid its `create`. Matching the node's `state.instanceId` also sees the renamer, whose\n * row still sits at its FORMER FQN, as a row without state.\n */\nconst withState = (\n stack: CompiledStack<unknown, unknown>,\n nodes: Readonly<Record<string, StatefulNode>>,\n) =>\n Effect.gen(function* () {\n const store = yield* yield* State;\n const found = yield* Effect.all(\n Object.entries(nodes).map(([fqn, node]) =>\n store\n .get({ fqn, stack: stack.name, stage: stack.stage })\n .pipe(\n Effect.map((row) =>\n row === undefined ||\n isActionState(row) ||\n (row as { readonly instanceId?: unknown }).instanceId !== node.state?.instanceId\n ? []\n : [fqn],\n ),\n ),\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, planned.resources);\n // ★ Before the rows are judged: an adopted `noop` beside a differing read is asked again.\n yield* Effect.all(\n Object.entries(planned.resources).map(([fqn, node]) => recheck(fqn, node, seen)),\n { concurrency: 8 },\n );\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",
10
+ "/**\n * The scope Plan.ts gives every provider call it makes, for the calls the verifier makes itself\n * after the plan: `--all`'s reads (verify.ts) and the recheck's diffs (recheck.ts).\n *\n * ⚠️ RE-CREATED, BECAUSE Plan.ts DOES NOT EXPORT IT (`providePlanScope`, alchemy beta.79): a scoped\n * `Artifacts` bag over the run's `ArtifactStore` (a fresh one when none is provided) and the row's\n * `InstanceId`. A kit family that consults either — the ownership helpers do — sees what it\n * would see under `alchemy plan`. Moved here from verify.ts once a second caller needed it.\n */\nimport {\n ArtifactStore,\n Artifacts,\n createArtifactStore,\n makeScopedArtifacts,\n} from 'alchemy/Artifacts';\nimport { InstanceId } from 'alchemy/InstanceId';\nimport * as Effect from 'effect/Effect';\nimport * as Option from 'effect/Option';\n\nexport const inPlanScope =\n (fqn: string, instanceId: string) =>\n <A, E, R>(\n effect: Effect.Effect<A, E, R>,\n ): Effect.Effect<A, E, Exclude<R, Artifacts | InstanceId>> =>\n Effect.gen(function* () {\n const store = Option.getOrElse(\n yield* Effect.serviceOption(ArtifactStore),\n createArtifactStore,\n );\n return yield* effect.pipe(\n Effect.provideService(Artifacts, makeScopedArtifacts(store, fqn)),\n Effect.provideService(InstanceId, instanceId),\n );\n }) as Effect.Effect<A, E, Exclude<R, Artifacts | InstanceId>>;\n",
11
+ "/**\n * Ask an adopted row's `diff` a second time, with the live values as its recorded props, when its\n * first `noop` sits beside a read that differs from the declaration.\n *\n * 🔴 WHY (red-teamed 2026-09-21). Plan.ts hands an adopted row's diff `olds: news`, because the\n * adopted state carries `props: news`. A diff that compares the RECORDED props with the\n * declaration therefore answers `noop` whatever the cloud holds, and the forced reconcile then\n * writes. Alchemy's own `Cloudflare.R2Bucket` diff is that shape (beta.79\n * Cloudflare/R2/Bucket.ts, `olds.domains` against `news.domains`). The verifier said `ok`,\n * exited 0, and listed the drift under `changed` on the same line. rows.ts already refused to\n * trust a MISSING diff for this reason; a diff that never looks at the live object is the same\n * blind spot one level down.\n *\n * ★ THE QUESTION IS \"WOULD YOU STILL SAY noop IF STATE HAD RECORDED WHAT THE READ FOUND?\" It is the\n * planner's own input, replayed with one change: `olds` carries the read's value for each field\n * in `changed`. A diff that reads the live object ignores `olds` and answers as before. Every kit\n * PVE/PBS family re-reads (`pveOperations.diff`), so a field its `matches` leaves out, such as\n * CephPool's autoscaled `pg_num`, still passes. A diff that compares `olds` now sees the drift.\n *\n * ⛔ ONLY `diff`, a read by Alchemy's contract, and through the WATCHED provider, so every write\n * path still answers with spy.ts's refusal. The first answer is put back afterwards: the report's\n * `diff` column stays what the planner heard.\n * ⚠️ IT CAN ONLY TURN A PASS INTO A FAIL. It runs on a row that would otherwise pass (adopted,\n * `noop`, something in `changed`), and anything but a second `noop`, a failure included, fails it.\n * ⚠️ SAME-NAMED FIELDS ONLY, like `changed`: a drift in a field whose attribute has another name is\n * invisible to both. Whether a `reconcile` PUTs identical values anyway is still up to the\n * provider (docs/adopt-verify.md, Limits).\n */\nimport type { ProviderService } from 'alchemy/Provider';\nimport * as Effect from 'effect/Effect';\nimport { inPlanScope } from './plan-scope.ts';\nimport { changedFields } from './rows.ts';\nimport { type Observations, type RecheckAnswer, diffAnswer } from './spy.ts';\n\n/** The plan-node fields a recheck needs. */\nexport interface RecheckNode {\n readonly action: string;\n readonly provider?: ProviderService;\n readonly state?: { readonly instanceId: string };\n}\n\n/** Recheck one planned row if it needs it; the answer lands in `seen` as `recheck`. */\nexport const recheck = (fqn: string, node: RecheckNode, seen: Observations): Effect.Effect<void> =>\n Effect.gen(function* () {\n const observed = seen.get(fqn);\n const first = observed?.diff;\n const input = first?.input;\n const diff = node.provider?.diff;\n if (node.action !== 'adopted' || first?.answer !== 'noop' || input === undefined) return;\n const live = observed?.read?.attributes as Record<string, unknown> | undefined;\n const changed = changedFields(input.news, live);\n if (live === undefined || changed.length === 0) return;\n if (diff === undefined || node.state === undefined) return;\n const olds = {\n ...(input.olds as object),\n ...Object.fromEntries(changed.map((key) => [key, live[key]])),\n };\n // ⚠️ THE CAST IS THE ENGINE'S BOUNDARY, as in verify.ts: the diff's requirements are the\n // session's, which verifySession provides around this call.\n const asked = diff({ ...input, olds } as typeof input) as Effect.Effect<unknown>;\n const answer: RecheckAnswer = yield* asked.pipe(\n inPlanScope(fqn, node.state.instanceId),\n Effect.map(diffAnswer),\n Effect.catchCause(() => Effect.succeed('failed' as const)),\n );\n // The watched diff recorded the replay over the planner's answer; put the planner's back.\n if (observed !== undefined) {\n observed.diff = first;\n observed.recheck = answer;\n }\n });\n"
10
12
  ],
11
- "mappings": "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",
12
- "debugId": "903C6B880AA12D2264756E2164756E21",
13
+ "mappings": "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",
14
+ "debugId": "495B7940B300E88664756E2164756E21",
13
15
  "names": []
14
16
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The scope Plan.ts gives every provider call it makes, for the calls the verifier makes itself
3
+ * after the plan: `--all`'s reads (verify.ts) and the recheck's diffs (recheck.ts).
4
+ *
5
+ * ⚠️ RE-CREATED, BECAUSE Plan.ts DOES NOT EXPORT IT (`providePlanScope`, alchemy beta.79): a scoped
6
+ * `Artifacts` bag over the run's `ArtifactStore` (a fresh one when none is provided) and the row's
7
+ * `InstanceId`. A kit family that consults either — the ownership helpers do — sees what it
8
+ * would see under `alchemy plan`. Moved here from verify.ts once a second caller needed it.
9
+ */
10
+ import { Artifacts } from 'alchemy/Artifacts';
11
+ import { InstanceId } from 'alchemy/InstanceId';
12
+ import * as Effect from 'effect/Effect';
13
+ export declare const inPlanScope: (fqn: string, instanceId: string) => <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Artifacts | InstanceId>>;
14
+ //# sourceMappingURL=plan-scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan-scope.d.ts","sourceRoot":"","sources":["../../src/verify/plan-scope.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAEL,SAAS,EAGV,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,eAAO,MAAM,WAAW,QAChB,MAAM,cAAc,MAAM,MAC/B,CAAC,EAAE,CAAC,EAAE,CAAC,UACE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC7B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC,CAUM,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Ask an adopted row's `diff` a second time, with the live values as its recorded props, when its
3
+ * first `noop` sits beside a read that differs from the declaration.
4
+ *
5
+ * 🔴 WHY (red-teamed 2026-09-21). Plan.ts hands an adopted row's diff `olds: news`, because the
6
+ * adopted state carries `props: news`. A diff that compares the RECORDED props with the
7
+ * declaration therefore answers `noop` whatever the cloud holds, and the forced reconcile then
8
+ * writes. Alchemy's own `Cloudflare.R2Bucket` diff is that shape (beta.79
9
+ * Cloudflare/R2/Bucket.ts, `olds.domains` against `news.domains`). The verifier said `ok`,
10
+ * exited 0, and listed the drift under `changed` on the same line. rows.ts already refused to
11
+ * trust a MISSING diff for this reason; a diff that never looks at the live object is the same
12
+ * blind spot one level down.
13
+ *
14
+ * ★ THE QUESTION IS "WOULD YOU STILL SAY noop IF STATE HAD RECORDED WHAT THE READ FOUND?" It is the
15
+ * planner's own input, replayed with one change: `olds` carries the read's value for each field
16
+ * in `changed`. A diff that reads the live object ignores `olds` and answers as before. Every kit
17
+ * PVE/PBS family re-reads (`pveOperations.diff`), so a field its `matches` leaves out, such as
18
+ * CephPool's autoscaled `pg_num`, still passes. A diff that compares `olds` now sees the drift.
19
+ *
20
+ * ⛔ ONLY `diff`, a read by Alchemy's contract, and through the WATCHED provider, so every write
21
+ * path still answers with spy.ts's refusal. The first answer is put back afterwards: the report's
22
+ * `diff` column stays what the planner heard.
23
+ * ⚠️ IT CAN ONLY TURN A PASS INTO A FAIL. It runs on a row that would otherwise pass (adopted,
24
+ * `noop`, something in `changed`), and anything but a second `noop`, a failure included, fails it.
25
+ * ⚠️ SAME-NAMED FIELDS ONLY, like `changed`: a drift in a field whose attribute has another name is
26
+ * invisible to both. Whether a `reconcile` PUTs identical values anyway is still up to the
27
+ * provider (docs/adopt-verify.md, Limits).
28
+ */
29
+ import type { ProviderService } from 'alchemy/Provider';
30
+ import * as Effect from 'effect/Effect';
31
+ import { type Observations } from './spy.ts';
32
+ /** The plan-node fields a recheck needs. */
33
+ export interface RecheckNode {
34
+ readonly action: string;
35
+ readonly provider?: ProviderService;
36
+ readonly state?: {
37
+ readonly instanceId: string;
38
+ };
39
+ }
40
+ /** Recheck one planned row if it needs it; the answer lands in `seen` as `recheck`. */
41
+ export declare const recheck: (fqn: string, node: RecheckNode, seen: Observations) => Effect.Effect<void>;
42
+ //# sourceMappingURL=recheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recheck.d.ts","sourceRoot":"","sources":["../../src/verify/recheck.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAAE,KAAK,YAAY,EAAkC,MAAM,UAAU,CAAC;AAE7E,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,CAAC,EAAE,eAAe,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;CAClD;AAED,uFAAuF;AACvF,eAAO,MAAM,OAAO,QAAS,MAAM,QAAQ,WAAW,QAAQ,YAAY,KAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CA4B3F,CAAC"}
@@ -1,8 +1,11 @@
1
- import type { DiffAnswer, Observations, ReadAnswer } from './spy.ts';
1
+ import type { DiffAnswer, Observations, ReadAnswer, RecheckAnswer } from './spy.ts';
2
2
  export interface AdoptRow {
3
3
  readonly fqn: string;
4
4
  readonly type: string;
5
- /** Whether the state store holds a row for this FQN. An adoption is a row without one. */
5
+ /**
6
+ * Whether the row plans from a row the state store holds for it (verify.ts `withState`). An
7
+ * adoption is a row without one.
8
+ */
6
9
  readonly stateRow: boolean;
7
10
  /** The engine's action — what `alchemy plan` prints (`adopted`, `create`, `noop`, …). */
8
11
  readonly planned: string;
@@ -13,9 +16,16 @@ export interface AdoptRow {
13
16
  /**
14
17
  * Declared fields whose live value differs, by NAME only — values can be secrets. Compared on
15
18
  * same-named keys of the declaration and the read's attributes, so a family whose attributes
16
- * rename a field cannot show it here; the diff answer, not this list, is the verdict.
19
+ * rename a field cannot show it here. The diff answer is the verdict; a name here beside an
20
+ * adopted `noop` only makes recheck.ts ask that diff again.
17
21
  */
18
22
  readonly changed: readonly string[];
23
+ /**
24
+ * What the provider's `diff` answered when asked again with the live values of `changed` as its
25
+ * recorded props (recheck.ts). Absent when it was not asked: only an adopted `noop` with
26
+ * something in `changed` is.
27
+ */
28
+ readonly recheck?: RecheckAnswer;
19
29
  /** Bindings the engine will create, update or delete — reconcile applies them even on `noop`. */
20
30
  readonly bindings: number;
21
31
  readonly ok: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/verify/rows.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAErE,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,gGAAgG;IAChG,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5D;AAED,8FAA8F;AAC9F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAED,sDAAsD;AACtD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvD,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;CACnE;AAED,0EAA0E;AAC1E,eAAO,MAAM,aAAa,aAAc,OAAO,QAAQ,OAAO,KAAG,MAAM,EAStE,CAAC;AAqCF,eAAO,MAAM,MAAM,QACZ,MAAM,QACL,QAAQ,YACJ,OAAO,QACX,YAAY,KACjB,QAmBF,CAAC;AAmBF;;;;GAIG;AACH,eAAO,MAAM,MAAM,SACX,QAAQ,aACH,WAAW,CAAC,MAAM,CAAC,QACxB,YAAY,OACb,OAAO,KACX,QAAQ,EAaV,CAAC"}
1
+ {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../src/verify/rows.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEpF,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,iFAAiF;IACjF,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;IACjC,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,gGAAgG;IAChG,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,yDAAyD;AACzD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC5D;AAED,8FAA8F;AAC9F,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AAED,sDAAsD;AACtD,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvD,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;IACnE,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAAC,CAAC,CAAC;CACnE;AAED,0EAA0E;AAC1E,eAAO,MAAM,aAAa,aAAc,OAAO,QAAQ,OAAO,KAAG,MAAM,EAStE,CAAC;AAkEF,eAAO,MAAM,MAAM,QACZ,MAAM,QACL,QAAQ,YACJ,OAAO,QACX,YAAY,KACjB,QAqBF,CAAC;AAmBF;;;;GAIG;AACH,eAAO,MAAM,MAAM,SACX,QAAQ,aACH,WAAW,CAAC,MAAM,CAAC,QACxB,YAAY,OACb,OAAO,KACX,QAAQ,EAaV,CAAC"}
@@ -2,21 +2,33 @@ import { type ProviderService } from 'alchemy/Provider';
2
2
  import * as Context from 'effect/Context';
3
3
  export type ReadAnswer = 'found' | 'unowned' | 'absent' | 'failed' | 'not-read';
4
4
  export type DiffAnswer = 'noop' | 'update' | 'replace' | 'none' | 'not-run';
5
+ /** What the diff answered when asked again with the live values as its recorded props (recheck.ts). */
6
+ export type RecheckAnswer = DiffAnswer | 'failed';
7
+ /** Exactly what the planner handed a provider's `diff`. */
8
+ export type DiffInput = Parameters<NonNullable<ProviderService['diff']>>[0];
5
9
  /** What one FQN's provider answered during the plan — the LAST answer of each kind. */
6
10
  export interface Seen {
7
11
  read?: {
8
12
  readonly answer: ReadAnswer;
9
13
  readonly attributes: unknown;
10
14
  };
15
+ /**
16
+ * ★ `input` IS THE WHOLE CALL, NOT ONLY `news`, so recheck.ts can ask the same question again
17
+ * with one field changed. In memory only; the report carries field names, never values. An
18
+ * observation built by hand without it (rows.ts's tests) is simply never rechecked.
19
+ */
11
20
  diff?: {
12
21
  readonly answer: DiffAnswer;
13
22
  readonly news: unknown;
23
+ readonly input?: DiffInput;
14
24
  };
25
+ recheck?: RecheckAnswer;
15
26
  }
16
27
  /** Keyed by FQN. Filled while the plan runs; read once it is done. */
17
28
  export type Observations = Map<string, Seen>;
18
29
  /** The refusal every write path answers with. Exported so a test can match it. */
19
30
  export declare const WRITE_REFUSED = "hf-adopt-verify never writes";
31
+ export declare const diffAnswer: (diff: unknown) => DiffAnswer;
20
32
  /**
21
33
  * One provider service, watched. ⚠️ The spread keeps `version`, `aliases`, `mode`, `stables`,
22
34
  * `list` and the rest exactly as registered — the planner reads several of them.
@@ -1 +1 @@
1
- {"version":3,"file":"spy.d.ts","sourceRoot":"","sources":["../../src/verify/spy.ts"],"names":[],"mappings":"AAsBA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAG1C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAChF,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAE5E,uFAAuF;AACvF,MAAM,WAAW,IAAI;IACnB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IACrE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC;CAChE;AAED,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAU7C,kFAAkF;AAClF,eAAO,MAAM,aAAa,iCAAiC,CAAC;AAkB5D;;;GAGG;AACH,eAAO,MAAM,UAAU,SAAU,MAAM,WAAW,eAAe,QAAQ,YAAY,2IAsCpF,CAAC;AAeF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,WACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QACrB,YAAY,KACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAanB,CAAC"}
1
+ {"version":3,"file":"spy.d.ts","sourceRoot":"","sources":["../../src/verify/spy.ts"],"names":[],"mappings":"AAsBA,OAAO,EAEL,KAAK,eAAe,EAGrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAG1C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC;AAChF,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAC5E,uGAAuG;AACvG,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElD,2DAA2D;AAC3D,MAAM,MAAM,SAAS,GAAG,UAAU,CAAC,WAAW,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE5E,uFAAuF;AACvF,MAAM,WAAW,IAAI;IACnB,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;KAAE,CAAC;IACrE;;;;OAIG;IACH,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;IAC3F,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,sEAAsE;AACtE,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAU7C,kFAAkF;AAClF,eAAO,MAAM,aAAa,iCAAiC,CAAC;AAa5D,eAAO,MAAM,UAAU,SAAU,OAAO,KAAG,UAG1C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,UAAU,SAAU,MAAM,WAAW,eAAe,QAAQ,YAAY,2IAsCpF,CAAC;AAeF;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,CAAC,WACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,QACrB,YAAY,KACjB,OAAO,CAAC,OAAO,CAAC,CAAC,CAanB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/verify/verify.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAExC,OAAO,EAAE,KAAK,QAAQ,EAAyB,MAAM,WAAW,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;CACpC;AAED,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5C;AAkDD;;;GAGG;AACH,eAAO,MAAM,aAAa,YACf,aAAa,YACb,aAAa,KACrB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAoCpC,CAAC;AAEF,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,WACd,YAAY,YACX,aAAa,KACrB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAIsB,CAAC"}
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/verify/verify.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,KAAK,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC/C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAGxC,OAAO,EAAE,KAAK,QAAQ,EAAyB,MAAM,WAAW,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,yFAAyF;IACzF,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,CAAC;CACpC;AAED,wFAAwF;AACxF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;CAC5C;AA+DD;;;GAGG;AACH,eAAO,MAAM,aAAa,YACf,aAAa,YACb,aAAa,KACrB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAyCpC,CAAC;AAEF,wEAAwE;AACxE,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,WACd,YAAY,YACX,aAAa,KACrB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,OAAO,CAIsB,CAAC"}
@@ -70,6 +70,16 @@ adopted row whose provider has no `diff` is not proven: the engine compared the
70
70
  itself. A row with state and no diff is trusted at `noop`, as `alchemy plan` trusts it. A stack
71
71
  task (an Alchemy action) that will `run` is always a failing row: no provider sees it.
72
72
 
73
+ An adopted `noop` with something under `changed` is asked once more (`src/verify/recheck.ts`).
74
+ Plan.ts hands an adopted row's `diff` the declaration as its recorded props (`olds: news`), so a
75
+ diff that compares recorded props with the declaration answers `noop` whatever the cloud holds.
76
+ Alchemy's own `Cloudflare.R2Bucket` diff has that shape (`olds.domains` against `news.domains`).
77
+ The replay passes the read's values for the changed fields as `olds`. A diff that reads the live
78
+ object ignores them and says `noop` again, so the row passes with a note: the family does not
79
+ manage those fields (CephPool's autoscaled `pg_num`). Anything else fails the row, and the JSON
80
+ report carries the answer as `recheck`. Before this check, such a row passed with its drift
81
+ listed beside the `ok`.
82
+
73
83
  Exit codes: `0` all no-op (or nothing to verify), `1` any row is not, `2` the plan could not be
74
84
  computed (a provider died, an `adopt(false)` row is not ours, state unreadable). A gate should stop
75
85
  on anything but `0`.
@@ -97,8 +107,9 @@ a compiled stack (`src/verify/fake-engine.ts`).
97
107
  - ⚠️ **`noop` covers what the family compares.** A family's `diff` and its `reconcile` guard share
98
108
  one predicate (`matches`). A field that `matches` leaves out is never written on a no-op
99
109
  adoption and never reported. `Proxmox.CephPool` `target_size_ratio` is the known case.
100
- - ⚠️ **`changed` is a hint, not the verdict.** It compares same-named keys. A family whose
101
- attributes rename or normalise a field shows nothing there, and its `diff` still decides.
110
+ - ⚠️ **`changed` is not the verdict.** It compares same-named keys, and only decides whether an
111
+ adopted `noop` is asked again. A drift in a field the attributes rename or normalise shows
112
+ nothing there, and a diff that ignores the live object then goes unchallenged.
102
113
  - ⚠️ **Reads are what `alchemy plan` does.** A kit PVE/PBS read mints a lease through OpenBao,
103
114
  which creates a short-lived API token on the cluster. `--all` adds one read per row with state.
104
115
  - ⚠️ **Outside the kit, `noop` is only the diff's word.** For a kit PVE/PBS family, `noop` means
@@ -57,8 +57,10 @@ lease (`readRole`, resource.ts). That is still a read.
57
57
  ² The PVE notification endpoints: `smtp` (the mail target), `sendmail`, `gotify`, `webhook`. The
58
58
  secrets are never props (notification-target.ts), so no write is ever needed to "reset" them.
59
59
  ³ Rewritten 2026-09-21 (PR 80, [proxmox-lxc.md](./proxmox-lxc.md)): its own reconcile, and its
60
- probe answers `Unowned`, so the deploy needs `--adopt`. Its `diff` also logs the keys a deploy
61
- would write, by name. Pinned by `src/proxmox/lxc-adopt.test.ts` ("deploys with no write").
60
+ probe answers `Unowned`, so the deploy needs `--adopt`. An adoption that would write fails the
61
+ plan instead, naming the keys ([proxmox-lxc-adopt.md](./proxmox-lxc-adopt.md)), so only the
62
+ no-write row applies. Pinned by `src/proxmox/lxc-adopt.test.ts` ("deploys with no write") and
63
+ `src/proxmox/lxc-strict-adopt.test.ts`.
62
64
  ⁴ `updateBody` sends `target_size_ratio`, but `matches` does not compare it (float equality). A
63
65
  declared ratio that differs from the live one is never written on a no-op adoption and never
64
66
  reported as a diff.