@thebackstoryis/engineering-with-ai 0.2.9

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 (334) hide show
  1. package/Docs/README.md +50 -0
  2. package/Docs/adoption/consultancy-and-multi-project-rollout.md +135 -0
  3. package/Docs/adoption/non-technical-team-guide.md +126 -0
  4. package/Docs/archaeology-technology-and-hosting-discovery.md +212 -0
  5. package/Docs/blast-radius-and-impact-routing-guide.md +325 -0
  6. package/Docs/blueprints/internal-blueprint-catalogue.md +146 -0
  7. package/Docs/blueprints/maintaining-organisation-blueprints.md +154 -0
  8. package/Docs/blueprints/validation-and-troubleshooting.md +168 -0
  9. package/Docs/cli-reference.md +113 -0
  10. package/Docs/completed-phase-evidence-amendments.md +74 -0
  11. package/Docs/consultancy-network-rollout-control-plane-guide.md +202 -0
  12. package/Docs/context-aware-delivery-companion-guide.md +198 -0
  13. package/Docs/context-aware-delivery-companion-user-guide.md +184 -0
  14. package/Docs/context-management-and-token-efficiency.md +113 -0
  15. package/Docs/design-systems/design-system-implementation-guide.md +85 -0
  16. package/Docs/design-systems/design-system-pack-authoring-guide.md +95 -0
  17. package/Docs/design-systems/design-system-review-guide.md +51 -0
  18. package/Docs/design-systems/design-system-user-guide.md +96 -0
  19. package/Docs/design-systems/product-owner-guide.md +49 -0
  20. package/Docs/designing-organisation-blueprint-packs.md +384 -0
  21. package/Docs/developer-delivery-guide.md +224 -0
  22. package/Docs/error-reporting-guide.md +110 -0
  23. package/Docs/error-reporting-provider-guide.md +49 -0
  24. package/Docs/examples/error-report-adapter.md +70 -0
  25. package/Docs/examples/meeting-review.md +76 -0
  26. package/Docs/examples/minimal-design-system.md +67 -0
  27. package/Docs/examples/prototype-review-inputs.md +175 -0
  28. package/Docs/examples/reproducible-archaeology-depth-example.md +144 -0
  29. package/Docs/examples/test-scenario-input.md +68 -0
  30. package/Docs/examples/worked-examples.md +147 -0
  31. package/Docs/existing-project-onboarding-guide.md +214 -0
  32. package/Docs/explanation/core-concepts.md +26 -0
  33. package/Docs/explanation/delivery-workflow.md +48 -0
  34. package/Docs/governance/governance-team-guide.md +139 -0
  35. package/Docs/governed-starter-project-materialisation-guide.md +284 -0
  36. package/Docs/guide-catalogue.md +117 -0
  37. package/Docs/guided-discovery-facilitator-guide.md +172 -0
  38. package/Docs/guided-intent-workspace-guide.md +109 -0
  39. package/Docs/guided-phase-evidence-drafting-guide.md +119 -0
  40. package/Docs/human-approval-and-assurance-guide.md +146 -0
  41. package/Docs/knowledge-proposals-implementer-guide.md +106 -0
  42. package/Docs/knowledge-proposals-user-guide.md +247 -0
  43. package/Docs/maintainers/context-benchmarks.md +29 -0
  44. package/Docs/maintainers/contributing.md +58 -0
  45. package/Docs/maintainers/evidence-depth-acceptance.md +72 -0
  46. package/Docs/maintainers/verification-walkthroughs.md +104 -0
  47. package/Docs/meeting-evidence-implementer-guide.md +132 -0
  48. package/Docs/meeting-evidence-user-guide.md +200 -0
  49. package/Docs/operations/dashboard-and-delivery-state.md +153 -0
  50. package/Docs/operations/dashboard-configuration.md +87 -0
  51. package/Docs/operations/installation-updating-and-entitlements.md +135 -0
  52. package/Docs/operations/premium-personas-setup.md +76 -0
  53. package/Docs/operations/troubleshooting-and-recovery.md +205 -0
  54. package/Docs/organisation-rollout-guide.md +142 -0
  55. package/Docs/persona-entitlement-provider-guide.md +199 -0
  56. package/Docs/persona-guided-prototype-iteration.md +129 -0
  57. package/Docs/personas/organisation-specific-personas.md +103 -0
  58. package/Docs/personas/persona-authoring-cookbook.md +176 -0
  59. package/Docs/personas/persona-engagement-ui.md +133 -0
  60. package/Docs/personas/persona-governance.md +118 -0
  61. package/Docs/platform-export-analysis-guide.md +336 -0
  62. package/Docs/policies/governance-owner-guide.md +36 -0
  63. package/Docs/policies/implementation-guide.md +42 -0
  64. package/Docs/policies/organisation-policy-design-gates.md +58 -0
  65. package/Docs/policies/policy-pack-authoring-guide.md +108 -0
  66. package/Docs/policies/product-owner-guide.md +43 -0
  67. package/Docs/policies/technical-owner-guide.md +37 -0
  68. package/Docs/product-owner-guide.md +327 -0
  69. package/Docs/project-portfolio-orchestration-guide.md +199 -0
  70. package/Docs/quality/manual-qa-and-acceptance.md +162 -0
  71. package/Docs/quality/persona-driven-test-scenarios.md +172 -0
  72. package/Docs/quality/reproducible-archaeology-depth-review-checklist.md +89 -0
  73. package/Docs/reference/capabilities-and-project-layout.md +678 -0
  74. package/Docs/reference/cli-and-configuration.md +398 -0
  75. package/Docs/reference/contributions-api.md +23 -0
  76. package/Docs/reference/security-adapter-authoring.md +81 -0
  77. package/Docs/reference/starter-adapter-authoring.md +74 -0
  78. package/Docs/repository-source-map-guide.md +381 -0
  79. package/Docs/reproducible-archaeology-and-discovery-depth.md +292 -0
  80. package/Docs/screen-prototype-creation-guide.md +324 -0
  81. package/Docs/security-validation-guide.md +353 -0
  82. package/Docs/solution-readiness-review-guide.md +123 -0
  83. package/Docs/standards/project-standards-authoring.md +157 -0
  84. package/Docs/team-hub-guide.md +162 -0
  85. package/Docs/team-hub-resource-registry-guide.md +167 -0
  86. package/Docs/tutorials/first-delivery.md +83 -0
  87. package/Docs/tutorials/first-session.md +62 -0
  88. package/Docs/using-lifecycle-hooks.md +381 -0
  89. package/Docs/working-with-personas.md +274 -0
  90. package/LICENSE +165 -0
  91. package/README.md +96 -0
  92. package/agents-src/claude/ewai-security-reviewer.md +15 -0
  93. package/bin/ewai +5 -0
  94. package/config/archaeology-record-families.yaml +59 -0
  95. package/config/delivery-artifacts.yaml +121 -0
  96. package/config/delivery-stages.yaml +77 -0
  97. package/config/design-system.schema.json +46 -0
  98. package/config/error-reporting.schema.json +79 -0
  99. package/config/evidence-depth.schema.json +53 -0
  100. package/config/intent.schema.json +90 -0
  101. package/config/knowledge-proposals-proposal.schema.json +34 -0
  102. package/config/lifecycle-event.schema.json +68 -0
  103. package/config/lifecycle-handler.schema.json +45 -0
  104. package/config/lifecycle-hook-ack.schema.json +19 -0
  105. package/config/meeting-evidence-candidate.schema.json +102 -0
  106. package/config/organisation-policy.schema.json +137 -0
  107. package/config/pack.schema.json +250 -0
  108. package/config/persona-pack.schema.json +21 -0
  109. package/config/persona.schema.json +17 -0
  110. package/config/policy-evaluation.schema.json +77 -0
  111. package/config/policy-facts.schema.json +140 -0
  112. package/config/portfolio.schema.json +68 -0
  113. package/config/project.schema.json +313 -0
  114. package/config/prototype-iteration.schema.json +128 -0
  115. package/config/rollout.schema.json +87 -0
  116. package/config/security-adapter.schema.json +31 -0
  117. package/config/security-scan-request.schema.json +64 -0
  118. package/config/security-scan-response.schema.json +52 -0
  119. package/config/security-validation-policy.schema.json +74 -0
  120. package/config/starter-source-acknowledgement.schema.json +13 -0
  121. package/config/starter-source-adapter.schema.json +38 -0
  122. package/config/starter-source-request.schema.json +61 -0
  123. package/package.json +77 -0
  124. package/packs/core/pack.yaml +7 -0
  125. package/packs/design-systems/default/experience-promise.md +9 -0
  126. package/packs/design-systems/default/intentional-review.md +10 -0
  127. package/packs/design-systems/default/interaction-and-entry.md +9 -0
  128. package/packs/design-systems/default/meaningful-content-and-states.md +9 -0
  129. package/packs/design-systems/default/pack.yaml +44 -0
  130. package/packs/design-systems/default/principles.md +10 -0
  131. package/packs/personas/core/pack.yaml +7 -0
  132. package/packs/personas/core/personas/archaeologist.md +37 -0
  133. package/packs/personas/core/personas/end-user.md +17 -0
  134. package/packs/personas/core/personas/maintainer.md +17 -0
  135. package/packs/personas/core/personas/operator.md +17 -0
  136. package/packs/personas/core/personas/specs-knowledge-curator.md +35 -0
  137. package/packs/technologies/laravel/pack.yaml +30 -0
  138. package/packs/technologies/laravel-nuxt/pack.yaml +30 -0
  139. package/packs/technologies/nuxt/pack.yaml +30 -0
  140. package/packs/technologies/power-platform/pack.yaml +31 -0
  141. package/packs/technologies/salesforce/pack.yaml +25 -0
  142. package/public/app.js +4896 -0
  143. package/public/apple-touch-icon.png +0 -0
  144. package/public/assets/backstory-icon.png +0 -0
  145. package/public/dashboard-navigation.js +98 -0
  146. package/public/favicon-16.png +0 -0
  147. package/public/favicon-32.png +0 -0
  148. package/public/favicon.ico +0 -0
  149. package/public/index.html +789 -0
  150. package/public/styles.css +2693 -0
  151. package/public/team-hub/app.js +202 -0
  152. package/public/team-hub/index.html +79 -0
  153. package/public/team-hub/styles.css +90 -0
  154. package/scripts/publication-check.mjs +140 -0
  155. package/scripts/setup.mjs +21 -0
  156. package/skills-src/ewai-archaeology/SKILL.md +334 -0
  157. package/skills-src/ewai-archaeology/agents/openai.yaml +4 -0
  158. package/skills-src/ewai-archaeology/references/archaeology-contract.md +201 -0
  159. package/skills-src/ewai-archaeology/references/lifecycle-reconstruction.md +177 -0
  160. package/skills-src/ewai-archaeology/references/maximum-detail-reconstruction.md +97 -0
  161. package/skills-src/ewai-archaeology/references/model-routing.md +26 -0
  162. package/skills-src/ewai-architecture/SKILL.md +108 -0
  163. package/skills-src/ewai-architecture/agents/openai.yaml +4 -0
  164. package/skills-src/ewai-architecture/references/architecture-contract.md +176 -0
  165. package/skills-src/ewai-context/SKILL.md +68 -0
  166. package/skills-src/ewai-context/agents/openai.yaml +4 -0
  167. package/skills-src/ewai-context-import/SKILL.md +118 -0
  168. package/skills-src/ewai-context-import/agents/openai.yaml +4 -0
  169. package/skills-src/ewai-context-import/references/context-import-contract.md +106 -0
  170. package/skills-src/ewai-dashboard-configuration/SKILL.md +20 -0
  171. package/skills-src/ewai-deliver/SKILL.md +122 -0
  172. package/skills-src/ewai-deliver/references/delivery-evidence.md +92 -0
  173. package/skills-src/ewai-deliver/references/phase-routing.md +31 -0
  174. package/skills-src/ewai-design-system-apply/SKILL.md +27 -0
  175. package/skills-src/ewai-design-system-apply/agents/openai.yaml +4 -0
  176. package/skills-src/ewai-design-system-apply/references/application-contract.md +36 -0
  177. package/skills-src/ewai-design-system-author/SKILL.md +28 -0
  178. package/skills-src/ewai-design-system-author/agents/openai.yaml +4 -0
  179. package/skills-src/ewai-design-system-author/references/authoring-contract.md +38 -0
  180. package/skills-src/ewai-design-system-review/SKILL.md +26 -0
  181. package/skills-src/ewai-design-system-review/agents/openai.yaml +4 -0
  182. package/skills-src/ewai-design-system-review/references/review-contract.md +40 -0
  183. package/skills-src/ewai-error-reporting/SKILL.md +46 -0
  184. package/skills-src/ewai-error-reporting/agents/openai.yaml +4 -0
  185. package/skills-src/ewai-error-reporting/references/provider-contract.md +74 -0
  186. package/skills-src/ewai-evidence-depth/SKILL.md +72 -0
  187. package/skills-src/ewai-evidence-depth/agents/openai.yaml +4 -0
  188. package/skills-src/ewai-evidence-depth/references/evidence-depth-contract.md +127 -0
  189. package/skills-src/ewai-intent/SKILL.md +68 -0
  190. package/skills-src/ewai-intent/agents/openai.yaml +4 -0
  191. package/skills-src/ewai-intent/references/intent-contract.md +42 -0
  192. package/skills-src/ewai-knowledge-proposals/SKILL.md +105 -0
  193. package/skills-src/ewai-knowledge-proposals/agents/openai.yaml +4 -0
  194. package/skills-src/ewai-knowledge-proposals/references/proposal-contract.md +59 -0
  195. package/skills-src/ewai-meeting-evidence/SKILL.md +106 -0
  196. package/skills-src/ewai-meeting-evidence/agents/openai.yaml +4 -0
  197. package/skills-src/ewai-meeting-evidence/references/candidate-contract.md +64 -0
  198. package/skills-src/ewai-organisation-policy/SKILL.md +62 -0
  199. package/skills-src/ewai-organisation-policy/agents/openai.yaml +4 -0
  200. package/skills-src/ewai-organisation-policy/references/policy-contract.md +94 -0
  201. package/skills-src/ewai-palace-housekeeping/SKILL.md +55 -0
  202. package/skills-src/ewai-palace-housekeeping/agents/openai.yaml +4 -0
  203. package/skills-src/ewai-persona-entitlement/SKILL.md +60 -0
  204. package/skills-src/ewai-persona-entitlement/agents/openai.yaml +4 -0
  205. package/skills-src/ewai-phase-evidence/SKILL.md +79 -0
  206. package/skills-src/ewai-phase-evidence/agents/openai.yaml +4 -0
  207. package/skills-src/ewai-pipeline/SKILL.md +130 -0
  208. package/skills-src/ewai-pipeline/agents/openai.yaml +4 -0
  209. package/skills-src/ewai-pipeline/references/cli.md +86 -0
  210. package/skills-src/ewai-pipeline/references/specs-contract.md +16 -0
  211. package/skills-src/ewai-portfolio/SKILL.md +70 -0
  212. package/skills-src/ewai-portfolio/agents/openai.yaml +4 -0
  213. package/skills-src/ewai-portfolio/references/portfolio-contract.md +67 -0
  214. package/skills-src/ewai-project-discovery/SKILL.md +95 -0
  215. package/skills-src/ewai-project-discovery/agents/openai.yaml +4 -0
  216. package/skills-src/ewai-project-discovery/references/discovery-contract.md +34 -0
  217. package/skills-src/ewai-prototype-iteration/SKILL.md +30 -0
  218. package/skills-src/ewai-prototype-iteration/agents/openai.yaml +4 -0
  219. package/skills-src/ewai-prototype-iteration/references/review-contract.md +49 -0
  220. package/skills-src/ewai-retro/SKILL.md +48 -0
  221. package/skills-src/ewai-retro/agents/openai.yaml +4 -0
  222. package/skills-src/ewai-retro/references/asset-routing.md +14 -0
  223. package/skills-src/ewai-rollout/SKILL.md +74 -0
  224. package/skills-src/ewai-rollout/agents/openai.yaml +4 -0
  225. package/skills-src/ewai-rollout/references/rollout-contract.md +74 -0
  226. package/skills-src/ewai-shape-intents/SKILL.md +84 -0
  227. package/skills-src/ewai-shape-intents/agents/openai.yaml +4 -0
  228. package/skills-src/ewai-shape-intents/references/intent-mapping-contract.md +109 -0
  229. package/skills-src/ewai-solution-readiness/SKILL.md +55 -0
  230. package/skills-src/ewai-solution-readiness/agents/openai.yaml +4 -0
  231. package/skills-src/ewai-standards-check/SKILL.md +93 -0
  232. package/skills-src/ewai-standards-check/agents/openai.yaml +4 -0
  233. package/skills-src/ewai-standards-check/references/report-contract.md +116 -0
  234. package/skills-src/ewai-test-scenarios/SKILL.md +94 -0
  235. package/skills-src/ewai-test-scenarios/agents/openai.yaml +4 -0
  236. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +88 -0
  237. package/src/afk-worker.mjs +16 -0
  238. package/src/archaeology.mjs +1333 -0
  239. package/src/checkin.mjs +261 -0
  240. package/src/cli.mjs +2427 -0
  241. package/src/companion-guidance.mjs +257 -0
  242. package/src/companion-opening.mjs +62 -0
  243. package/src/companion.mjs +256 -0
  244. package/src/context.mjs +210 -0
  245. package/src/dashboard-preferences.mjs +80 -0
  246. package/src/delivery-artifacts.mjs +204 -0
  247. package/src/delivery-documents.mjs +248 -0
  248. package/src/delivery-gates.mjs +317 -0
  249. package/src/delivery.mjs +1433 -0
  250. package/src/design-system-application.mjs +291 -0
  251. package/src/design-system-authoring.mjs +101 -0
  252. package/src/design-systems.mjs +466 -0
  253. package/src/discovery.mjs +1314 -0
  254. package/src/error-reporting.mjs +323 -0
  255. package/src/evidence-depth.mjs +543 -0
  256. package/src/execution-state.mjs +243 -0
  257. package/src/install.mjs +166 -0
  258. package/src/intent-dependencies.mjs +117 -0
  259. package/src/intent-maps.mjs +402 -0
  260. package/src/intents.mjs +747 -0
  261. package/src/knowledge-proposals.mjs +717 -0
  262. package/src/launcher.mjs +51 -0
  263. package/src/meeting-evidence.mjs +703 -0
  264. package/src/network-rollout.mjs +386 -0
  265. package/src/organisation-blueprints.mjs +438 -0
  266. package/src/organisation-policies.mjs +448 -0
  267. package/src/packs.mjs +44 -0
  268. package/src/paths.mjs +62 -0
  269. package/src/persona-entitlements.mjs +438 -0
  270. package/src/persona-licence-config.mjs +98 -0
  271. package/src/persona-website-provider.mjs +134 -0
  272. package/src/persona-zip.mjs +87 -0
  273. package/src/personas.mjs +159 -0
  274. package/src/platform-metadata-analysis.mjs +314 -0
  275. package/src/policy-design-gates.mjs +623 -0
  276. package/src/policy-gate-integration.mjs +318 -0
  277. package/src/portfolio.mjs +509 -0
  278. package/src/power-platform-source-map.mjs +190 -0
  279. package/src/project.mjs +449 -0
  280. package/src/prototype-iterations.mjs +730 -0
  281. package/src/repository-source-map.mjs +603 -0
  282. package/src/runtime/afk-conductor.mjs +973 -0
  283. package/src/runtime/context-assembly.mjs +457 -0
  284. package/src/runtime/context-benchmarks.mjs +115 -0
  285. package/src/runtime/dashboard-actions.mjs +109 -0
  286. package/src/runtime/dashboard-handoffs.mjs +149 -0
  287. package/src/runtime/dashboard-server.mjs +1272 -0
  288. package/src/runtime/dashboard.mjs +197 -0
  289. package/src/runtime/database.mjs +789 -0
  290. package/src/runtime/error-reporting.mjs +581 -0
  291. package/src/runtime/evidence-depth-workspace.mjs +412 -0
  292. package/src/runtime/execution-leases.mjs +299 -0
  293. package/src/runtime/guided-discovery.mjs +350 -0
  294. package/src/runtime/guided-intents.mjs +517 -0
  295. package/src/runtime/impact-analysis.mjs +535 -0
  296. package/src/runtime/intents.mjs +222 -0
  297. package/src/runtime/knowledge.mjs +86 -0
  298. package/src/runtime/lifecycle-hooks.mjs +1239 -0
  299. package/src/runtime/mcp-config.mjs +110 -0
  300. package/src/runtime/mcp-server.mjs +1885 -0
  301. package/src/runtime/palace.mjs +362 -0
  302. package/src/runtime/paths.mjs +58 -0
  303. package/src/runtime/persona-engagement.mjs +255 -0
  304. package/src/runtime/phase-contributions.mjs +594 -0
  305. package/src/runtime/policy-workspace.mjs +170 -0
  306. package/src/runtime/prototype-iterations.mjs +235 -0
  307. package/src/runtime/provider-adapters.mjs +163 -0
  308. package/src/runtime/repository-index.mjs +838 -0
  309. package/src/runtime/runs.mjs +185 -0
  310. package/src/runtime/security-validation.mjs +1230 -0
  311. package/src/runtime/starter-materialisation.mjs +1155 -0
  312. package/src/runtime/team-hub-client.mjs +479 -0
  313. package/src/runtime/team-hub-database.mjs +288 -0
  314. package/src/runtime/team-hub-server.mjs +191 -0
  315. package/src/runtime/team-hub.mjs +110 -0
  316. package/src/runtime/tree-sitter-index.mjs +390 -0
  317. package/src/runtime/version.mjs +1 -0
  318. package/src/runtime/work.mjs +633 -0
  319. package/src/salesforce-source-map.mjs +212 -0
  320. package/src/security-validation-config.mjs +224 -0
  321. package/src/solution-readiness.mjs +620 -0
  322. package/src/starter-materialisation-contract.mjs +407 -0
  323. package/src/task-graph.mjs +544 -0
  324. package/src/team-hub-resources.mjs +239 -0
  325. package/src/team-hub.mjs +242 -0
  326. package/src/test-scenarios.mjs +622 -0
  327. package/src/validation-config.mjs +289 -0
  328. package/templates/SPECS/1.Scope/personas/registry.yaml +12 -0
  329. package/templates/SPECS/5.Strategy/patterns/context-packet.md +119 -0
  330. package/templates/SPECS/6.Build/_tracker-template.md +16 -0
  331. package/templates/SPECS/pipeline.yaml +62 -0
  332. package/templates/discovery-answers.yaml +86 -0
  333. package/templates/intent-body.md +21 -0
  334. package/tests/fixtures/context-benchmarks.json +9 -0
@@ -0,0 +1,543 @@
1
+ import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { atomicJson, isWithin, sha256 } from './delivery-documents.mjs';
4
+ import { projectPaths } from './paths.mjs';
5
+
6
+ export const EVIDENCE_DEPTH_DIMENSIONS = Object.freeze([
7
+ 'architecture',
8
+ 'data',
9
+ 'security',
10
+ 'product',
11
+ 'delivery',
12
+ 'governance',
13
+ 'operations',
14
+ ]);
15
+
16
+ export const EVIDENCE_DEPTH_LEVELS = Object.freeze(['bounded', 'standard', 'deep']);
17
+
18
+ const dimensionCodes = Object.freeze({
19
+ architecture: 'ARC', data: 'DAT', security: 'SEC', product: 'PRO',
20
+ delivery: 'DEL', governance: 'GOV', operations: 'OPS',
21
+ });
22
+ const evidenceAuthorities = new Set(['observed', 'inferred', 'declared', 'confirmed', 'advisory', 'decided']);
23
+ const coverageStatuses = new Set(['supported', 'partial', 'owner-evidence-required', 'contradictory', 'blocked', 'not-applicable']);
24
+ const contradictionStates = new Set(['none', 'declared-versus-observed', 'observed-versus-observed', 'declared-versus-declared', 'unresolved']);
25
+ const gapDispositions = new Set(['open', 'resolved', 'deferred', 'excluded', 'shared']);
26
+ const groupingDispositions = new Set(['owned', 'shared', 'deferred', 'excluded']);
27
+ const tiers = new Set(['project', 'premium', 'personal', 'core']);
28
+ const safeTokenPattern = /^[a-z0-9][a-z0-9._:@/-]{0,199}$/i;
29
+ const digestPattern = /^sha256:[a-z0-9._-]{3,200}$/i;
30
+
31
+ function fail(message) {
32
+ throw new Error(`Evidence depth: ${message}`);
33
+ }
34
+
35
+ function object(value, label) {
36
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be an object`);
37
+ return value;
38
+ }
39
+
40
+ function knownFields(value, allowed, label) {
41
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) fail(`${label} has unknown field ${key}`);
42
+ }
43
+
44
+ function text(value, label, maximum = 200, { optional = false } = {}) {
45
+ const cleaned = String(value ?? '').trim();
46
+ if (!cleaned && !optional) fail(`${label} is required`);
47
+ if (cleaned.length > maximum) fail(`${label} exceeds ${maximum} characters`);
48
+ return cleaned;
49
+ }
50
+
51
+ function token(value, label, options = {}) {
52
+ const cleaned = text(value, label, options.maximum ?? 200, options);
53
+ if (cleaned && (!safeTokenPattern.test(cleaned) || cleaned.includes('..') || cleaned.startsWith('/'))) {
54
+ fail(`${label} must be a bounded safe identifier`);
55
+ }
56
+ return cleaned;
57
+ }
58
+
59
+ function digest(value, label) {
60
+ const cleaned = text(value, label, 220);
61
+ if (!digestPattern.test(cleaned)) fail(`${label} must be a sha256 identifier`);
62
+ return cleaned.toLowerCase();
63
+ }
64
+
65
+ function boundedInteger(value, label, maximum = 1_000_000) {
66
+ const parsed = Number(value);
67
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > maximum) fail(`${label} must be an integer from 0 to ${maximum}`);
68
+ return parsed;
69
+ }
70
+
71
+ function list(value, label, maximum) {
72
+ if (!Array.isArray(value)) fail(`${label} must be an array`);
73
+ if (value.length > maximum) fail(`${label} exceeds ${maximum} entries`);
74
+ return value;
75
+ }
76
+
77
+ function uniqueSorted(values, label, maximum = 200) {
78
+ const result = list(values ?? [], label, maximum).map((value, index) => token(value, `${label} ${index + 1}`));
79
+ if (new Set(result).size !== result.length) fail(`${label} contains duplicate values`);
80
+ return result.sort((left, right) => left.localeCompare(right));
81
+ }
82
+
83
+ function canonical(value) {
84
+ if (Array.isArray(value)) return value.map(canonical);
85
+ if (!value || typeof value !== 'object') return value;
86
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
87
+ }
88
+
89
+ function canonicalJson(value) {
90
+ return JSON.stringify(canonical(value));
91
+ }
92
+
93
+ function hash(value) {
94
+ return `sha256:${sha256(canonicalJson(value))}`;
95
+ }
96
+
97
+ function normalizeProvider(value = {}) {
98
+ const provider = object(value, 'provider');
99
+ knownFields(provider, ['id', 'modelFamily', 'capabilityRevision'], 'provider');
100
+ return {
101
+ id: token(provider.id ?? 'unspecified', 'provider id'),
102
+ modelFamily: token(provider.modelFamily ?? 'unspecified', 'provider model family'),
103
+ capabilityRevision: token(provider.capabilityRevision ?? 'unspecified', 'provider capability revision'),
104
+ };
105
+ }
106
+
107
+ function normalizeSourceMap(value) {
108
+ const source = object(value, 'sourceMap');
109
+ knownFields(source, ['runId', 'digest', 'profileDigest', 'freshness'], 'sourceMap');
110
+ const freshness = token(source.freshness, 'Source Map freshness');
111
+ if (!['fresh', 'stale'].includes(freshness)) fail('Source Map freshness must be fresh or stale');
112
+ return {
113
+ runId: boundedInteger(source.runId, 'Source Map run ID'),
114
+ digest: digest(source.digest, 'Source Map digest'),
115
+ profileDigest: digest(source.profileDigest, 'Source Map profile digest'),
116
+ freshness,
117
+ };
118
+ }
119
+
120
+ function normalizePersonas(value) {
121
+ const personas = list(value, 'personas', 8).map((entry, index) => {
122
+ const persona = object(entry, `persona ${index + 1}`);
123
+ knownFields(persona, ['id', 'tier', 'reasonCode'], `persona ${index + 1}`);
124
+ const tier = token(persona.tier, `persona ${index + 1} tier`);
125
+ if (!tiers.has(tier)) fail(`persona ${index + 1} tier is unsupported`);
126
+ return {
127
+ id: token(persona.id, `persona ${index + 1} id`),
128
+ tier,
129
+ reasonCode: token(persona.reasonCode, `persona ${index + 1} reason code`),
130
+ };
131
+ }).sort((left, right) => left.id.localeCompare(right.id));
132
+ if (new Set(personas.map(({ id }) => id)).size !== personas.length) fail('personas contains duplicate IDs');
133
+ return personas;
134
+ }
135
+
136
+ function normalizeEvidence(value) {
137
+ const entries = list(value, 'evidence', 500).map((entry, index) => {
138
+ const item = object(entry, `evidence ${index + 1}`);
139
+ knownFields(item, ['id', 'authority', 'kind', 'digest'], `evidence ${index + 1}`);
140
+ const authority = token(item.authority, `evidence ${index + 1} authority`);
141
+ if (!evidenceAuthorities.has(authority)) fail(`evidence ${index + 1} authority is unsupported`);
142
+ return {
143
+ id: token(item.id, `evidence ${index + 1} id`),
144
+ authority,
145
+ kind: token(item.kind, `evidence ${index + 1} kind`),
146
+ digest: digest(item.digest, `evidence ${index + 1} digest`),
147
+ };
148
+ }).sort((left, right) => left.id.localeCompare(right.id));
149
+ if (new Set(entries.map(({ id }) => id)).size !== entries.length) fail('evidence contains duplicate IDs');
150
+ return entries;
151
+ }
152
+
153
+ function normalizeCoverage(value, label) {
154
+ const coverage = object(value, `${label} coverage`);
155
+ knownFields(coverage, ['status', 'required', 'supported', 'excluded', 'failed'], `${label} coverage`);
156
+ const status = token(coverage.status, `${label} coverage status`);
157
+ if (!coverageStatuses.has(status)) fail(`${label} coverage status is unsupported`);
158
+ const normalized = {
159
+ status,
160
+ required: boundedInteger(coverage.required, `${label} required coverage`, 10_000),
161
+ supported: boundedInteger(coverage.supported, `${label} supported coverage`, 10_000),
162
+ excluded: boundedInteger(coverage.excluded ?? 0, `${label} excluded coverage`, 10_000),
163
+ failed: boundedInteger(coverage.failed ?? 0, `${label} failed coverage`, 10_000),
164
+ };
165
+ if (normalized.supported > normalized.required) fail(`${label} supported coverage cannot exceed required coverage`);
166
+ return normalized;
167
+ }
168
+
169
+ function normalizeDimensions(value, evidenceIds) {
170
+ const byId = new Map();
171
+ for (const [index, entry] of list(value, 'dimensions', 7).entries()) {
172
+ const dimension = object(entry, `dimension ${index + 1}`);
173
+ knownFields(dimension, ['id', 'recommendedDepth', 'drivers', 'evidenceRefs', 'coverage'], `dimension ${index + 1}`);
174
+ const id = token(dimension.id, `dimension ${index + 1} id`);
175
+ if (!EVIDENCE_DEPTH_DIMENSIONS.includes(id)) fail(`dimension ${id} is unsupported`);
176
+ if (byId.has(id)) fail(`dimension ${id} is duplicated`);
177
+ const recommendedDepth = token(dimension.recommendedDepth, `${id} recommended depth`);
178
+ if (!EVIDENCE_DEPTH_LEVELS.includes(recommendedDepth)) fail(`${id} recommended depth is unsupported`);
179
+ const evidenceRefs = uniqueSorted(dimension.evidenceRefs, `${id} evidence references`, 100);
180
+ for (const reference of evidenceRefs) if (!evidenceIds.has(reference)) fail(`${id} references unknown evidence ${reference}`);
181
+ byId.set(id, {
182
+ id,
183
+ recommendedDepth,
184
+ drivers: uniqueSorted(dimension.drivers, `${id} drivers`, 20),
185
+ evidenceRefs,
186
+ coverage: normalizeCoverage(dimension.coverage, id),
187
+ });
188
+ }
189
+ if (byId.size !== EVIDENCE_DEPTH_DIMENSIONS.length) fail('dimensions must contain every evidence-depth dimension exactly once');
190
+ return EVIDENCE_DEPTH_DIMENSIONS.map((id) => byId.get(id));
191
+ }
192
+
193
+ function normalizeGaps(value, evidenceIds) {
194
+ const gaps = list(value, 'gaps', 200).map((entry, index) => {
195
+ const gap = object(entry, `gap ${index + 1}`);
196
+ knownFields(gap, ['dimension', 'condition', 'currentStateCode', 'intendedStateCode', 'evidenceRefs', 'contradiction', 'owner', 'disposition'], `gap ${index + 1}`);
197
+ const dimension = token(gap.dimension, `gap ${index + 1} dimension`);
198
+ if (!EVIDENCE_DEPTH_DIMENSIONS.includes(dimension)) fail(`gap ${index + 1} dimension is unsupported`);
199
+ const contradiction = token(gap.contradiction ?? 'none', `gap ${index + 1} contradiction`);
200
+ if (!contradictionStates.has(contradiction)) fail(`gap ${index + 1} contradiction is unsupported`);
201
+ const disposition = token(gap.disposition ?? 'open', `gap ${index + 1} disposition`);
202
+ if (!gapDispositions.has(disposition)) fail(`gap ${index + 1} disposition is unsupported`);
203
+ const identity = {
204
+ dimension,
205
+ condition: token(gap.condition, `gap ${index + 1} condition`),
206
+ currentStateCode: token(gap.currentStateCode, `gap ${index + 1} current state code`),
207
+ intendedStateCode: token(gap.intendedStateCode, `gap ${index + 1} intended state code`),
208
+ evidenceRefs: uniqueSorted(gap.evidenceRefs, `gap ${index + 1} evidence references`, 50),
209
+ };
210
+ for (const reference of identity.evidenceRefs) if (!evidenceIds.has(reference)) fail(`gap ${index + 1} references unknown evidence ${reference}`);
211
+ return {
212
+ id: `GAP-${dimensionCodes[dimension]}-${sha256(canonicalJson(identity)).slice(0, 12)}`,
213
+ ...identity,
214
+ contradiction,
215
+ owner: token(gap.owner ?? 'unassigned', `gap ${index + 1} owner`),
216
+ disposition,
217
+ };
218
+ }).sort((left, right) => left.id.localeCompare(right.id));
219
+ if (new Set(gaps.map(({ id }) => id)).size !== gaps.length) fail('gaps contains duplicate stable identities');
220
+ return gaps;
221
+ }
222
+
223
+ function normalizeInput(value) {
224
+ const input = object(value, 'input');
225
+ knownFields(input, [
226
+ 'schema', 'projectFingerprint', 'sourceMap', 'contractVersions', 'packVersions',
227
+ 'provider', 'exclusions', 'predecessorId', 'personas', 'evidence', 'dimensions',
228
+ 'gaps', 'grouping',
229
+ ], 'input');
230
+ if (input.schema !== 'ewai.evidence-depth-input/v1') fail('input schema must be ewai.evidence-depth-input/v1');
231
+ const evidence = normalizeEvidence(input.evidence);
232
+ const evidenceIds = new Set(evidence.map(({ id }) => id));
233
+ const grouping = object(input.grouping ?? {}, 'grouping proposal');
234
+ knownFields(grouping, ['strategy', 'assignments'], 'grouping proposal');
235
+ return {
236
+ schema: input.schema,
237
+ projectFingerprint: digest(input.projectFingerprint, 'project fingerprint'),
238
+ sourceMap: normalizeSourceMap(input.sourceMap),
239
+ contractVersions: uniqueSorted(input.contractVersions, 'contract versions', 50),
240
+ packVersions: uniqueSorted(input.packVersions, 'pack versions', 50),
241
+ provider: normalizeProvider(input.provider),
242
+ exclusions: uniqueSorted(input.exclusions, 'exclusions', 100),
243
+ predecessorId: token(input.predecessorId ?? '', 'predecessor ID', { optional: true }),
244
+ personas: normalizePersonas(input.personas),
245
+ evidence,
246
+ dimensions: normalizeDimensions(input.dimensions, evidenceIds),
247
+ gaps: normalizeGaps(input.gaps, evidenceIds),
248
+ groupingStrategy: token(grouping.strategy ?? 'user-outcome', 'grouping strategy'),
249
+ };
250
+ }
251
+
252
+ function preparationIdentity(normalized) {
253
+ return {
254
+ schema: 'ewai.evidence-depth-identity/v1',
255
+ projectFingerprint: normalized.projectFingerprint,
256
+ sourceMap: normalized.sourceMap,
257
+ contractVersions: normalized.contractVersions,
258
+ packVersions: normalized.packVersions,
259
+ provider: normalized.provider,
260
+ exclusions: normalized.exclusions,
261
+ predecessorId: normalized.predecessorId,
262
+ personas: normalized.personas,
263
+ evidence: normalized.evidence,
264
+ dimensions: normalized.dimensions,
265
+ gaps: normalized.gaps,
266
+ };
267
+ }
268
+
269
+ export function prepareEvidenceDepth(input, options = {}) {
270
+ const normalized = normalizeInput(input);
271
+ if (normalized.sourceMap.freshness !== 'fresh') fail('a fresh Source Map is required for preparation');
272
+ const identity = preparationIdentity(normalized);
273
+ const preparationDigest = hash(identity);
274
+ return {
275
+ schema: 'ewai.evidence-depth-preparation/v1',
276
+ preparedAt: options.now ?? new Date().toISOString(),
277
+ preparationDigest,
278
+ inputFingerprint: hash({ projectFingerprint: normalized.projectFingerprint, sourceMap: normalized.sourceMap, contractVersions: normalized.contractVersions, packVersions: normalized.packVersions, provider: normalized.provider, exclusions: normalized.exclusions, predecessorId: normalized.predecessorId }),
279
+ evidenceFingerprint: hash(normalized.evidence),
280
+ personaFingerprint: hash(normalized.personas),
281
+ depthFingerprint: hash(normalized.dimensions.map(({ id, recommendedDepth, drivers }) => ({ id, recommendedDepth, drivers }))),
282
+ coverageFingerprint: hash(normalized.dimensions.map(({ id, coverage }) => ({ id, coverage }))),
283
+ gapFingerprint: hash(normalized.gaps.map(({ id, contradiction, disposition, owner }) => ({ id, contradiction, disposition, owner }))),
284
+ sourceMap: normalized.sourceMap,
285
+ provider: normalized.provider,
286
+ contractVersions: normalized.contractVersions,
287
+ packVersions: normalized.packVersions,
288
+ exclusions: normalized.exclusions,
289
+ predecessorId: normalized.predecessorId,
290
+ activePersonas: normalized.personas,
291
+ evidence: normalized.evidence,
292
+ dimensions: normalized.dimensions,
293
+ gaps: normalized.gaps,
294
+ proposedGroupingStrategy: normalized.groupingStrategy,
295
+ guidance: {
296
+ advisory: true,
297
+ humanReviewRequired: true,
298
+ intentCountIsDepth: false,
299
+ repositoryEvidenceAuthority: 'observed-or-inferred',
300
+ ownerEvidenceAuthority: 'declared-or-confirmed',
301
+ },
302
+ };
303
+ }
304
+
305
+ export function validateEvidenceDepthPreparation(value) {
306
+ const preparation = object(value, 'preparation');
307
+ knownFields(preparation, [
308
+ 'schema', 'preparedAt', 'preparationDigest', 'inputFingerprint', 'evidenceFingerprint',
309
+ 'personaFingerprint', 'depthFingerprint', 'coverageFingerprint', 'gapFingerprint',
310
+ 'sourceMap', 'provider', 'contractVersions', 'packVersions', 'exclusions',
311
+ 'predecessorId', 'activePersonas', 'evidence', 'dimensions', 'gaps',
312
+ 'proposedGroupingStrategy', 'guidance',
313
+ ], 'preparation');
314
+ if (preparation.schema !== 'ewai.evidence-depth-preparation/v1') fail('preparation schema is unsupported');
315
+ const evidence = normalizeEvidence(preparation.evidence);
316
+ const evidenceIds = new Set(evidence.map(({ id }) => id));
317
+ const dimensions = normalizeDimensions(preparation.dimensions, evidenceIds);
318
+ const gapInputs = list(preparation.gaps, 'prepared gaps', 200).map((entry, index) => {
319
+ const gap = object(entry, `prepared gap ${index + 1}`);
320
+ knownFields(gap, ['id', 'dimension', 'condition', 'currentStateCode', 'intendedStateCode', 'evidenceRefs', 'contradiction', 'owner', 'disposition'], `prepared gap ${index + 1}`);
321
+ const { id, ...input } = gap;
322
+ return { id: token(id, `prepared gap ${index + 1} ID`), input };
323
+ });
324
+ const gaps = normalizeGaps(gapInputs.map(({ input }) => input), evidenceIds);
325
+ for (const gap of gaps) {
326
+ const supplied = gapInputs.find(({ id }) => id === gap.id);
327
+ if (!supplied) fail(`prepared gap identity does not match ${gap.id}`);
328
+ }
329
+ const guidance = object(preparation.guidance, 'preparation guidance');
330
+ knownFields(guidance, ['advisory', 'humanReviewRequired', 'intentCountIsDepth', 'repositoryEvidenceAuthority', 'ownerEvidenceAuthority'], 'preparation guidance');
331
+ if (guidance.advisory !== true || guidance.humanReviewRequired !== true || guidance.intentCountIsDepth !== false) {
332
+ fail('preparation guidance authority is invalid');
333
+ }
334
+ const normalized = {
335
+ schema: preparation.schema,
336
+ preparedAt: text(preparation.preparedAt, 'preparedAt', 80),
337
+ preparationDigest: digest(preparation.preparationDigest, 'preparation digest'),
338
+ inputFingerprint: digest(preparation.inputFingerprint, 'input fingerprint'),
339
+ evidenceFingerprint: digest(preparation.evidenceFingerprint, 'evidence fingerprint'),
340
+ personaFingerprint: digest(preparation.personaFingerprint, 'persona fingerprint'),
341
+ depthFingerprint: digest(preparation.depthFingerprint, 'depth fingerprint'),
342
+ coverageFingerprint: digest(preparation.coverageFingerprint, 'coverage fingerprint'),
343
+ gapFingerprint: digest(preparation.gapFingerprint, 'gap fingerprint'),
344
+ sourceMap: normalizeSourceMap(preparation.sourceMap),
345
+ provider: normalizeProvider(preparation.provider),
346
+ contractVersions: uniqueSorted(preparation.contractVersions, 'contract versions', 50),
347
+ packVersions: uniqueSorted(preparation.packVersions, 'pack versions', 50),
348
+ exclusions: uniqueSorted(preparation.exclusions, 'exclusions', 100),
349
+ predecessorId: token(preparation.predecessorId ?? '', 'predecessor ID', { optional: true }),
350
+ activePersonas: normalizePersonas(preparation.activePersonas),
351
+ evidence,
352
+ dimensions,
353
+ gaps,
354
+ proposedGroupingStrategy: token(preparation.proposedGroupingStrategy, 'proposed grouping strategy'),
355
+ guidance: {
356
+ advisory: true,
357
+ humanReviewRequired: true,
358
+ intentCountIsDepth: false,
359
+ repositoryEvidenceAuthority: token(guidance.repositoryEvidenceAuthority, 'repository evidence authority'),
360
+ ownerEvidenceAuthority: token(guidance.ownerEvidenceAuthority, 'owner evidence authority'),
361
+ },
362
+ };
363
+ for (const field of ['evidenceFingerprint', 'personaFingerprint', 'depthFingerprint', 'coverageFingerprint', 'gapFingerprint']) {
364
+ const expected = {
365
+ evidenceFingerprint: hash(normalized.evidence),
366
+ personaFingerprint: hash(normalized.activePersonas),
367
+ depthFingerprint: hash(normalized.dimensions.map(({ id, recommendedDepth, drivers }) => ({ id, recommendedDepth, drivers }))),
368
+ coverageFingerprint: hash(normalized.dimensions.map(({ id, coverage }) => ({ id, coverage }))),
369
+ gapFingerprint: hash(normalized.gaps.map(({ id, contradiction, disposition, owner }) => ({ id, contradiction, disposition, owner }))),
370
+ }[field];
371
+ if (normalized[field] !== expected) fail(`${field} does not match the preparation content`);
372
+ }
373
+ return normalized;
374
+ }
375
+
376
+ function normalizeDimensionReview(preparation, value) {
377
+ const byId = new Map();
378
+ for (const [index, entry] of list(value, 'review dimensions', 7).entries()) {
379
+ const decision = object(entry, `review dimension ${index + 1}`);
380
+ knownFields(decision, ['id', 'selectedDepth', 'rationale'], `review dimension ${index + 1}`);
381
+ const id = token(decision.id, `review dimension ${index + 1} id`);
382
+ const source = preparation.dimensions.find((dimension) => dimension.id === id);
383
+ if (!source) fail(`review dimension ${id} is unsupported`);
384
+ if (byId.has(id)) fail(`review dimension ${id} is duplicated`);
385
+ const selectedDepth = token(decision.selectedDepth, `${id} selected depth`);
386
+ if (!EVIDENCE_DEPTH_LEVELS.includes(selectedDepth)) fail(`${id} selected depth is unsupported`);
387
+ const rationale = text(decision.rationale ?? '', `${id} rationale`, 500, { optional: true });
388
+ if (EVIDENCE_DEPTH_LEVELS.indexOf(selectedDepth) < EVIDENCE_DEPTH_LEVELS.indexOf(source.recommendedDepth) && rationale.length < 12) {
389
+ fail(`${id} requires a substantive rationale to reduce recommended depth`);
390
+ }
391
+ byId.set(id, { id, recommendedDepth: source.recommendedDepth, selectedDepth, rationale });
392
+ }
393
+ if (byId.size !== 7) fail('review dimensions must contain every evidence-depth dimension exactly once');
394
+ return EVIDENCE_DEPTH_DIMENSIONS.map((id) => byId.get(id));
395
+ }
396
+
397
+ function normalizeGrouping(preparation, value) {
398
+ const grouping = object(value, 'review grouping');
399
+ knownFields(grouping, ['strategy', 'assignments'], 'review grouping');
400
+ const gapIds = new Set(preparation.gaps.map(({ id }) => id));
401
+ const assignments = list(grouping.assignments, 'grouping assignments', 200).map((entry, index) => {
402
+ const assignment = object(entry, `grouping assignment ${index + 1}`);
403
+ knownFields(assignment, ['gapId', 'groupId', 'disposition'], `grouping assignment ${index + 1}`);
404
+ const gapId = token(assignment.gapId, `grouping assignment ${index + 1} gap ID`);
405
+ if (!gapIds.has(gapId)) fail(`grouping assignment references unknown gap ${gapId}`);
406
+ const disposition = token(assignment.disposition, `grouping assignment ${index + 1} disposition`);
407
+ if (!groupingDispositions.has(disposition)) fail(`grouping assignment ${index + 1} disposition is unsupported`);
408
+ return { gapId, groupId: token(assignment.groupId, `grouping assignment ${index + 1} group ID`), disposition };
409
+ }).sort((left, right) => left.gapId.localeCompare(right.gapId));
410
+ if (new Set(assignments.map(({ gapId }) => gapId)).size !== assignments.length) fail('each gap may appear in grouping exactly once');
411
+ if (assignments.length !== preparation.gaps.length) fail('every eligible gap must appear in grouping exactly once');
412
+ return { strategy: token(grouping.strategy, 'grouping strategy'), assignments };
413
+ }
414
+
415
+ function runIdentity(preparation, reviewedBy, dimensions, grouping) {
416
+ return {
417
+ schema: 'ewai.evidence-depth-run-identity/v1',
418
+ preparationDigest: preparation.preparationDigest,
419
+ reviewedBy,
420
+ dimensions: dimensions.map(({ id, recommendedDepth, selectedDepth }) => ({ id, recommendedDepth, selectedDepth })),
421
+ grouping,
422
+ };
423
+ }
424
+
425
+ function evidenceRoot(projectRoot) {
426
+ const { specsRoot } = projectPaths(projectRoot);
427
+ const root = resolve(specsRoot, '3.Evidence/discovery-depth/runs');
428
+ if (!isWithin(specsRoot, root)) fail('configured evidence root is unsafe');
429
+ return root;
430
+ }
431
+
432
+ export function recordEvidenceDepthRun(projectRoot, preparation, input, options = {}) {
433
+ preparation = validateEvidenceDepthPreparation(preparation);
434
+ const review = object(input, 'review');
435
+ knownFields(review, ['schema', 'expectedPreparationDigest', 'reviewedBy', 'dimensions', 'grouping'], 'review');
436
+ if (review.schema !== 'ewai.evidence-depth-review/v1') fail('review schema must be ewai.evidence-depth-review/v1');
437
+ if (review.expectedPreparationDigest !== preparation.preparationDigest) fail('a newer preparation exists or the expected digest is stale');
438
+ const reviewedBy = text(review.reviewedBy, 'reviewedBy', 120);
439
+ const dimensions = normalizeDimensionReview(preparation, review.dimensions);
440
+ const grouping = normalizeGrouping(preparation, review.grouping);
441
+ const identity = runIdentity(preparation, reviewedBy, dimensions, grouping);
442
+ const runId = sha256(canonicalJson(identity)).slice(0, 16);
443
+ const stable = {
444
+ schema: 'ewai.evidence-depth-run/v1',
445
+ runId,
446
+ preparationDigest: preparation.preparationDigest,
447
+ reviewedBy,
448
+ inputFingerprint: preparation.inputFingerprint,
449
+ evidenceFingerprint: preparation.evidenceFingerprint,
450
+ personaFingerprint: preparation.personaFingerprint,
451
+ depthFingerprint: hash(dimensions.map(({ id, recommendedDepth, selectedDepth }) => ({ id, recommendedDepth, selectedDepth }))),
452
+ coverageFingerprint: preparation.coverageFingerprint,
453
+ gapFingerprint: preparation.gapFingerprint,
454
+ groupingFingerprint: hash(grouping),
455
+ sourceMap: preparation.sourceMap,
456
+ provider: preparation.provider,
457
+ activePersonas: preparation.activePersonas,
458
+ dimensions,
459
+ gaps: preparation.gaps,
460
+ grouping,
461
+ };
462
+ const contentDigest = hash(stable);
463
+ const run = { ...stable, contentDigest, recordedAt: options.now ?? new Date().toISOString() };
464
+ if (options.persist !== false) {
465
+ if (!projectRoot) fail('project root is required to persist a reviewed run');
466
+ const root = evidenceRoot(projectRoot);
467
+ const path = resolve(root, `${runId}.json`);
468
+ if (!isWithin(root, path)) fail('run path is unsafe');
469
+ if (existsSync(path)) {
470
+ const existing = JSON.parse(readFileSync(path, 'utf8'));
471
+ if (existing.contentDigest !== contentDigest) fail(`run ${runId} conflicts with existing evidence`);
472
+ } else {
473
+ atomicJson(path, run);
474
+ }
475
+ }
476
+ return {
477
+ schema: 'ewai.evidence-depth-recording/v1',
478
+ status: options.persist === false ? 'prepared' : 'recorded',
479
+ runId,
480
+ contentDigest,
481
+ run,
482
+ };
483
+ }
484
+
485
+ function validateRun(run, label) {
486
+ const value = object(run, label);
487
+ if (value.schema !== 'ewai.evidence-depth-run/v1') fail(`${label} schema is unsupported`);
488
+ token(value.runId, `${label} run ID`);
489
+ for (const field of ['contentDigest', 'preparationDigest', 'inputFingerprint', 'evidenceFingerprint', 'personaFingerprint', 'depthFingerprint', 'coverageFingerprint', 'gapFingerprint', 'groupingFingerprint']) {
490
+ digest(value[field], `${label} ${field}`);
491
+ }
492
+ return value;
493
+ }
494
+
495
+ function change(left, right, explanation) {
496
+ const changed = left !== right;
497
+ return { status: changed ? 'changed' : 'same', explained: !changed || Boolean(explanation), ...(changed && explanation ? { explanation } : {}) };
498
+ }
499
+
500
+ export function compareEvidenceDepthRuns(leftInput, rightInput) {
501
+ const left = validateRun(leftInput, 'left run');
502
+ const right = validateRun(rightInput, 'right run');
503
+ const inputs = change(left.inputFingerprint, right.inputFingerprint, 'The governed project, Source Map, contract, pack, provider or exclusion input changed.');
504
+ const evidence = change(left.evidenceFingerprint, right.evidenceFingerprint, inputs.status === 'changed' ? 'The governed evidence set changed with the run inputs.' : 'The governed evidence set changed.');
505
+ const personas = change(left.personaFingerprint, right.personaFingerprint, 'The active persona identity or tier set changed.');
506
+ const depth = change(left.depthFingerprint, right.depthFingerprint, 'The recommended or owner-selected depth profile changed.');
507
+ const gaps = change(left.gapFingerprint, right.gapFingerprint, evidence.status === 'changed' || depth.status === 'changed' ? 'Stable gaps changed alongside evidence or depth.' : '');
508
+ const grouping = change(left.groupingFingerprint, right.groupingFingerprint, 'The explicit gap-grouping strategy or assignment changed.');
509
+ const coverage = change(left.coverageFingerprint, right.coverageFingerprint, inputs.status === 'changed' || evidence.status === 'changed' || depth.status === 'changed' ? 'Coverage changed alongside a material input, evidence or depth change.' : '');
510
+ const changes = { inputs, evidence, personas, depth, coverage, gaps, grouping };
511
+ const unexplained = Object.entries(changes)
512
+ .filter(([, value]) => value.status === 'changed' && value.explained === false)
513
+ .map(([field]) => ({ field, reason: `The ${field} fingerprint changed without a material upstream explanation.` }));
514
+ return {
515
+ schema: 'ewai.evidence-depth-comparison/v1',
516
+ leftRunId: left.runId,
517
+ rightRunId: right.runId,
518
+ order: ['inputs', 'evidence', 'personas', 'depth', 'coverage', 'gaps', 'grouping'],
519
+ changes,
520
+ unexplained,
521
+ reproducible: unexplained.length === 0,
522
+ guidance: { comparisonIsApproval: false, intentCountIsDepth: false },
523
+ };
524
+ }
525
+
526
+ export function readEvidenceDepthRun(projectRoot, runId) {
527
+ const safeId = token(runId, 'run ID');
528
+ const root = evidenceRoot(projectRoot);
529
+ const path = resolve(root, `${safeId}.json`);
530
+ if (!isWithin(root, path) || !existsSync(path) || lstatSync(path).isSymbolicLink()) fail(`run ${safeId} does not exist`);
531
+ return validateRun(JSON.parse(readFileSync(path, 'utf8')), `run ${safeId}`);
532
+ }
533
+
534
+ export function readEvidenceDepthStatus(projectRoot) {
535
+ const root = evidenceRoot(projectRoot);
536
+ if (!existsSync(root)) return { schema: 'ewai.evidence-depth-status/v1', status: 'not-prepared', runs: [] };
537
+ const runs = readdirSync(root, { withFileTypes: true })
538
+ .filter((entry) => entry.isFile() && !entry.isSymbolicLink() && /^[a-f0-9]{16}\.json$/.test(entry.name))
539
+ .map((entry) => validateRun(JSON.parse(readFileSync(resolve(root, entry.name), 'utf8')), entry.name))
540
+ .sort((left, right) => String(right.recordedAt ?? '').localeCompare(String(left.recordedAt ?? '')))
541
+ .map((run) => ({ runId: run.runId, contentDigest: run.contentDigest, preparationDigest: run.preparationDigest, recordedAt: run.recordedAt, reviewedBy: run.reviewedBy }));
542
+ return { schema: 'ewai.evidence-depth-status/v1', status: runs.length ? 'recorded' : 'not-prepared', runs };
543
+ }