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

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -9,7 +9,7 @@
9
9
  "work_id": { "type": "string", "minLength": 1, "maxLength": 256 },
10
10
  "packet_sha256": { "$ref": "#/$defs/hash" },
11
11
  "wave": { "type": "integer", "minimum": 1, "maximum": 2 },
12
- "outcome": { "enum": ["accepted", "blocked"] },
12
+ "outcome": { "enum": ["accepted", "reconciled", "blocked"] },
13
13
  "summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
14
14
  "changes": {
15
15
  "type": "array", "maxItems": 512,
@@ -29,12 +29,19 @@
29
29
  "properties": {
30
30
  "finding_id": { "type": "string", "minLength": 1, "maxLength": 128 },
31
31
  "summary": { "type": "string", "minLength": 1, "maxLength": 16384 },
32
- "delta_paths": { "type": "array", "minItems": 1, "maxItems": 512, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } },
32
+ "delta_paths": { "type": "array", "maxItems": 512, "uniqueItems": true, "items": { "type": "string", "minLength": 1, "maxLength": 4096 } },
33
33
  "evidence": { "type": "array", "minItems": 1, "maxItems": 256, "items": { "$ref": "#/$defs/reference" } }
34
34
  }
35
35
  }
36
36
  }
37
37
  },
38
+ "allOf": [
39
+ { "if": { "properties": { "outcome": { "const": "accepted" } } },
40
+ "then": { "properties": { "changes": { "type": "array", "minItems": 1 }, "resolutions": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "delta_paths": { "type": "array", "minItems": 1 } } } } } } },
41
+ { "if": { "properties": { "outcome": { "const": "reconciled" } } },
42
+ "then": { "properties": { "changes": { "type": "array", "maxItems": 0 }, "resolutions": { "type": "array", "minItems": 1,
43
+ "items": { "type": "object", "properties": { "delta_paths": { "type": "array", "maxItems": 0 } } } } } } }
44
+ ],
38
45
  "$defs": {
39
46
  "hash": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
40
47
  "reference": {
@@ -81,7 +81,7 @@
81
81
  "uniqueItems": true,
82
82
  "items": {
83
83
  "type": "string",
84
- "enum": ["mb-init.md", "mb-upgrade.md", "mb-distill.md"]
84
+ "enum": ["mb-init.md", "mb-upgrade.md", "mb-distill.md", "mb-audit.md", "review.md"]
85
85
  }
86
86
  },
87
87
  "generated_allow_patterns": {
@@ -7,10 +7,10 @@
7
7
  "required": ["schema_id", "from_version", "to_version", "minimum_upgrade_mode", "domains", "path_migration", "runtime_migration", "required_checks"],
8
8
  "properties": {
9
9
  "schema_id": {"const": "dd-flow/release-impact@1"},
10
- "from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
11
- "to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"},
10
+ "from_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
11
+ "to_version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[A-Za-z0-9.-]+)?$"},
12
12
  "minimum_upgrade_mode": {"enum": ["patch", "targeted_minor", "full_migration"]},
13
- "domains": {"type": "array", "items": {"enum": ["canonical_layer", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
13
+ "domains": {"type": "array", "items": {"enum": ["canonical_layer", "flow_contracts", "root_structure", "product", "system", "engineering", "operations", "adr", "plans", "scenarios", "ui", "guides", "protocol_evidence", "skills", "archive", "code_traceability"]}, "uniqueItems": true},
14
14
  "path_migration": {"type": "boolean"},
15
15
  "runtime_migration": {"type": "boolean"},
16
16
  "required_checks": {"type": "array", "items": {"type": "string"}, "minItems": 1},
@@ -19,6 +19,10 @@
19
19
  "stage_root": {"type": "string", "minLength": 1},
20
20
  "archive_path": {"type": ["string", "null"]},
21
21
  "prompt_path": {"type": "string", "minLength": 1},
22
+ "prompt_delivery": {
23
+ "type": "object", "additionalProperties": false, "required": ["path", "sha256"],
24
+ "properties": {"path": {"type": "string", "minLength": 1}, "sha256": {"type": "string", "pattern": "^[a-f0-9]{64}$"}}
25
+ },
22
26
  "aliases": {"type": "object", "additionalProperties": {"type": "string"}},
23
27
  "resolved_context": {"type": "object", "additionalProperties": true},
24
28
  "plan_ref": {"type": "string", "pattern": "^\\.memory-bank/protocol/[^/]+/plan\\.json$"},
@@ -15,12 +15,24 @@ export function acceptedIntentReviewFingerprint(sources) {
15
15
  * clarification may use the existing coordinator correction, explicitly named
16
16
  * as such; it never retroactively expands the independent review's coverage. */
17
17
  export function validateAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, acknowledgement) {
18
+ // Keep file validation outside callers' SQL writer; the prepared validator
19
+ // below consumes only accepted bytes/ordered SQL events.
18
20
  const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
19
21
  if (!reviewed)
20
- return null; // Historical source contract has no new authority.
22
+ return null;
21
23
  if (contextFingerprint !== reviewed)
22
24
  throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
23
25
  const currentSources = prepareAcceptedIntentSources(context, { ...scope, stageDirectory: "intake", marked: true }).sources;
26
+ return validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement);
27
+ }
28
+ export function validatePreparedAcceptedIntentReview(context, scope, reviewedSources, contextFingerprint, currentSources, acknowledgement) {
29
+ const reviewed = acceptedIntentReviewFingerprint(reviewedSources);
30
+ if (!reviewed)
31
+ return null; // Historical source contract has no new authority.
32
+ if (contextFingerprint !== reviewed)
33
+ throw new AppError("accepted_intent_review_stale", "Review context does not bind the retained accepted inputs", 1, { stage: scope.stage });
34
+ if (currentSources.length > 256)
35
+ throw new AppError("accepted_intent_source_invalid", "Accepted intent source count exceeds its bound", 1);
24
36
  const current = acceptedIntentReviewFingerprint(currentSources);
25
37
  const reviewedEvent = Math.max(0, ...(reviewedSources ?? []).filter(source => source.role.startsWith("accepted_hitl:")).map(source => Number(source.role.split(":")[1])));
26
38
  const rows = context.db.all("SELECT id, payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.stage_resumed' AND json_extract(payload_json, '$.run_id') = ? AND id > ? ORDER BY id LIMIT 256", [scope.projectId, scope.runId, reviewedEvent]);
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import { AppError } from "../shared/errors.js";
5
5
  import { prepareError } from "../cli/input-preparation.js";
6
+ import { resolveDdFlowHome } from "../storage/paths.js";
7
+ import { readSetupStatus } from "./setup.js";
6
8
  const canonRootKey = "canon.root";
7
9
  export function prepareCanonRegistration(context, input) {
8
10
  if (typeof input.root !== "string" || !input.root.trim())
@@ -41,16 +43,21 @@ export function getCanonStatus(context, input = {}) {
41
43
  export function resolveCanonRoot(context, input = {}) {
42
44
  return resolveCanonCandidates(canonCandidates(context, input));
43
45
  }
44
- /** Read only: resolve an explicit/environment/nearby canon without opening DD_FLOW_HOME. */
46
+ /** Read only: resolve canonical data without opening SQLite or running migrations. */
45
47
  export function resolveCanonRootBeforeContext(env, input = {}) {
46
48
  const candidates = [];
47
49
  if (input.explicitRoot)
48
50
  candidates.push(validateCanonCandidate("explicit", input.explicitRoot));
49
51
  else if (env.DD_MEMORYBANK)
50
52
  candidates.push(validateCanonCandidate("env", env.DD_MEMORYBANK));
51
- else
52
- for (const known of knownCanonRoots())
53
- candidates.push(validateCanonCandidate("known", known));
53
+ else {
54
+ const installed = installedCanonCandidate(resolveDdFlowHome(env));
55
+ if (installed)
56
+ candidates.push(installed);
57
+ else
58
+ for (const known of knownCanonRoots())
59
+ candidates.push(validateCanonCandidate("known", known));
60
+ }
54
61
  return resolveCanonCandidates(uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`));
55
62
  }
56
63
  function resolveCanonCandidates(candidates) {
@@ -67,7 +74,7 @@ function resolveCanonCandidates(candidates) {
67
74
  const memorybankRoot = candidate.memorybank_root ?? root;
68
75
  const mbbRoot = candidate.mbb_root ?? path.join(memorybankRoot, "mbb");
69
76
  const layout = candidate.layout ?? "legacy_root";
70
- const metadata = readCanonMetadata(root, flowRoot);
77
+ const metadata = candidate.metadata ?? readCanonMetadata(root, flowRoot);
71
78
  return {
72
79
  ok: true,
73
80
  canon: {
@@ -114,6 +121,30 @@ function resolveCanonCandidates(candidates) {
114
121
  ]
115
122
  };
116
123
  }
124
+ /** Archive provenance belongs to the selected release, not an ancestor Git
125
+ * checkout. A damaged installation is not permission to select nearby code. */
126
+ function installedCanonCandidate(home) {
127
+ if (!fs.existsSync(path.join(home, "installation.json")))
128
+ return null;
129
+ const status = readSetupStatus(home), installation = status.installation;
130
+ const invalid = (root, reason) => ({
131
+ source: "installed", root, realpath: null, valid: false, blockers: [reason]
132
+ });
133
+ if (!installation || status.installation_readiness !== "installed")
134
+ return invalid(home, "Installed canonical release is degraded; reconcile setup before selecting its canon.");
135
+ const roots = fs.readdirSync(installation.canon_root);
136
+ if (roots.length !== 1)
137
+ return invalid(installation.canon_root, "Installed canonical archive has no unique root.");
138
+ const candidate = validateCanonCandidate("installed", path.join(installation.canon_root, roots[0]));
139
+ if (!candidate.valid)
140
+ return candidate;
141
+ const version = readTrimmed(path.join(candidate.root, "VERSION"));
142
+ const flowContract = readFlowContractId(candidate.flow_root);
143
+ if (version !== installation.manifest.canon.version || !flowContract)
144
+ return invalid(candidate.root, "Installed canonical metadata differs from its release selection.");
145
+ return { ...candidate, metadata: { version, commit: installation.manifest.canon.commit,
146
+ flow_contract: flowContract, status: "present", diagnostics: [] } };
147
+ }
117
148
  function canonCandidates(context, input) {
118
149
  const candidates = [];
119
150
  if (input.explicitRoot) {
@@ -129,8 +160,13 @@ function canonCandidates(context, input) {
129
160
  candidates.push(validateCanonCandidate("registered", registered.root));
130
161
  }
131
162
  }
132
- for (const known of knownCanonRoots()) {
133
- candidates.push(validateCanonCandidate("known", known));
163
+ if (!candidates.length) {
164
+ const installed = installedCanonCandidate(context.ddFlowHome);
165
+ if (installed)
166
+ return [installed];
167
+ for (const known of knownCanonRoots()) {
168
+ candidates.push(validateCanonCandidate("known", known));
169
+ }
134
170
  }
135
171
  return uniqueBy(candidates, (candidate) => `${candidate.source}:${candidate.root}`);
136
172
  }
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { AppError } from "../shared/errors.js";
3
+ export function validateCheckEnvironment(value) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ const entries = value && typeof value === "object" && !Array.isArray(value) ? Object.entries(value) : [];
7
+ if (!entries.length || entries.some(([key, selected]) => !["CODEX_HOME", "PLAYWRIGHT_BROWSERS_PATH"].includes(key)
8
+ || typeof selected !== "string" || !selected || selected.length > 4096 || selected.includes("\0") || !path.isAbsolute(selected)))
9
+ throw new AppError("invalid_check_environment", "Owned checks require absolute known resource paths", 2, { effect: "no_effect" });
10
+ return { ...value };
11
+ }
12
+ export function retainedBrowserEnvironment(environment) {
13
+ return environment.PLAYWRIGHT_BROWSERS_PATH === undefined ? undefined
14
+ : validateCheckEnvironment({ PLAYWRIGHT_BROWSERS_PATH: environment.PLAYWRIGHT_BROWSERS_PATH });
15
+ }
16
+ export function ownedCheckEnvironment(environment, requested) {
17
+ const selected = validateCheckEnvironment(requested), retained = retainedBrowserEnvironment(environment);
18
+ if (retained?.PLAYWRIGHT_BROWSERS_PATH && selected?.PLAYWRIGHT_BROWSERS_PATH && retained.PLAYWRIGHT_BROWSERS_PATH !== selected.PLAYWRIGHT_BROWSERS_PATH)
19
+ throw new AppError("invalid_check_environment", "Owned browser resource cannot be rebound", 2, { effect: "no_effect" });
20
+ return retained || selected ? { ...retained, ...selected } : undefined;
21
+ }
@@ -5,14 +5,24 @@ import path from "node:path";
5
5
  import { parse } from "shell-quote";
6
6
  import { AppError } from "../shared/errors.js";
7
7
  import { validateObservationDuration } from "../harness-runtime/lib/observation-clock.mjs";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import { fileURLToPath } from "node:url";
8
10
  function object(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); }
9
11
  function boundedString(value, bytes) { return typeof value === "string" && Buffer.byteLength(value) <= bytes; }
10
12
  const validatedBrowserProofs = new WeakSet();
13
+ const unsupportedObservationContracts = new WeakSet();
14
+ const validatedNativeProofs = new WeakSet();
15
+ const validatedChildren = new WeakMap();
16
+ const validatedReports = new WeakMap();
17
+ const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
18
+ const absoluteFile = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !value.includes("\0") && path.isAbsolute(value);
19
+ export function candidateAbsencesValid(value) { return Array.isArray(value) && value.length <= 64 && value.every(item => object(item) && absoluteFile(item.file) && item.path_state === "missing" && object(item.declaration) && absoluteFile(item.declaration.file) && hash(item.declaration.sha256)); }
11
20
  function relativeProofPath(value) { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value) <= 4096 && !path.isAbsolute(value) && !value.includes("\\") && value.split("/").every(part => part !== "" && part !== "." && part !== ".."); }
12
21
  function proofArtifact(value) { return object(value) && relativeProofPath(value.relative_path) && typeof value.sha256 === "string" && /^[a-f0-9]{64}$/.test(value.sha256); }
13
22
  function browserProofShape(proof) {
14
23
  const attempt = proof.attempt, step = proof.step;
15
- return proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
24
+ return (proof.observation_contract === undefined || proof.observation_contract === "dd-tasks/browser-observation@2" && object(proof.target) && [proof.target.page_id, proof.target.context_id].every(value => boundedString(value, 256) && Boolean(value)) && Number.isSafeInteger(proof.window_epoch) && Number(proof.window_epoch) > 0)
25
+ && proof.native_code === "PLAYWRIGHT_ACTION_INACTIVITY" && object(attempt) && boundedString(attempt.invocation_id, 4096) && boundedString(attempt.test_id, 4096) && Boolean(attempt.test_id)
16
26
  && [attempt.retry, attempt.repeat_each_index, attempt.worker_index].every(value => Number.isSafeInteger(value) && value >= 0)
17
27
  && object(step) && boundedString(step.id, 4096) && Boolean(step.id) && ["pw:api", "expect"].includes(String(step.category)) && step.file === proof.file && Number.isSafeInteger(step.line) && step.line > 0
18
28
  && proofArtifact(proof.worker_snapshot) && proofArtifact(proof.native_result);
@@ -29,6 +39,21 @@ export function runnerFailureValid(value) {
29
39
  return false;
30
40
  if (value.repair_evidence !== undefined) {
31
41
  const proof = value.repair_evidence;
42
+ if (object(proof) && proof.rule === "candidate_test_runtime_error")
43
+ return ["TypeError", "ReferenceError"].includes(String(proof.exception_type))
44
+ && absoluteFile(proof.file) && hash(proof.file_sha256) && ["test", "setup", "collection"].includes(String(proof.phase))
45
+ && [proof.line, proof.column].every(value => Number.isSafeInteger(value) && Number(value) > 0) && proofArtifact(proof.native_artifact);
46
+ if (object(proof) && proof.rule === "candidate_migration_sql")
47
+ return proof.native_code === "42601" && absoluteFile(proof.file) && hash(proof.file_sha256)
48
+ && boundedString(proof.operation_id, 4096) && Boolean(proof.operation_id) && proofArtifact(proof.native_artifact) && object(proof.world)
49
+ && boundedString(proof.world.invocation_id, 4096) && typeof proof.world.world_id === "string" && /^[a-f0-9]{32}$/.test(proof.world.world_id)
50
+ && boundedString(proof.world.role, 128) && Boolean(proof.world.role) && hash(proof.world.migrations_sha256) && proof.world.phase === "migrating" && proof.world.setup_completed === false;
51
+ if (object(proof) && proof.rule === "candidate_tool_diagnostic")
52
+ return absoluteFile(proof.file) && proofArtifact(proof.native_artifact)
53
+ && object(proof.tool) && { biome: "2.5.6", typescript: "5.9.3", docs: "1", "value-absence": "1" }[String(proof.tool.name)] === proof.tool.version
54
+ && object(proof.step) && boundedString(proof.step.id, 256) && Boolean(proof.step.id) && Number.isSafeInteger(proof.step.ordinal) && proof.step.ordinal >= 0 && hash(proof.step.command_sha256)
55
+ && boundedString(proof.diagnostic_code, 256) && (proof.tool.name === "biome" ? ["format", "assist/source/organizeImports"].includes(String(proof.diagnostic_code)) || /^lint\/[\w/-]+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "typescript" ? /^TS\d+$/.test(String(proof.diagnostic_code)) : proof.tool.name === "docs" ? ["required_path_missing", "docs_contract"].includes(String(proof.diagnostic_code)) : proof.diagnostic_code === "value_absence")
56
+ && (proof.path_state === "present" && hash(proof.file_sha256) || proof.path_state === "missing" && proof.tool.name === "docs" && proof.diagnostic_code === "required_path_missing" && proof.file_sha256 === null && object(proof.declaration) && absoluteFile(proof.declaration.file) && hash(proof.declaration.sha256));
32
57
  if (!object(proof) || !boundedString(proof.rule, 128) || !boundedString(proof.native_code, 128) || !boundedString(proof.file, 4096) || !object(proof.world))
33
58
  return false;
34
59
  const world = proof.world;
@@ -45,23 +70,132 @@ function candidateFile(root, file) {
45
70
  if (!root || !path.isAbsolute(file))
46
71
  return false;
47
72
  try {
48
- const relative = path.relative(fs.realpathSync(root), fs.realpathSync(file));
49
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative) && fs.statSync(file).isFile();
73
+ const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(file));
74
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
75
+ return false;
76
+ let current = file;
77
+ while (fs.realpathSync(current) !== realRoot) {
78
+ if (fs.lstatSync(current).isSymbolicLink())
79
+ return false;
80
+ const parent = path.dirname(current);
81
+ if (parent === current)
82
+ return false;
83
+ current = parent;
84
+ }
85
+ return fs.statSync(file).isFile();
50
86
  }
51
87
  catch {
52
88
  return false;
53
89
  }
54
90
  }
55
91
  export function candidateFailureFiles(evidence, root) {
56
- return [...new Set((evidence?.failures ?? []).flatMap(failure => failure.file && candidateFile(root, failure.file) ? [failure.file] : []))];
92
+ return [...new Set((evidence?.failures ?? []).flatMap(failure => {
93
+ const child = validatedChildren.get(failure);
94
+ if (child)
95
+ return candidateFailureFiles({ ...child.evidence, failures: [child.failure] }, root);
96
+ const proof = failure.repair_evidence;
97
+ const checksum = proof && "file_sha256" in proof ? proof.file_sha256 : undefined;
98
+ try {
99
+ return failure.file && candidateFile(root, failure.file) && (checksum === undefined || checksum === crypto.createHash("sha256").update(fs.readFileSync(failure.file)).digest("hex")) ? [failure.file] : [];
100
+ }
101
+ catch {
102
+ return [];
103
+ }
104
+ }))];
105
+ }
106
+ function absentCandidate(root, file) {
107
+ if (!root || !absoluteFile(file))
108
+ return false;
109
+ try {
110
+ let current = path.resolve(file);
111
+ try {
112
+ fs.lstatSync(current);
113
+ return false;
114
+ }
115
+ catch (error) {
116
+ if (error.code !== "ENOENT")
117
+ return false;
118
+ }
119
+ for (;;) {
120
+ try {
121
+ if (fs.lstatSync(current).isSymbolicLink())
122
+ return false;
123
+ break;
124
+ }
125
+ catch (error) {
126
+ if (error.code !== "ENOENT")
127
+ return false;
128
+ const parent = path.dirname(current);
129
+ if (parent === current)
130
+ return false;
131
+ current = parent;
132
+ }
133
+ }
134
+ const realRoot = fs.realpathSync(root), relative = path.relative(realRoot, fs.realpathSync(current));
135
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
136
+ return false;
137
+ while (fs.realpathSync(current) !== realRoot) {
138
+ if (fs.lstatSync(current).isSymbolicLink())
139
+ return false;
140
+ current = path.dirname(current);
141
+ }
142
+ return true;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
57
147
  }
58
- function candidateFailure(failure, evidence, root, capturedFiles) {
59
- const owned = (file) => capturedFiles === undefined ? candidateFile(root, file) : capturedFiles.includes(file);
148
+ export function candidateFailureAbsences(evidence, root) {
149
+ return (evidence?.failures ?? []).flatMap(failure => {
150
+ const child = validatedChildren.get(failure);
151
+ if (child)
152
+ return candidateFailureAbsences({ ...child.evidence, failures: [child.failure] }, root);
153
+ try {
154
+ const proof = failure.repair_evidence;
155
+ if (proof?.rule !== "candidate_tool_diagnostic" || proof.path_state !== "missing" || !proof.declaration || !validatedNativeProofs.has(proof)
156
+ || !absentCandidate(root, proof.file) || !candidateFile(root, proof.declaration.file) || crypto.createHash("sha256").update(fs.readFileSync(proof.declaration.file)).digest("hex") !== proof.declaration.sha256)
157
+ return [];
158
+ return [{ file: proof.file, declaration: proof.declaration, path_state: "missing" }];
159
+ }
160
+ catch {
161
+ return [];
162
+ }
163
+ });
164
+ }
165
+ function candidateFailure(failure, evidence, root, capturedFiles, capturedAbsences) {
166
+ const child = validatedChildren.get(failure);
167
+ if (child)
168
+ return candidateFailure(child.failure, child.evidence, root, capturedFiles, capturedAbsences);
169
+ const owned = (file) => {
170
+ if (capturedFiles !== undefined)
171
+ return capturedFiles.includes(file);
172
+ if (!candidateFile(root, file))
173
+ return false;
174
+ const proof = failure.repair_evidence;
175
+ try {
176
+ return !proof || !("file_sha256" in proof) || proof.file_sha256 === crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
177
+ }
178
+ catch {
179
+ return false;
180
+ }
181
+ };
60
182
  if (failure.origin === "tooling" || failure.origin === "unknown")
61
183
  return false;
62
184
  if (failure.category === "assertion")
63
185
  return failure.origin === undefined || failure.origin === "candidate" && failure.file !== undefined && owned(failure.file);
64
186
  const proof = failure.repair_evidence;
187
+ if (proof?.rule === "candidate_test_runtime_error")
188
+ return validatedNativeProofs.has(proof) && failure.category === "unclassified" && failure.origin === "candidate"
189
+ && failure.phase === proof.phase && failure.native_code === proof.exception_type && failure.file === proof.file && owned(proof.file);
190
+ if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic") {
191
+ if (!validatedNativeProofs.has(proof) || failure.category !== "unclassified" || failure.origin !== "candidate" || failure.file !== proof.file || !["setup", "collection", "test"].includes(failure.phase ?? ""))
192
+ return false;
193
+ if (proof.rule === "candidate_migration_sql")
194
+ return failure.phase === "setup" && failure.native_code === "42601" && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
195
+ if (failure.phase !== "collection" || failure.native_code !== proof.diagnostic_code)
196
+ return false;
197
+ return proof.path_state === "missing" ? Boolean(proof.declaration && (capturedAbsences ?? (root ? candidateFailureAbsences(evidence, root) : [])).some(item => item.file === proof.file && isDeepStrictEqual(item.declaration, proof.declaration))) : owned(proof.file);
198
+ }
65
199
  if (proof?.rule === "candidate_browser_action_inactivity")
66
200
  return validatedBrowserProofs.has(proof) && failure.category === "unclassified" && failure.phase === "test" && failure.origin === "candidate" && failure.native_code === proof.native_code && failure.file === proof.file && failure.test_id === proof.attempt.test_id && proof.attempt.invocation_id === evidence.binding.invocation_id && owned(proof.file);
67
201
  return failure.category === "unclassified" && failure.origin === "candidate" && ["test", "setup"].includes(failure.phase ?? "") && proof?.rule === "candidate_sql_integrity" && ["23502", "23503", "23505", "23514"].includes(proof.native_code) && failure.native_code === proof.native_code && failure.file === proof.file && proof.world.invocation_id === evidence.binding.invocation_id && owned(proof.file);
@@ -107,7 +241,7 @@ export function readRunnerProgress(file, invocationId, lastSequence = 0, now = D
107
241
  if (!value || typeof value !== "object" || Array.isArray(value) || value.schema_id !== "dd-flow/check-runner-progress@1" || value.invocation_id !== invocationId
108
242
  || !Number.isSafeInteger(value.sequence) || value.sequence <= 0 || typeof value.at !== "string" || !Number.isFinite(Date.parse(value.at))
109
243
  || !["startup", "execution"].includes(value.phase)
110
- || !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_completed", "run_completed"].includes(value.event))
244
+ || !["setup_step_completed", "run_started", "module_collected", "test_started", "test_completed", "step_started", "step_completed", "run_completed"].includes(value.event))
111
245
  throw new Error("Malformed official progress record");
112
246
  if (value.sequence <= lastSequence || Date.parse(value.at) > now)
113
247
  continue;
@@ -164,6 +298,9 @@ function readProofArtifact(root, ref, limit = 256 * 1024) {
164
298
  function matches(actual, expected) { return object(actual) && Object.entries(expected).every(([key, value]) => actual[key] === value); }
165
299
  function validateBrowserArtifacts(root, proof, binding) {
166
300
  const snapshot = readProofArtifact(root, proof.worker_snapshot), native = readProofArtifact(root, proof.native_result);
301
+ if (proof.observation_contract !== undefined && (proof.observation_contract !== "dd-tasks/browser-observation@2" || snapshot.observation_contract !== proof.observation_contract || native.observation_contract !== proof.observation_contract
302
+ || !isDeepStrictEqual(snapshot.target, proof.target) || !isDeepStrictEqual(native.target, proof.target) || snapshot.window_epoch !== proof.window_epoch || native.window_epoch !== proof.window_epoch))
303
+ return false;
167
304
  if (!Array.isArray(native.errors) || native.errors.length < 1 || native.errors.length > 64 || native.errors.some(error => !boundedString(error, 4096) || !/^(?:Error: )?(?:[A-Za-z][\w.]*: )?Test timeout of [1-9]\d*ms exceeded\.(?:\nCall log:[\s\S]*)?$/.test(stripVTControlCharacters(String(error)))))
168
305
  return false;
169
306
  if (!matches(snapshot.binding, binding) || !matches(native.binding, binding) || !matches(snapshot.attempt, proof.attempt) || !matches(native.attempt, proof.attempt)
@@ -175,7 +312,13 @@ function validateBrowserArtifacts(root, proof, binding) {
175
312
  return Number.isSafeInteger(quiet) && quiet > 0 && [last, deadline, observed, health, ended].every(Number.isFinite) && Math.abs(deadline - last - quiet) <= 2 && observed >= last && observed <= deadline && health <= observed && health >= last && deadline - health <= Math.min(1000, quiet / 4) && ended >= deadline && ended <= Date.now() && ended - deadline <= 5000;
176
313
  }
177
314
  export function runnerProofArtifacts(evidence) {
178
- return [...new Set((evidence?.failures ?? []).flatMap(failure => { const proof = failure.repair_evidence; return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : [])]; }))];
315
+ return [...new Set([...(evidence?.failures ?? []).flatMap(failure => {
316
+ const child = validatedChildren.get(failure);
317
+ if (child && failure.child_evidence)
318
+ return [failure.child_evidence.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(failure.child_evidence.report.relative_path), file))];
319
+ const proof = failure.repair_evidence;
320
+ return [...(failure.diagnostic_artifact ? [failure.diagnostic_artifact.relative_path] : []), ...(proof?.rule === "candidate_browser_action_inactivity" ? [proof.worker_snapshot.relative_path, proof.native_result.relative_path] : proof && "native_artifact" in proof ? [proof.native_artifact.relative_path] : [])];
321
+ }), ...(evidence ? validatedReports.get(evidence) ?? [] : []).flatMap(child => [child.report.relative_path, ...runnerProofArtifacts(child.evidence).map(file => path.posix.join(path.posix.dirname(child.report.relative_path), file))])])];
179
322
  }
180
323
  export function runnerContractsValid(value) {
181
324
  if (value === undefined)
@@ -186,13 +329,15 @@ export function runnerContractsValid(value) {
186
329
  if (!raw || typeof raw !== "object" || Array.isArray(raw))
187
330
  return false;
188
331
  const contract = raw;
189
- return alias.startsWith("@check/") && contract.schema_id === "dd-flow/check-runner-evidence@1" && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
332
+ return alias.startsWith("@check/") && ["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(String(contract.schema_id)) && typeof contract.path === "string" && contract.path.length > 0 && contract.path.split("/")[0] !== "engine-outcome.json" && !contract.path.includes("\\") && !path.isAbsolute(contract.path) && !contract.path.split("/").some(part => part === ".." || part === "" || part === ".");
190
333
  });
191
334
  }
192
- export function readRunnerEvidence(root, contract, binding) {
335
+ export function readRunnerEvidence(root, contract, binding, depth = 0, budget = { reports: 0 }) {
193
336
  try {
337
+ if (depth > 4 || ++budget.reports > 64)
338
+ return null;
194
339
  let current = root;
195
- if (!fs.lstatSync(current).isDirectory())
340
+ if (!fs.lstatSync(current).isDirectory() || fs.lstatSync(current).isSymbolicLink())
196
341
  return null;
197
342
  for (const part of contract.path.split(/[\\/]/)) {
198
343
  current = path.join(current, part);
@@ -200,34 +345,141 @@ export function readRunnerEvidence(root, contract, binding) {
200
345
  return null;
201
346
  }
202
347
  const evidence = JSON.parse(readBoundedProofFile(current, 1024 * 1024).toString("utf8"));
348
+ if (!["dd-flow/check-runner-evidence@1", "dd-flow/check-runner-evidence@2"].includes(contract.schema_id))
349
+ return null;
350
+ const partial = evidence.failure_projection;
351
+ if (partial !== undefined && (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || !object(partial) || partial.state !== "partial" || !["count_bound", "byte_bound", "interrupt"].includes(partial.reason)
352
+ || ![partial.represented, partial.omitted].every(value => Number.isSafeInteger(value) && value >= 0)))
353
+ return null;
203
354
  if (evidence.schema_id !== contract.schema_id || !evidence.binding || Object.entries(binding).some(([key, value]) => evidence.binding[key] !== value)
204
355
  || typeof evidence.runner?.name !== "string" || !evidence.runner.name || typeof evidence.runner.version !== "string" || !evidence.runner.version || typeof evidence.completed !== "boolean" || !evidence.counts
205
356
  || ![evidence.counts.passed, evidence.counts.failed, evidence.counts.skipped].every(n => Number.isSafeInteger(n) && n >= 0)
206
357
  || !Array.isArray(evidence.failures) || evidence.failures.length > 64 || evidence.failures.some(f => !runnerFailureValid(f))
207
- || evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
358
+ || !partial && evidence.counts.failed > evidence.failures.length || evidence.observation_unknown !== undefined && typeof evidence.observation_unknown !== "boolean")
208
359
  return null;
360
+ const children = new Map();
361
+ const consumed = new Map();
362
+ if (evidence.steps !== undefined) {
363
+ if (!Array.isArray(evidence.steps) || evidence.steps.length > 64)
364
+ return null;
365
+ const ids = new Set(), invocations = new Set();
366
+ let stopped = false;
367
+ for (const [ordinal, step] of evidence.steps.entries()) {
368
+ if (!step || !boundedString(step.id, 256) || !step.id || ids.has(step.id) || step.ordinal !== ordinal || !hash(step.command_sha256) || !["passed", "failed", "unreached"].includes(step.state))
369
+ return null;
370
+ ids.add(step.id);
371
+ if (step.state === "unreached") {
372
+ if (!stopped || step.binding !== undefined || step.report !== undefined || step.exit_code !== undefined)
373
+ return null;
374
+ continue;
375
+ }
376
+ if (stopped)
377
+ return null;
378
+ if (step.state === "failed")
379
+ stopped = true;
380
+ if (!step.binding || !proofArtifact(step.report) || !Number.isSafeInteger(step.exit_code) || step.state === "passed" && step.exit_code !== 0 || step.state === "failed" && step.exit_code === 0 || invocations.has(step.binding.invocation_id) || step.binding.invocation_id === binding.invocation_id)
381
+ return null;
382
+ if (step.binding.invocation_id !== `${binding.invocation_id}:step:${step.id}` || step.binding.check_id !== `${binding.check_id}/${step.id}` || step.binding.verification_epoch !== binding.verification_epoch || step.binding.input_hash !== crypto.createHash("sha256").update(JSON.stringify([binding.input_hash, step.command_sha256])).digest("hex"))
383
+ return null;
384
+ if (crypto.createHash("sha256").update(step.binding.command).digest("hex") !== step.command_sha256)
385
+ return null;
386
+ invocations.add(step.binding.invocation_id);
387
+ readProofArtifact(root, step.report, 1024 * 1024);
388
+ const child = readRunnerEvidence(path.join(root, path.dirname(step.report.relative_path)), { schema_id: contract.schema_id, path: path.basename(step.report.relative_path) }, step.binding, depth + 1, budget);
389
+ if (!child || step.state === "passed" && (child.counts.failed !== 0 || child.failures.length !== 0) || step.state === "failed" && !child.failures.length && child.completed && !child.observation_unknown)
390
+ return null;
391
+ if (!child.completed || child.observation_unknown)
392
+ stopped = true;
393
+ children.set(step.id, child);
394
+ }
395
+ for (const key of ["passed", "failed", "skipped"])
396
+ if (evidence.counts[key] !== [...children.values()].reduce((total, child) => total + child.counts[key], 0))
397
+ return null;
398
+ }
209
399
  for (const failure of evidence.failures) {
400
+ if (failure.child_evidence) {
401
+ const ref = failure.child_evidence, step = evidence.steps?.find(item => item.id === ref.step_id), child = children.get(ref.step_id);
402
+ if (!step || !child || !isDeepStrictEqual(ref.binding, step.binding) || !isDeepStrictEqual(ref.report, step.report))
403
+ return null;
404
+ const original = { ...failure };
405
+ delete original.child_evidence;
406
+ const used = consumed.get(child) ?? new Set();
407
+ consumed.set(child, used);
408
+ const index = child.failures.findIndex((item, index) => { const native = { ...item }; delete native.child_evidence; return !used.has(index) && isDeepStrictEqual(native, original); });
409
+ if (index < 0)
410
+ return null;
411
+ used.add(index);
412
+ const matched = child.failures[index];
413
+ if (!matched.child_evidence && matched.repair_evidence?.rule === "candidate_tool_diagnostic" && !isDeepStrictEqual(matched.repair_evidence.step, { id: step.id, ordinal: step.ordinal, command_sha256: step.command_sha256 }))
414
+ return null;
415
+ validatedChildren.set(failure, { failure: matched, evidence: child });
416
+ continue;
417
+ }
210
418
  if (failure.diagnostic_artifact && !matches(readProofArtifact(root, failure.diagnostic_artifact, 2 * 1024 * 1024).binding, binding))
211
419
  return null;
212
420
  const proof = failure.repair_evidence;
421
+ if (proof?.rule === "candidate_migration_sql" || proof?.rule === "candidate_tool_diagnostic" || proof?.rule === "candidate_test_runtime_error") {
422
+ const native = readProofArtifact(root, proof.native_artifact, 2 * 1024 * 1024), facts = Object.fromEntries(Object.entries(proof).filter(([key]) => key !== "native_artifact"));
423
+ if (!isDeepStrictEqual(native.binding, binding) || Object.entries(facts).some(([key, value]) => !isDeepStrictEqual(native[key], value)))
424
+ return null;
425
+ if (proof.rule === "candidate_test_runtime_error") {
426
+ if (evidence.schema_id !== "dd-flow/check-runner-evidence@2" || native.executing_file !== proof.file || native.exception_name !== proof.exception_type
427
+ || !isDeepStrictEqual(native.top_frame, { file: proof.file, line: proof.line, column: proof.column }) || !boundedString(native.stack, 16384) || !String(native.stack).startsWith(`${proof.exception_type}:`))
428
+ return null;
429
+ const firstFrame = String(native.stack).split("\n").find(line => /^\s*at\s/.test(line));
430
+ const frame = firstFrame?.match(/^\s*at\s+(?:.*?\()?((?:file:\/\/)?\/[^\n]+):(\d+):(\d+)\)?$/);
431
+ if (!frame || (frame[1].startsWith("file://") ? fileURLToPath(frame[1]) : frame[1]) !== proof.file || Number(frame[2]) !== proof.line || Number(frame[3]) !== proof.column)
432
+ return null;
433
+ }
434
+ validatedNativeProofs.add(proof);
435
+ }
213
436
  if (proof?.rule === "candidate_browser_action_inactivity") {
214
- if (!validateBrowserArtifacts(root, proof, binding))
437
+ if (proof.observation_contract === undefined && evidence.schema_id === "dd-flow/check-runner-evidence@1") {
438
+ // Legacy diagnostics remain readable; they do not prove the new window/target guarantee.
439
+ if (!validateBrowserArtifacts(root, proof, binding))
440
+ return null;
441
+ evidence.completed = false;
442
+ unsupportedObservationContracts.add(evidence);
443
+ continue;
444
+ }
445
+ if (proof.observation_contract !== "dd-tasks/browser-observation@2" || !validateBrowserArtifacts(root, proof, binding))
215
446
  return null;
216
447
  validatedBrowserProofs.add(proof);
217
448
  }
218
449
  }
450
+ const represented = [...consumed.values()].reduce((total, used) => total + used.size, 0), childFailures = [...children.values()].reduce((total, child) => total + child.failures.length, 0);
451
+ if (partial) {
452
+ if (children.size ? partial.represented !== represented || partial.omitted !== childFailures - represented : partial.represented !== evidence.failures.filter(failure => failure.code !== "report_truncated").length)
453
+ return null;
454
+ evidence.completed = false;
455
+ }
456
+ else if (represented !== childFailures)
457
+ return null;
458
+ // Valid but incomplete native observation is diagnostic evidence, not a
459
+ // malformed schema. Preserve the primary failure and references through
460
+ // every composite, including a parent that incorrectly claims completion.
461
+ if ([...children.values()].some(child => !child.completed || child.observation_unknown)) {
462
+ evidence.completed = false;
463
+ if ([...children.values()].some(child => unsupportedObservationContracts.has(child)))
464
+ unsupportedObservationContracts.add(evidence);
465
+ if (evidence.schema_id === "dd-flow/check-runner-evidence@1" && [...children.values()].some(child => !child.completed && !unsupportedObservationContracts.has(child)) || [...children.values()].some(child => child.observation_unknown))
466
+ evidence.observation_unknown = true;
467
+ }
468
+ validatedReports.set(evidence, (evidence.steps ?? []).flatMap(step => step.report && children.has(step.id) ? [{ report: step.report, evidence: children.get(step.id) }] : []));
219
469
  return evidence;
220
470
  }
221
471
  catch {
222
472
  return null;
223
473
  }
224
474
  }
225
- export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
475
+ export function runnerEvidencePass(evidence) { return Boolean(evidence?.completed && !evidence.failure_projection && !evidence.observation_unknown && evidence.counts.failed === 0 && evidence.failures.length === 0); }
226
476
  export function checkRepairDisposition(receipt) {
227
477
  if (receipt.fingerprint_scope != null && receipt.fingerprint_scope !== "check_inputs")
228
478
  return "tooling_blocker";
229
479
  if (receipt.status === "running" || receipt.status === "aborted" || receipt.runner_evidence?.observation_unknown)
230
480
  return "settlement_required";
481
+ if (receipt.runner_evidence && unsupportedObservationContracts.has(receipt.runner_evidence) || receipt.runner_evidence?.failure_projection || receipt.runner_evidence?.schema_id === "dd-flow/check-runner-evidence@2" && !receipt.runner_evidence.completed)
482
+ return "tooling_blocker";
231
483
  if (receipt.status === "passed")
232
484
  return "passed";
233
485
  if (receipt.execution_failures?.length || receipt.mutation_paths?.length || receipt.before_fingerprint !== receipt.after_fingerprint || receipt.diagnostic_evidence_valid === false)
@@ -241,7 +493,7 @@ export function checkRepairDisposition(receipt) {
241
493
  return "tooling_blocker";
242
494
  if (!evidence.completed)
243
495
  return "unclassified";
244
- if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files)))
496
+ if (evidence.failures.some(f => !runnerFailureValid(f) || !candidateFailure(f, evidence, receipt.candidate_root, receipt.candidate_files, receipt.candidate_absences)))
245
497
  return "unclassified";
246
498
  return "source_repair";
247
499
  }
@@ -32,7 +32,7 @@ export function isMbUpgradeCliOperation(args, env = process.env) {
32
32
  export function requiresExplicitUpgradeAuthorization(args) {
33
33
  const positional = positionalArgs(args);
34
34
  const [family, command] = positional;
35
- if (family === "migration" && command === "apply")
35
+ if (family === "migration" && ["apply", "readiness"].includes(command ?? ""))
36
36
  return true;
37
37
  if (family === "run" && command === "start" && optionValue(args, "flow-kind") === "mb-upgrade")
38
38
  return true;