slidemind 0.3.20 → 0.4.1

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 (187) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/AGENTS.md +592 -16
  4. package/LICENSE +4 -0
  5. package/README.md +203 -8
  6. package/THIRD-PARTY-NOTICES.md +201 -0
  7. package/bin/slidemind.mjs +50 -2
  8. package/package.json +5 -3
  9. package/skills/create-slides/SKILL.md +484 -886
  10. package/skills/search/SKILL.md +46 -23
  11. package/src/bridge/build-and-push.mjs +548 -73
  12. package/src/bridge/bundle.mjs +100 -31
  13. package/src/bridge/capture.mjs +14 -17
  14. package/src/bridge/client/brands.mjs +157 -18
  15. package/src/bridge/client/engine-routing.mjs +48 -13
  16. package/src/bridge/client/engine-search.mjs +24 -1
  17. package/src/bridge/client/render.mjs +27 -1
  18. package/src/bridge/client/transport.mjs +499 -23
  19. package/src/bridge/client.mjs +10 -0
  20. package/src/bridge/config.mjs +67 -6
  21. package/src/bridge/engine-download.mjs +0 -10
  22. package/src/bridge/graph-auth.mjs +55 -3
  23. package/src/bridge/image-intent-envelope.mjs +239 -0
  24. package/src/bridge/package-preview.mjs +67 -0
  25. package/src/bridge/sharepoint.mjs +914 -0
  26. package/src/builders/asset-fetch.mjs +203 -0
  27. package/src/builders/block-dividers.mjs +83 -0
  28. package/src/builders/build.mjs +171 -87
  29. package/src/builders/card-height.mjs +294 -0
  30. package/src/builders/chart-hole.mjs +183 -0
  31. package/src/builders/chart-jsx.mjs +410 -0
  32. package/src/builders/chart-labels.mjs +326 -0
  33. package/src/builders/concept-geometry.mjs +370 -0
  34. package/src/builders/concept-intent.mjs +200 -0
  35. package/src/builders/concept-layout.mjs +79 -0
  36. package/src/builders/concept-lines.mjs +509 -0
  37. package/src/builders/concept-lists.mjs +270 -0
  38. package/src/builders/concept-marks.mjs +84 -0
  39. package/src/builders/concept-relationships.mjs +12 -0
  40. package/src/builders/concept-spec.mjs +645 -0
  41. package/src/builders/concept-text.mjs +140 -0
  42. package/src/builders/concept-waves.mjs +57 -0
  43. package/src/builders/copy-fit.mjs +139 -0
  44. package/src/builders/design-tokens.mjs +347 -0
  45. package/src/builders/draft-check.mjs +552 -340
  46. package/src/builders/draft-lines.mjs +369 -0
  47. package/src/builders/edit-route.mjs +29 -0
  48. package/src/builders/figure-sketch.mjs +375 -0
  49. package/src/builders/fold-sets.mjs +639 -0
  50. package/src/builders/gate-server.mjs +9 -4
  51. package/src/builders/gate-session.js +273 -12
  52. package/src/builders/gate-shell.js +1494 -0
  53. package/src/builders/image-intent-check.mjs +308 -0
  54. package/src/builders/image-use-reconcile.mjs +107 -0
  55. package/src/builders/kit-helpers.mjs +121 -0
  56. package/src/builders/layout-names.mjs +32 -0
  57. package/src/builders/legacy-reference-uses.mjs +179 -0
  58. package/src/builders/line-fit.mjs +224 -0
  59. package/src/builders/line-native.mjs +895 -0
  60. package/src/builders/line-relations.mjs +230 -0
  61. package/src/builders/lint-fix.mjs +117 -9
  62. package/src/builders/lint.mjs +25 -20
  63. package/src/builders/list-jsx.mjs +88 -0
  64. package/src/builders/placement.mjs +133 -0
  65. package/src/builders/plan-checks.mjs +135 -0
  66. package/src/builders/plan-comments.mjs +19 -0
  67. package/src/builders/plan-handlers.mjs +95 -0
  68. package/src/builders/plan-markdown-edit.mjs +3 -3
  69. package/src/builders/plan-overlay.js +1530 -1608
  70. package/src/builders/plan.mjs +689 -186
  71. package/src/builders/preset-definitions.json +1 -0
  72. package/src/builders/preset-eval.mjs +244 -0
  73. package/src/builders/preview.html +51 -120
  74. package/src/builders/red-thread-backbone.mjs +135 -36
  75. package/src/builders/repair.mjs +205 -0
  76. package/src/builders/repeat-groups.mjs +540 -0
  77. package/src/builders/resolve-skill.mjs +5 -4
  78. package/src/builders/review-overlay.js +296 -293
  79. package/src/builders/review.mjs +305 -84
  80. package/src/builders/rule-shift.mjs +96 -0
  81. package/src/builders/seat.mjs +354 -0
  82. package/src/builders/settle-driver.mjs +236 -0
  83. package/src/builders/settle.mjs +1448 -0
  84. package/src/builders/slide-prep.mjs +67 -0
  85. package/src/builders/spec-assets.mjs +236 -0
  86. package/src/builders/spec-match.mjs +56 -0
  87. package/src/builders/stack.mjs +413 -0
  88. package/src/builders/storyboard-markdown.mjs +70 -27
  89. package/src/builders/storyboard-schema.mjs +54 -114
  90. package/src/builders/text-blocks.mjs +212 -0
  91. package/src/builders/text-card-fit.mjs +552 -0
  92. package/src/builders/text-stack.mjs +276 -0
  93. package/src/builders/transcribe.mjs +1069 -0
  94. package/src/cli/assets.mjs +164 -0
  95. package/src/cli/common.mjs +173 -12
  96. package/src/cli/concept-spec-support.mjs +76 -0
  97. package/src/cli/concepts.mjs +1002 -103
  98. package/src/cli/deck.mjs +401 -65
  99. package/src/cli/diagnose.mjs +4 -2
  100. package/src/cli/engine-helpers.mjs +17 -10
  101. package/src/cli/engine-results.mjs +76 -3
  102. package/src/cli/help.mjs +232 -28
  103. package/src/cli/install.mjs +9 -0
  104. package/src/cli/maintenance.mjs +104 -0
  105. package/src/cli/read.mjs +2 -1
  106. package/src/cli/ready.mjs +6 -3
  107. package/src/cli/record.mjs +522 -0
  108. package/src/cli/remote.mjs +818 -0
  109. package/src/cli/revision.mjs +686 -0
  110. package/src/cli/search.mjs +30 -11
  111. package/src/cli/session.mjs +56 -13
  112. package/src/cli/setup.mjs +46 -2
  113. package/src/cli/status.mjs +142 -33
  114. package/src/cli/tasks.mjs +246 -0
  115. package/src/cli/transcribe.mjs +854 -0
  116. package/src/cli/workspace-ui.mjs +260 -0
  117. package/src/cli/workspace.mjs +212 -34
  118. package/src/install-channel.mjs +64 -7
  119. package/src/lib/adopt.mjs +346 -25
  120. package/src/lib/concept-spec-binding.mjs +96 -0
  121. package/src/lib/conversions.mjs +70 -6
  122. package/src/lib/convert-folded.mjs +107 -0
  123. package/src/lib/deck-paths.mjs +418 -0
  124. package/src/lib/durable-ids.mjs +47 -0
  125. package/src/lib/edit-operations.mjs +316 -0
  126. package/src/lib/edit-state.mjs +1244 -0
  127. package/src/lib/edit-transitions.mjs +120 -0
  128. package/src/lib/extensions.mjs +501 -0
  129. package/src/lib/image-assets.mjs +465 -0
  130. package/src/lib/image-derivatives.mjs +234 -0
  131. package/src/lib/image-intent-fingerprint.mjs +70 -0
  132. package/src/lib/image-intent-tags.mjs +176 -0
  133. package/src/lib/image-intent.mjs +802 -0
  134. package/src/lib/image-use-overrides.mjs +279 -0
  135. package/src/lib/inline-components.mjs +613 -0
  136. package/src/lib/lint-converted.mjs +68 -4
  137. package/src/lib/merge-json.mjs +163 -0
  138. package/src/lib/overwrite-guard.mjs +3 -3
  139. package/src/lib/presentation-context.mjs +531 -0
  140. package/src/lib/preservation-tags.mjs +157 -0
  141. package/src/lib/preservation-verdict.mjs +1342 -0
  142. package/src/lib/publish.mjs +1557 -0
  143. package/src/lib/rebase.mjs +945 -0
  144. package/src/lib/reconcile-ack.mjs +4 -6
  145. package/src/lib/reconcile.mjs +62 -24
  146. package/src/lib/record-client.mjs +175 -0
  147. package/src/lib/record-state.mjs +151 -0
  148. package/src/lib/remote.mjs +661 -0
  149. package/src/lib/revision.mjs +1210 -0
  150. package/src/lib/slide-guids.mjs +4 -6
  151. package/src/lib/slide-preservation.mjs +423 -0
  152. package/src/lib/slide-selection.mjs +9 -0
  153. package/src/lib/workspace-transaction.mjs +117 -0
  154. package/src/pptx/content-digest.mjs +334 -0
  155. package/src/pptx/deck-assemble.mjs +1278 -0
  156. package/src/pptx/fill/engine.mjs +2 -3
  157. package/src/pptx/fill/freeform.mjs +6 -3
  158. package/src/pptx/ooxml.mjs +20 -0
  159. package/src/pptx/package-evidence.mjs +383 -0
  160. package/src/pptx/tags.mjs +163 -11
  161. package/src/update-check.mjs +22 -4
  162. package/src/workspace/adapters.mjs +222 -0
  163. package/src/workspace/auth.mjs +256 -0
  164. package/src/workspace/autostart.mjs +204 -0
  165. package/src/workspace/daemon.mjs +360 -0
  166. package/src/workspace/discovery.mjs +180 -0
  167. package/src/workspace/edit.mjs +167 -0
  168. package/src/workspace/events.mjs +52 -0
  169. package/src/workspace/gate-client.mjs +290 -0
  170. package/src/workspace/handback.mjs +73 -0
  171. package/src/workspace/lifecycle.mjs +635 -0
  172. package/src/workspace/operations.mjs +200 -0
  173. package/src/workspace/pick-folder.mjs +152 -0
  174. package/src/workspace/registry.mjs +359 -0
  175. package/src/workspace/remote-links.mjs +397 -0
  176. package/src/workspace/remote-status.mjs +359 -0
  177. package/src/workspace/routes.mjs +1331 -0
  178. package/src/workspace/server.mjs +159 -0
  179. package/src/workspace/sync.mjs +1119 -0
  180. package/src/workspace/task-actions.mjs +215 -0
  181. package/src/workspace/tasks.mjs +507 -0
  182. package/src/workspace/ui/app.js +1886 -0
  183. package/src/workspace/ui/index.html +141 -0
  184. package/src/workspace/ui/plan-bridge.js +120 -0
  185. package/src/workspace/ui/sharepoint.svg +59 -0
  186. package/src/workspace/ui/styles.css +423 -0
  187. package/src/builders/visual-families.mjs +0 -46
@@ -0,0 +1,215 @@
1
+ // What a task's answer DOES (tasks.md 4.4): the authoritative action behind
2
+ // `respondTask`'s `invoke`, per task kind and per deck owner.
3
+ //
4
+ // plan-edit done pack: validate (+ re-render) · core: respondPlan
5
+ // plan-approval approve pack: approve prepare → finalize · core: respondPlan
6
+ // request-changes the comment on the task; the record is untouched
7
+ // preview-review approve/comment core: respondPreview
8
+ // sync-conflict resolve sync.resolveConflict (7.5)
9
+ //
10
+ // "Done planning" and "Approve" are two tasks: done only proves the plan is
11
+ // structurally valid and refreshes the page; approve stamps the frozen approved
12
+ // plan and context through the pack. The pack, not this module, owns approval
13
+ // semantics — this module carries the subject hash in and the evidence out.
14
+ //
15
+ // Two answers need the network, and the network is never called under the deck
16
+ // lock (contracts §5): a SHARED approval and a conflict resolution. Inside the
17
+ // response transaction `invokeTaskAction` only prepares and answers
18
+ // `approval_requires_sync` / `conflict_requires_resolve`; `respondTaskAction`
19
+ // — the entry point routes call — then runs the coordinator (sync.mjs) with
20
+ // its own operations and records the evidence in a second, plain transaction.
21
+
22
+ import os from "node:os";
23
+ import path from "node:path";
24
+
25
+ import { loadPlanState, planSubjectHash, respondPlan } from "../builders/plan.mjs";
26
+ import { loadReviewState, previewHash, respondPreview } from "../builders/review.mjs";
27
+ import { resolveWorkspace } from "../lib/deck-paths.mjs";
28
+ import { readRecordState } from "../lib/record-state.mjs";
29
+ import { dispatchAdapter } from "./adapters.mjs";
30
+ import { coded } from "./registry.mjs";
31
+ import { approveShared, readSync, resolveConflict } from "./sync.mjs";
32
+ import { KINDS, getTask, respondTask, supersedeTask } from "./tasks.mjs";
33
+
34
+ export const CORE_ADAPTER = "core";
35
+
36
+ /** The actions a task of `kind` offers (contracts §4). Adapters do not extend the set in v1. */
37
+ export const taskActionsFor = (kind) => KINDS[kind] ?? null;
38
+
39
+ const blocked = (code, message, details) => ({ blocked: { code, message, ...(details ? { details } : {}) } });
40
+ const wsOf = (deckRoot) => resolveWorkspace(path.resolve(deckRoot));
41
+ // An installed pack (`entry`), or — as in sync.mjs — an in-process stand-in exposing the same methods.
42
+ const isPack = (adapter) => Boolean(adapter?.entry || typeof adapter?.read === "function");
43
+ const packCall = (adapter, { method, deckRoot, payload = {}, operationId, ownerToken }) => {
44
+ if (adapter.entry) return dispatchAdapter(adapter.entry, { method, deckRoot, payload, operationId, ownerToken });
45
+ const fn = adapter[{ "import-workbook": "importWorkbook" }[method] ?? method];
46
+ if (typeof fn !== "function") return Promise.resolve({ status: "error", error: { code: "extension_incompatible", message: `no ${method}` } });
47
+ return Promise.resolve(fn.call(adapter, payload, ownerToken)).then(({ ok, code, message, ...data }) => (ok ? { status: "ok", data } : { status: "blocked", error: { code: code ?? "validation_failed", message: message ?? code }, data }));
48
+ };
49
+
50
+ /** Who answered: a self-declared name from the page, else the OS account — never invented. */
51
+ export function taskActor(payload = {}, fallback = os.userInfo().username) {
52
+ const declared = typeof payload.approvedBy === "string" ? payload.approvedBy.trim() : "";
53
+ return declared ? { actor: declared, selfDeclared: true } : { actor: fallback, selfDeclared: false };
54
+ }
55
+
56
+ /**
57
+ * What a task is about, derived from the deck — never from the caller. A pack
58
+ * deck answers through its adapter's `read`; a core deck hashes plan.md, or the
59
+ * reviewed slides for preview-review (the same hash respondPreview checks).
60
+ */
61
+ export async function deriveTaskSubject({ deckRoot, adapter, kind }) {
62
+ if (isPack(adapter) && kind !== "preview-review") {
63
+ const res = await packCall(adapter, { method: "read", deckRoot });
64
+ if (res.status !== "ok") {
65
+ throw Object.assign(new Error(res.error?.message ?? `adapter read ${res.status}`), { code: res.error?.code ?? "extension_incompatible" });
66
+ }
67
+ return { adapterId: adapter.id, subject: { hash: res.data.hash, remoteETag: res.data.remoteETag ?? null } };
68
+ }
69
+ const ws = wsOf(deckRoot);
70
+ try {
71
+ const hash = kind === "preview-review" ? previewHash(ws) : planSubjectHash(ws);
72
+ return { adapterId: CORE_ADAPTER, subject: { hash, remoteETag: null } };
73
+ } catch (err) {
74
+ if (err?.code !== "ENOENT") throw err;
75
+ throw Object.assign(new Error("nothing to ask about yet: the plan does not exist"), { code: "validation_failed" });
76
+ }
77
+ }
78
+
79
+ // The generic gates answer `{ status, body }`; a task wants evidence or a refusal.
80
+ function fromGate(res, task, record) {
81
+ if (res.status === 200) return { evidence: { record, hash: res.body.hash } };
82
+ const e = res.body.error;
83
+ return blocked(e.code === "plan_missing" ? "stale_subject" : e.code, e.message, e.details);
84
+ }
85
+
86
+ async function packAction({ deckRoot, adapter, task, response, token, payload }) {
87
+ const call = (method, p) => packCall(adapter, { method, deckRoot, payload: p, operationId: response.operationId, ownerToken: token });
88
+ const current = await call("read", {});
89
+ if (current.status !== "ok") return blocked(current.error.code, current.error.message);
90
+ if (current.data.hash !== task.subject.hash) {
91
+ return blocked("stale_subject", "the plan changed since this task was requested", { current: current.data.hash, supersede: true });
92
+ }
93
+ switch (response.action) {
94
+ case "done": {
95
+ const v = await call("validate", {});
96
+ if (v.status !== "ok") return blocked(v.error.code, v.error.message, v.error.details);
97
+ if (!v.data.valid) return blocked("validation_failed", "the plan is not valid yet; fix the issues and mark it done again", { errors: v.data.errors });
98
+ const r = await call("render", { viewId: "plan" });
99
+ if (r.status !== "ok") return blocked(r.error.code, r.error.message);
100
+ return { evidence: { record: "plan", hash: task.subject.hash } };
101
+ }
102
+ case "request-changes":
103
+ return { evidence: { record: "task", hash: task.subject.hash } };
104
+ case "approve": {
105
+ const prep = await call("approve", { phase: "prepare", approvedBy: response.actor, expectedHash: task.subject.hash, approvedAt: response.at });
106
+ if (prep.status !== "ok") return blocked(prep.error.code, prep.error.message, { ...(prep.error.details ?? {}), supersede: prep.error.code === "stale_subject" });
107
+ if (prep.data.shared) {
108
+ // the network is not called under this lock: respondTaskAction runs approveShared with `prepared`, then records the evidence
109
+ return blocked("approval_requires_sync", "this plan is shared; the reviewed version is synchronized before it is approved", { prepared: prep.data.prepared });
110
+ }
111
+ const fin = await call("approve", { phase: "finalize", prepared: prep.data.prepared });
112
+ // finalize writes: a pack that refused wrote nothing, a pack that died may have — that outcome is unknown, so the intent stays for recovery
113
+ if (fin.status === "blocked") return blocked(fin.error.code, fin.error.message, fin.error.details);
114
+ if (fin.status !== "ok") throw Object.assign(new Error(`approval outcome unknown: ${fin.error.message}`), { code: "outcome_unknown", details: fin.error.details });
115
+ return { evidence: { record: `context.json#${fin.data.planHash}`, hash: task.subject.hash } };
116
+ }
117
+ default:
118
+ return blocked("not_implemented", `${response.action} has no workspace binding yet`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * `respondTask`'s invoke. `{ evidence }` when the action landed for exactly
124
+ * `task.subject.hash`; `{ blocked: { code, message, details } }` when nothing
125
+ * was written (stale subject, invalid plan, shared approval) — the task stays
126
+ * pending and its intent is cleared. `details.supersede` marks a subject that
127
+ * moved on, so the route voids the task.
128
+ */
129
+ export async function invokeTaskAction({ deckRoot, adapter, task, response, token, payload = {} }) {
130
+ if (task.kind === "sync-conflict") {
131
+ // the resolution itself needs the latest remote: respondTaskAction runs sync.resolveConflict outside this lock
132
+ const c = readSync(wsOf(deckRoot)).conflict;
133
+ if (!c || c.expectedHash !== task.subject.hash) return blocked("stale_subject", "this conflict is no longer the current one", { supersede: true, conflictId: c?.conflictId ?? null });
134
+ if (!payload.value || typeof payload.value !== "object") return blocked("validation_failed", "a resolution carries the whole resolved plan as payload.value");
135
+ return blocked("conflict_requires_resolve", "the resolution is committed against the latest shared record", { conflictId: c.conflictId });
136
+ }
137
+ if (isPack(adapter) && task.kind !== "preview-review") return packAction({ deckRoot, adapter, task, response, token, payload });
138
+ const ws = wsOf(deckRoot);
139
+ const args = { action: response.action, expectedHash: task.subject.hash, payload: { ...payload, comments: payload.comments ?? (response.comment ? [response.comment] : undefined) }, actor: response.actor, operationId: response.operationId, at: response.at };
140
+ if (task.kind === "preview-review") return fromGate(respondPreview(ws, args), task, "state/review.json");
141
+ const res = fromGate(respondPlan(ws, args), task, "state/plan-review.json");
142
+ if (res.blocked?.code === "stale_subject") res.blocked.details = { ...(res.blocked.details ?? {}), supersede: true };
143
+ return res;
144
+ }
145
+
146
+ /** The signed-in account a SHARED approval is attributed to: the binding's backend session, or `client.identity` when injected. Null when nobody is signed in. */
147
+ export function sharedIdentity(binding, { client = null, getSessionFor = null } = {}) {
148
+ if (typeof client?.identity === "string" && client.identity) return client.identity;
149
+ if (!binding?.backendURL || !getSessionFor) return null;
150
+ const name = getSessionFor(binding.backendURL)?.msGraphUsername;
151
+ return typeof name === "string" && name ? name : null;
152
+ }
153
+
154
+ /**
155
+ * The entry point for a task answer: `respondTask` with `invokeTaskAction`,
156
+ * plus the two answers whose action lives outside the response transaction.
157
+ *
158
+ * approve on a shared plan → attributed to the signed-in account (never a
159
+ * self-declared name; `login_required` without
160
+ * one), then sync.approveShared, then the
161
+ * evidence recorded
162
+ * resolve on a conflict → sync.resolveConflict, then the evidence recorded
163
+ *
164
+ * Throws what respondTask throws; a coordinator refusal is thrown with its code
165
+ * and `details.supersede` when the task was voided (the content moved on).
166
+ */
167
+ export async function respondTaskAction(deckRoot, id, request, { adapter, client = null, owner, verify, getSessionFor = null } = {}) {
168
+ const ws = wsOf(deckRoot);
169
+ const task = getTask(deckRoot, id);
170
+ const binding = readRecordState(ws);
171
+ const shared = Boolean(binding?.url || (binding?.driveId && binding?.itemId));
172
+ let req = { ...request };
173
+ if (task?.kind === "plan-approval" && req.action === "approve" && shared && isPack(adapter)) {
174
+ const identity = sharedIdentity(binding, { client, getSessionFor: getSessionFor ?? (await import("../bridge/config.mjs")).getSessionFor });
175
+ if (!identity) throw coded("login_required", "a shared plan is approved as the signed-in Microsoft 365 account; run `slidemind login` first");
176
+ if (req.selfDeclared && req.actor !== identity) throw coded("validation_failed", `a shared approval is attributed to the signed-in account (${identity}), not to a typed name`);
177
+ req = { ...req, actor: identity, selfDeclared: false };
178
+ }
179
+ const payload = req.payload ?? {};
180
+ const invoke = (t, response, { token }) => invokeTaskAction({ deckRoot, adapter, task: t, response, token, payload });
181
+ try {
182
+ return await respondTask(deckRoot, id, req, { invoke, owner, verify });
183
+ } catch (err) {
184
+ if (err?.code !== "approval_requires_sync" && err?.code !== "conflict_requires_resolve") throw err;
185
+ const response = { operationId: req.operationId, action: req.action, at: new Date().toISOString(), actor: req.actor, comment: payload.comment ?? null, ...(req.selfDeclared ? { selfDeclared: true } : {}) };
186
+ const current = getTask(deckRoot, id);
187
+ const out =
188
+ err.code === "approval_requires_sync"
189
+ ? await approveShared(ws, { adapter, client, owner, task: current, response })
190
+ : await resolveConflict(ws, { adapter, client, owner, conflictId: err.details.conflictId, expectedHash: req.expectedHash, value: payload.value });
191
+ if (!out.ok) {
192
+ if (out.supersede) await supersedeTask(deckRoot, id, { owner });
193
+ throw coded(out.code, out.message, { details: { ...out, supersede: out.supersede === true } });
194
+ }
195
+ if (out.task?.status === "completed") return { task: out.task, result: out.task.evidence, replayed: false };
196
+ const evidence = out.evidence ?? { record: "plan", hash: current.subject.hash };
197
+ return respondTask(deckRoot, id, req, { invoke: () => ({ evidence }), owner, verify });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Sync evidence check for `taskRecoverer`: did THIS operation land for exactly
203
+ * this subject? Core gates say so from their record; a pack deck cannot be read
204
+ * synchronously here, so it stays pending (never approved by a guess).
205
+ */
206
+ export function taskEvidenceVerifier({ deckRoot, adapter }) {
207
+ const ws = wsOf(deckRoot);
208
+ return (task, response) => {
209
+ if (isPack(adapter) && task.kind !== "preview-review") return null;
210
+ const s = task.kind === "preview-review" ? loadReviewState(ws) : loadPlanState(ws);
211
+ const hash = task.kind === "preview-review" ? s.deckHash : s.planHash;
212
+ if (s.operationId !== response.operationId || `sha256:${hash}` !== task.subject.hash) return null;
213
+ return { record: task.kind === "preview-review" ? "state/review.json" : "state/plan-review.json", hash: task.subject.hash };
214
+ };
215
+ }