@awebai/oats 0.23.1 → 0.24.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 (159) hide show
  1. package/README.md +7 -4
  2. package/bin/oats-pi-sdk-host.mjs +17 -0
  3. package/bin/oats.mjs +442 -51
  4. package/capabilities/oats-okf/bin/oats-okf-binding.mjs +14 -0
  5. package/capabilities/oats-okf/bin/oats-okf.mjs +79 -11
  6. package/capabilities/oats-okf/lib/binding-wire.mjs +268 -0
  7. package/capabilities/oats-okf/lib/captured-worker.mjs +101 -0
  8. package/capabilities/oats-okf/lib/config.mjs +2 -1
  9. package/capabilities/oats-okf/lib/inspection.mjs +16 -1
  10. package/capabilities/oats-okf/lib/invocation-context.mjs +111 -0
  11. package/capabilities/oats-okf/lib/invocation-shape.mjs +135 -0
  12. package/capabilities/oats-okf/lib/io.mjs +1 -1
  13. package/capabilities/oats-okf/lib/portable-binding.mjs +199 -0
  14. package/capabilities/oats-okf/lib/source-contract.mjs +46 -0
  15. package/capabilities/oats-okf/lib/sources.mjs +123 -3
  16. package/capabilities/oats-okf/lib/stores.mjs +104 -25
  17. package/capabilities/oats-okf/lib/worker.mjs +69 -10
  18. package/capabilities/oats-okf/oats.json +35 -7
  19. package/capabilities/oats-okf/schemas/okf-portable-declaration.schema.json +87 -0
  20. package/capabilities/oats-okf/schemas/okf-portable-payload.schema.json +113 -0
  21. package/capabilities/oats-okf/skills/memory-harvest/SKILL.md +23 -5
  22. package/capabilities/oats-okf/skills/okf/SKILL.md +42 -1
  23. package/docs/artifact-approvals.schema.json +7 -0
  24. package/docs/capability-manifest.schema.json +37 -66
  25. package/docs/captured-invocation-context.schema.json +7 -0
  26. package/docs/captured-resolution.schema.json +7 -0
  27. package/docs/design/2026-09-14-artifact-retention-contract.md +190 -0
  28. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +708 -0
  29. package/docs/design/2026-09-14-portable-souls-contract-amendments.md +85 -0
  30. package/docs/design/2026-09-14-portable-souls-explainer.md +750 -0
  31. package/docs/design/2026-09-15-captured-dispatch.md +127 -0
  32. package/docs/design/2026-09-15-captured-resolution-records.md +143 -0
  33. package/docs/design/2026-09-15-package-preparation.md +100 -0
  34. package/docs/design/2026-09-15-portable-data-contract.md +121 -0
  35. package/docs/design/2026-09-15-portable-declarations.md +189 -0
  36. package/docs/design/2026-09-15-portable-souls-handoff.md +150 -0
  37. package/docs/design/2026-09-15-portable-souls-implementation.md +417 -0
  38. package/docs/design/2026-09-15-selection-lock-and-approval.md +122 -0
  39. package/docs/design/2026-09-15-source-observation.md +119 -0
  40. package/docs/design/2026-09-16-captured-admission.md +77 -0
  41. package/docs/design/2026-09-16-captured-helper-dispatch.md +105 -0
  42. package/docs/design/2026-09-16-captured-launch-inputs.md +42 -0
  43. package/docs/design/2026-09-16-command-profile-preparation.md +86 -0
  44. package/docs/design/2026-09-16-fresh-install-first-rollout.md +47 -0
  45. package/docs/design/2026-09-16-fresh-operator-walkthrough.md +277 -0
  46. package/docs/design/2026-09-16-knowledge-capability-contract.md +61 -0
  47. package/docs/design/2026-09-16-messaging-capability-contract.md +59 -0
  48. package/docs/design/2026-09-16-portable-migration-evidence.md +156 -0
  49. package/docs/design/2026-09-16-portable-onboarding.md +177 -0
  50. package/docs/design/2026-09-16-prepare-request-transport.md +26 -0
  51. package/docs/design/2026-09-16-provider-binding-codecs.md +98 -0
  52. package/docs/design/2026-09-16-provider-binding-wire.md +247 -0
  53. package/docs/design/2026-09-17-capability-helper-input-contract.md +95 -0
  54. package/docs/design/2026-09-17-captured-backend-parity.md +53 -0
  55. package/docs/design/2026-09-17-captured-native-start.md +58 -0
  56. package/docs/design/2026-09-17-portable-boundary-hookup.md +19 -0
  57. package/docs/design/2026-09-17-portable-boundary-resources.md +52 -0
  58. package/docs/design/2026-09-17-public-captured-start.md +108 -0
  59. package/docs/design/2026-09-17-public-prepare-request.md +90 -0
  60. package/docs/design/2026-09-18-captured-pi-host.md +205 -0
  61. package/docs/design/2026-09-18-first-cut-release-checklist.md +131 -0
  62. package/docs/design/2026-09-18-herdr-protocol-compatibility.md +60 -0
  63. package/docs/desktop-cli-api.md +5 -2
  64. package/docs/execution-capsule.schema.json +108 -0
  65. package/docs/execution-targets.md +20 -7
  66. package/docs/oats-lock-v3.schema.json +7 -0
  67. package/docs/oats-member.schema.json +38 -0
  68. package/docs/oats-workspace.schema.json +68 -0
  69. package/docs/portable.schema.json +2512 -0
  70. package/docs/provider-check-input.schema.json +7 -0
  71. package/docs/release-notes/v0.23.2.md +49 -0
  72. package/docs/release-notes/v0.24.0.md +104 -0
  73. package/docs/schedules.md +126 -14
  74. package/docs/soul.schema.json +82 -0
  75. package/injects/oats-portable.md +17 -0
  76. package/injects/portable-instance-boundary.md +39 -0
  77. package/injects/portable-work-directory.md +29 -0
  78. package/lib/artifact-approvals.mjs +120 -0
  79. package/lib/artifact-tree.mjs +141 -0
  80. package/lib/capability-artifacts.mjs +179 -0
  81. package/lib/capability-execution.mjs +15 -0
  82. package/lib/capability-inputs.mjs +39 -0
  83. package/lib/capability-provenance.mjs +231 -0
  84. package/lib/captured-action-shape.mjs +21 -0
  85. package/lib/captured-admission-shape.mjs +20 -0
  86. package/lib/captured-binding-file.mjs +36 -0
  87. package/lib/captured-dispatch.mjs +66 -0
  88. package/lib/captured-instance-index.mjs +277 -0
  89. package/lib/captured-invocation-context.mjs +130 -0
  90. package/lib/captured-launch-request.mjs +46 -0
  91. package/lib/captured-operation-process.mjs +15 -0
  92. package/lib/captured-pi-custody.mjs +29 -0
  93. package/lib/captured-pi-host.mjs +167 -0
  94. package/lib/captured-pi-outcome.mjs +172 -0
  95. package/lib/captured-resolutions.mjs +275 -0
  96. package/lib/captured-scaffold.mjs +87 -0
  97. package/lib/captured-selector.mjs +28 -0
  98. package/lib/captured-session-backend.mjs +52 -0
  99. package/lib/captured-source-receipt-file.mjs +72 -0
  100. package/lib/config-data.mjs +104 -0
  101. package/lib/core.mjs +918 -562
  102. package/lib/errors.mjs +7 -0
  103. package/lib/helper-injection-policy.mjs +98 -0
  104. package/lib/herdr.mjs +18 -7
  105. package/lib/instruction-composition.mjs +31 -0
  106. package/lib/legacy-lock-codec.mjs +106 -0
  107. package/lib/manifest-settings.mjs +84 -0
  108. package/lib/package-closure.mjs +48 -0
  109. package/lib/package-materialization.mjs +83 -0
  110. package/lib/pi-sdk-host.mjs +229 -0
  111. package/lib/portable-artifacts.mjs +115 -0
  112. package/lib/portable-choices.mjs +82 -0
  113. package/lib/portable-composition.mjs +136 -0
  114. package/lib/portable-digest.mjs +105 -0
  115. package/lib/portable-files.mjs +26 -0
  116. package/lib/portable-identity.mjs +40 -0
  117. package/lib/portable-lock.mjs +117 -0
  118. package/lib/portable-migration-artifacts.mjs +135 -0
  119. package/lib/portable-migration-evidence.mjs +305 -0
  120. package/lib/portable-migration-store.mjs +199 -0
  121. package/lib/portable-migration.mjs +104 -0
  122. package/lib/portable-onboarding-acceptance.mjs +66 -0
  123. package/lib/portable-onboarding-request.mjs +49 -0
  124. package/lib/portable-onboarding.mjs +230 -0
  125. package/lib/portable-package-preparation.mjs +188 -0
  126. package/lib/portable-policy.mjs +44 -0
  127. package/lib/portable-shape.mjs +35 -0
  128. package/lib/portable-soul.mjs +38 -0
  129. package/lib/portable-state.mjs +80 -0
  130. package/lib/portable-values.mjs +181 -0
  131. package/lib/prepare-composition.mjs +151 -0
  132. package/lib/prepared-bindings.mjs +78 -0
  133. package/lib/prepared-resources.mjs +127 -0
  134. package/lib/provider-binding-broker.mjs +59 -0
  135. package/lib/provider-binding-wire.mjs +110 -0
  136. package/lib/provider-binding.mjs +22 -0
  137. package/lib/repository-observation.mjs +226 -0
  138. package/lib/resolution-shape.mjs +393 -0
  139. package/lib/schedule-capsule.mjs +206 -0
  140. package/lib/schedule.mjs +259 -38
  141. package/lib/servers.mjs +15 -0
  142. package/lib/soul-constraints.mjs +40 -0
  143. package/lib/source-projection.mjs +84 -0
  144. package/lib/source-spec.mjs +189 -0
  145. package/lib/workspace-definition.mjs +126 -0
  146. package/lib/workspace-discovery.mjs +146 -0
  147. package/package-catalog.json +1 -1
  148. package/package.json +3 -2
  149. package/packages/record/lib/capture-cc.mjs +14 -6
  150. package/packages/record/lib/formats.mjs +14 -3
  151. package/packages/record/lib/native-history.mjs +277 -7
  152. package/packages/record/lib/session-snapshot.mjs +25 -5
  153. package/packages/record/lib/sessions-for-home.mjs +30 -13
  154. package/skills/oats/SKILL.md +12 -7
  155. package/skills/oats-config/SKILL.md +12 -9
  156. package/skills/oats-packages/SKILL.md +12 -8
  157. package/skills/oats-portable/SKILL.md +116 -0
  158. package/skills/oats-portable-artifacts/SKILL.md +63 -0
  159. package/skills/oats-portable-setup/SKILL.md +69 -0
@@ -0,0 +1,305 @@
1
+ /** Read-only historical migration evidence inventory.
2
+ *
3
+ * Inputs are an explicit bounded target list supplied by the deployment owner.
4
+ * This module does not discover topology, resolve current config/source, verify
5
+ * legacy lock semantics, execute providers, or write migration state. */
6
+ import { lstatSync, realpathSync } from "node:fs";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+ import { readPortableBytes } from "./portable-files.mjs";
9
+ import { bytesIntegrity, validateIntegrity, BYTES_FORMAT } from "./portable-digest.mjs";
10
+ import { canonicalJson, compareUtf8, parseStrictJson } from "./portable-values.mjs";
11
+ import { portableScope } from "./portable-state.mjs";
12
+ import { validateExecutionBinding, validateExecutionCapsule, validateExecutionTemplate } from "./schedule-capsule.mjs";
13
+ import { oatsError } from "./errors.mjs";
14
+
15
+ export const MIGRATION_INVENTORY_VERSION = 1;
16
+ const MAX_TARGETS = 10_000;
17
+ const SHA256 = /^sha256-[a-f0-9]{64}$/;
18
+
19
+ const plain = (value) => !!value && typeof value === "object" && !Array.isArray(value);
20
+ const problem = (code, message, pointer = "") => ({ code, message, pointer });
21
+ const errorProblem = (error, pointer = "") => problem(error?.code || "invalid-evidence", String(error?.message || error), pointer);
22
+
23
+ function canonicalTarget(deployment, input, kind, { allowRoot = false } = {}) {
24
+ if (typeof input !== "string" || !input || input.includes("\0")) throw oatsError("invalid-declaration", `${kind} target must be a non-empty path without NUL`);
25
+ const lexical = resolve(deployment, input);
26
+ const part = relative(deployment, lexical);
27
+ if ((!allowRoot && !part) || part === ".." || part.startsWith(`..${sep}`) || isAbsolute(part)) {
28
+ throw oatsError("path-escape", `${kind} target is outside the explicit migration deployment`);
29
+ }
30
+ let parent;
31
+ try { parent = realpathSync(dirname(lexical)); }
32
+ catch (error) {
33
+ if (error.code === "ENOENT") return { absolute: lexical, path: part || ".", parentMissing: true };
34
+ throw error;
35
+ }
36
+ if (parent !== dirname(lexical)) throw oatsError("path-escape", `${kind} target has a symlinked or aliased parent`);
37
+ return { absolute: lexical, path: part || ".", parentMissing: false };
38
+ }
39
+
40
+ function targetDirectory(deployment, input, kind, { allowRoot = false } = {}) {
41
+ const target = canonicalTarget(deployment, input, kind, { allowRoot });
42
+ if (target.parentMissing) return target;
43
+ let stat;
44
+ try { stat = lstatSync(target.absolute); }
45
+ catch (error) { if (error.code === "ENOENT") return { ...target, missing: true }; throw error; }
46
+ if (!stat.isDirectory() || realpathSync(target.absolute) !== target.absolute) {
47
+ throw oatsError("path-escape", `${kind} target must be a real, non-symlinked directory`);
48
+ }
49
+ return target;
50
+ }
51
+
52
+ function uniqueTargets(deployment, values, kind, options) {
53
+ if (!Array.isArray(values) || values.length > MAX_TARGETS || values.some((value) => typeof value !== "string")) {
54
+ throw oatsError("resource-limit", `${kind} target list must be an array of at most ${MAX_TARGETS} paths`);
55
+ }
56
+ const targets = values.map((value) => options?.directory
57
+ ? targetDirectory(deployment, value, kind, options)
58
+ : canonicalTarget(deployment, value, kind, options));
59
+ targets.sort((a, b) => compareUtf8(a.path, b.path));
60
+ for (let i = 1; i < targets.length; i++) if (targets[i - 1].path === targets[i].path) {
61
+ throw oatsError("invalid-declaration", `duplicate ${kind} migration target ${targets[i].path}`);
62
+ }
63
+ return targets;
64
+ }
65
+
66
+ function readDocument(target) {
67
+ if (target.parentMissing) return { receipt: { path: target.path, state: "absent" }, value: null, bytes: null };
68
+ const bytes = readPortableBytes(target.absolute, { allowMissing: true, invalidCode: "invalid-evidence" });
69
+ if (bytes === null) return { receipt: { path: target.path, state: "absent" }, value: null, bytes: null };
70
+ const integrity = bytesIntegrity(bytes);
71
+ try {
72
+ return { receipt: { path: target.path, state: "present", integrity }, value: parseStrictJson(bytes), bytes };
73
+ } catch (error) {
74
+ return { receipt: { path: target.path, state: "invalid", integrity, problems: [errorProblem(error)] }, value: null, bytes };
75
+ }
76
+ }
77
+
78
+ function mapIds(value, key, pointer, problems) {
79
+ const map = value?.[key];
80
+ if (map === undefined) return [];
81
+ if (!plain(map)) { problems.push(problem("invalid-evidence", `${key} must be an object map`, pointer)); return []; }
82
+ return Object.keys(map).sort(compareUtf8);
83
+ }
84
+
85
+ function summarizeLock(target, legacyLockDecoder) {
86
+ const { receipt, value, bytes } = readDocument(target);
87
+ if (receipt.state !== "present") return { ...receipt, format: "unknown", rowCounts: { packages: 0, capabilities: 0 }, rowIds: { packages: [], capabilities: [] }, legacyTrustClaims: 0, semanticVerification: "required" };
88
+ const problems = [];
89
+ if (!plain(value)) problems.push(problem("invalid-evidence", "historical lock root is not an object", ""));
90
+ const rawVersion = plain(value) ? value.lockfileVersion : undefined;
91
+ const version = rawVersion === undefined ? 1 : rawVersion;
92
+ const format = version === 1 ? "legacy-v1" : version === 2 ? "materialized-v2" : version === 3 ? "portable-v3" : "unknown";
93
+ if (!Number.isSafeInteger(version)) problems.push(problem("invalid-evidence", "lockfileVersion is not an integer", "/lockfileVersion"));
94
+ else if (![1, 2, 3].includes(version)) problems.push(problem("unsupported-wire-version", `unsupported historical lockfileVersion ${version}`, "/lockfileVersion"));
95
+ const rowIds = {
96
+ packages: plain(value) ? mapIds(value, "packages", "/packages", problems) : [],
97
+ capabilities: plain(value) ? mapIds(value, "capabilities", "/capabilities", problems) : [],
98
+ };
99
+ const capabilityRows = plain(value?.capabilities) ? Object.values(value.capabilities) : [];
100
+ const legacyTrustClaims = capabilityRows.filter((row) => plain(row) && (row.trusted === true || row.trustedExecutables === true)).length;
101
+ const rowCounts = { packages: rowIds.packages.length, capabilities: rowIds.capabilities.length };
102
+ let semanticVerification = version === 3 ? "current-reader-required" : "legacy-reader-required";
103
+ if ((version === 1 || version === 2) && legacyLockDecoder) {
104
+ try {
105
+ const decoded = legacyLockDecoder(Buffer.from(bytes), Object.freeze({ path: receipt.path, version }));
106
+ if (!plain(decoded) || decoded.version !== version) throw oatsError("invalid-lock", "legacy lock decoder returned a different version");
107
+ const expectedPackages = version === 2 ? rowIds.packages : [];
108
+ const expectedCapabilities = rowIds.capabilities;
109
+ const packages = plain(decoded.packages) ? Object.keys(decoded.packages).sort(compareUtf8) : [];
110
+ const capabilitiesSource = version === 1 ? decoded.legacyCapabilities : decoded.capabilities;
111
+ const capabilities = plain(capabilitiesSource) ? Object.keys(capabilitiesSource).sort(compareUtf8) : [];
112
+ if (canonicalJson(packages) !== canonicalJson(expectedPackages) || canonicalJson(capabilities) !== canonicalJson(expectedCapabilities)) {
113
+ throw oatsError("invalid-lock", "legacy lock decoder changed the historical row identity set");
114
+ }
115
+ semanticVerification = "verified";
116
+ } catch (error) { problems.push(errorProblem(error)); }
117
+ }
118
+ return { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}), format, rowCounts, rowIds, legacyTrustClaims, semanticVerification };
119
+ }
120
+
121
+ function bindingSummary(value, problems) {
122
+ if (!Object.hasOwn(value, "executionBinding")) return { state: "absent" };
123
+ try {
124
+ validateExecutionBinding(value.executionBinding);
125
+ return { state: "valid-shape", value: JSON.parse(canonicalJson(value.executionBinding)) };
126
+ } catch (error) {
127
+ problems.push(errorProblem(error, "/executionBinding"));
128
+ return { state: "invalid" };
129
+ }
130
+ }
131
+
132
+ function capabilitySummaries(value, pointer, problems) {
133
+ if (value === undefined) return [];
134
+ if (!Array.isArray(value)) { problems.push(problem("invalid-evidence", "capabilityRuntime must be an array", pointer)); return []; }
135
+ const out = [];
136
+ for (let index = 0; index < value.length; index++) {
137
+ const row = value[index], at = `${pointer}/${index}`;
138
+ if (!plain(row) || typeof row.id !== "string" || !row.id) { problems.push(problem("invalid-evidence", "capability runtime row lacks an identity", at)); continue; }
139
+ const integrity = typeof row.trust?.integrity === "string" && SHA256.test(row.trust.integrity) ? row.trust.integrity : null;
140
+ const hookNames = plain(row.hooks) ? Object.keys(row.hooks).sort(compareUtf8) : [];
141
+ out.push({ id: row.id, integrity, trustedClaim: row.trust?.trusted === true, hookNames });
142
+ }
143
+ return out.sort((a, b) => compareUtf8(a.id, b.id));
144
+ }
145
+
146
+ function summarizeCleanup(target) {
147
+ const document = canonicalTarget(target.deployment, join(target.absolute, ".oats-rollback-incomplete.json"), "cleanup evidence");
148
+ const { receipt, value } = readDocument(document);
149
+ const problems = [];
150
+ if (receipt.state !== "present") return { document: receipt, capabilities: [], outstanding: { hooks: 0, git: 0, directory: false } };
151
+ if (!plain(value)) problems.push(problem("invalid-evidence", "cleanup descriptor root is not an object", ""));
152
+ const cleanup = plain(value?.cleanup) ? value.cleanup : null;
153
+ if (!cleanup) problems.push(problem("invalid-evidence", "cleanup descriptor has no cleanup object", "/cleanup"));
154
+ const outstanding = plain(cleanup?.outstanding) ? cleanup.outstanding : {};
155
+ const capabilities = capabilitySummaries(cleanup?.capabilityRuntime, "/cleanup/capabilityRuntime", problems);
156
+ return {
157
+ document: { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}) },
158
+ capabilities,
159
+ outstanding: { hooks: Array.isArray(outstanding.hooks) ? outstanding.hooks.length : 0,
160
+ git: Array.isArray(outstanding.git) ? outstanding.git.length : 0, directory: outstanding.directory === true },
161
+ ...(problems.length ? { problems } : {}),
162
+ };
163
+ }
164
+
165
+ function summarizeHome(deployment, target) {
166
+ const document = canonicalTarget(deployment, join(target.absolute, "instance.json"), "instance evidence");
167
+ const { receipt, value } = readDocument(document), problems = [];
168
+ let instance = basename(target.absolute), executionBinding = { state: "absent" }, capabilities = [];
169
+ if (receipt.state === "present") {
170
+ if (!plain(value)) problems.push(problem("invalid-evidence", "instance metadata root is not an object", ""));
171
+ else {
172
+ if (typeof value.instance === "string" && value.instance) instance = value.instance;
173
+ else problems.push(problem("invalid-evidence", "instance metadata has no stable instance name", "/instance"));
174
+ executionBinding = bindingSummary(value, problems);
175
+ capabilities = capabilitySummaries(value.capabilityRuntime, "/capabilityRuntime", problems);
176
+ }
177
+ }
178
+ const cleanup = summarizeCleanup({ ...target, deployment });
179
+ return { path: target.path, instance, document: { ...receipt, ...(problems.length ? { state: "invalid", problems } : {}) },
180
+ executionBinding, capabilities, cleanup };
181
+ }
182
+
183
+ function executionSummary(value, kind, pointer, problems) {
184
+ if (value === undefined) return { state: "absent" };
185
+ try {
186
+ if (kind === "capsule") validateExecutionCapsule(value); else validateExecutionTemplate(value);
187
+ return { state: "valid-shape", schemaVersion: value.schemaVersion,
188
+ ...(Object.hasOwn(value, "executionId") ? { executionId: value.executionId } : {}),
189
+ deployment: value.deployment, resolution: JSON.parse(canonicalJson(value.resolution)),
190
+ action: JSON.parse(canonicalJson(value.action)) };
191
+ } catch (error) {
192
+ problems.push(errorProblem(error, pointer));
193
+ return { state: "invalid" };
194
+ }
195
+ }
196
+
197
+ function summarizeScheduleJob(id, definition, state) {
198
+ const problems = [];
199
+ if (definition !== undefined && !plain(definition)) problems.push(problem("invalid-evidence", "schedule definition is not an object", `/definitions/jobs/${id}`));
200
+ if (state !== undefined && !plain(state)) problems.push(problem("invalid-evidence", "schedule state is not an object", `/state/jobs/${id}`));
201
+ const attempt = plain(state?.attempt)
202
+ ? state.attempt.execution === undefined ? { state: "legacy" }
203
+ : executionSummary(state.attempt.execution, "capsule", `/state/jobs/${id}/attempt/execution`, problems)
204
+ : state?.attempt === undefined ? { state: "absent" } : (problems.push(problem("invalid-evidence", "schedule attempt is not an object", `/state/jobs/${id}/attempt`)), { state: "invalid" });
205
+ const lastRun = plain(state?.lastRun)
206
+ ? state.lastRun.execution === undefined ? state.lastRun.outcome === "unknown" ? { state: "legacy" } : { state: "absent" }
207
+ : executionSummary(state.lastRun.execution, "capsule", `/state/jobs/${id}/lastRun/execution`, problems)
208
+ : state?.lastRun === undefined ? { state: "absent" } : (problems.push(problem("invalid-evidence", "schedule lastRun is not an object", `/state/jobs/${id}/lastRun`)), { state: "invalid" });
209
+ const template = plain(definition) ? executionSummary(definition.execution, "template", `/definitions/jobs/${id}/execution`, problems) : { state: "absent" };
210
+ return { id, kind: typeof definition?.kind === "string" ? definition.kind : null,
211
+ definitionVersion: Number.isSafeInteger(definition?.definitionVersion) ? definition.definitionVersion : null,
212
+ recurrencePolicy: typeof definition?.recurrencePolicy === "string" ? definition.recurrencePolicy : null,
213
+ execution: template, attempt, lastRun, ...(problems.length ? { problems } : {}) };
214
+ }
215
+
216
+ function summarizeSchedules(deployment, target) {
217
+ const definitionsTarget = canonicalTarget(deployment, join(target.absolute, "oats-schedules.json"), "schedule definitions evidence");
218
+ const stateTarget = canonicalTarget(deployment, join(target.absolute, ".agents", "schedules", "state.json"), "schedule state evidence");
219
+ const definitions = readDocument(definitionsTarget), state = readDocument(stateTarget), problems = [];
220
+ const definitionsJobs = plain(definitions.value?.jobs) ? definitions.value.jobs : Object.create(null);
221
+ const stateJobs = plain(state.value?.jobs) ? state.value.jobs : Object.create(null);
222
+ if (definitions.receipt.state === "present" && !plain(definitions.value?.jobs)) problems.push(problem("invalid-evidence", "schedule definitions have no jobs map", "/definitions/jobs"));
223
+ if (state.receipt.state === "present" && !plain(state.value?.jobs)) problems.push(problem("invalid-evidence", "schedule state has no jobs map", "/state/jobs"));
224
+ const ids = [...new Set([...Object.keys(definitionsJobs), ...Object.keys(stateJobs)])].sort(compareUtf8);
225
+ return { path: target.path, definitions: definitions.receipt, state: state.receipt,
226
+ jobs: ids.map((id) => summarizeScheduleJob(id, definitionsJobs[id], stateJobs[id])), ...(problems.length ? { problems } : {}) };
227
+ }
228
+
229
+ /** Inventory only explicitly supplied targets. No recursive discovery occurs. */
230
+ export function readPortableMigrationInventory(scope, { lockFiles = [], instanceHomes = [], scheduleScopes = [] } = {}, { legacyLockDecoder } = {}) {
231
+ if (legacyLockDecoder !== undefined && typeof legacyLockDecoder !== "function") throw oatsError("invalid-declaration", "legacyLockDecoder must be a function");
232
+ const deployment = portableScope(scope);
233
+ const locks = uniqueTargets(deployment, lockFiles, "lock evidence").map((target) => summarizeLock(target, legacyLockDecoder));
234
+ const homes = uniqueTargets(deployment, instanceHomes, "instance home", { directory: true }).map((target) => summarizeHome(deployment, target));
235
+ const schedules = uniqueTargets(deployment, scheduleScopes, "schedule scope", { directory: true, allowRoot: true }).map((target) => summarizeSchedules(deployment, target));
236
+ const inventory = { schemaVersion: MIGRATION_INVENTORY_VERSION, deployment, locks, homes, schedules };
237
+ canonicalJson(inventory);
238
+ return inventory;
239
+ }
240
+
241
+ function assertReceipt(receipt) {
242
+ if (!plain(receipt) || typeof receipt.path !== "string" || !["present", "absent", "invalid"].includes(receipt.state)) {
243
+ throw oatsError("invalid-evidence", "migration inventory contains an invalid document receipt");
244
+ }
245
+ if (receipt.state !== "absent") validateIntegrity(receipt.integrity, [BYTES_FORMAT]);
246
+ }
247
+
248
+ /** Minimal persisted-inventory validation; evidence authority still comes from
249
+ * re-reading and comparing all literal document witnesses below. */
250
+ export function validatePortableMigrationInventory(inventory) {
251
+ canonicalJson(inventory);
252
+ if (!plain(inventory) || inventory.schemaVersion !== MIGRATION_INVENTORY_VERSION || typeof inventory.deployment !== "string"
253
+ || !Array.isArray(inventory.locks) || !Array.isArray(inventory.homes) || !Array.isArray(inventory.schedules)) {
254
+ throw oatsError("invalid-evidence", "invalid migration inventory envelope");
255
+ }
256
+ for (const lock of inventory.locks) assertReceipt(lock);
257
+ for (const home of inventory.homes) { assertReceipt(home.document); assertReceipt(home.cleanup.document); }
258
+ for (const schedule of inventory.schedules) { assertReceipt(schedule.definitions); assertReceipt(schedule.state); }
259
+ return inventory;
260
+ }
261
+
262
+ /** Re-read every explicit witness and require the complete inventory projection
263
+ * to be unchanged. This is still read-only and performs no migration apply. */
264
+ export function verifyPortableMigrationInventory(scope, inventory, adapters = {}) {
265
+ validatePortableMigrationInventory(inventory);
266
+ const deployment = portableScope(scope);
267
+ if (deployment !== inventory.deployment) throw oatsError("selection-changed", "migration deployment identity changed");
268
+ const current = readPortableMigrationInventory(deployment, {
269
+ lockFiles: inventory.locks.map((entry) => entry.path),
270
+ instanceHomes: inventory.homes.map((entry) => entry.path),
271
+ scheduleScopes: inventory.schedules.map((entry) => entry.path),
272
+ }, adapters);
273
+ if (canonicalJson(current) !== canonicalJson(inventory)) throw oatsError("selection-changed", "historical migration evidence changed after inventory");
274
+ return current;
275
+ }
276
+
277
+ /** Re-read one explicit lock witness and return its strictly decoded candidate
278
+ * rows. This does not correlate the lock to a home/job, verify artifact bytes,
279
+ * infer missing source, or transfer its historical trust fields. */
280
+ export function verifyHistoricalLockCandidate(scope, inventory, lockPath, { legacyLockDecoder } = {}) {
281
+ if (typeof legacyLockDecoder !== "function") throw oatsError("migration-required", "historical lock candidate verification needs the strict legacy-lock decoder");
282
+ const current = verifyPortableMigrationInventory(scope, inventory, { legacyLockDecoder });
283
+ const target = canonicalTarget(current.deployment, lockPath, "lock evidence");
284
+ const evidence = current.locks.find((entry) => entry.path === target.path);
285
+ if (!evidence) throw oatsError("invalid-evidence", "historical lock target is not in the verified inventory");
286
+ if (evidence.state !== "present" || evidence.semanticVerification !== "verified" || !["legacy-v1", "materialized-v2"].includes(evidence.format)) {
287
+ throw oatsError("migration-held", "historical lock candidate is absent, invalid, unsupported or not strictly verified");
288
+ }
289
+ const document = readDocument(target);
290
+ if (document.receipt.state !== "present" || canonicalJson(document.receipt.integrity) !== canonicalJson(evidence.integrity)) {
291
+ throw oatsError("selection-changed", "historical lock changed during candidate verification");
292
+ }
293
+ const version = evidence.format === "legacy-v1" ? 1 : 2;
294
+ const decoded = legacyLockDecoder(Buffer.from(document.bytes), Object.freeze({ path: evidence.path, version }));
295
+ const copied = parseStrictJson(canonicalJson(decoded));
296
+ if (copied.version !== version) throw oatsError("invalid-lock", "legacy lock decoder returned a different candidate version");
297
+ const packageIds = Object.keys(copied.packages || {}).sort(compareUtf8);
298
+ const capabilityIds = Object.keys(version === 1 ? copied.legacyCapabilities || {} : copied.capabilities || {}).sort(compareUtf8);
299
+ if (canonicalJson(packageIds) !== canonicalJson(evidence.rowIds.packages) || canonicalJson(capabilityIds) !== canonicalJson(evidence.rowIds.capabilities)) {
300
+ throw oatsError("invalid-lock", "legacy lock decoder changed the verified candidate row identity set");
301
+ }
302
+ return { schemaVersion: 1, path: evidence.path, integrity: evidence.integrity, version,
303
+ packages: copied.packages, capabilities: copied.capabilities, legacyCapabilities: copied.legacyCapabilities,
304
+ trustAuthority: "none", evidence: [{ kind: "migration-evidence", document: { kind: "deployment", path: evidence.path, integrity: evidence.integrity }, pointer: "" }] };
305
+ }
@@ -0,0 +1,199 @@
1
+ /** Immutable, unselectable historical evidence records.
2
+ *
3
+ * Partial/unknown evidence lives outside `.agents/resolutions` and can never be
4
+ * consumed as dispatch authority. Publication re-verifies all inventoried input
5
+ * witnesses first and changes no lock, home, schedule, session or approval. */
6
+ import { linkSync, lstatSync, mkdirSync, mkdtempSync, opendirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { ensureStoreIgnore } from "./capability-artifacts.mjs";
9
+ import { readPortableBytes } from "./portable-files.mjs";
10
+ import { JSON_FORMAT, jsonIntegrity, validateIntegrity } from "./portable-digest.mjs";
11
+ import { canonicalJson, compareUtf8, parseStrictJson } from "./portable-values.mjs";
12
+ import { portableScope } from "./portable-state.mjs";
13
+ import { objectAt, stringAt, versionAt } from "./portable-shape.mjs";
14
+ import { validateOrigin, validateResolutionRef } from "./resolution-shape.mjs";
15
+ import { validatePortableMigrationInventory, verifyPortableMigrationInventory } from "./portable-migration-evidence.mjs";
16
+ import { planPortableMigration } from "./portable-migration.mjs";
17
+ import { verifyHistoricalHomeCapabilityCandidate } from "./portable-migration-artifacts.mjs";
18
+ import { oatsError } from "./errors.mjs";
19
+
20
+ export const RESOLUTION_EVIDENCE_VERSION = 1;
21
+ const TARGET_KINDS = new Set(["selection-lock", "instance-home", "scheduled-job"]);
22
+ const ACTIONS = new Set(["preserve", "verify", "hold"]);
23
+ const exists = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
24
+
25
+ export function validateResolutionEvidence(value) {
26
+ canonicalJson(value);
27
+ objectAt(value, ["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"],
28
+ ["schemaVersion", "target", "status", "evidence", "knownInputs", "unresolved", "resolution"]);
29
+ versionAt(value.schemaVersion);
30
+ objectAt(value.target, ["kind", "id"], ["kind", "id"]);
31
+ if (!TARGET_KINDS.has(value.target.kind)) throw oatsError("invalid-evidence", "unsupported migration evidence target kind");
32
+ stringAt(value.target.id, "/target/id");
33
+ if (!Array.isArray(value.evidence)) throw oatsError("invalid-evidence", "migration evidence origins must be an array");
34
+ value.evidence.forEach(validateOrigin);
35
+ objectAt(value.knownInputs, ["inventory", "action", "preserve"], ["inventory", "action", "preserve"]);
36
+ validateIntegrity(value.knownInputs.inventory, [JSON_FORMAT]);
37
+ if (!ACTIONS.has(value.knownInputs.action)) throw oatsError("invalid-evidence", "invalid planned migration action");
38
+ canonicalJson(value.knownInputs.preserve);
39
+ if (!Array.isArray(value.unresolved)) throw oatsError("invalid-evidence", "migration unresolved inputs must be an array");
40
+ for (const item of value.unresolved) {
41
+ objectAt(item, ["pointer", "code", "origins", "reason"], ["pointer", "code", "origins", "reason"]);
42
+ stringAt(item.pointer, "/unresolved/pointer", { empty: true, pattern: /^(?:\/(?:[^~]|~[01])*)*$/ });
43
+ stringAt(item.code, "/unresolved/code"); stringAt(item.reason, "/unresolved/reason");
44
+ if (!Array.isArray(item.origins)) throw oatsError("invalid-evidence", "unresolved origins must be an array");
45
+ item.origins.forEach(validateOrigin);
46
+ }
47
+ if (!['reconstructed', 'partial', 'unknown'].includes(value.status)) throw oatsError("invalid-evidence", "invalid migration evidence status");
48
+ if (value.status === "reconstructed") {
49
+ validateResolutionRef(value.resolution);
50
+ if (!value.evidence.length) throw oatsError("resolution-incomplete", "reconstructed evidence requires historical witnesses");
51
+ if (value.unresolved.length) throw oatsError("resolution-incomplete", "reconstructed evidence cannot retain unresolved managed inputs");
52
+ } else if (value.resolution !== null) {
53
+ throw oatsError("resolution-incomplete", "partial or unknown evidence cannot reference a captured resolution");
54
+ }
55
+ return value;
56
+ }
57
+
58
+ export function validateResolutionEvidenceRef(value) {
59
+ canonicalJson(value, { maxBytes: 1024, maxDepth: 4, maxEntries: 8 });
60
+ objectAt(value, ["schemaVersion", "id"], ["schemaVersion", "id"]);
61
+ versionAt(value.schemaVersion);
62
+ stringAt(value.id, "/id", { pattern: /^sha256-[a-f0-9]{64}$/ });
63
+ return value;
64
+ }
65
+
66
+ function store(scope, create) {
67
+ let root = portableScope(scope);
68
+ for (const name of [".agents", "resolution-evidence"]) {
69
+ root = join(root, name);
70
+ if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
71
+ const stat = exists(root);
72
+ if (!stat) throw oatsError("evidence-not-found", "resolution evidence store is absent");
73
+ if (!stat.isDirectory() || realpathSync(root) !== root) throw oatsError("invalid-evidence", "resolution evidence store component is not a real directory");
74
+ }
75
+ if (create) ensureStoreIgnore(root);
76
+ return root;
77
+ }
78
+
79
+ function decode(path, reference) {
80
+ const bytes = readPortableBytes(path, { missingCode: "evidence-not-found", invalidCode: "invalid-evidence" });
81
+ const value = parseStrictJson(bytes);
82
+ if (jsonIntegrity(value).value !== reference.id || !bytes.equals(Buffer.from(canonicalJson(value)))) {
83
+ throw oatsError("integrity-drift", "resolution evidence bytes differ from their canonical address");
84
+ }
85
+ validateResolutionEvidence(value);
86
+ return value;
87
+ }
88
+
89
+ export function readResolutionEvidence(scope, reference) {
90
+ validateResolutionEvidenceRef(reference);
91
+ return decode(join(store(scope, false), `${reference.id}.json`), reference);
92
+ }
93
+
94
+ /** Bounded diagnostic enumeration of immutable evidence. Staging and managed
95
+ * ignore metadata are not records; every other entry must be one valid file. */
96
+ export function listResolutionEvidence(scope, { maxEntries = 512, maxRecords = 256, maxBytes = 32 * 1024 * 1024, includeDocuments = false } = {}) {
97
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 10_000
98
+ || !Number.isSafeInteger(maxRecords) || maxRecords < 1 || maxRecords > 10_000
99
+ || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > 256 * 1024 * 1024
100
+ || typeof includeDocuments !== "boolean") {
101
+ throw oatsError("invalid-evidence", "invalid resolution evidence listing options");
102
+ }
103
+ let root;
104
+ try { root = store(scope, false); }
105
+ catch (error) { if (error.code === "evidence-not-found") return []; throw error; }
106
+ const entries = [];
107
+ let visited = 0;
108
+ const directory = opendirSync(root);
109
+ try {
110
+ for (;;) {
111
+ const entry = directory.readSync();
112
+ if (entry === null) break;
113
+ if (++visited > maxEntries) throw oatsError("resource-limit", "resolution evidence visited-entry limit exceeded");
114
+ if (entry.name === ".gitignore" || entry.name.startsWith(".evidence-")) continue;
115
+ if (entries.length >= maxRecords) throw oatsError("resource-limit", "resolution evidence record limit exceeded");
116
+ entries.push(entry);
117
+ }
118
+ } finally { directory.closeSync(); }
119
+ const results = [];
120
+ let totalBytes = 0;
121
+ for (const entry of entries.sort((a, b) => compareUtf8(a.name, b.name))) {
122
+ const match = /^(sha256-[a-f0-9]{64})\.json$/.exec(entry.name);
123
+ if (!entry.isFile() || !match) throw oatsError("invalid-evidence", "resolution evidence store contains an unsupported entry");
124
+ const reference = { schemaVersion: 1, id: match[1] }, document = decode(join(root, entry.name), reference);
125
+ totalBytes += Buffer.byteLength(canonicalJson(document));
126
+ if (totalBytes > maxBytes) throw oatsError("resource-limit", "aggregate resolution evidence byte limit exceeded");
127
+ results.push({ reference, target: document.target, status: document.status, unresolved: document.unresolved.length,
128
+ ...(includeDocuments ? { document } : {}) });
129
+ }
130
+ return results;
131
+ }
132
+
133
+ function targetKey(target) { return canonicalJson(target); }
134
+ function entryFor(plan, target) {
135
+ const wanted = targetKey(target), entry = plan.targets.find((candidate) => targetKey(candidate.target) === wanted);
136
+ if (!entry) throw oatsError("invalid-evidence", "migration target is not in the verified inventory plan");
137
+ if (entry.status === "reconstructed") throw oatsError("migration-required", "reconstructed publication requires the dedicated historical evidence verifier");
138
+ return entry;
139
+ }
140
+ function publishEvidence(deployment, document) {
141
+ validateResolutionEvidence(document);
142
+ const reference = { schemaVersion: 1, id: jsonIntegrity(document).value };
143
+ const root = store(deployment, true), destination = join(root, `${reference.id}.json`);
144
+ if (exists(destination)) { decode(destination, reference); return reference; }
145
+ const staging = mkdtempSync(join(root, ".evidence-"));
146
+ let failed = false, primary;
147
+ try {
148
+ const candidate = join(staging, "record.json");
149
+ writeFileSync(candidate, canonicalJson(document), { flag: "wx", mode: 0o600 });
150
+ decode(candidate, reference);
151
+ try { linkSync(candidate, destination); } catch (error) { if (error.code !== "EEXIST") throw error; }
152
+ decode(destination, reference);
153
+ return reference;
154
+ } catch (error) { failed = true; primary = error; throw error; }
155
+ finally {
156
+ try { rmSync(staging, { recursive: true, force: true }); }
157
+ catch (cleanup) {
158
+ const error = failed ? new AggregateError([primary, cleanup], "evidence publication and cleanup failed", { cause: primary })
159
+ : new Error("evidence staging cleanup failed", { cause: cleanup });
160
+ error.code = failed ? primary.code : cleanup.code; error.stagingPath = staging; throw error;
161
+ }
162
+ }
163
+ }
164
+
165
+ /** Publish exactly one target from the deterministic plan after re-reading all
166
+ * inventory witnesses. Reconstructed records require a later dedicated verifier
167
+ * and are deliberately outside this function. */
168
+ export function commitPlannedResolutionEvidence(scope, inventory, target, adapters = {}) {
169
+ validatePortableMigrationInventory(inventory);
170
+ const current = verifyPortableMigrationInventory(scope, inventory, adapters);
171
+ const plan = planPortableMigration(current), entry = entryFor(plan, target);
172
+ const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION,
173
+ target: entry.target, status: entry.status, evidence: entry.evidence,
174
+ knownInputs: { inventory: plan.inventory, action: entry.action, preserve: entry.preserve ?? null },
175
+ unresolved: entry.unresolved, resolution: null };
176
+ return publishEvidence(current.deployment, document);
177
+ }
178
+
179
+ /** Persist a narrow verified home→historical capability association as
180
+ * partial evidence. Raw legacy source/settings/commands are intentionally not
181
+ * copied; their literal document remains addressed by the evidence origins. */
182
+ export function commitHistoricalHomeCapabilityEvidence(scope, inventory, request, adapters = {}) {
183
+ validatePortableMigrationInventory(inventory);
184
+ const candidate = verifyHistoricalHomeCapabilityCandidate(scope, inventory, request, adapters);
185
+ const current = verifyPortableMigrationInventory(scope, inventory, adapters), plan = planPortableMigration(current);
186
+ const entry = entryFor(plan, { kind: "instance-home", id: candidate.home.path });
187
+ const origins = [...entry.evidence, ...candidate.artifact.evidence];
188
+ const seen = new Set(), evidence = origins.filter((origin) => { const key = canonicalJson(origin); if (seen.has(key)) return false; seen.add(key); return true; })
189
+ .sort((a, b) => compareUtf8(canonicalJson(a), canonicalJson(b)));
190
+ const proof = { kind: candidate.artifact.package ? "home-capability-v2" : "home-capability-v1", home: candidate.home.path,
191
+ capability: { id: candidate.artifact.capability.id, version: candidate.artifact.capability.row.version,
192
+ package: candidate.artifact.package?.id ?? null, path: candidate.artifact.package ? candidate.artifact.capability.row.path : null },
193
+ artifact: candidate.artifact.artifact, association: candidate.association,
194
+ legacyTrustedClaim: candidate.legacyTrustedClaim, selectionAuthority: candidate.artifact.selectionAuthority,
195
+ approvalAuthority: candidate.approvalAuthority, retentionStatus: candidate.artifact.retentionStatus };
196
+ const document = { schemaVersion: RESOLUTION_EVIDENCE_VERSION, target: entry.target, status: "partial", evidence,
197
+ knownInputs: { inventory: plan.inventory, action: "hold", preserve: proof }, unresolved: entry.unresolved, resolution: null };
198
+ return publishEvidence(current.deployment, document);
199
+ }
@@ -0,0 +1,104 @@
1
+ /** Pure planning over literal historical evidence. No filesystem access, writes,
2
+ * provider execution, source resolution, approval transfer or publication. */
3
+ import { canonicalJson, compareUtf8 } from "./portable-values.mjs";
4
+ import { jsonIntegrity } from "./portable-digest.mjs";
5
+ import { validatePortableMigrationInventory } from "./portable-migration-evidence.mjs";
6
+ import { oatsError } from "./errors.mjs";
7
+
8
+ export const MIGRATION_PLAN_VERSION = 1;
9
+ const originFor = (receipt, pointer = "") => receipt.state === "absent" ? [] : [{ kind: "migration-evidence",
10
+ document: { kind: "deployment", path: receipt.path, integrity: receipt.integrity }, pointer }];
11
+ const unresolved = (code, reason, receipt, pointer = "") => ({ pointer, code, origins: originFor(receipt, pointer), reason });
12
+
13
+ function lockPlan(lock) {
14
+ const target = { kind: "selection-lock", id: lock.path };
15
+ if (lock.state === "absent") return { target, status: "unknown", action: "hold", evidence: [],
16
+ unresolved: [unresolved("migration-held", "the historical lock file is absent", lock)] };
17
+ const evidence = originFor(lock);
18
+ if (lock.state === "invalid" || lock.format === "unknown") return { target, status: "unknown", action: "hold", evidence,
19
+ unresolved: [unresolved("invalid-lock", "the literal historical lock is malformed or unsupported", lock)] };
20
+ if (lock.format === "portable-v3") return { target, status: "partial", action: "verify", evidence,
21
+ unresolved: [unresolved("migration-required", "the current lock still needs its authoritative lock-v3 reader; a lock is not historical dispatch authority", lock)] };
22
+ const empty = lock.rowCounts.packages === 0 && lock.rowCounts.capabilities === 0;
23
+ return { target, status: empty ? "unknown" : "partial", action: "hold", evidence,
24
+ unresolved: [
25
+ ...(lock.semanticVerification === "verified" ? [] : [unresolved("migration-required", "the strict legacy lock reader must verify every row without reinterpretation", lock)]),
26
+ unresolved("migration-held", "a deployment lock does not prove which revision a particular home or job used", lock),
27
+ unresolved("approval-required", "legacy trust is evidence only and cannot grant new-format exact-artifact approval", lock),
28
+ ] };
29
+ }
30
+
31
+ function homePlan(home) {
32
+ const target = { kind: "instance-home", id: home.path };
33
+ const receipts = [home.document, home.cleanup.document].filter((entry) => entry.state !== "absent");
34
+ const evidence = receipts.flatMap((entry) => originFor(entry));
35
+ if (!receipts.length) return { target, status: "unknown", action: "hold", evidence: [],
36
+ unresolved: [unresolved("migration-held", "the named home has no instance or quarantine metadata", home.document)] };
37
+ if (home.executionBinding.state === "valid-shape") return { target, status: "partial", action: "verify", evidence,
38
+ preserve: { executionBinding: home.executionBinding.value },
39
+ unresolved: [
40
+ unresolved("migration-required", "the referenced captured resolution and all retained inputs must be verified before adoption", home.document, "/executionBinding"),
41
+ unresolved("approval-required", "current exact-artifact approval and action-specific readiness remain separate", home.document, "/executionBinding"),
42
+ ] };
43
+ const hasRuntime = home.capabilities.length || home.cleanup.capabilities.length;
44
+ const invalid = home.document.state === "invalid" || home.cleanup.document.state === "invalid" || home.executionBinding.state === "invalid";
45
+ return { target, status: hasRuntime && !invalid ? "partial" : "unknown", action: "hold", evidence,
46
+ unresolved: [
47
+ ...(invalid ? [unresolved("invalid-evidence", "historical home metadata is malformed or has an invalid captured binding", home.document)] : []),
48
+ unresolved("migration-held", "the home does not carry a verified complete captured resolution", home.document),
49
+ unresolved("migration-held", "historical source revision, full managed resources and helper closure are not established by home metadata", home.document),
50
+ unresolved("approval-required", "recorded legacy trust cannot authorize a reconstructed owner-exec artifact", home.document),
51
+ ...(home.cleanup.document.state !== "absent" ? [unresolved("migration-held", "cleanup obligations must remain retryable and cannot be discarded during migration", home.cleanup.document)] : []),
52
+ ] };
53
+ }
54
+
55
+ function executionCandidates(job) {
56
+ return [
57
+ ["definition", job.execution],
58
+ ["attempt", job.attempt],
59
+ ["lastRun", job.lastRun],
60
+ ].filter(([, value]) => value.state !== "absent");
61
+ }
62
+
63
+ function schedulePlan(schedule, job) {
64
+ const target = { kind: "scheduled-job", id: `${schedule.path}:${job.id}` };
65
+ const evidence = [schedule.definitions, schedule.state].filter((entry) => entry.state !== "absent").flatMap((entry) => originFor(entry));
66
+ const executions = executionCandidates(job), valid = executions.filter(([, value]) => value.state === "valid-shape");
67
+ const invalid = executions.some(([, value]) => value.state === "invalid") || !!job.problems?.length
68
+ || schedule.definitions.state === "invalid" || schedule.state.state === "invalid";
69
+ const unresolvedCustody = [job.attempt.state, job.lastRun.state].some((state) => state === "legacy" || state === "invalid");
70
+ if (unresolvedCustody) return { target, status: "unknown", action: "hold", evidence,
71
+ unresolved: [
72
+ ...(invalid ? [unresolved("invalid-evidence", "the admitted or unknown attempt evidence is malformed", schedule.state)] : []),
73
+ unresolved("migration-held", "an existing admitted or unknown historical attempt has no valid captured execution; preserve it exactly and require owner reconciliation", schedule.state),
74
+ ] };
75
+ if (valid.length) {
76
+ const preserve = { executions: valid.map(([source, value]) => ({ source, ...value })) };
77
+ return { target, status: "partial", action: "preserve", evidence, preserve,
78
+ unresolved: [
79
+ ...(invalid ? [unresolved("invalid-evidence", "one or more schedule records disagree with the valid captured authority and must remain held", schedule.state)] : []),
80
+ unresolved("migration-required", "each referenced resolution and admitted execution must be verified without rebinding", schedule.state),
81
+ ] };
82
+ }
83
+ const hasAttempt = job.attempt.state !== "absent" || job.lastRun.state !== "absent";
84
+ return { target, status: "unknown", action: "hold", evidence,
85
+ unresolved: [unresolved("migration-held", hasAttempt
86
+ ? "the admitted or unknown historical attempt has no valid captured execution; preserve it exactly and require owner reconciliation"
87
+ : "the legacy schedule has no captured software authority", schedule.state)] };
88
+ }
89
+
90
+ /** Produce a deterministic no-write plan. No target can become reconstructed at
91
+ * this stage: reconstruction requires the dedicated historical verifier. */
92
+ export function planPortableMigration(inventory) {
93
+ validatePortableMigrationInventory(inventory);
94
+ const targets = [
95
+ ...inventory.locks.map(lockPlan),
96
+ ...inventory.homes.map(homePlan),
97
+ ...inventory.schedules.flatMap((schedule) => schedule.jobs.map((job) => schedulePlan(schedule, job))),
98
+ ].sort((a, b) => compareUtf8(`${a.target.kind}:${a.target.id}`, `${b.target.kind}:${b.target.id}`));
99
+ if (targets.some((entry) => entry.status === "reconstructed")) throw oatsError("invalid-evidence", "unverified planner output cannot claim reconstruction");
100
+ const plan = { schemaVersion: MIGRATION_PLAN_VERSION, inventory: jsonIntegrity(inventory), readyToApply: false,
101
+ effects: { writes: false, providerExecution: false, sessionChanges: false, scheduleChanges: false, trustTransfer: false }, targets };
102
+ canonicalJson(plan);
103
+ return plan;
104
+ }