@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.145

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 (197) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +9 -3
  5. package/dist/cli/command-metadata.js +33 -14
  6. package/dist/cli/help.js +32 -1
  7. package/dist/cli/hook-ingress.js +27 -3
  8. package/dist/cli/memory-flow-admission.js +7 -3
  9. package/dist/cli/memory-init-invocation.js +10 -3
  10. package/dist/cli/memory-init.js +74 -68
  11. package/dist/cli/memory-storage-admission.js +3 -2
  12. package/dist/cli/memory-storage-qualification.js +105 -0
  13. package/dist/cli/memory-storage-transition.js +123 -0
  14. package/dist/cli/memory-upgrade.js +189 -0
  15. package/dist/cli/native-hook-ingress.js +14 -4
  16. package/dist/cli/native-hook-preparation.js +55 -0
  17. package/dist/cli/response-file.js +2 -1
  18. package/dist/cli/run-cli.js +70 -32
  19. package/dist/cli/setup.js +2 -1
  20. package/dist/cli.js +12 -0
  21. package/dist/domain/stage-handler-identities.js +8 -0
  22. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  23. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  24. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  25. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +62 -20
  27. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  28. package/dist/harness-runtime/lib/dd-droid.mjs +1 -1
  29. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +70 -13
  30. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  31. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  32. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  33. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  34. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  35. package/dist/harness-runtime/lib/managed-daemon.mjs +13 -6
  36. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  37. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  38. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  39. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +4 -2
  40. package/dist/harness-runtime/lib/provider-overload-policy.mjs +19 -6
  41. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  42. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  43. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  44. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  45. package/dist/schemas/release-impact.schema.json +3 -3
  46. package/dist/schemas/stage-start-response.schema.json +4 -0
  47. package/dist/services/accepted-intent.js +13 -1
  48. package/dist/services/canon.js +43 -7
  49. package/dist/services/cli-operation-classifier.js +1 -1
  50. package/dist/services/cli-upgrade-allowlist.js +1 -1
  51. package/dist/services/code-checks.js +11 -4
  52. package/dist/services/compatibility-preflight.js +5 -2
  53. package/dist/services/controller-fanout.js +21 -4
  54. package/dist/services/controller-memory-task.js +45 -0
  55. package/dist/services/controller-memory-worker.js +48 -0
  56. package/dist/services/dashboard.js +2 -2
  57. package/dist/services/descriptor-run-start.js +26 -19
  58. package/dist/services/descriptor-stage-coordinator.js +30 -10
  59. package/dist/services/descriptor-stage-start.js +40 -4
  60. package/dist/services/engines.js +64 -14
  61. package/dist/services/eval-snapshots.js +104 -93
  62. package/dist/services/external-work-launch.js +5 -3
  63. package/dist/services/flow-catalog.js +8 -4
  64. package/dist/services/frozen-engine.js +113 -11
  65. package/dist/services/hook-memory-routing.js +188 -0
  66. package/dist/services/hook-recovery-guard.js +14 -0
  67. package/dist/services/hook-session-observation.js +24 -0
  68. package/dist/services/hooks.js +27 -8
  69. package/dist/services/hybrid-contracts.js +2 -13
  70. package/dist/services/hybrid-digest.js +14 -0
  71. package/dist/services/installed-flow-source.js +25 -4
  72. package/dist/services/lifecycle-invocations.js +33 -5
  73. package/dist/services/memory-accepted-composition.js +5 -4
  74. package/dist/services/memory-completed-correction-wave.js +23 -15
  75. package/dist/services/memory-compose-handler.js +163 -21
  76. package/dist/services/memory-composition-acceptance.js +186 -10
  77. package/dist/services/memory-composition-graph.js +42 -11
  78. package/dist/services/memory-composition-registration.js +23 -8
  79. package/dist/services/memory-composition-work-custody.js +14 -9
  80. package/dist/services/memory-composition-work-results.js +15 -3
  81. package/dist/services/memory-corrected-review-batch.js +10 -3
  82. package/dist/services/memory-corrected-review-findings.js +4 -2
  83. package/dist/services/memory-corrected-review-inputs.js +10 -5
  84. package/dist/services/memory-corrected-review-receipt.js +17 -9
  85. package/dist/services/memory-corrected-review-registration.js +3 -2
  86. package/dist/services/memory-correction-check-receipt.js +20 -4
  87. package/dist/services/memory-custody-snapshot.js +36 -0
  88. package/dist/services/memory-finish-evidence.js +44 -22
  89. package/dist/services/memory-finish-handler.js +37 -23
  90. package/dist/services/memory-finish-integration.js +218 -48
  91. package/dist/services/memory-finish-readiness.js +66 -19
  92. package/dist/services/memory-git-integration.js +87 -18
  93. package/dist/services/memory-historical-corrected-review.js +21 -15
  94. package/dist/services/memory-historical-original-review.js +22 -9
  95. package/dist/services/memory-historical-repair-registration.js +14 -8
  96. package/dist/services/memory-historical-second-review.js +30 -11
  97. package/dist/services/memory-init-contexts.js +28 -2
  98. package/dist/services/memory-init-controller-start.js +19 -8
  99. package/dist/services/memory-init-git-policy.js +50 -21
  100. package/dist/services/memory-init-lint-capability.js +21 -3
  101. package/dist/services/memory-init-native-capacity.js +13 -0
  102. package/dist/services/memory-init-preflight.js +11 -1
  103. package/dist/services/memory-init-request-candidate.js +73 -17
  104. package/dist/services/memory-init-request-input.js +23 -7
  105. package/dist/services/memory-init-run-bundle.js +49 -12
  106. package/dist/services/memory-init-storage.js +17 -7
  107. package/dist/services/memory-layer.js +68 -4
  108. package/dist/services/memory-lint-config.js +111 -2
  109. package/dist/services/memory-native-review-copy.js +5 -67
  110. package/dist/services/memory-normal-flow-entry.js +12 -0
  111. package/dist/services/memory-ordinary-defaults.js +22 -5
  112. package/dist/services/memory-preflight-handler.js +15 -0
  113. package/dist/services/memory-repair-current-view.js +1 -1
  114. package/dist/services/memory-repair-document-delta.js +70 -33
  115. package/dist/services/memory-repair-history.js +12 -7
  116. package/dist/services/memory-repair-physical-binding.js +1 -119
  117. package/dist/services/memory-repair-publication-plan.js +5 -3
  118. package/dist/services/memory-repair-publication-source.js +1 -1
  119. package/dist/services/memory-repair-publication.js +3 -2
  120. package/dist/services/memory-repair-registration.js +2 -1
  121. package/dist/services/memory-repair-work-baseline.js +4 -2
  122. package/dist/services/memory-repair-work-completion.js +32 -12
  123. package/dist/services/memory-repair-work-results.js +21 -2
  124. package/dist/services/memory-repair-workspace.js +11 -2
  125. package/dist/services/memory-repair-writer-copy.js +2 -1
  126. package/dist/services/memory-result-binding.js +78 -12
  127. package/dist/services/memory-retained-review-authority.js +17 -8
  128. package/dist/services/memory-retained-second-baseline.js +1 -1
  129. package/dist/services/memory-retained-second-repairs.js +12 -6
  130. package/dist/services/memory-review-correction-plan.js +47 -12
  131. package/dist/services/memory-review-handler.js +17 -17
  132. package/dist/services/memory-review-inputs.js +55 -9
  133. package/dist/services/memory-review-limits.js +10 -0
  134. package/dist/services/memory-review-receipt.js +2 -2
  135. package/dist/services/memory-review-registration.js +13 -4
  136. package/dist/services/memory-review-work-custody.js +3 -1
  137. package/dist/services/memory-review-work-input.js +26 -7
  138. package/dist/services/memory-review-work-results.js +26 -4
  139. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  140. package/dist/services/memory-second-correction-plan.js +2 -1
  141. package/dist/services/memory-second-repair-launch.js +4 -4
  142. package/dist/services/memory-second-repair-source.js +1 -1
  143. package/dist/services/memory-stage-dispatch.js +38 -1
  144. package/dist/services/memory-stage-results.js +113 -22
  145. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  146. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  147. package/dist/services/memory-upgrade-evidence.js +100 -0
  148. package/dist/services/memory-upgrade-inventory.js +442 -0
  149. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  150. package/dist/services/memory-upgrade-publication.js +413 -0
  151. package/dist/services/memory-upgrade-selection.js +430 -0
  152. package/dist/services/memory-upgrade-start.js +60 -0
  153. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  154. package/dist/services/memory-verification-acceptance.js +38 -17
  155. package/dist/services/memory-verification-checks.js +77 -23
  156. package/dist/services/memory-verification-receipt.js +6 -5
  157. package/dist/services/memory-verify-handler.js +15 -13
  158. package/dist/services/memory-work-result-json.js +12 -0
  159. package/dist/services/memory-work-result-preparation.js +5 -4
  160. package/dist/services/merge-server.js +4 -2
  161. package/dist/services/migrations.js +127 -98
  162. package/dist/services/physical-startup.js +45 -0
  163. package/dist/services/project-registry.js +97 -0
  164. package/dist/services/project-summary.js +1 -1
  165. package/dist/services/projects.js +3 -93
  166. package/dist/services/prompt-delivery.js +71 -0
  167. package/dist/services/provider-capacity.js +69 -8
  168. package/dist/services/recovery-snapshot-database.js +32 -4
  169. package/dist/services/repair-intents.js +1 -12
  170. package/dist/services/repair-receipt.js +13 -0
  171. package/dist/services/run-control-worker.js +35 -18
  172. package/dist/services/run-controller-process.js +2 -6
  173. package/dist/services/run-controller-recovery.js +6 -1
  174. package/dist/services/run-controller.js +131 -80
  175. package/dist/services/run-recovery-guard.js +30 -0
  176. package/dist/services/run-recovery.js +11 -44
  177. package/dist/services/run-start-custody.js +17 -5
  178. package/dist/services/runtime-scope-worker.js +46 -19
  179. package/dist/services/sessions.js +3 -24
  180. package/dist/services/setup.js +63 -13
  181. package/dist/services/stage-handlers.js +9 -7
  182. package/dist/services/stage-pause.js +108 -44
  183. package/dist/services/version-status.js +1 -1
  184. package/dist/services/vnext-fanout.js +21 -9
  185. package/dist/services/vnext-plan-review.js +3 -5
  186. package/dist/services/work-registry.js +80 -33
  187. package/dist/services/workspace-files.js +1 -1
  188. package/dist/shared/entity-references.js +1 -8
  189. package/dist/storage/data-preservation.js +65 -0
  190. package/dist/storage/database.js +53 -19
  191. package/dist/storage/import-guard.js +84 -27
  192. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  193. package/dist/storage/paired-writer-migration.js +424 -0
  194. package/dist/storage/writer-contract.js +11 -4
  195. package/dist/storage/writer-migration.js +313 -52
  196. package/dist/storage/writer-restore.js +217 -0
  197. package/package.json +8 -4
@@ -1,6 +1,6 @@
1
1
  import { AppError } from "../shared/errors.js";
2
2
  import { PreparedPublishedMemoryRepairWorkResult } from "./memory-repair-work-custody.js";
3
- import { prepareMemoryRepairPrivateOutputFromStage, prepareMemoryRepairPublicationSourceFromStage, registerMemoryRepairPublicationSourceFromStage, reserveMemoryRepairPublicationAgainstSourceFromStage, reserveMemorySecondRepairPublicationFromStage, restoreMemoryRepairPublicationFromStage, prepareMemorySecondRepairPublicationFromStage, restoreStartedMemorySecondRepairLaunchFromStage } from "./memory-review-handler.js";
3
+ import { prepareMemoryRepairPrivateOutputFromStage, prepareMemoryRepairPublicationSourceFromStage, restoreMemoryRepairPublicationSourceFromStage, reserveMemoryRepairPublicationAgainstSourceFromStage, reserveMemorySecondRepairPublicationFromStage, restoreMemoryRepairPublicationFromStage, prepareMemorySecondRepairPublicationFromStage, restoreStartedMemorySecondRepairLaunchFromStage } from "./memory-review-handler.js";
4
4
  function refuse(reason) {
5
5
  throw new AppError("memory_work_result_preparation_required", "Repair completion requires registered private output and physical start custody", 1, { reason, effect: "no_effect" });
6
6
  }
@@ -47,47 +47,67 @@ export function prepareMemoryRepairWorkCompletion(context, work, result) {
47
47
  const publication = restore();
48
48
  if (!publication.bytes().equals(bytes))
49
49
  refuse("retained_result_changed");
50
- return { scope, bytes, wave, publication };
50
+ return { scope, bytes, wave, publication, source: null };
51
51
  }
52
52
  const outcome = wave === 1 ? prepareMemoryRepairPrivateOutputFromStage(context, { ...scope, bytes }).result().outcome
53
53
  : restoreStartedMemorySecondRepairLaunchFromStage(context, scope).observeOutput(context, bytes).result.outcome;
54
- if (outcome !== "accepted")
54
+ if (outcome !== "accepted" && outcome !== "reconciled")
55
55
  refuse("accepted_private_output_required_use_managed_fail_for_blocker");
56
- if (wave === 1)
57
- prepareMemoryRepairPublicationSourceFromStage(context, scope);
58
- return { scope, bytes, wave, publication: null };
56
+ let source = null;
57
+ if (wave === 1) {
58
+ try {
59
+ source = restoreMemoryRepairPublicationSourceFromStage(context, scope);
60
+ }
61
+ catch (error) {
62
+ if (!(error instanceof AppError) || error.code !== "memory_repair_publication_source_invalid" || error.details.reason !== "registered_source_required")
63
+ throw error;
64
+ source = prepareMemoryRepairPublicationSourceFromStage(context, scope);
65
+ }
66
+ }
67
+ return { scope, bytes, wave, publication: null, source };
59
68
  }
60
69
  /** Called by managed Work finish after its normal Hook/actor admission.
61
- * Preparation is repeated before effects; no caller-supplied object grants
70
+ * Sealed owners revalidate before effects; no caller-supplied object grants
62
71
  * authority. Every action is terminal and advances the retained finite cursor. */
63
- export function publishMemoryRepairWorkCompletion(context, work, result) {
72
+ export function publishMemoryRepairWorkCompletion(context, work, result, progress) {
73
+ progress?.(`Memory repair ${work.work_id}: preparing publication`);
64
74
  const prepared = prepareMemoryRepairWorkCompletion(context, work, result);
65
75
  const { scope, bytes, wave } = prepared;
76
+ progress?.(`Memory repair ${work.work_id}: publication input verified`);
66
77
  try {
67
78
  if (!prepared.publication) {
68
79
  if (wave === 1) {
69
- registerMemoryRepairPublicationSourceFromStage(context, scope);
80
+ // Registration revalidates this invocation's source before and after
81
+ // its SQL effect; do not rebuild the entire same history graph again.
82
+ prepared.source.register(context);
83
+ progress?.(`Memory repair ${work.work_id}: publication source registered`);
70
84
  reserveMemoryRepairPublicationAgainstSourceFromStage(context, { ...scope, bytes });
71
85
  }
72
86
  else
73
87
  reserveMemorySecondRepairPublicationFromStage(context, { ...scope, bytes });
88
+ progress?.(`Memory repair ${work.work_id}: publication reserved`);
74
89
  }
75
90
  const publication = prepared.publication ?? (wave === 1 ? restoreMemoryRepairPublicationFromStage(context, scope) : prepareMemorySecondRepairPublicationFromStage(context, scope));
91
+ progress?.(`Memory repair ${work.work_id}: publication restored`);
76
92
  const initial = publication.revalidate(context);
77
93
  let completed = initial.completed;
94
+ progress?.(`Memory repair ${work.work_id}: publication ready (${completed}/${initial.total})`);
78
95
  // The actual retained plan, not agent output or a guessed retry count,
79
96
  // bounds execution. Even an already applied plan reconciles completion.
80
97
  do {
81
98
  const step = publication.applyNextAction(context);
82
99
  if (step.total !== initial.total || step.completed < completed || step.completed > initial.total)
83
100
  refuse("publication_cursor_changed");
101
+ if (step.status !== "applied" && step.completed !== completed + 1)
102
+ refuse("publication_did_not_advance");
103
+ progress?.(`Memory repair ${work.work_id}: publication advanced (${step.completed}/${step.total})`);
84
104
  if (step.status === "applied")
85
105
  break;
86
- if (step.completed !== completed + 1)
87
- refuse("publication_did_not_advance");
88
106
  completed = step.completed;
89
107
  } while (completed < initial.total);
90
- return PreparedPublishedMemoryRepairWorkResult.prepare(context, publication);
108
+ const published = PreparedPublishedMemoryRepairWorkResult.prepare(context, publication);
109
+ progress?.(`Memory repair ${work.work_id}: published result verified`);
110
+ return published;
91
111
  }
92
112
  catch (error) {
93
113
  throw new AppError("memory_repair_completion_pending", "Repair publication requires exact-request reconciliation before another completion attempt", 1, { effect: error instanceof AppError && error.details.effect === "unknown" ? "unknown" : "partial",
@@ -4,6 +4,16 @@ import { AppError } from "../shared/errors.js";
4
4
  import { AdmittedFrozenEngine } from "./frozen-engine.js";
5
5
  import { memoryRepairWorkerSchema } from "./memory-repair-registration.js";
6
6
  import { memorySourceKey } from "./memory-stage-results.js";
7
+ import { parseMemoryWorkResultProposal } from "./memory-work-result-json.js";
8
+ /** Claim shape only, never settlement or finding closure. Reconciliation
9
+ * preserves documents and requires current grounded evidence plus the same
10
+ * fresh check and independent re-review owners as a physical repair. */
11
+ export function isCompletedMemoryRepairClaim(value) {
12
+ return Array.isArray(value?.changes) && (value.outcome === "accepted" && value.changes.length > 0
13
+ || value.outcome === "reconciled" && value.changes.length === 0 && Array.isArray(value.resolutions) && value.resolutions.length > 0
14
+ && value.resolutions.every(resolution => Array.isArray(resolution?.delta_paths) && resolution.delta_paths.length === 0
15
+ && Array.isArray(resolution.evidence) && resolution.evidence.some(ref => ref?.root === "workspace")));
16
+ }
7
17
  function refuse(reason) {
8
18
  throw new AppError("memory_repair_work_result_invalid", "Repair result differs from registered findings and bounded write paths", 2, { reason, effect: "no_effect" });
9
19
  }
@@ -24,7 +34,7 @@ export function validateMemoryRepairWorkResult(bytes, engine, binding) {
24
34
  || !binding.workId || binding.packet?.result_schema !== memoryRepairWorkerSchema)
25
35
  refuse("invalid_preparation_binding");
26
36
  const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(engine.schema("memory-repair-work-result")));
27
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
37
+ const value = parseMemoryWorkResultProposal(bytes, "memory_repair_work_result_invalid");
28
38
  if (schema.$id !== memoryRepairWorkerSchema)
29
39
  refuse("invalid_result_schema");
30
40
  const packet = binding.packet;
@@ -67,6 +77,15 @@ export function validateMemoryRepairWorkResult(bytes, engine, binding) {
67
77
  if (sources.get(key) !== ref.sha256)
68
78
  refuse("foreign_or_stale_resolution_evidence");
69
79
  }
80
+ if (value.outcome === "reconciled") {
81
+ if (value.changes.length || resolution.delta_paths.length)
82
+ refuse("reconciliation_has_document_delta");
83
+ if (!resolution.evidence.some(ref => ref.root === "workspace"))
84
+ refuse("reconciliation_without_current_evidence");
85
+ continue;
86
+ }
87
+ if (!resolution.delta_paths.length)
88
+ refuse("resolution_without_actual_delta");
70
89
  const causal = value.changes.filter(change => resolution.delta_paths.includes(change.path));
71
90
  if (causal.some(change => change.after_sha256 !== null)) {
72
91
  if (!resolution.evidence.some(ref => ref.root === "workspace" && causal.some(change => change.path === ref.path && change.after_sha256 !== null && change.after_sha256 === ref.sha256)))
@@ -75,7 +94,7 @@ export function validateMemoryRepairWorkResult(bytes, engine, binding) {
75
94
  else if (!resolution.evidence.some(ref => ref.root === "run"))
76
95
  refuse("deletion_without_retained_evidence");
77
96
  }
78
- if (value.outcome === "accepted" && (!value.changes.length || ids.size !== packet.findings.length))
97
+ if (value.outcome !== "blocked" && (!isCompletedMemoryRepairClaim(value) || ids.size !== packet.findings.length))
79
98
  refuse("incomplete_repair_claim");
80
99
  if (value.outcome === "blocked" && (value.changes.length || value.resolutions.length))
81
100
  refuse("blocked_repair_has_productive_claims");
@@ -72,16 +72,25 @@ export class PreparedMemoryRepairWorkspace {
72
72
  const exportedFiles = new Set(exported.entries().filter(entry => entry.kind === "file").map(entry => entry.path));
73
73
  const selected = candidates.filter(logical => allowed(path.join(source, logical))
74
74
  && logical.split("/").every(part => ![".git", "node_modules", ".dd-flow"].includes(part.normalize("NFC").toLowerCase()))).filter(logical => {
75
+ const stat = fs.lstatSync(path.join(source, logical), { throwIfNoEntry: false });
76
+ // The sanitized writer copy contains regular inputs only. This applies
77
+ // to project command links too; never follow or materialize their targets.
78
+ if (stat?.isSymbolicLink())
79
+ return false;
75
80
  if (!logical.startsWith(".memory-bank/") || exportedFiles.has(logical))
76
81
  return true;
77
82
  // Git's cached inventory includes tracked deletions. A retained export
78
83
  // can explain an absent MB file, never a present unselected object.
79
- if (fs.lstatSync(path.join(source, logical), { throwIfNoEntry: false }))
84
+ if (stat)
80
85
  refuse("exported_source_changed");
81
86
  return false;
82
87
  });
83
88
  const observed = ObservedMemorySources.capture({ workspace: source }, selected.map(logical => ({ root: "workspace", path: logical })));
84
- const hashes = observed.hashes(), entries = new Map(exported.entries().map(entry => [entry.path, entry]));
89
+ const hashes = observed.hashes(), entries = new Map(exported.entries().map(entry => {
90
+ if (entry.kind === "link")
91
+ refuse("linked_exported_source");
92
+ return [entry.path, { ...entry, kind: entry.kind }];
93
+ }));
85
94
  for (const logical of selected) {
86
95
  const file = path.join(source, logical), stat = fs.lstatSync(file);
87
96
  const entry = { path: logical, kind: "file", mode: stat.mode & 0o7777, sha256: hashes.get(memorySourceKey("workspace", logical)) };
@@ -9,6 +9,7 @@ import { PreparedSecondMemoryRepairBaseline } from "./memory-second-repair-basel
9
9
  import { PreparedMemoryRepairWorkspace } from "./memory-repair-workspace.js";
10
10
  import { runtimeHarness } from "./execution-policy.js";
11
11
  import { hookSessionIdentity } from "./hooks.js";
12
+ import { isCompletedMemoryRepairClaim } from "./memory-repair-work-results.js";
12
13
  import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
13
14
  import { PreparedMemoryRepairPublicationSource } from "./memory-repair-publication-source.js";
14
15
  import { retainedMemoryRepairPublicationPlans } from "./memory-repair-publication-intent.js";
@@ -668,7 +669,7 @@ export class PreparedMemoryRepairPrivateOutput {
668
669
  * Reservation alone grants no permission to apply files or settle Work.
669
670
  */
670
671
  reservePublication(context) {
671
- if (this.#result.outcome !== "accepted")
672
+ if (!isCompletedMemoryRepairClaim(this.#result))
672
673
  refuse("accepted_private_output_required");
673
674
  this.revalidate(context);
674
675
  const start = this.#copy.startedReceipt(), actions = this.#transition.actions();
@@ -1,4 +1,5 @@
1
1
  import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import { readMemoryUpgradeEvidence } from "./memory-upgrade-evidence.js";
2
3
  import { AppError } from "../shared/errors.js";
3
4
  import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
4
5
  import { FrozenGenericExecution } from "./generic-execution.js";
@@ -14,6 +15,9 @@ import { stageLifecycleMatchKey } from "./hooks.js";
14
15
  import { captureRegisteredMemoryCompositionSources } from "./memory-preflight-handler.js";
15
16
  import { memoryCompositionRegistrationDigest } from "./memory-composition-registration.js";
16
17
  import { PreparedCheckEvidence } from "./code-checks.js";
18
+ import { readMemoryUpgradeBootstrapReadiness } from "./memory-upgrade-bootstrap-readiness.js";
19
+ import { readUpgradeStorageReadiness } from "./memory-upgrade-storage-readiness.js";
20
+ import { upgradeAuthoringPaths } from "./memory-upgrade-inventory.js";
17
21
  function refuse() { throw new AppError("memory_result_binding_custody_invalid", "Memory result binding lacks independently accepted stage custody", 1, { effect: "no_effect" }); }
18
22
  /** Select identity, predecessor and unit coverage from SQL, not result fields. */
19
23
  function select(context, input) {
@@ -26,15 +30,17 @@ function select(context, input) {
26
30
  const run = context.db.get("SELECT index_json, project_root, workspace_root FROM runs WHERE project_id = ? AND id = ?", [input.projectId, input.runId]);
27
31
  if (!run)
28
32
  refuse();
29
- const index = JSON.parse(run.index_json), frozen = input.execution.snapshot();
33
+ const index = JSON.parse(run.index_json), frozen = input.execution.snapshot(), flowId = input.flow.descriptor.id;
34
+ const upgrade = flowId === "mb-upgrade";
35
+ const schemaId = (stage) => upgrade ? "dd-flow/mb-upgrade-stage-result@1" : `dd-flow/mb-init-${stage}-result@1`;
30
36
  if (assertRunBundleSqlAuthority(context, input, index.execution_bundle)) {
31
37
  if (!input.bundle || input.bundle.execution.sha256 !== input.execution.sha256 || input.bundle.descriptor.sha256 !== input.flow.sha256)
32
38
  refuse();
33
39
  assertPreparedRunBundleSqlAuthority(context, input, input.bundle);
34
40
  }
35
41
  const stage = index.stage_runs?.find((item) => item.stage === input.stage);
36
- if (input.flow.descriptor.id !== "mb-init" || frozen.settings.flow_id !== "mb-init" || frozen.settings.descriptor_sha256 !== input.flow.sha256
37
- || index.execution_bundle && (index.execution_bundle.flow_id !== "mb-init" || index.execution_bundle.descriptor_sha256 !== input.flow.sha256)
42
+ if (!["mb-init", "mb-upgrade"].includes(flowId) || frozen.settings.flow_id !== flowId || frozen.settings.descriptor_sha256 !== input.flow.sha256
43
+ || index.execution_bundle && (index.execution_bundle.flow_id !== flowId || index.execution_bundle.descriptor_sha256 !== input.flow.sha256)
38
44
  || frozen.scope.projectRoot !== run.project_root || frozen.scope.workspace !== run.workspace_root
39
45
  || (input.acceptedReplay ? stage?.status !== "done" : index.current_stage !== input.stage || stage?.status !== "running") || stage.dir !== input.flow.stage(input.stage).directory
40
46
  || typeof stage.attempt !== "string" || !/^try-[0-9]{3,}$/.test(stage.attempt)
@@ -42,7 +48,7 @@ function select(context, input) {
42
48
  refuse();
43
49
  const accepted = input.acceptedReplay ? readAcceptedStageData(context, input) : null;
44
50
  const acceptedValue = accepted ? JSON.parse(accepted.bytes.toString("utf8")) : null;
45
- if (accepted && (accepted.data_schema_id !== `dd-flow/mb-init-${input.stage}-result@1`
51
+ if (accepted && (accepted.data_schema_id !== schemaId(input.stage)
46
52
  || acceptedValue.schema_id !== accepted.data_schema_id || acceptedValue.run_id !== input.runId
47
53
  || acceptedValue.stage !== input.stage || !(acceptedValue.outcome === "accepted" || input.stage === "verify" && acceptedValue.outcome === "needs_repair" && input.flow.successor("verify", "needs_repair") === "review")
48
54
  || acceptedValue.input_sha256 !== stage.input_sha256))
@@ -56,33 +62,87 @@ function select(context, input) {
56
62
  const value = predecessor ? JSON.parse(predecessor.bytes.toString("utf8")) : null;
57
63
  const preflight = entry ? null : readAcceptedStageData(context, { ...input, stage: "preflight" });
58
64
  const preflightValue = preflight ? JSON.parse(preflight.bytes.toString("utf8")) : null;
59
- if (preflight && (preflight.data_schema_id !== "dd-flow/mb-init-preflight-result@1"
65
+ if (preflight && (preflight.data_schema_id !== schemaId("preflight")
60
66
  || preflightValue.schema_id !== preflight.data_schema_id || preflightValue.run_id !== input.runId
61
67
  || preflightValue.stage !== "preflight" || preflightValue.outcome !== "accepted"
62
68
  || preflightValue.input_sha256 !== preflight.input_sha256
63
69
  || typeof preflight.input_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(preflight.input_sha256)))
64
70
  refuse();
65
- if (predecessor && (predecessor.data_schema_id !== `dd-flow/mb-init-${edges[0].from}-result@1`
71
+ if (predecessor && (predecessor.data_schema_id !== schemaId(edges[0].from)
66
72
  || value.schema_id !== predecessor.data_schema_id || value.run_id !== input.runId || value.stage !== edges[0].from
67
73
  || !(value.outcome === "accepted" || edges[0].from === "verify" && value.outcome === "needs_repair" && input.flow.successor("verify", "needs_repair") === input.stage)
68
74
  || typeof predecessor.input_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(predecessor.input_sha256)
69
75
  || value.input_sha256 !== predecessor.input_sha256 || !Array.isArray(value.units)))
70
76
  refuse();
71
- const unitIds = ["preflight", "discover"].includes(input.stage) ? null : value?.units.map((unit) => unit.id);
77
+ const unitIds = ["preflight", upgrade ? "diff-analysis" : "discover"].includes(input.stage) ? null : value?.units.map((unit) => unit.id);
72
78
  if (unitIds !== null && (!Array.isArray(unitIds) || unitIds.some(id => typeof id !== "string") || new Set(unitIds).size !== unitIds.length))
73
79
  refuse();
80
+ let upgradeBinding;
81
+ let upgradeAuthority = null;
82
+ let upgradeDiffValue = null;
83
+ const retainedEvidence = new Set(input.historicalEvidence ?? []);
84
+ if (upgrade) {
85
+ const scope = { requestId: frozen.scope.requestId, projectRoot: frozen.scope.projectRoot, workspaceRoot: frozen.scope.workspace };
86
+ const ready = readMemoryUpgradeBootstrapReadiness(context, scope), storage = readUpgradeStorageReadiness(context, scope);
87
+ if (!ready?.hasPreservationBaseline || ready.projectId !== input.projectId || !storage?.selection
88
+ || ready.authority.binding.execution_sha256 !== input.execution.sha256
89
+ || ready.authority.binding.storage_sha256 !== storage.sha256)
90
+ refuse();
91
+ const selected = storage.selection.payload;
92
+ if (selected.impact.chain.status !== "ok" || !selected.current.old_tree)
93
+ refuse();
94
+ const modes = ["patch", "targeted_minor", "full_migration"];
95
+ let minimum = selected.current.flow_pack_canon_version ? 2 : 0;
96
+ for (const step of selected.impact.chain.steps) {
97
+ const rank = modes.indexOf(step.minimum_upgrade_mode);
98
+ if (rank < 0 || typeof step.path_migration !== "boolean" || typeof step.runtime_migration !== "boolean")
99
+ refuse();
100
+ minimum = Math.max(minimum, step.path_migration || step.runtime_migration ? 2 : rank);
101
+ }
102
+ const invariants = preflightValue?.details.preservation_invariants;
103
+ if (!entry && !Array.isArray(invariants))
104
+ refuse();
105
+ const oldEvidence = readMemoryUpgradeEvidence(context, scope), evidence = oldEvidence.reference;
106
+ upgradeBinding = { minimumMode: modes[minimum], evidenceIndex: { path: evidence.path, sha256: evidence.sha256 }, ...(entry ? {} : { preservationInvariants: structuredClone(invariants) }) };
107
+ let diffAuthority = null;
108
+ if (!["preflight", "diff-analysis"].includes(input.stage)) {
109
+ const diff = readAcceptedStageData(context, { ...input, stage: "diff-analysis" });
110
+ const diffValue = JSON.parse(diff.bytes.toString("utf8"));
111
+ if (diff.data_schema_id !== schemaId("diff-analysis") || diffValue.schema_id !== diff.data_schema_id
112
+ || diffValue.stage !== "diff-analysis" || diffValue.run_id !== input.runId || diffValue.outcome !== "accepted"
113
+ || diffValue.input_sha256 !== diff.input_sha256 || !Array.isArray(diffValue.details?.path_map))
114
+ refuse();
115
+ upgradeBinding.authoringPaths = upgradeAuthoringPaths(selected.current.inventory.inventory, diffValue.details.path_map);
116
+ upgradeBinding.mechanicalUnits = new Set(diffValue.details.path_map
117
+ .map(change => change.unit_id).filter(id => !upgradeBinding.authoringPaths.has(id)));
118
+ upgradeDiffValue = diffValue;
119
+ diffAuthority = diff.authority_sha256;
120
+ }
121
+ // Old snapshots authorize historical evidence only, never current artifacts
122
+ // or reads through links/private paths. They do not replace source observation.
123
+ for (const { source } of oldEvidence.index.entries) {
124
+ retainedEvidence.add(`${memorySourceKey("workspace", source.path)}\0${source.sha256}`);
125
+ if (scope.projectRoot === scope.workspaceRoot)
126
+ retainedEvidence.add(`${memorySourceKey("project", source.path)}\0${source.sha256}`);
127
+ }
128
+ upgradeAuthority = hybridDigest({ bootstrap: ready.authority, storage: storage.sha256, diff: diffAuthority,
129
+ upgrade: { ...upgradeBinding, ...(upgradeBinding.authoringPaths ? { authoringPaths: [...upgradeBinding.authoringPaths] } : {}),
130
+ ...(upgradeBinding.mechanicalUnits ? { mechanicalUnits: [...upgradeBinding.mechanicalUnits].sort() } : {}) } });
131
+ }
74
132
  const binding = { runId: input.runId, stage: input.stage, inputSha256: stage.input_sha256,
75
133
  predecessors: predecessor ? [{ stage: edges[0].from, result: predecessor.result, input_sha256: predecessor.input_sha256 }] : [],
76
134
  unitIds, sources: new Map([...input.sources.hashes(), ...(input.checks?.artifactReferences() ?? []).map(ref => [memorySourceKey(ref.root, ref.path), ref.sha256])]),
77
- historicalEvidence: new Set([...input.historicalEvidence ?? [], ...(value?.units ?? []).flatMap((unit) => unit.evidence.map(ref => `${memorySourceKey(ref.root, ref.path)}\0${ref.sha256}`))]),
135
+ historicalEvidence: new Set([...retainedEvidence, ...(value?.units ?? []).flatMap((unit) => unit.evidence.map(ref => `${memorySourceKey(ref.root, ref.path)}\0${ref.sha256}`))]),
78
136
  execution: input.execution, executionSha256: input.execution.sha256,
137
+ ...(upgradeBinding ? { upgrade: upgradeBinding } : {}),
79
138
  ...(preflight ? { preflight: { result: preflight.result, input_sha256: preflight.input_sha256,
80
139
  semantic_scope: preflightValue.details.semantic_scope } } : {}) };
81
140
  const authority = hybridDigest({ projectId: input.projectId, runId: input.runId, stage: input.stage,
82
141
  attempt: stage.attempt, input: stage.input_sha256, directory: stage.dir, predecessor: predecessor?.authority_sha256 ?? null,
83
142
  anchor: index.execution_bundle ?? null, execution: input.execution.sha256, preflight: preflight?.authority_sha256 ?? null,
84
- accepted: accepted?.authority_sha256 ?? null, settlement: settlement ? hybridDigest(settlement) : null });
85
- return { binding, authority, predecessorStage: edges[0]?.from, value, preflightValue, accepted, acceptedValue };
143
+ accepted: accepted?.authority_sha256 ?? null, settlement: settlement ? hybridDigest(settlement) : null,
144
+ ...(upgrade ? { upgrade: upgradeAuthority } : {}) });
145
+ return { binding, authority, predecessorStage: edges[0]?.from, value, preflightValue, accepted, acceptedValue, upgradeDiffValue };
86
146
  }
87
147
  /** Controller-owned input for result preparation and writer-safe rechecking.
88
148
  * Semantic/check acceptance still belongs to productive stage handlers.
@@ -127,9 +187,14 @@ export class PreparedMemoryResultBinding {
127
187
  if (!new Ajv2020({ strict: true, strictRequired: false, validateFormats: false }).compile(schema)(selected.preflightValue))
128
188
  refuse();
129
189
  }
190
+ if (selected.upgradeDiffValue) {
191
+ const schema = JSON.parse(input.flow.stageBlob("diff-analysis", "result_schema").toString("utf8"));
192
+ if (!new Ajv2020({ strict: true, strictRequired: false, validateFormats: false }).compile(schema)(selected.upgradeDiffValue))
193
+ refuse();
194
+ }
130
195
  // Productive bound discovery/composition preserve independently accepted source
131
196
  // bytes. Unbound data-only SDK fixtures gain no bootstrap qualification.
132
- const preservation = input.bundle && ["discover", "compose"].includes(input.stage) ? PreparedMemoryBaselinePreservation.prepare(context, input.execution) : null;
197
+ const preservation = input.flow.descriptor.id === "mb-init" && input.bundle && ["discover", "compose"].includes(input.stage) ? PreparedMemoryBaselinePreservation.prepare(context, input.execution) : null;
133
198
  const prepared = new PreparedMemoryResultBinding(preparedInput, selected, preservation);
134
199
  prepared.assertCustody(context);
135
200
  return prepared;
@@ -139,7 +204,7 @@ export class PreparedMemoryResultBinding {
139
204
  || input.bundle.execution.sha256 !== input.execution.sha256 || input.bundle.descriptor.sha256 !== input.flow.sha256)
140
205
  refuse();
141
206
  const observed = captureRegisteredMemoryCompositionSources(context, input.bundle, input.registrationScope);
142
- const prepared = this.prepare(context, { ...input, stage: "compose", sources: observed.sources });
207
+ const prepared = this.prepare(context, { ...input, stage: input.registrationScope.stage, sources: observed.sources });
143
208
  if (prepared.#binding.inputSha256 !== observed.inputSha256)
144
209
  refuse();
145
210
  prepared.#registration = { scope: structuredClone(input.registrationScope), sha256: observed.registrationSha256 };
@@ -149,6 +214,7 @@ export class PreparedMemoryResultBinding {
149
214
  }
150
215
  binding() {
151
216
  return { ...this.#binding, historicalEvidence: new Set(this.#binding.historicalEvidence), predecessors: structuredClone(this.#binding.predecessors), unitIds: structuredClone(this.#binding.unitIds),
217
+ ...(this.#binding.upgrade ? { upgrade: structuredClone(this.#binding.upgrade) } : {}),
152
218
  ...(this.#binding.preflight ? { preflight: structuredClone(this.#binding.preflight) } : {}), sources: new Map(this.#binding.sources) };
153
219
  }
154
220
  /** Retained accepted data for handler replay, never caller-supplied bytes. */
@@ -18,16 +18,17 @@ function refuse(reason) {
18
18
  }
19
19
  function original(context, scope, bundle) {
20
20
  const phase = readMemoryReviewPhase(context, scope, bundle), registration = readRegisteredMemoryReview(context, phase, 0);
21
- const compose = readAcceptedStageData(context, { ...scope, stage: "compose" });
21
+ const upgrade = bundle.descriptor.descriptor.id === "mb-upgrade", authoring = upgrade ? "upgrade" : "compose";
22
+ const compose = readAcceptedStageData(context, { ...scope, stage: authoring });
22
23
  const verify = readAcceptedStageData(context, { ...scope, stage: "verify" });
23
24
  const composed = JSON.parse(compose.bytes.toString("utf8")), verified = JSON.parse(verify.bytes.toString("utf8"));
24
25
  const view = registration.view;
25
26
  if (registration.input_sha256 !== hybridDigest(view) || view.wave !== 0 || view.scope.bundle_sha256 !== bundle.sha256
26
27
  || view.scope.execution_sha256 !== bundle.execution.sha256 || view.source_policy !== "review-inputs@1"
27
- || compose.data_schema_id !== "dd-flow/mb-init-compose-result@1" || composed.schema_id !== compose.data_schema_id
28
- || composed.run_id !== scope.runId || composed.stage !== "compose" || composed.outcome !== "accepted"
28
+ || compose.data_schema_id !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : "dd-flow/mb-init-compose-result@1") || composed.schema_id !== compose.data_schema_id
29
+ || composed.run_id !== scope.runId || composed.stage !== authoring || composed.outcome !== "accepted"
29
30
  || composed.input_sha256 !== compose.input_sha256 || hybridDigest(composed.units) !== hybridDigest(view.unit_inventory)
30
- || verify.data_schema_id !== "dd-flow/mb-init-verify-result@1" || verified.schema_id !== verify.data_schema_id
31
+ || verify.data_schema_id !== (upgrade ? "dd-flow/mb-upgrade-stage-result@1" : "dd-flow/mb-init-verify-result@1") || verified.schema_id !== verify.data_schema_id
31
32
  || verified.run_id !== scope.runId || verified.stage !== "verify" || !["accepted", "needs_repair"].includes(verified.outcome)
32
33
  || verified.input_sha256 !== verify.input_sha256)
33
34
  refuse("changed_original_review_binding");
@@ -74,27 +75,35 @@ export class PreparedRetainedMemoryReviewAuthority {
74
75
  Object.freeze(this);
75
76
  }
76
77
  static prepare(context, scope, bundle) {
77
- if (!(bundle instanceof RetainedExecutionBundle) || bundle.descriptor.descriptor.id !== "mb-init")
78
+ if (!(bundle instanceof RetainedExecutionBundle) || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id))
78
79
  refuse("registered_memory_bundle_required");
79
80
  const generation = assertRunMutationAllowed(context, scope.projectId, scope.runId);
80
81
  const selected = context.db.readSnapshot(() => original(context, scope, bundle));
81
82
  const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), root = run.run_root ?? run.run_dir;
82
- const observed = ObservedMemorySources.capture({ run: root }, [selected.compose, selected.verify].map(data => ({ root: "run", path: data.result.path })));
83
+ const upgrade = bundle.descriptor.descriptor.id === "mb-upgrade";
84
+ const refs = upgrade ? selected.registration.view.run_inputs.filter(ref => ref.root === "run" || !ref.path.startsWith(".memory-bank/"))
85
+ : [selected.compose, selected.verify].map(data => ({ root: "run", ...data.result }));
86
+ const observed = ObservedMemorySources.capture({ run: root, workspace: bundle.execution.snapshot().scope.workspace }, refs.map(({ root, path }) => ({ root, path })));
83
87
  const hashes = observed.hashes();
84
88
  if ([selected.compose, selected.verify].some(data => hashes.get(memorySourceKey("run", data.result.path)) !== data.result.sha256))
85
89
  refuse("retained_predecessor_bytes_changed");
90
+ if (refs.some(ref => hashes.get(memorySourceKey(ref.root, ref.path)) !== ref.sha256))
91
+ refuse("retained_source_bytes_changed");
86
92
  const coordinator = PreparedDescriptorStageCoordinator.prepare(context, {
87
93
  projectRoot: bundle.execution.snapshot().scope.projectRoot, runId: scope.runId, stage: "review"
88
94
  });
89
95
  coordinator.assertAllocatedCustody(context);
90
96
  const session = readDescriptorCoordinatorSession(context, { scope: { ...scope, stage: "review" }, bundle,
91
97
  workId: selected.registration.scope.parent_work_id }, refuse);
92
- const preservation = PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
93
98
  // Historical accepted compose binding intentionally observes only retained
94
99
  // predecessor files. It does not claim today's repaired documents still
95
100
  // have the original output hashes or rerun current project policy.
96
- const compose = PreparedMemoryResultBinding.prepare(context, { ...scope, stage: "compose", flow: bundle.descriptor,
101
+ const compose = PreparedMemoryResultBinding.prepare(context, { ...scope, stage: upgrade ? "upgrade" : "compose", flow: bundle.descriptor,
97
102
  execution: bundle.execution, bundle, acceptedReplay: true, sources: observed });
103
+ // The historical upgrade binding retains its original bootstrap/old tree;
104
+ // observed inputs preserve old RUN copies and original non-MB sources.
105
+ // Current repaired MB bytes belong to separately settled repair views.
106
+ const preservation = upgrade ? compose : PreparedMemoryBaselinePreservation.prepare(context, bundle.execution);
98
107
  if (!compose.acceptedBytes()?.equals(selected.compose.bytes))
99
108
  refuse("accepted_composition_changed");
100
109
  const verify = PreparedAcceptedMemoryVerification.prepare(context, { ...scope, bytes: selected.verify.bytes });
@@ -46,7 +46,7 @@ export class PreparedRetainedSecondMemoryBaseline {
46
46
  static prepare(context, input) {
47
47
  if (!PreparedRetainedSecondMemoryRepairs.isPrepared(input.registration))
48
48
  refuse("retained_second_registration_required");
49
- if (!(input.bundle instanceof RetainedExecutionBundle) || input.bundle.descriptor.descriptor.id !== "mb-init")
49
+ if (!(input.bundle instanceof RetainedExecutionBundle) || !["mb-init", "mb-upgrade"].includes(input.bundle.descriptor.descriptor.id))
50
50
  refuse("registered_memory_bundle_required");
51
51
  if (context.db.isOwnedWriteTransaction?.())
52
52
  refuse("preparation_outside_writer_required");
@@ -1,11 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { assertMemorySqlCustody } from "./memory-custody-snapshot.js";
2
3
  import { AppError } from "../shared/errors.js";
3
4
  import { RetainedExecutionBundle } from "./execution-bundles.js";
4
5
  import { hybridDigest } from "./hybrid-contracts.js";
5
6
  import { PreparedHistoricalOriginalMemoryReview } from "./memory-historical-original-review.js";
6
7
  import { PreparedHistoricalCorrectedMemoryReview } from "./memory-historical-corrected-review.js";
7
8
  import { readRetainedCorrectedMemoryReviewRegistration } from "./memory-corrected-review-registration.js";
8
- import { validateMemoryReviewCorrectionPlan } from "./memory-review-correction-plan.js";
9
+ import { validateMemoryReviewCorrectionPlan, memoryRepairCanonicalPaths } from "./memory-review-correction-plan.js";
9
10
  import { memoryRepairBatch, memoryRepairWorkerSchema } from "./memory-repair-registration.js";
10
11
  import { readWorkBatchCustody } from "./work-registry.js";
11
12
  const token = Symbol("retained-second-memory-repairs");
@@ -59,6 +60,7 @@ function assertAllocation(context, receipt, batch) {
59
60
  /** Retained second graph only. Does not grant current source/write admission,
60
61
  * allocate replacement Works or dispatch providers after receipt loss. */
61
62
  export class PreparedRetainedSecondMemoryRepairs {
63
+ #custodyKey = {};
62
64
  static isPrepared(value) {
63
65
  return typeof value === "object" && value !== null && #receipt in value;
64
66
  }
@@ -86,7 +88,7 @@ export class PreparedRetainedSecondMemoryRepairs {
86
88
  return this.#prepare(context, input, true);
87
89
  }
88
90
  static #prepare(context, input, closed) {
89
- if (!(input.bundle instanceof RetainedExecutionBundle) || input.bundle.descriptor.descriptor.id !== "mb-init")
91
+ if (!(input.bundle instanceof RetainedExecutionBundle) || !["mb-init", "mb-upgrade"].includes(input.bundle.descriptor.descriptor.id))
90
92
  refuse("registered_memory_bundle_required");
91
93
  if (context.db.isOwnedWriteTransaction?.())
92
94
  refuse("preparation_outside_writer_required");
@@ -105,6 +107,7 @@ export class PreparedRetainedSecondMemoryRepairs {
105
107
  refuse("changed_first_review");
106
108
  const view = first.view, assignments = new Map(view.assignments.map(item => [item.unit.id, item.unit]));
107
109
  const plan = validateMemoryReviewCorrectionPlan(bytes, input.bundle.engine(), { runId: scope.run_id, wave: 2,
110
+ canonicalPaths: memoryRepairCanonicalPaths(context, input.bundle),
108
111
  reviewSha256: receipt.review_sha256, units: view.unit_inventory.map(unit => assignments.get(unit.id) ?? unit), findings: history.projection().findings });
109
112
  if (history.projection().outcome === "blocked")
110
113
  refuse("blocked_first_review");
@@ -147,10 +150,13 @@ export class PreparedRetainedSecondMemoryRepairs {
147
150
  firstEffectiveReviewProof(context) { this.assertCustody(context); return this.#history.effectiveReviewProof(context); }
148
151
  verificationProfileHash(context) { this.assertCustody(context); return this.#history.verificationProfileHash(context); }
149
152
  assertCustody(context) {
150
- this.#history.assertCustody(context);
151
- if (hybridDigest(selected(context, this.#receipt.scope)) !== hybridDigest(this.#receipt))
152
- refuse("registration_changed");
153
- assertAllocation(context, this.#receipt, this.#batch);
153
+ const history = this.#history;
154
+ assertMemorySqlCustody(context, this.#custodyKey, () => {
155
+ history.assertCustody(context);
156
+ if (hybridDigest(selected(context, this.#receipt.scope)) !== hybridDigest(this.#receipt))
157
+ refuse("registration_changed");
158
+ assertAllocation(context, this.#receipt, this.#batch);
159
+ });
154
160
  }
155
161
  revalidate(context) { this.#history.revalidate(context); this.assertCustody(context); }
156
162
  }
@@ -8,32 +8,66 @@ import { ObservedMemorySources } from "./memory-source-observations.js";
8
8
  import { memorySourceKey } from "./memory-stage-results.js";
9
9
  import { reviewSourceHash, reviewEnvironmentAllowed } from "./review-copy.js";
10
10
  import { memoryProjectConfigurationPath } from "./memory-composition-graph.js";
11
+ import { RetainedExecutionBundle } from "./execution-bundles.js";
12
+ import { readUpgradeStorageReadiness } from "./memory-upgrade-storage-readiness.js";
13
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
14
+ import { upgradeAuthoringPaths } from "./memory-upgrade-inventory.js";
11
15
  function refuse(reason) {
12
16
  throw new AppError("memory_review_correction_invalid", "Correction plan differs from retained material findings", 2, { reason, effect: "no_effect" });
13
17
  }
14
18
  const alias = (value) => value.normalize("NFC").toLowerCase();
15
- const overlap = (a, b) => alias(a) === alias(b) || alias(a).startsWith(`${alias(b)}/`) || alias(b).startsWith(`${alias(a)}/`);
19
+ const overlapAliases = (a, b) => a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
20
+ const overlap = (a, b) => overlapAliases(alias(a), alias(b));
16
21
  /** Exact project-owned document paths shared by graph and physical admission. */
17
- export function assertMemoryRepairWritePaths(paths) {
22
+ export function assertMemoryRepairWritePaths(paths, canonicalPaths = new Set()) {
18
23
  if (!Array.isArray(paths) || !paths.length || paths.length > 512)
19
24
  refuse("foreign_correction_write");
20
25
  if (paths.some(logical => typeof logical !== "string"))
21
26
  refuse("foreign_correction_write");
22
- if (new Set(paths.map(alias)).size !== paths.length)
27
+ const normalizedPaths = paths.map(alias);
28
+ if (new Set(normalizedPaths).size !== paths.length)
23
29
  refuse("aliased_correction_paths");
24
- for (const logical of paths) {
25
- if (typeof logical !== "string" || !logical.startsWith(".memory-bank/") || logical.includes("\\") || logical.includes("\0")
26
- || logical.length > 4096 || logical.split("/").some(part => !part || part === "." || part === "..")
27
- || logical.split("/").some(part => !reviewEnvironmentAllowed(alias(part)) || [".git", "node_modules", ".dd-flow"].includes(alias(part)))
28
- || !memoryProjectConfigurationPath(logical)
29
- && [".memory-bank/dd-flow", ".memory-bank/mbb"].some(canonical => overlap(logical, canonical)))
30
+ for (const [index, logical] of paths.entries()) {
31
+ if (typeof logical !== "string")
32
+ refuse("foreign_correction_write");
33
+ const parts = logical.split("/");
34
+ if (!logical.startsWith(".memory-bank/") || logical.includes("\\") || logical.includes("\0")
35
+ || logical.length > 4096 || parts.some(part => !part || part === "." || part === "..")
36
+ || parts.some(part => { const normalized = alias(part); return !reviewEnvironmentAllowed(normalized) || [".git", "node_modules", ".dd-flow"].includes(normalized); })
37
+ || !memoryProjectConfigurationPath(logical) && !canonicalPaths.has(logical)
38
+ && [".memory-bank/dd-flow", ".memory-bank/mbb"].some(canonical => overlapAliases(normalizedPaths[index], canonical)))
30
39
  refuse("foreign_correction_write");
31
40
  }
32
41
  }
42
+ /** Exact exceptions from the original CLI-selected ownership inventory, not
43
+ * path names or authority fields supplied in a repair proposal. Canonical
44
+ * defaults, generated markers, links and directory aliases remain excluded. */
45
+ export function memoryRepairCanonicalPaths(context, bundle) {
46
+ if (!(bundle instanceof RetainedExecutionBundle) || !["mb-init", "mb-upgrade"].includes(bundle.descriptor.descriptor.id))
47
+ refuse("registered_memory_bundle_required");
48
+ if (bundle.descriptor.descriptor.id === "mb-init")
49
+ return new Set();
50
+ const frozen = bundle.execution.snapshot().scope;
51
+ const storage = readUpgradeStorageReadiness(context, { requestId: frozen.requestId, projectRoot: frozen.projectRoot, workspaceRoot: frozen.workspace }), selected = storage?.selection;
52
+ if (!selected || selected.payload.execution.sha256 !== bundle.execution.sha256)
53
+ refuse("retained_upgrade_selection_required");
54
+ const data = readAcceptedStageData(context, { projectId: storage.receipt.project_id, runId: bundle.manifest.run_id, stage: "diff-analysis" });
55
+ const diff = JSON.parse(data.bytes.toString("utf8"));
56
+ if (data.data_schema_id !== "dd-flow/mb-upgrade-stage-result@1" || diff.stage !== "diff-analysis" || diff.outcome !== "accepted")
57
+ refuse("accepted_upgrade_diff_required");
58
+ const inventory = selected.payload.current.inventory.inventory;
59
+ const paths = [...upgradeAuthoringPaths(inventory, diff.details.path_map).values()].flat();
60
+ return new Set(paths.filter(logical => {
61
+ const entry = inventory.entries.find(entry => `.memory-bank/${entry.path}` === logical);
62
+ return [".memory-bank/dd-flow/", ".memory-bank/mbb/"].some(prefix => logical.startsWith(prefix))
63
+ && logical.split("/").every(part => reviewEnvironmentAllowed(alias(part)) && ![".git", "node_modules", ".dd-flow"].includes(alias(part)))
64
+ && (!entry || ["project", "local-customized"].includes(entry.ownership));
65
+ }));
66
+ }
33
67
  /** Semantic graph checks after strict frozen-schema admission. This projection
34
68
  * is not Work allocation, source-byte admission or filesystem permission.
35
69
  */
36
- export function validateMemoryReviewCorrectionGraph(plan, units, findings) {
70
+ export function validateMemoryReviewCorrectionGraph(plan, units, findings, canonicalPaths = new Set()) {
37
71
  const material = new Map(findings.filter(finding => finding.severity !== "minor" && finding.status === "open").map(finding => [finding.id, finding]));
38
72
  if (!material.size)
39
73
  refuse("no_material_correction_required");
@@ -47,7 +81,7 @@ export function validateMemoryReviewCorrectionGraph(plan, units, findings) {
47
81
  refuse("missing_correction_task");
48
82
  if (work.depends_on.some(key => !byKey.has(key) || key === work.key))
49
83
  refuse("foreign_correction_dependency");
50
- assertMemoryRepairWritePaths(work.write_paths);
84
+ assertMemoryRepairWritePaths(work.write_paths, canonicalPaths);
51
85
  const targetUnits = new Set();
52
86
  for (const id of work.finding_ids) {
53
87
  const finding = material.get(id);
@@ -109,7 +143,7 @@ export function validateMemoryReviewCorrectionPlan(bytes, engine, binding) {
109
143
  refuse("invalid_correction_schema");
110
144
  if (value.run_id !== binding.runId || value.wave !== binding.wave || value.review_sha256 !== binding.reviewSha256)
111
145
  refuse("foreign_correction_identity");
112
- validateMemoryReviewCorrectionGraph(value, binding.units, binding.findings);
146
+ validateMemoryReviewCorrectionGraph(value, binding.units, binding.findings, binding.canonicalPaths);
113
147
  return structuredClone(value);
114
148
  }
115
149
  catch (error) {
@@ -144,6 +178,7 @@ export class PreparedMemoryInitialCorrectionPlan {
144
178
  if (results.some(result => result.outcome === "blocked"))
145
179
  refuse("blocked_review_requires_resolution");
146
180
  const value = validateMemoryReviewCorrectionPlan(bytes, bundle.engine(), { runId: registration.run_id, wave: 1,
181
+ canonicalPaths: memoryRepairCanonicalPaths(context, bundle),
147
182
  reviewSha256: review.sha256, units: registration.view.unit_inventory, findings: memoryReviewWaveProjection(results, registration.view.diagnostic_findings).findings });
148
183
  for (const work of value.works)
149
184
  bundle.execution.work("review", work.profile_id ?? undefined);