harnery 0.36.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (311) hide show
  1. package/dist/commander.js +4 -0
  2. package/dist/commands/agents.d.ts.map +1 -1
  3. package/dist/commands/agents.js +87 -46
  4. package/dist/commands/artifacts.d.ts.map +1 -1
  5. package/dist/commands/artifacts.js +26 -1
  6. package/dist/commands/browse.d.ts.map +1 -1
  7. package/dist/commands/browse.js +11 -0
  8. package/dist/commands/diagnostics.d.ts +4 -0
  9. package/dist/commands/diagnostics.d.ts.map +1 -0
  10. package/dist/commands/diagnostics.js +229 -0
  11. package/dist/commands/files.d.ts +12 -0
  12. package/dist/commands/files.d.ts.map +1 -1
  13. package/dist/commands/files.js +54 -1
  14. package/dist/commands/governor.d.ts.map +1 -1
  15. package/dist/commands/governor.js +4 -0
  16. package/dist/commands/ledger-v3.d.ts.map +1 -1
  17. package/dist/commands/ledger-v3.js +71 -2
  18. package/dist/commands/qa-run.d.ts +4 -0
  19. package/dist/commands/qa-run.d.ts.map +1 -0
  20. package/dist/commands/qa-run.js +141 -0
  21. package/dist/commands/resources.d.ts +4 -0
  22. package/dist/commands/resources.d.ts.map +1 -0
  23. package/dist/commands/resources.js +29 -0
  24. package/dist/commands/supervisor.d.ts +4 -0
  25. package/dist/commands/supervisor.d.ts.map +1 -0
  26. package/dist/commands/supervisor.js +101 -0
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +17 -0
  29. package/dist/commands/workflow.d.ts.map +1 -1
  30. package/dist/commands/workflow.js +4 -0
  31. package/dist/core/agents/cli.js +7 -3
  32. package/dist/core/agents/health.d.ts +17 -0
  33. package/dist/core/agents/health.d.ts.map +1 -0
  34. package/dist/core/agents/health.js +109 -0
  35. package/dist/core/agents/mailbox.d.ts +92 -0
  36. package/dist/core/agents/mailbox.d.ts.map +1 -0
  37. package/dist/core/agents/mailbox.js +325 -0
  38. package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
  39. package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
  40. package/dist/core/agents/reconcile-coordination-v3.js +58 -0
  41. package/dist/core/agents/render/prompt-context.d.ts +5 -0
  42. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  43. package/dist/core/agents/render/prompt-context.js +34 -13
  44. package/dist/core/agents/render/session-context.d.ts +8 -0
  45. package/dist/core/agents/render/session-context.d.ts.map +1 -1
  46. package/dist/core/agents/render/session-context.js +34 -3
  47. package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
  48. package/dist/core/agents/rules/commit-conflict.js +50 -1
  49. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  50. package/dist/core/agents/rules/stop-hook.js +6 -10
  51. package/dist/core/agents/session-finalizer-v3.js +4 -1
  52. package/dist/core/artifacts/index.d.ts +26 -1
  53. package/dist/core/artifacts/index.d.ts.map +1 -1
  54. package/dist/core/artifacts/index.js +146 -6
  55. package/dist/core/config.d.ts +12 -0
  56. package/dist/core/config.d.ts.map +1 -1
  57. package/dist/core/config.js +43 -0
  58. package/dist/core/diagnostics/advice.d.ts +9 -0
  59. package/dist/core/diagnostics/advice.d.ts.map +1 -0
  60. package/dist/core/diagnostics/advice.js +112 -0
  61. package/dist/core/diagnostics/bundle.d.ts +35 -0
  62. package/dist/core/diagnostics/bundle.d.ts.map +1 -0
  63. package/dist/core/diagnostics/bundle.js +874 -0
  64. package/dist/core/diagnostics/comparison.d.ts +5 -0
  65. package/dist/core/diagnostics/comparison.d.ts.map +1 -0
  66. package/dist/core/diagnostics/comparison.js +444 -0
  67. package/dist/core/diagnostics/contract.d.ts +245 -0
  68. package/dist/core/diagnostics/contract.d.ts.map +1 -0
  69. package/dist/core/diagnostics/contract.js +18 -0
  70. package/dist/core/diagnostics/identity.d.ts +3 -0
  71. package/dist/core/diagnostics/identity.d.ts.map +1 -0
  72. package/dist/core/diagnostics/identity.js +10 -0
  73. package/dist/core/diagnostics/index.d.ts +8 -0
  74. package/dist/core/diagnostics/index.d.ts.map +1 -0
  75. package/dist/core/diagnostics/index.js +7 -0
  76. package/dist/core/diagnostics/replay.d.ts +8 -0
  77. package/dist/core/diagnostics/replay.d.ts.map +1 -0
  78. package/dist/core/diagnostics/replay.js +213 -0
  79. package/dist/core/diagnostics/sanitize.d.ts +9 -0
  80. package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
  81. package/dist/core/diagnostics/sanitize.js +84 -0
  82. package/dist/core/events/legacy-storage/compression.d.ts +13 -0
  83. package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
  84. package/dist/core/events/legacy-storage/compression.js +107 -0
  85. package/dist/core/events/legacy-storage/index.d.ts +1 -0
  86. package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
  87. package/dist/core/events/legacy-storage/index.js +1 -0
  88. package/dist/core/events/v3/archive-retention.d.ts +33 -0
  89. package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
  90. package/dist/core/events/v3/archive-retention.js +195 -0
  91. package/dist/core/events/v3/capabilities.d.ts +1 -1
  92. package/dist/core/events/v3/capabilities.d.ts.map +1 -1
  93. package/dist/core/events/v3/capabilities.js +1 -0
  94. package/dist/core/events/v3/index.d.ts +1 -0
  95. package/dist/core/events/v3/index.d.ts.map +1 -1
  96. package/dist/core/events/v3/index.js +1 -0
  97. package/dist/core/events/v3/live-routing.d.ts.map +1 -1
  98. package/dist/core/events/v3/live-routing.js +1 -0
  99. package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
  100. package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
  101. package/dist/core/events/v3/producers/hook-base.js +13 -2
  102. package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
  103. package/dist/core/events/v3/producers/intake.js +1 -0
  104. package/dist/core/events/v3/producers/recorder.d.ts +16 -0
  105. package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
  106. package/dist/core/events/v3/producers/recorder.js +88 -1
  107. package/dist/core/hooks/adapter/output.d.ts +8 -0
  108. package/dist/core/hooks/adapter/output.d.ts.map +1 -1
  109. package/dist/core/hooks/adapter/output.js +11 -1
  110. package/dist/core/hooks/cli.js +183 -192
  111. package/dist/core/hooks/health.d.ts +59 -0
  112. package/dist/core/hooks/health.d.ts.map +1 -0
  113. package/dist/core/hooks/health.js +84 -0
  114. package/dist/core/resources/contract.d.ts +90 -0
  115. package/dist/core/resources/contract.d.ts.map +1 -0
  116. package/dist/core/resources/contract.js +2 -0
  117. package/dist/core/resources/index.d.ts +6 -0
  118. package/dist/core/resources/index.d.ts.map +1 -0
  119. package/dist/core/resources/index.js +5 -0
  120. package/dist/core/resources/sampler.d.ts +27 -0
  121. package/dist/core/resources/sampler.d.ts.map +1 -0
  122. package/dist/core/resources/sampler.js +357 -0
  123. package/dist/core/resources/service-status.d.ts +3 -0
  124. package/dist/core/resources/service-status.d.ts.map +1 -0
  125. package/dist/core/resources/service-status.js +48 -0
  126. package/dist/core/resources/service.d.ts +26 -0
  127. package/dist/core/resources/service.d.ts.map +1 -0
  128. package/dist/core/resources/service.js +292 -0
  129. package/dist/core/resources/storage.d.ts +13 -0
  130. package/dist/core/resources/storage.d.ts.map +1 -0
  131. package/dist/core/resources/storage.js +30 -0
  132. package/dist/core/storage/atomic-json.d.ts +3 -0
  133. package/dist/core/storage/atomic-json.d.ts.map +1 -0
  134. package/dist/core/storage/atomic-json.js +15 -0
  135. package/dist/core/storage/builtins.d.ts.map +1 -1
  136. package/dist/core/storage/builtins.js +45 -6
  137. package/dist/core/storage/logger.d.ts +2 -0
  138. package/dist/core/storage/logger.d.ts.map +1 -1
  139. package/dist/core/storage/logger.js +2 -0
  140. package/dist/core/storage/query.d.ts +14 -0
  141. package/dist/core/storage/query.d.ts.map +1 -1
  142. package/dist/core/storage/query.js +51 -0
  143. package/dist/core/supervisor/activity.d.ts +4 -0
  144. package/dist/core/supervisor/activity.d.ts.map +1 -0
  145. package/dist/core/supervisor/activity.js +141 -0
  146. package/dist/core/supervisor/contract.d.ts +268 -0
  147. package/dist/core/supervisor/contract.d.ts.map +1 -0
  148. package/dist/core/supervisor/contract.js +35 -0
  149. package/dist/core/supervisor/explanations.d.ts +4 -0
  150. package/dist/core/supervisor/explanations.d.ts.map +1 -0
  151. package/dist/core/supervisor/explanations.js +71 -0
  152. package/dist/core/supervisor/findings.d.ts +20 -0
  153. package/dist/core/supervisor/findings.d.ts.map +1 -0
  154. package/dist/core/supervisor/findings.js +399 -0
  155. package/dist/core/supervisor/history.d.ts +9 -0
  156. package/dist/core/supervisor/history.d.ts.map +1 -0
  157. package/dist/core/supervisor/history.js +46 -0
  158. package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
  159. package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
  160. package/dist/core/supervisor/hook-health-alerts.js +70 -0
  161. package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
  162. package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
  163. package/dist/core/supervisor/hook-health-storage.js +24 -0
  164. package/dist/core/supervisor/hook-health.d.ts +72 -0
  165. package/dist/core/supervisor/hook-health.d.ts.map +1 -0
  166. package/dist/core/supervisor/hook-health.js +203 -0
  167. package/dist/core/supervisor/hooks.d.ts +10 -0
  168. package/dist/core/supervisor/hooks.d.ts.map +1 -0
  169. package/dist/core/supervisor/hooks.js +28 -0
  170. package/dist/core/supervisor/index.d.ts +15 -0
  171. package/dist/core/supervisor/index.d.ts.map +1 -0
  172. package/dist/core/supervisor/index.js +14 -0
  173. package/dist/core/supervisor/log-feed.d.ts +9 -0
  174. package/dist/core/supervisor/log-feed.d.ts.map +1 -0
  175. package/dist/core/supervisor/log-feed.js +103 -0
  176. package/dist/core/supervisor/service.d.ts +29 -0
  177. package/dist/core/supervisor/service.d.ts.map +1 -0
  178. package/dist/core/supervisor/service.js +450 -0
  179. package/dist/core/supervisor/services.d.ts +11 -0
  180. package/dist/core/supervisor/services.d.ts.map +1 -0
  181. package/dist/core/supervisor/services.js +155 -0
  182. package/dist/core/supervisor/status.d.ts +3 -0
  183. package/dist/core/supervisor/status.d.ts.map +1 -0
  184. package/dist/core/supervisor/status.js +48 -0
  185. package/dist/core/supervisor/storage.d.ts +30 -0
  186. package/dist/core/supervisor/storage.d.ts.map +1 -0
  187. package/dist/core/supervisor/storage.js +73 -0
  188. package/dist/core/supervisor/timeline.d.ts +3 -0
  189. package/dist/core/supervisor/timeline.d.ts.map +1 -0
  190. package/dist/core/supervisor/timeline.js +87 -0
  191. package/dist/core/workflow/admission.d.ts +23 -0
  192. package/dist/core/workflow/admission.d.ts.map +1 -0
  193. package/dist/core/workflow/admission.js +136 -0
  194. package/dist/core/workflow/engine.d.ts.map +1 -1
  195. package/dist/core/workflow/engine.js +77 -2
  196. package/dist/core/workflow/index.d.ts +3 -2
  197. package/dist/core/workflow/index.d.ts.map +1 -1
  198. package/dist/core/workflow/index.js +2 -1
  199. package/dist/core/workflow/proof.d.ts +2 -1
  200. package/dist/core/workflow/proof.d.ts.map +1 -1
  201. package/dist/core/workflow/proof.js +39 -1
  202. package/dist/core/workflow/run-state.d.ts +3 -2
  203. package/dist/core/workflow/run-state.d.ts.map +1 -1
  204. package/dist/core/workflow/run-state.js +8 -1
  205. package/dist/core/workflow/types.d.ts +33 -1
  206. package/dist/core/workflow/types.d.ts.map +1 -1
  207. package/dist/core/workflow/types.js +2 -1
  208. package/dist/lib/browser/client.d.ts +5 -0
  209. package/dist/lib/browser/client.d.ts.map +1 -1
  210. package/dist/lib/browser/client.js +1 -0
  211. package/dist/lib/browser/index.d.ts +2 -0
  212. package/dist/lib/browser/index.d.ts.map +1 -1
  213. package/dist/lib/browser/index.js +2 -0
  214. package/dist/lib/browser/qa-run-contracts.d.ts +173 -0
  215. package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
  216. package/dist/lib/browser/qa-run-contracts.js +226 -0
  217. package/dist/lib/browser/qa-run.d.ts +51 -0
  218. package/dist/lib/browser/qa-run.d.ts.map +1 -0
  219. package/dist/lib/browser/qa-run.js +684 -0
  220. package/dist/lib/coord-root-id.d.ts +9 -0
  221. package/dist/lib/coord-root-id.d.ts.map +1 -0
  222. package/dist/lib/coord-root-id.js +17 -0
  223. package/dist/lib/instructions/apply.d.ts +3 -1
  224. package/dist/lib/instructions/apply.d.ts.map +1 -1
  225. package/dist/lib/instructions/apply.js +74 -3
  226. package/dist/lib/instructions/templates.d.ts.map +1 -1
  227. package/dist/lib/instructions/templates.js +6 -0
  228. package/package.json +12 -1
  229. package/src/commander.ts +26 -0
  230. package/src/commands/agents.ts +102 -49
  231. package/src/commands/artifacts.ts +40 -2
  232. package/src/commands/browse.ts +15 -0
  233. package/src/commands/diagnostics.ts +271 -0
  234. package/src/commands/files.ts +102 -2
  235. package/src/commands/governor.ts +8 -0
  236. package/src/commands/ledger-v3.ts +82 -0
  237. package/src/commands/qa-run.ts +184 -0
  238. package/src/commands/resources.ts +38 -0
  239. package/src/commands/supervisor.ts +141 -0
  240. package/src/commands/web.ts +19 -0
  241. package/src/commands/workflow.ts +8 -0
  242. package/src/core/agents/cli.ts +7 -3
  243. package/src/core/agents/health.ts +142 -0
  244. package/src/core/agents/mailbox.ts +373 -0
  245. package/src/core/agents/reconcile-coordination-v3.ts +77 -0
  246. package/src/core/agents/render/prompt-context.ts +42 -12
  247. package/src/core/agents/render/session-context.ts +37 -3
  248. package/src/core/agents/rules/commit-conflict.ts +58 -1
  249. package/src/core/agents/rules/stop-hook.ts +6 -12
  250. package/src/core/agents/session-finalizer-v3.ts +4 -1
  251. package/src/core/artifacts/index.ts +196 -10
  252. package/src/core/config.ts +111 -2
  253. package/src/core/diagnostics/advice.ts +147 -0
  254. package/src/core/diagnostics/bundle.ts +1142 -0
  255. package/src/core/diagnostics/comparison.ts +565 -0
  256. package/src/core/diagnostics/contract.ts +311 -0
  257. package/src/core/diagnostics/identity.ts +11 -0
  258. package/src/core/diagnostics/index.ts +7 -0
  259. package/src/core/diagnostics/replay.ts +314 -0
  260. package/src/core/diagnostics/sanitize.ts +97 -0
  261. package/src/core/events/legacy-storage/compression.ts +130 -0
  262. package/src/core/events/legacy-storage/index.ts +1 -0
  263. package/src/core/events/v3/archive-retention.ts +251 -0
  264. package/src/core/events/v3/capabilities.ts +2 -0
  265. package/src/core/events/v3/index.ts +9 -0
  266. package/src/core/events/v3/live-routing.ts +1 -0
  267. package/src/core/events/v3/producers/hook-base.ts +14 -2
  268. package/src/core/events/v3/producers/intake.ts +1 -0
  269. package/src/core/events/v3/producers/recorder.ts +110 -1
  270. package/src/core/hooks/adapter/output.ts +12 -1
  271. package/src/core/hooks/cli.ts +192 -215
  272. package/src/core/hooks/health.ts +146 -0
  273. package/src/core/resources/contract.ts +98 -0
  274. package/src/core/resources/index.ts +5 -0
  275. package/src/core/resources/sampler.ts +458 -0
  276. package/src/core/resources/service-status.ts +62 -0
  277. package/src/core/resources/service.ts +337 -0
  278. package/src/core/resources/storage.ts +44 -0
  279. package/src/core/storage/atomic-json.ts +15 -0
  280. package/src/core/storage/builtins.ts +62 -6
  281. package/src/core/storage/logger.ts +2 -0
  282. package/src/core/storage/query.ts +58 -0
  283. package/src/core/supervisor/activity.ts +193 -0
  284. package/src/core/supervisor/contract.ts +312 -0
  285. package/src/core/supervisor/explanations.ts +104 -0
  286. package/src/core/supervisor/findings.ts +788 -0
  287. package/src/core/supervisor/history.ts +61 -0
  288. package/src/core/supervisor/hook-health-alerts.ts +108 -0
  289. package/src/core/supervisor/hook-health-storage.ts +25 -0
  290. package/src/core/supervisor/hook-health.ts +301 -0
  291. package/src/core/supervisor/hooks.ts +35 -0
  292. package/src/core/supervisor/index.ts +14 -0
  293. package/src/core/supervisor/log-feed.ts +131 -0
  294. package/src/core/supervisor/service.ts +511 -0
  295. package/src/core/supervisor/services.ts +203 -0
  296. package/src/core/supervisor/status.ts +59 -0
  297. package/src/core/supervisor/storage.ts +126 -0
  298. package/src/core/supervisor/timeline.ts +133 -0
  299. package/src/core/workflow/admission.ts +215 -0
  300. package/src/core/workflow/engine.ts +93 -1
  301. package/src/core/workflow/index.ts +10 -0
  302. package/src/core/workflow/proof.ts +54 -1
  303. package/src/core/workflow/run-state.ts +13 -1
  304. package/src/core/workflow/types.ts +39 -1
  305. package/src/lib/browser/client.ts +6 -0
  306. package/src/lib/browser/index.ts +29 -0
  307. package/src/lib/browser/qa-run-contracts.ts +380 -0
  308. package/src/lib/browser/qa-run.ts +784 -0
  309. package/src/lib/coord-root-id.ts +23 -0
  310. package/src/lib/instructions/apply.ts +81 -2
  311. package/src/lib/instructions/templates.ts +6 -0
@@ -0,0 +1,565 @@
1
+ import type { SupervisorCapability, SupervisorFinding } from "../supervisor/contract.ts";
2
+ import { validateDiagnosticBundle } from "./bundle.ts";
3
+ import {
4
+ DIAGNOSTIC_COMPARISON_SCHEMA_VERSION,
5
+ type DiagnosticBundleComparison,
6
+ type DiagnosticBundleComparisonRef,
7
+ type DiagnosticCapabilityComparison,
8
+ type DiagnosticComparisonImpact,
9
+ type DiagnosticComparisonWarning,
10
+ type DiagnosticFindingComparison,
11
+ type DiagnosticFindingFieldChange,
12
+ type DiagnosticHookAggregateSummary,
13
+ type DiagnosticHookHealthSummary,
14
+ type DiagnosticOptionalSourceComparison,
15
+ type DiagnosticPressure,
16
+ type DiagnosticShadowAdmissionSummary,
17
+ type ValidatedDiagnosticBundle,
18
+ } from "./contract.ts";
19
+ import { canonicalJson, sha256 } from "./replay.ts";
20
+
21
+ const HOOK_SOURCE = "supervisor.hook-health";
22
+ const ADMISSION_SOURCE = "workflow.diagnostic-admission";
23
+
24
+ export function compareDiagnosticBundles(
25
+ repoRoot: string,
26
+ beforeRef: string,
27
+ afterRef: string,
28
+ ): DiagnosticBundleComparison {
29
+ return compareValidatedDiagnosticBundles(
30
+ validateDiagnosticBundle(repoRoot, beforeRef),
31
+ validateDiagnosticBundle(repoRoot, afterRef),
32
+ );
33
+ }
34
+
35
+ export function compareValidatedDiagnosticBundles(
36
+ before: ValidatedDiagnosticBundle,
37
+ after: ValidatedDiagnosticBundle,
38
+ ): DiagnosticBundleComparison {
39
+ const warnings: DiagnosticComparisonWarning[] = [];
40
+ if (before.manifest.threshold_digest !== after.manifest.threshold_digest) {
41
+ warnings.push({
42
+ code: "threshold_digest_changed",
43
+ summary: "Finding thresholds changed between captures.",
44
+ });
45
+ }
46
+ if (before.manifest.engine_version !== after.manifest.engine_version) {
47
+ warnings.push({
48
+ code: "engine_version_changed",
49
+ summary: "The captures were produced by different Harnery builds.",
50
+ });
51
+ }
52
+ if (before.manifest.machine_id !== after.manifest.machine_id) {
53
+ warnings.push({
54
+ code: "machine_changed",
55
+ summary: "The captures came from different pseudonymous machines.",
56
+ });
57
+ }
58
+ if (Date.parse(after.manifest.captured_at) < Date.parse(before.manifest.captured_at)) {
59
+ warnings.push({
60
+ code: "capture_order_reversed",
61
+ summary: "The after capture predates the before capture.",
62
+ });
63
+ }
64
+
65
+ const beforeIndex = indexFindings(before.expected.findings);
66
+ const afterIndex = indexFindings(after.expected.findings);
67
+ if (beforeIndex.duplicateCount + afterIndex.duplicateCount > 0) {
68
+ warnings.push({
69
+ code: "duplicate_fingerprint",
70
+ summary: `${beforeIndex.duplicateCount + afterIndex.duplicateCount} older finding episode(s) shared a fingerprint and were omitted.`,
71
+ });
72
+ }
73
+
74
+ const capabilities = compareCapabilities(before, after);
75
+ if (capabilities.some((row) => row.change !== "unchanged")) {
76
+ warnings.push({
77
+ code: "source_capability_changed",
78
+ summary: "One or more captured source capabilities changed.",
79
+ });
80
+ }
81
+
82
+ const hookHealth = optionalSourceComparison(before, after, HOOK_SOURCE, hookHealthSummary);
83
+ if (!hookHealth.before || !hookHealth.after) {
84
+ warnings.push({
85
+ code: "hook_health_unavailable",
86
+ summary:
87
+ "Completed-hook comparison is partial because one or both bundles lack hook-health evidence.",
88
+ });
89
+ }
90
+ const shadowAdmission = optionalSourceComparison(
91
+ before,
92
+ after,
93
+ ADMISSION_SOURCE,
94
+ shadowAdmissionSummary,
95
+ );
96
+ if (!shadowAdmission.before || !shadowAdmission.after) {
97
+ warnings.push({
98
+ code: "shadow_admission_unavailable",
99
+ summary:
100
+ "Shadow-admission comparison is partial because one or both bundles lack admission evidence.",
101
+ });
102
+ }
103
+
104
+ const rows = compareFindings(beforeIndex.rows, afterIndex.rows);
105
+ const base = {
106
+ schema_version: DIAGNOSTIC_COMPARISON_SCHEMA_VERSION,
107
+ observer_only: true as const,
108
+ before: bundleRef(before),
109
+ after: bundleRef(after),
110
+ comparability: warnings.length === 0 ? ("comparable" as const) : ("partial" as const),
111
+ warnings,
112
+ advice: {
113
+ before: before.expected.advice,
114
+ after: after.expected.advice,
115
+ direction: adviceDirection(before.expected.advice.pressure, after.expected.advice.pressure),
116
+ },
117
+ findings: {
118
+ total: rows.length,
119
+ regressions: countImpact(rows, "regression"),
120
+ worsened: countImpact(rows, "worsened"),
121
+ changed: countImpact(rows, "changed"),
122
+ improved: countImpact(rows, "improved"),
123
+ recoveries: countImpact(rows, "recovery"),
124
+ unchanged: countImpact(rows, "unchanged"),
125
+ rows,
126
+ },
127
+ capabilities,
128
+ hook_health: hookHealth,
129
+ shadow_admission: shadowAdmission,
130
+ };
131
+ return {
132
+ ...base,
133
+ comparison_digest: sha256(canonicalJson(base)),
134
+ };
135
+ }
136
+
137
+ export function renderDiagnosticBundleComparison(comparison: DiagnosticBundleComparison): string {
138
+ const lines = [
139
+ `diagnostic comparison: ${comparison.before.artifact_id} -> ${comparison.after.artifact_id}`,
140
+ `comparability: ${comparison.comparability}; pressure: ${comparison.before.pressure} -> ${comparison.after.pressure} (${comparison.advice.direction})`,
141
+ `findings: ${comparison.findings.regressions} regression, ${comparison.findings.worsened} worsened, ${comparison.findings.changed} changed, ${comparison.findings.improved} improved, ${comparison.findings.recoveries} recovery, ${comparison.findings.unchanged} unchanged`,
142
+ ];
143
+ if (comparison.warnings.length > 0) {
144
+ lines.push("warnings:");
145
+ for (const warning of comparison.warnings)
146
+ lines.push(` - ${warning.code}: ${warning.summary}`);
147
+ }
148
+ if (comparison.findings.rows.length > 0) {
149
+ lines.push("signals:");
150
+ for (const row of comparison.findings.rows) {
151
+ const finding = row.after ?? row.before!;
152
+ lines.push(
153
+ ` ${row.impact.padEnd(10)} ${finding.finding_kind} ${finding.scope_kind}:${finding.scope_id} (${row.fingerprint})`,
154
+ );
155
+ for (const change of row.field_changes) {
156
+ lines.push(` ${change.field}: ${String(change.before)} -> ${String(change.after)}`);
157
+ }
158
+ }
159
+ }
160
+ lines.push(`comparison digest: ${comparison.comparison_digest}`);
161
+ lines.push("observer only: no thresholds, processes, hooks, or workflow dispatch changed");
162
+ return `${lines.join("\n")}\n`;
163
+ }
164
+
165
+ function bundleRef(bundle: ValidatedDiagnosticBundle): DiagnosticBundleComparisonRef {
166
+ return {
167
+ artifact_id: bundle.manifest.artifact_id,
168
+ captured_at: bundle.manifest.captured_at,
169
+ machine_id: bundle.manifest.machine_id,
170
+ engine_version: bundle.manifest.engine_version,
171
+ threshold_digest: bundle.manifest.threshold_digest,
172
+ finding_count: bundle.expected.findings.length,
173
+ pressure: bundle.expected.advice.pressure,
174
+ };
175
+ }
176
+
177
+ function indexFindings(findings: readonly SupervisorFinding[]): {
178
+ rows: ReadonlyMap<string, SupervisorFinding>;
179
+ duplicateCount: number;
180
+ } {
181
+ const sorted = [...findings].sort(
182
+ (left, right) =>
183
+ Date.parse(right.observed_at) - Date.parse(left.observed_at) ||
184
+ left.id.localeCompare(right.id),
185
+ );
186
+ const rows = new Map<string, SupervisorFinding>();
187
+ let duplicateCount = 0;
188
+ for (const finding of sorted) {
189
+ if (rows.has(finding.fingerprint)) duplicateCount += 1;
190
+ else rows.set(finding.fingerprint, finding);
191
+ }
192
+ return { rows, duplicateCount };
193
+ }
194
+
195
+ function compareFindings(
196
+ before: ReadonlyMap<string, SupervisorFinding>,
197
+ after: ReadonlyMap<string, SupervisorFinding>,
198
+ ): DiagnosticFindingComparison[] {
199
+ const fingerprints = [...new Set([...before.keys(), ...after.keys()])].sort();
200
+ return fingerprints
201
+ .map((fingerprint): DiagnosticFindingComparison => {
202
+ const previous = before.get(fingerprint);
203
+ const current = after.get(fingerprint);
204
+ if (!previous && current) {
205
+ return {
206
+ fingerprint,
207
+ change_class: "added",
208
+ impact: current.state === "opened" ? "regression" : "changed",
209
+ after: current,
210
+ field_changes: [],
211
+ };
212
+ }
213
+ if (previous && !current) {
214
+ return {
215
+ fingerprint,
216
+ change_class: "resolved",
217
+ impact: previous.state === "opened" ? "recovery" : "changed",
218
+ before: previous,
219
+ field_changes: [],
220
+ };
221
+ }
222
+ const fieldChanges = compareFindingFields(previous!, current!);
223
+ return {
224
+ fingerprint,
225
+ change_class: "persistent",
226
+ impact: persistentImpact(fieldChanges),
227
+ before: previous,
228
+ after: current,
229
+ field_changes: fieldChanges,
230
+ };
231
+ })
232
+ .sort(compareFindingRows);
233
+ }
234
+
235
+ function compareFindingFields(
236
+ before: SupervisorFinding,
237
+ after: SupervisorFinding,
238
+ ): DiagnosticFindingFieldChange[] {
239
+ const rows: DiagnosticFindingFieldChange[] = [];
240
+ addRankedChange(rows, "severity", before.severity, after.severity, severityRank);
241
+ addRankedChange(rows, "state", before.state, after.state, stateRank);
242
+ addRankedChange(
243
+ rows,
244
+ "occurrence_count",
245
+ before.occurrence_count,
246
+ after.occurrence_count,
247
+ numberRank,
248
+ );
249
+ if (
250
+ before.peak_unit === after.peak_unit &&
251
+ before.peak_observed_value !== undefined &&
252
+ after.peak_observed_value !== undefined
253
+ ) {
254
+ addRankedChange(
255
+ rows,
256
+ "peak_observed_value",
257
+ before.peak_observed_value,
258
+ after.peak_observed_value,
259
+ numberRank,
260
+ );
261
+ }
262
+ addChangedField(rows, "owner", owner(before), owner(after));
263
+ addChangedField(
264
+ rows,
265
+ "workload_relationship",
266
+ before.workload_context?.relationship ?? null,
267
+ after.workload_context?.relationship ?? null,
268
+ );
269
+ return rows;
270
+ }
271
+
272
+ function addRankedChange<T extends string | number>(
273
+ rows: DiagnosticFindingFieldChange[],
274
+ field: DiagnosticFindingFieldChange["field"],
275
+ before: T,
276
+ after: T,
277
+ rank: (value: T) => number,
278
+ ): void {
279
+ if (before === after) return;
280
+ rows.push({
281
+ field,
282
+ before,
283
+ after,
284
+ impact: rank(after) > rank(before) ? "worsened" : "improved",
285
+ });
286
+ }
287
+
288
+ function addChangedField(
289
+ rows: DiagnosticFindingFieldChange[],
290
+ field: DiagnosticFindingFieldChange["field"],
291
+ before: string | null,
292
+ after: string | null,
293
+ ): void {
294
+ if (before === after) return;
295
+ rows.push({ field, before, after, impact: "changed" });
296
+ }
297
+
298
+ function persistentImpact(
299
+ changes: readonly DiagnosticFindingFieldChange[],
300
+ ): DiagnosticComparisonImpact {
301
+ if (changes.length === 0) return "unchanged";
302
+ const worsened = changes.some((row) => row.impact === "worsened");
303
+ const improved = changes.some((row) => row.impact === "improved");
304
+ const changed = changes.some((row) => row.impact === "changed");
305
+ if (worsened && !improved && !changed) return "worsened";
306
+ if (improved && !worsened && !changed) return "improved";
307
+ return "changed";
308
+ }
309
+
310
+ function compareFindingRows(
311
+ left: DiagnosticFindingComparison,
312
+ right: DiagnosticFindingComparison,
313
+ ): number {
314
+ const impact = impactRank(left.impact) - impactRank(right.impact);
315
+ if (impact !== 0) return impact;
316
+ const leftFinding = left.after ?? left.before!;
317
+ const rightFinding = right.after ?? right.before!;
318
+ const severity = severityRank(rightFinding.severity) - severityRank(leftFinding.severity);
319
+ return (
320
+ severity ||
321
+ leftFinding.finding_kind.localeCompare(rightFinding.finding_kind) ||
322
+ leftFinding.scope_kind.localeCompare(rightFinding.scope_kind) ||
323
+ leftFinding.scope_id.localeCompare(rightFinding.scope_id) ||
324
+ left.fingerprint.localeCompare(right.fingerprint)
325
+ );
326
+ }
327
+
328
+ function compareCapabilities(
329
+ before: ValidatedDiagnosticBundle,
330
+ after: ValidatedDiagnosticBundle,
331
+ ): DiagnosticCapabilityComparison[] {
332
+ const previous = capabilityMap(before.manifest.capabilities);
333
+ const current = capabilityMap(after.manifest.capabilities);
334
+ const sourceKinds = [...new Set([...previous.keys(), ...current.keys()])].sort();
335
+ return sourceKinds.map((sourceKind) => {
336
+ const left = previous.get(sourceKind);
337
+ const right = current.get(sourceKind);
338
+ return {
339
+ source_kind: sourceKind,
340
+ change: !left
341
+ ? "added"
342
+ : !right
343
+ ? "removed"
344
+ : sameCapability(left, right)
345
+ ? "unchanged"
346
+ : "changed",
347
+ ...(left ? { before: left } : {}),
348
+ ...(right ? { after: right } : {}),
349
+ };
350
+ });
351
+ }
352
+
353
+ function capabilityMap(rows: readonly SupervisorCapability[]): Map<string, SupervisorCapability> {
354
+ return new Map(
355
+ [...rows]
356
+ .sort((a, b) => a.source_kind.localeCompare(b.source_kind))
357
+ .map((row) => [row.source_kind, row]),
358
+ );
359
+ }
360
+
361
+ function sameCapability(left: SupervisorCapability, right: SupervisorCapability): boolean {
362
+ return (
363
+ left.state === right.state &&
364
+ left.reason_code === right.reason_code &&
365
+ left.detail === right.detail
366
+ );
367
+ }
368
+
369
+ function optionalSourceComparison<T>(
370
+ before: ValidatedDiagnosticBundle,
371
+ after: ValidatedDiagnosticBundle,
372
+ sourceKind: string,
373
+ parse: (value: unknown) => T | undefined,
374
+ ): DiagnosticOptionalSourceComparison<T> {
375
+ const previous = optionalSource(before, sourceKind, parse);
376
+ const current = optionalSource(after, sourceKind, parse);
377
+ return {
378
+ before_capability: previous.capability,
379
+ after_capability: current.capability,
380
+ ...(previous.value ? { before: previous.value } : {}),
381
+ ...(current.value ? { after: current.value } : {}),
382
+ };
383
+ }
384
+
385
+ function optionalSource<T>(
386
+ bundle: ValidatedDiagnosticBundle,
387
+ sourceKind: string,
388
+ parse: (value: unknown) => T | undefined,
389
+ ): { capability: SupervisorCapability; value?: T } {
390
+ const source = bundle.observations.sources.find(
391
+ (candidate) => candidate.source_kind === sourceKind,
392
+ );
393
+ if (!source) {
394
+ return {
395
+ capability: {
396
+ source_kind: sourceKind,
397
+ state: "unsupported",
398
+ reason_code: "source_not_captured",
399
+ },
400
+ };
401
+ }
402
+ const capability: SupervisorCapability = {
403
+ source_kind: sourceKind,
404
+ state: source.capability,
405
+ ...(source.reason_code ? { reason_code: source.reason_code } : {}),
406
+ };
407
+ if (source.value === undefined) return { capability };
408
+ const value = parse(source.value);
409
+ return value
410
+ ? { capability, value }
411
+ : {
412
+ capability: {
413
+ source_kind: sourceKind,
414
+ state: "malformed",
415
+ reason_code: "source_projection_malformed",
416
+ },
417
+ };
418
+ }
419
+
420
+ function hookHealthSummary(value: unknown): DiagnosticHookHealthSummary | undefined {
421
+ const record = object(value);
422
+ const summary = object(record?.summary);
423
+ const aggregates = Array.isArray(record?.aggregates) ? record.aggregates : undefined;
424
+ if (!record || !summary || !aggregates) return undefined;
425
+ const parsedSummary = integerFields(summary, [
426
+ "invocation_count",
427
+ "degraded_count",
428
+ "faulted_count",
429
+ "slow_count",
430
+ "high_memory_count",
431
+ "retry_count",
432
+ ] as const);
433
+ if (!parsedSummary) return undefined;
434
+ const parsedAggregates: DiagnosticHookAggregateSummary[] = [];
435
+ for (const value of aggregates.slice(0, 40)) {
436
+ const row = object(value);
437
+ if (!row) return undefined;
438
+ const numbers = integerFields(row, [
439
+ "invocation_count",
440
+ "degraded_count",
441
+ "faulted_count",
442
+ "retry_count",
443
+ "duration_p95_ms",
444
+ "rss_end_max_bytes",
445
+ ] as const);
446
+ if (
447
+ !numbers ||
448
+ typeof row.key !== "string" ||
449
+ typeof row.hook_name !== "string" ||
450
+ typeof row.adapter !== "string"
451
+ ) {
452
+ return undefined;
453
+ }
454
+ parsedAggregates.push({
455
+ key: row.key,
456
+ hook_name: row.hook_name,
457
+ adapter: row.adapter,
458
+ ...numbers,
459
+ });
460
+ }
461
+ return {
462
+ ...parsedSummary,
463
+ aggregates: parsedAggregates.sort((left, right) => left.key.localeCompare(right.key)),
464
+ };
465
+ }
466
+
467
+ function shadowAdmissionSummary(value: unknown): DiagnosticShadowAdmissionSummary | undefined {
468
+ const record = object(value);
469
+ const rows = Array.isArray(record?.records) ? record.records : undefined;
470
+ if (!record || !rows) return undefined;
471
+ const summary: DiagnosticShadowAdmissionSummary = {
472
+ record_count: rows.length,
473
+ observed_count: 0,
474
+ not_needed_count: 0,
475
+ unavailable_count: 0,
476
+ normal_count: 0,
477
+ elevated_count: 0,
478
+ critical_count: 0,
479
+ unknown_count: 0,
480
+ max_wait_ms: 0,
481
+ };
482
+ for (const value of rows) {
483
+ const row = object(value);
484
+ if (!row || (row.state !== "observed" && row.state !== "not-needed")) return undefined;
485
+ if (row.state === "not-needed") {
486
+ summary.not_needed_count += 1;
487
+ continue;
488
+ }
489
+ summary.observed_count += 1;
490
+ const pressure = row.pressure;
491
+ if (pressure === "normal") summary.normal_count += 1;
492
+ else if (pressure === "elevated") summary.elevated_count += 1;
493
+ else if (pressure === "critical") summary.critical_count += 1;
494
+ else summary.unknown_count += 1;
495
+ if (row.freshness === "unavailable") summary.unavailable_count += 1;
496
+ if (typeof row.wait_ms === "number" && Number.isSafeInteger(row.wait_ms) && row.wait_ms >= 0) {
497
+ summary.max_wait_ms = Math.max(summary.max_wait_ms, row.wait_ms);
498
+ } else return undefined;
499
+ }
500
+ return summary;
501
+ }
502
+
503
+ function integerFields<const Keys extends readonly string[]>(
504
+ record: Record<string, unknown>,
505
+ keys: Keys,
506
+ ): { [Key in Keys[number]]: number } | undefined {
507
+ const result: Record<string, number> = {};
508
+ for (const key of keys) {
509
+ const value = record[key];
510
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) return undefined;
511
+ result[key] = value;
512
+ }
513
+ return result as { [Key in Keys[number]]: number };
514
+ }
515
+
516
+ function adviceDirection(
517
+ before: DiagnosticPressure,
518
+ after: DiagnosticPressure,
519
+ ): "escalated" | "deescalated" | "unchanged" | "changed" {
520
+ if (before === after) return "unchanged";
521
+ if (before === "unknown" || after === "unknown") return "changed";
522
+ return pressureRank(after) > pressureRank(before) ? "escalated" : "deescalated";
523
+ }
524
+
525
+ function countImpact(
526
+ rows: readonly DiagnosticFindingComparison[],
527
+ impact: DiagnosticComparisonImpact,
528
+ ): number {
529
+ return rows.filter((row) => row.impact === impact).length;
530
+ }
531
+
532
+ function owner(finding: SupervisorFinding): string | null {
533
+ const attribution = finding.attribution;
534
+ if (!attribution) return null;
535
+ if (attribution.state === "attributed") {
536
+ return `${attribution.owner_kind ?? "unknown"}:${attribution.owner_id ?? "unknown"}`;
537
+ }
538
+ return `unattributed:${attribution.reason_code ?? "unknown"}`;
539
+ }
540
+
541
+ function severityRank(value: SupervisorFinding["severity"]): number {
542
+ return value === "critical" ? 2 : value === "warning" ? 1 : 0;
543
+ }
544
+
545
+ function stateRank(value: SupervisorFinding["state"]): number {
546
+ return value === "opened" ? 1 : 0;
547
+ }
548
+
549
+ function numberRank(value: number): number {
550
+ return value;
551
+ }
552
+
553
+ function pressureRank(value: Exclude<DiagnosticPressure, "unknown">): number {
554
+ return value === "critical" ? 2 : value === "elevated" ? 1 : 0;
555
+ }
556
+
557
+ function impactRank(value: DiagnosticComparisonImpact): number {
558
+ return ["regression", "worsened", "changed", "improved", "recovery", "unchanged"].indexOf(value);
559
+ }
560
+
561
+ function object(value: unknown): Record<string, unknown> | undefined {
562
+ return value && typeof value === "object" && !Array.isArray(value)
563
+ ? (value as Record<string, unknown>)
564
+ : undefined;
565
+ }