@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,1333 @@
1
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import { dirname, isAbsolute, relative, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import YAML from 'yaml';
6
+ import { projectPaths } from './paths.mjs';
7
+ import { ASSURANCE_NOTICE } from './security-validation-config.mjs';
8
+ import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
9
+ import {
10
+ repositoryIndexFreshness,
11
+ repositorySourceMapCoverage,
12
+ repositorySourceMapFiles,
13
+ repositorySourceMapProfiles
14
+ } from './runtime/repository-index.mjs';
15
+
16
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
17
+ const defaultFamiliesPath = resolve(moduleDir, '../config/archaeology-record-families.yaml');
18
+ const terminalStatuses = new Set(['created', 'not-applicable', 'blocked']);
19
+ const reviewDecisions = new Set(['accepted', 'accepted-with-corrections', 'rejected', 'deferred']);
20
+ const personaPasses = [
21
+ 'purpose-and-actors',
22
+ 'user-processes',
23
+ 'domain-and-data',
24
+ 'architecture-and-integrations',
25
+ 'security-and-trust',
26
+ 'operations-and-assurance',
27
+ 'code-quality'
28
+ ];
29
+ const baselinePersonaIds = ['ewai.core.archaeologist', 'ewai.core.specs-knowledge-curator'];
30
+
31
+ function inside(parent, child) {
32
+ const path = relative(parent, child);
33
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
34
+ }
35
+
36
+ function parseYaml(path, label, errors) {
37
+ if (!existsSync(path)) {
38
+ errors.push(`Missing ${label}: ${path}`);
39
+ return null;
40
+ }
41
+ try {
42
+ return YAML.parse(readFileSync(path, 'utf8')) ?? {};
43
+ } catch (error) {
44
+ errors.push(`Invalid ${label}: ${error.message}`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ function substantive(value, minimum = 20) {
50
+ return typeof value === 'string' && value.trim().length >= minimum;
51
+ }
52
+
53
+ function isoTimestamp(value) {
54
+ return typeof value === 'string'
55
+ && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
56
+ && !Number.isNaN(Date.parse(value));
57
+ }
58
+
59
+ function archaeologyBundleRoot(projectRoot, bundlePath) {
60
+ const paths = projectPaths(projectRoot);
61
+ const archaeologyRoot = resolve(paths.specsRoot, '3.Evidence/archaeology');
62
+ const bundleRoot = resolve(paths.projectRoot, bundlePath);
63
+ if (!inside(archaeologyRoot, bundleRoot)) throw new Error('Archaeology bundle must be under SPECS/3.Evidence/archaeology');
64
+ return bundleRoot;
65
+ }
66
+
67
+ function personaRoutingErrors(routing) {
68
+ const errors = [];
69
+ if (!routing || routing.schema !== 'ewai.archaeology-persona-routing/v1') {
70
+ return ['Persona routing must use schema ewai.archaeology-persona-routing/v1'];
71
+ }
72
+
73
+ const inventory = Array.isArray(routing.inventory) ? routing.inventory : [];
74
+ const personaIds = new Set(inventory.map((persona) => String(persona?.id ?? '').trim()).filter(Boolean));
75
+ if (!inventory.length) errors.push('Persona routing requires an installed persona inventory');
76
+ for (const baseline of baselinePersonaIds) {
77
+ if (!personaIds.has(baseline)) errors.push(`Persona routing inventory is missing baseline persona ${baseline}`);
78
+ }
79
+
80
+ if (routing.reconnaissance?.status !== 'complete') errors.push('Persona routing requires completed initial reconnaissance');
81
+ if (!substantive(routing.reconnaissance?.summary)) errors.push('Persona routing reconnaissance requires a substantive summary');
82
+ if (!Array.isArray(routing.reconnaissance?.evidence) || !routing.reconnaissance.evidence.length) {
83
+ errors.push('Persona routing reconnaissance requires evidence references');
84
+ }
85
+
86
+ const assessments = Array.isArray(routing.assessments) ? routing.assessments : [];
87
+ const assessmentsByPass = new Map(assessments.map((assessment) => [assessment?.pass, assessment]));
88
+ for (const pass of personaPasses) {
89
+ const assessment = assessmentsByPass.get(pass);
90
+ if (!assessment) {
91
+ errors.push(`Persona routing is missing assessment pass ${pass}`);
92
+ continue;
93
+ }
94
+ if (assessment.status !== 'assessed') errors.push(`Persona routing pass ${pass} has not been assessed`);
95
+ if (!substantive(assessment.rationale)) errors.push(`Persona routing pass ${pass} requires a substantive rationale`);
96
+ if (!substantive(assessment.expected_benefit)) errors.push(`Persona routing pass ${pass} requires an expected-benefit statement`);
97
+ const recommendedPersonas = Array.isArray(assessment.recommended_personas) ? assessment.recommended_personas : [];
98
+ if (!Array.isArray(assessment.recommended_personas)) errors.push(`Persona routing pass ${pass} requires a recommended-personas list`);
99
+ for (const persona of recommendedPersonas) {
100
+ if (!personaIds.has(persona)) errors.push(`Persona routing pass ${pass} references unavailable persona ${persona}`);
101
+ }
102
+ }
103
+
104
+ const review = routing.user_review ?? {};
105
+ if (review.presented_to_user !== true) errors.push('Persona recommendations must be presented to the user before deep Archaeology');
106
+ if (!substantive(review.presented_summary)) errors.push('Persona routing requires the concise recommendation summary shown to the user');
107
+ if (!['confirmed', 'declined'].includes(review.status)) errors.push('Persona routing requires a confirmed or declined user decision');
108
+ if (!substantive(review.decided_by, 2)) errors.push('Persona routing requires an accountable decision maker');
109
+ if (!review.decided_at) errors.push('Persona routing requires a decision timestamp');
110
+ else if (!isoTimestamp(review.decided_at)) errors.push('Persona routing decision timestamp must be ISO-8601');
111
+ if (review.status === 'declined' && !substantive(review.notes)) errors.push('A declined persona ensemble requires explanatory notes');
112
+
113
+ const selected = Array.isArray(review.selected_personas) ? review.selected_personas : [];
114
+ if (review.status === 'confirmed' && !selected.some((persona) => !baselinePersonaIds.includes(persona))) {
115
+ errors.push('A confirmed persona ensemble must select at least one non-baseline advisory persona');
116
+ }
117
+ if (review.status === 'declined' && selected.length) errors.push('A declined persona ensemble cannot contain selected personas');
118
+ for (const persona of selected) {
119
+ if (!personaIds.has(persona)) errors.push(`Persona routing selects unavailable persona ${persona}`);
120
+ }
121
+
122
+ const assignments = Array.isArray(routing.assignments) ? routing.assignments : [];
123
+ if (review.status === 'declined' && assignments.length) errors.push('A declined persona ensemble cannot contain persona assignments');
124
+ const allowedAssignments = new Set([...baselinePersonaIds, ...selected]);
125
+ for (const assignment of assignments) {
126
+ if (!personaIds.has(assignment?.persona)) errors.push(`Persona assignment references unavailable persona ${assignment?.persona || '(missing)'}`);
127
+ if (!allowedAssignments.has(assignment?.persona)) errors.push(`Persona assignment uses unconfirmed persona ${assignment?.persona || '(missing)'}`);
128
+ if (!personaPasses.includes(assignment?.pass)) errors.push(`Persona assignment has unknown pass ${assignment?.pass || '(missing)'}`);
129
+ if (!substantive(assignment?.contribution)) errors.push(`Persona assignment for ${assignment?.persona || '(missing)'} requires an expected contribution`);
130
+ }
131
+ if (review.status === 'confirmed') {
132
+ const assigned = new Set(assignments.map((assignment) => assignment?.persona));
133
+ for (const persona of selected) {
134
+ if (!assigned.has(persona)) errors.push(`Selected persona ${persona} has no analysis-pass assignment`);
135
+ }
136
+ }
137
+ return errors;
138
+ }
139
+
140
+ export function prepareArchaeologyPersonaGate(projectRoot, bundlePath, personas, options = {}) {
141
+ const bundleRoot = archaeologyBundleRoot(projectRoot, bundlePath);
142
+ const routingPath = resolve(bundleRoot, 'persona-routing.yaml');
143
+ if (existsSync(routingPath) && !options.force) {
144
+ throw new Error('Archaeology persona routing already exists; preserve the user decision or use an explicitly approved force refresh');
145
+ }
146
+
147
+ const unique = new Map();
148
+ for (const persona of personas ?? []) {
149
+ const id = String(persona?.id ?? '').trim();
150
+ if (!id) continue;
151
+ unique.set(id, {
152
+ id,
153
+ name: persona.name || id,
154
+ description: persona.description || '',
155
+ category: persona.category || '',
156
+ tier: persona.tier || '',
157
+ tags: Array.isArray(persona.tags) ? persona.tags : [],
158
+ capabilities: Array.isArray(persona.capabilities) ? persona.capabilities : []
159
+ });
160
+ }
161
+ const inventory = [...unique.values()].sort((left, right) => left.name.localeCompare(right.name));
162
+ const countsByTier = {};
163
+ for (const persona of inventory) countsByTier[persona.tier || 'unspecified'] = (countsByTier[persona.tier || 'unspecified'] ?? 0) + 1;
164
+
165
+ const routing = {
166
+ schema: 'ewai.archaeology-persona-routing/v1',
167
+ status: 'awaiting-assessment',
168
+ library_summary: { total: inventory.length, by_tier: countsByTier },
169
+ baseline_personas: baselinePersonaIds,
170
+ inventory,
171
+ reconnaissance: { status: 'pending', summary: '', evidence: [] },
172
+ assessments: personaPasses.map((pass) => ({
173
+ pass,
174
+ status: 'pending',
175
+ recommended_personas: [],
176
+ rationale: '',
177
+ expected_benefit: '',
178
+ perspective_gaps: []
179
+ })),
180
+ assignments: [],
181
+ user_review: {
182
+ presented_to_user: false,
183
+ presented_summary: '',
184
+ status: 'pending',
185
+ selected_personas: [],
186
+ decided_by: null,
187
+ decided_at: null,
188
+ notes: ''
189
+ }
190
+ };
191
+ mkdirSync(bundleRoot, { recursive: true });
192
+ writeFileSync(routingPath, YAML.stringify(routing, { lineWidth: 0 }), 'utf8');
193
+ return {
194
+ schema: 'ewai.archaeology-persona-preparation/v1',
195
+ bundleRoot,
196
+ routingPath,
197
+ personaCount: inventory.length,
198
+ passes: personaPasses
199
+ };
200
+ }
201
+
202
+ export function validateArchaeologyPersonaGate(projectRoot, bundlePath) {
203
+ let bundleRoot;
204
+ try {
205
+ bundleRoot = archaeologyBundleRoot(projectRoot, bundlePath);
206
+ } catch (error) {
207
+ return { schema: 'ewai.archaeology-persona-validation/v1', valid: false, errors: [error.message] };
208
+ }
209
+ const errors = [];
210
+ const routing = parseYaml(resolve(bundleRoot, 'persona-routing.yaml'), 'persona routing', errors);
211
+ if (routing) errors.push(...personaRoutingErrors(routing));
212
+ return {
213
+ schema: 'ewai.archaeology-persona-validation/v1',
214
+ valid: errors.length === 0,
215
+ bundleRoot,
216
+ errors
217
+ };
218
+ }
219
+
220
+ export function validateArchaeologyBundle(projectRoot, bundlePath, options = {}) {
221
+ const errors = [];
222
+ const warnings = [];
223
+ const paths = projectPaths(projectRoot);
224
+ const archaeologyRoot = resolve(paths.specsRoot, '3.Evidence/archaeology');
225
+ const bundleRoot = resolve(paths.projectRoot, bundlePath);
226
+ if (!inside(archaeologyRoot, bundleRoot)) {
227
+ return {
228
+ schema: 'ewai.archaeology-validation/v1',
229
+ valid: false,
230
+ errors: ['Archaeology bundle must be under SPECS/3.Evidence/archaeology'],
231
+ warnings,
232
+ bundleRoot
233
+ };
234
+ }
235
+
236
+ const families = parseYaml(options.familiesPath ?? defaultFamiliesPath, 'record-family contract', errors);
237
+ const capabilityCatalog = parseYaml(resolve(bundleRoot, 'capability-catalog.yaml'), 'capability catalog', errors);
238
+ const coverageLedger = parseYaml(resolve(bundleRoot, 'coverage-ledger.yaml'), 'coverage ledger', errors);
239
+ const reconstructionLedger = parseYaml(resolve(bundleRoot, 'specs-reconstruction-ledger.yaml'), 'SPECS reconstruction ledger', errors);
240
+ const manifest = parseYaml(resolve(bundleRoot, 'archaeology-artifact-manifest.yaml'), 'archaeology artefact manifest', errors);
241
+ const personaRouting = parseYaml(resolve(bundleRoot, 'persona-routing.yaml'), 'persona routing', errors);
242
+
243
+ if (!families || !capabilityCatalog || !coverageLedger || !reconstructionLedger || !manifest || !personaRouting) {
244
+ return {
245
+ schema: 'ewai.archaeology-validation/v1',
246
+ valid: false,
247
+ errors,
248
+ warnings,
249
+ bundleRoot
250
+ };
251
+ }
252
+ errors.push(...personaRoutingErrors(personaRouting));
253
+
254
+ if (manifest.depth !== families.depth) {
255
+ errors.push(`Manifest depth must be ${families.depth}`);
256
+ }
257
+ if (capabilityCatalog.schema !== 'ewai.archaeology-capability-catalog/v1') {
258
+ errors.push('Capability catalog must use schema ewai.archaeology-capability-catalog/v1');
259
+ }
260
+ if (coverageLedger.schema !== 'ewai.archaeology-coverage-ledger/v1') {
261
+ errors.push('Coverage ledger must use schema ewai.archaeology-coverage-ledger/v1');
262
+ }
263
+ if (reconstructionLedger.schema !== 'ewai.specs-reconstruction-ledger/v1') {
264
+ errors.push('SPECS reconstruction ledger must use schema ewai.specs-reconstruction-ledger/v1');
265
+ }
266
+ if (manifest.schema !== 'ewai.archaeology-artifact-manifest/v1') {
267
+ errors.push('Artefact manifest must use schema ewai.archaeology-artifact-manifest/v1');
268
+ }
269
+ const coveredSurfaces = Array.isArray(coverageLedger.surfaces) ? coverageLedger.surfaces : [];
270
+ const coverageStatuses = new Set(['mapped', 'partial', 'blocked', 'not-applicable']);
271
+ if (!coveredSurfaces.length) errors.push('Coverage ledger must contain material surfaces');
272
+ for (const surface of coveredSurfaces) {
273
+ if (!surface?.id || !surface?.surface) errors.push('Every coverage surface requires an id and name');
274
+ if (surface?.status === 'unexamined') errors.push(`Coverage surface ${surface.id || '(unknown)'} remains unexamined`);
275
+ else if (!coverageStatuses.has(surface?.status)) errors.push(`Coverage surface ${surface.id || '(unknown)'} has invalid status ${surface?.status || '(missing)'}`);
276
+ if (surface?.status === 'partial') {
277
+ if (!substantive(surface?.limitation)) errors.push(`Partial coverage surface ${surface.id || '(unknown)'} requires a substantive limitation`);
278
+ if (!substantive(surface?.review_owner, 3)) errors.push(`Partial coverage surface ${surface.id || '(unknown)'} requires a review owner`);
279
+ if (surface?.review_status !== 'accepted') errors.push(`Partial coverage surface ${surface.id || '(unknown)'} requires an accepted limitation`);
280
+ }
281
+ if (surface?.status === 'blocked') {
282
+ if (!substantive(surface?.rationale)) errors.push(`Blocked coverage surface ${surface.id || '(unknown)'} requires a substantive rationale`);
283
+ if (!substantive(surface?.review_owner, 3)) errors.push(`Blocked coverage surface ${surface.id || '(unknown)'} requires a review owner`);
284
+ if (!substantive(surface?.next_action)) errors.push(`Blocked coverage surface ${surface.id || '(unknown)'} requires a concrete next action`);
285
+ }
286
+ if (surface?.status === 'not-applicable') {
287
+ if (!substantive(surface?.rationale)) errors.push(`Not-applicable coverage surface ${surface.id || '(unknown)'} requires a substantive rationale`);
288
+ if (!Array.isArray(surface?.investigation_refs) || !surface.investigation_refs.length) {
289
+ errors.push(`Not-applicable coverage surface ${surface.id || '(unknown)'} requires investigation references`);
290
+ }
291
+ }
292
+ }
293
+ if (!Array.isArray(reconstructionLedger.records) || !reconstructionLedger.records.length) {
294
+ errors.push('SPECS reconstruction ledger must contain reconstruction records');
295
+ }
296
+ const capabilities = Array.isArray(capabilityCatalog.capabilities) ? capabilityCatalog.capabilities : [];
297
+ if (!capabilities.length) errors.push('Capability catalog must contain at least one material capability');
298
+ const capabilityIds = new Set();
299
+ for (const capability of capabilities) {
300
+ const id = String(capability?.id ?? '').trim();
301
+ if (!id) {
302
+ errors.push('Every capability requires an id');
303
+ continue;
304
+ }
305
+ if (capabilityIds.has(id)) errors.push(`Duplicate capability id: ${id}`);
306
+ capabilityIds.add(id);
307
+ if (!substantive(capability?.description, 30)) errors.push(`Capability ${id} requires a substantive description`);
308
+ if (!Array.isArray(capability?.evidence) || !capability.evidence.length) errors.push(`Capability ${id} requires evidence references`);
309
+ }
310
+
311
+ const records = Array.isArray(manifest.records) ? manifest.records : [];
312
+ if (!records.length) errors.push('Artefact manifest must contain detailed record entries');
313
+ const coverage = new Map();
314
+ const proposedPaths = new Set();
315
+ const recordIds = new Set();
316
+ let created = 0;
317
+ let notApplicable = 0;
318
+ let blocked = 0;
319
+
320
+ for (const [index, record] of records.entries()) {
321
+ const prefix = `Record ${record?.id || index + 1}`;
322
+ const recordId = String(record?.id ?? '').trim();
323
+ const subject = String(record?.subject ?? '').trim();
324
+ const family = String(record?.family ?? '').trim();
325
+ const status = String(record?.status ?? '').trim();
326
+ if (!recordId) errors.push(`${prefix} requires an id`);
327
+ else if (recordIds.has(recordId)) errors.push(`Duplicate artefact record id: ${recordId}`);
328
+ else recordIds.add(recordId);
329
+ if (!subject || !family) {
330
+ errors.push(`${prefix} requires subject and family`);
331
+ continue;
332
+ }
333
+ const key = `${subject}|${family}`;
334
+ coverage.set(key, (coverage.get(key) ?? 0) + 1);
335
+ if (!terminalStatuses.has(status)) {
336
+ errors.push(`${prefix} has non-terminal status ${status || '(missing)'}`);
337
+ continue;
338
+ }
339
+
340
+ if (status === 'created') {
341
+ created += 1;
342
+ if (!substantive(record?.record_type, 3)) errors.push(`${prefix} requires a record type`);
343
+ if (!substantive(record?.title, 5)) errors.push(`${prefix} requires a substantive title`);
344
+ if (!substantive(record?.review_owner, 3)) errors.push(`${prefix} requires a review owner`);
345
+ const proposedPath = String(record?.proposed_path ?? '').trim();
346
+ if (!proposedPath.startsWith('proposals/SPECS/')) {
347
+ errors.push(`${prefix} must point to an individual file under proposals/SPECS/`);
348
+ continue;
349
+ }
350
+ if (proposedPaths.has(proposedPath)) errors.push(`${prefix} reuses proposed path ${proposedPath}; detailed records require individual files`);
351
+ proposedPaths.add(proposedPath);
352
+ const artefactPath = resolve(bundleRoot, proposedPath);
353
+ if (!inside(resolve(bundleRoot, 'proposals/SPECS'), artefactPath)) {
354
+ errors.push(`${prefix} escapes the proposals/SPECS boundary`);
355
+ } else if (!existsSync(artefactPath) || !lstatSync(artefactPath).isFile()) {
356
+ errors.push(`${prefix} is missing proposed artefact ${proposedPath}`);
357
+ } else if (statSync(artefactPath).size < (options.minimumArtefactBytes ?? 200)) {
358
+ errors.push(`${prefix} proposed artefact is too small to be a detailed reconstruction: ${proposedPath}`);
359
+ }
360
+ if (!Array.isArray(record?.evidence) || !record.evidence.length) errors.push(`${prefix} requires evidence references`);
361
+ }
362
+
363
+ if (status === 'not-applicable') {
364
+ notApplicable += 1;
365
+ if (!substantive(record?.rationale)) errors.push(`${prefix} requires a substantive not-applicable rationale`);
366
+ if (!Array.isArray(record?.investigation_refs) || !record.investigation_refs.length) {
367
+ errors.push(`${prefix} requires investigation references supporting not-applicable status`);
368
+ }
369
+ }
370
+
371
+ if (status === 'blocked') {
372
+ blocked += 1;
373
+ if (!substantive(record?.rationale)) errors.push(`${prefix} requires a substantive blocked rationale`);
374
+ if (!substantive(record?.review_owner, 3)) errors.push(`${prefix} requires a review owner`);
375
+ if (!substantive(record?.next_action)) errors.push(`${prefix} requires a concrete next action`);
376
+ }
377
+ }
378
+
379
+ for (const capabilityId of capabilityIds) {
380
+ for (const family of families.capability_families ?? []) {
381
+ if (!coverage.has(`${capabilityId}|${family}`)) errors.push(`Capability ${capabilityId} is missing record family ${family}`);
382
+ }
383
+ }
384
+ for (const family of families.project_families ?? []) {
385
+ if (!coverage.has(`project|${family}`)) errors.push(`Project reconstruction is missing record family ${family}`);
386
+ }
387
+
388
+ const knownFamilies = new Set([...(families.capability_families ?? []), ...(families.project_families ?? [])]);
389
+ for (const record of records) {
390
+ if (record?.family && !knownFamilies.has(record.family)) warnings.push(`Additional record family: ${record.family}`);
391
+ if (record?.subject && record.subject !== 'project' && !capabilityIds.has(record.subject)) {
392
+ errors.push(`Record ${record.id || '(unknown)'} references unknown capability ${record.subject}`);
393
+ }
394
+ }
395
+
396
+ return {
397
+ schema: 'ewai.archaeology-validation/v1',
398
+ valid: errors.length === 0,
399
+ depth: manifest.depth,
400
+ bundleRoot,
401
+ counts: {
402
+ capabilities: capabilityIds.size,
403
+ records: records.length,
404
+ created,
405
+ notApplicable,
406
+ blocked
407
+ },
408
+ errors,
409
+ warnings
410
+ };
411
+ }
412
+
413
+ function validatedBundle(projectRoot, bundlePath, options) {
414
+ const validation = validateArchaeologyBundle(projectRoot, bundlePath, options);
415
+ if (!validation.valid) {
416
+ throw new Error(`Archaeology bundle is not ready for review:\n${validation.errors.slice(0, 20).join('\n')}`);
417
+ }
418
+ return validation;
419
+ }
420
+
421
+ function canonicalRelative(proposedPath) {
422
+ return proposedPath.slice('proposals/SPECS/'.length);
423
+ }
424
+
425
+ function fileHash(path) {
426
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
427
+ }
428
+
429
+ export function prepareArchaeologyReview(projectRoot, bundlePath, options = {}) {
430
+ const validation = validatedBundle(projectRoot, bundlePath, options);
431
+ const manifestPath = resolve(validation.bundleRoot, 'archaeology-artifact-manifest.yaml');
432
+ const manifest = YAML.parse(readFileSync(manifestPath, 'utf8'));
433
+ const decisionsPath = resolve(validation.bundleRoot, 'review-decisions.yaml');
434
+ const guidePath = resolve(validation.bundleRoot, 'review-guide.md');
435
+ const questionPlanPath = resolve(validation.bundleRoot, 'review-question-plan.yaml');
436
+ if ((existsSync(decisionsPath) || existsSync(guidePath) || existsSync(questionPlanPath)) && !options.force) {
437
+ throw new Error('Archaeology review files already exist; preserve existing decisions or use an explicitly approved force refresh');
438
+ }
439
+
440
+ const records = manifest.records.map((record) => ({
441
+ id: record.id,
442
+ subject: record.subject,
443
+ family: record.family,
444
+ title: record.title || record.record_type || record.family,
445
+ proposed_path: record.proposed_path || null,
446
+ canonical_path: record.proposed_path ? `SPECS/${canonicalRelative(record.proposed_path)}` : null,
447
+ reconstruction_status: record.status,
448
+ decision: 'pending',
449
+ notes: '',
450
+ corrections_applied: false,
451
+ reviewed_by: null,
452
+ reviewed_at: null
453
+ }));
454
+ writeFileSync(decisionsPath, YAML.stringify({
455
+ schema: 'ewai.archaeology-review-decisions/v1',
456
+ mode: 'undecided',
457
+ records
458
+ }, { lineWidth: 0 }), 'utf8');
459
+ writeFileSync(questionPlanPath, YAML.stringify({
460
+ schema: 'ewai.archaeology-review-question-plan/v1',
461
+ purpose: 'Minimise human review effort by asking evidence-backed questions whose answers apply to clearly named groups of records.',
462
+ questions: []
463
+ }, { lineWidth: 0 }), 'utf8');
464
+
465
+ const created = records.filter((record) => record.reconstruction_status === 'created');
466
+ const rows = records.map((record) => `| ${record.id} | ${record.subject} | ${record.family} | ${record.title} | ${record.reconstruction_status} | ${record.canonical_path ?? 'Evidence-backed exception'} | Pending |`).join('\n');
467
+ writeFileSync(guidePath, `# Archaeology review guide\n\nThe maximum-detail investigation passed validation. Choose either self-review or an AI-guided walkthrough. For a guided review, first populate \`review-question-plan.yaml\` with the smallest useful set of cross-record questions. Each question must identify the evidence, decision sought, and every affected record; a clarification must not be treated as approval. Record accept, accept with corrections, reject, or defer decisions in \`review-decisions.yaml\`. Review created records and evidence-backed exceptions. Nothing is filed into canonical SPECS without explicit approval.\n\n## Detailed reconstruction inventory\n\n| ID | Subject | Family | Record | Reconstruction | Canonical destination | Decision |\n|---|---|---|---|---|---|---|\n${rows}\n`, 'utf8');
468
+
469
+ return {
470
+ schema: 'ewai.archaeology-review-preparation/v1',
471
+ bundleRoot: validation.bundleRoot,
472
+ guidePath,
473
+ decisionsPath,
474
+ questionPlanPath,
475
+ counts: { total: records.length, created: created.length }
476
+ };
477
+ }
478
+
479
+ export function curateArchaeologyBundle(projectRoot, bundlePath, options = {}) {
480
+ if (!options.confirmed) throw new Error('Automatic Archaeology filing requires explicit user confirmation');
481
+ const validation = validatedBundle(projectRoot, bundlePath, options);
482
+ const paths = projectPaths(projectRoot);
483
+ const manifestPath = resolve(validation.bundleRoot, 'archaeology-artifact-manifest.yaml');
484
+ const manifest = YAML.parse(readFileSync(manifestPath, 'utf8'));
485
+ const decisionsPath = resolve(validation.bundleRoot, 'review-decisions.yaml');
486
+ const review = parseYaml(decisionsPath, 'review decisions', []);
487
+ if (!review || review.schema !== 'ewai.archaeology-review-decisions/v1') {
488
+ throw new Error('Review decisions must use schema ewai.archaeology-review-decisions/v1');
489
+ }
490
+ const decisions = new Map((review.records ?? []).map((record) => [record.id, record]));
491
+ const createdRecords = manifest.records.filter((record) => record.status === 'created');
492
+ const pending = manifest.records.filter((record) => !reviewDecisions.has(decisions.get(record.id)?.decision));
493
+ if (pending.length) throw new Error(`Review decisions remain pending for ${pending.length} reconstruction record(s)`);
494
+ const unowned = manifest.records.filter((record) => !substantive(decisions.get(record.id)?.reviewed_by, 2));
495
+ if (unowned.length) throw new Error(`Review decisions require an accountable reviewer for ${unowned.length} reconstruction record(s)`);
496
+ const unexplained = manifest.records.filter((record) => {
497
+ const decision = decisions.get(record.id);
498
+ return decision?.decision !== 'accepted' && !substantive(decision?.notes);
499
+ });
500
+ if (unexplained.length) {
501
+ throw new Error(`Corrected, rejected, or deferred decisions require explanatory notes for ${unexplained.length} reconstruction record(s)`);
502
+ }
503
+ const unappliedCorrections = manifest.records.filter((record) => {
504
+ const decision = decisions.get(record.id);
505
+ return decision?.decision === 'accepted-with-corrections' && decision?.corrections_applied !== true;
506
+ });
507
+ if (unappliedCorrections.length) {
508
+ throw new Error(`Accepted corrections must be applied to ${unappliedCorrections.length} proposed record(s) before filing`);
509
+ }
510
+
511
+ const accepted = createdRecords.filter((record) => {
512
+ const decision = decisions.get(record.id)?.decision;
513
+ return decision === 'accepted' || decision === 'accepted-with-corrections';
514
+ });
515
+ const actions = accepted.map((record) => {
516
+ const source = resolve(validation.bundleRoot, record.proposed_path);
517
+ const canonicalPath = canonicalRelative(record.proposed_path);
518
+ const destination = resolve(paths.specsRoot, canonicalPath);
519
+ if (!inside(paths.specsRoot, destination)) throw new Error(`Canonical destination escapes SPECS: ${canonicalPath}`);
520
+ if (lstatSync(source).isSymbolicLink()) throw new Error(`Symlinked Archaeology proposals cannot be filed: ${record.proposed_path}`);
521
+ if (existsSync(destination) && lstatSync(destination).isSymbolicLink()) {
522
+ throw new Error(`Symlinked canonical SPECS destinations require manual reconciliation: SPECS/${canonicalPath}`);
523
+ }
524
+ const sourceHash = fileHash(source);
525
+ const destinationHash = existsSync(destination) && statSync(destination).isFile() ? fileHash(destination) : null;
526
+ return {
527
+ record,
528
+ decision: decisions.get(record.id),
529
+ source,
530
+ sourceHash,
531
+ destination,
532
+ canonicalPath: `SPECS/${canonicalPath}`,
533
+ destinationHash,
534
+ action: destinationHash === sourceHash ? 'already-current' : destinationHash ? 'conflict' : 'copy'
535
+ };
536
+ });
537
+ const conflicts = actions.filter((action) => action.action === 'conflict');
538
+ if (conflicts.length) {
539
+ throw new Error(`Canonical SPECS conflicts require reconciliation before automatic filing:\n${conflicts.map((item) => item.canonicalPath).join('\n')}`);
540
+ }
541
+
542
+ const filedAt = new Date(options.now ?? Date.now()).toISOString();
543
+ for (const record of manifest.records) {
544
+ const decision = decisions.get(record.id);
545
+ record.review_status = decision.decision;
546
+ record.reviewed_by = decision.reviewed_by;
547
+ record.reviewed_at = decision.reviewed_at || filedAt;
548
+ if (decision.decision === 'rejected' || decision.decision === 'deferred' || record.status !== 'created') {
549
+ record.curation_status = 'retained-in-bundle';
550
+ }
551
+ }
552
+ for (const action of actions) {
553
+ if (action.action === 'copy') {
554
+ mkdirSync(dirname(action.destination), { recursive: true });
555
+ cpSync(action.source, action.destination, { errorOnExist: true, force: false });
556
+ }
557
+ action.record.canonical_path = action.canonicalPath;
558
+ action.record.curation_status = action.action === 'copy' ? 'filed' : 'already-current';
559
+ }
560
+ writeFileSync(manifestPath, YAML.stringify(manifest, { lineWidth: 0 }), 'utf8');
561
+
562
+ const ledgerPath = resolve(validation.bundleRoot, 'curation-ledger.yaml');
563
+ const actionById = new Map(actions.map((action) => [action.record.id, action]));
564
+ writeFileSync(ledgerPath, YAML.stringify({
565
+ schema: 'ewai.archaeology-curation-ledger/v1',
566
+ filed_at: filedAt,
567
+ approved_by: options.approvedBy || 'Project owner',
568
+ records: manifest.records.map((record) => {
569
+ const decision = decisions.get(record.id);
570
+ const action = actionById.get(record.id);
571
+ return {
572
+ id: record.id,
573
+ reconstruction_status: record.status,
574
+ decision: decision.decision,
575
+ reviewed_by: decision.reviewed_by,
576
+ reviewed_at: decision.reviewed_at || filedAt,
577
+ notes: decision.notes || null,
578
+ source: record.proposed_path || null,
579
+ destination: action ? action.canonicalPath : null,
580
+ sha256: action ? action.sourceHash : null,
581
+ status: action ? record.curation_status : 'retained-in-bundle'
582
+ };
583
+ })
584
+ }, { lineWidth: 0 }), 'utf8');
585
+
586
+ return {
587
+ schema: 'ewai.archaeology-curation-result/v1',
588
+ bundleRoot: validation.bundleRoot,
589
+ ledgerPath,
590
+ filed: actions.filter((action) => action.action === 'copy').length,
591
+ alreadyCurrent: actions.filter((action) => action.action === 'already-current').length,
592
+ rejected: manifest.records.filter((record) => decisions.get(record.id)?.decision === 'rejected').length,
593
+ deferred: manifest.records.filter((record) => decisions.get(record.id)?.decision === 'deferred').length
594
+ };
595
+ }
596
+
597
+ export function validateArchaeologyCompletion(projectRoot, bundlePath, options = {}) {
598
+ const reconstruction = validateArchaeologyBundle(projectRoot, bundlePath, options);
599
+ const errors = reconstruction.valid ? [] : reconstruction.errors.map((error) => `Reconstruction: ${error}`);
600
+ const paths = projectPaths(projectRoot);
601
+ const bundleRoot = reconstruction.bundleRoot ?? archaeologyBundleRoot(projectRoot, bundlePath);
602
+ const manifest = parseYaml(resolve(bundleRoot, 'archaeology-artifact-manifest.yaml'), 'archaeology artefact manifest', errors);
603
+ const review = parseYaml(resolve(bundleRoot, 'review-decisions.yaml'), 'review decisions', errors);
604
+ const curation = parseYaml(resolve(bundleRoot, 'curation-ledger.yaml'), 'curation ledger', errors);
605
+ const transition = parseYaml(resolve(bundleRoot, 'future-work-transition.yaml'), 'future-work transition', errors);
606
+
607
+ if (review && review.schema !== 'ewai.archaeology-review-decisions/v1') {
608
+ errors.push('Review decisions must use schema ewai.archaeology-review-decisions/v1');
609
+ }
610
+ if (curation && curation.schema !== 'ewai.archaeology-curation-ledger/v1') {
611
+ errors.push('Curation ledger must use schema ewai.archaeology-curation-ledger/v1');
612
+ }
613
+
614
+ const manifestRecords = Array.isArray(manifest?.records) ? manifest.records : [];
615
+ const reviewById = new Map((review?.records ?? []).map((record) => [record.id, record]));
616
+ const curationById = new Map((curation?.records ?? []).map((record) => [record.id, record]));
617
+ for (const record of manifestRecords) {
618
+ const decision = reviewById.get(record.id)?.decision;
619
+ if (!reviewDecisions.has(decision)) {
620
+ errors.push(`Completion requires a terminal review decision for ${record.id || '(unknown)'}`);
621
+ continue;
622
+ }
623
+ const curated = curationById.get(record.id);
624
+ if (!curated) {
625
+ errors.push(`Curation ledger is missing reconstruction record ${record.id || '(unknown)'}`);
626
+ continue;
627
+ }
628
+ if (record.status !== 'created' || decision === 'rejected' || decision === 'deferred') {
629
+ if (curated.status !== 'retained-in-bundle') errors.push(`Non-promoted record ${record.id} must remain traceable in the Archaeology bundle`);
630
+ continue;
631
+ }
632
+
633
+ if (!['filed', 'already-current'].includes(curated.status)) {
634
+ errors.push(`Accepted record ${record.id} has not been promoted into canonical SPECS`);
635
+ continue;
636
+ }
637
+ const destination = String(curated.destination ?? '');
638
+ if (!destination.startsWith('SPECS/')) {
639
+ errors.push(`Accepted record ${record.id} has an invalid canonical destination`);
640
+ continue;
641
+ }
642
+ const canonicalPath = resolve(paths.specsRoot, destination.slice('SPECS/'.length));
643
+ if (!inside(paths.specsRoot, canonicalPath) || !existsSync(canonicalPath) || lstatSync(canonicalPath).isSymbolicLink() || !statSync(canonicalPath).isFile()) {
644
+ errors.push(`Accepted record ${record.id} is missing from canonical SPECS at ${destination}`);
645
+ continue;
646
+ }
647
+ if (!substantive(curated.sha256, 64) || fileHash(canonicalPath) !== curated.sha256) {
648
+ errors.push(`Accepted record ${record.id} no longer matches its curated canonical evidence`);
649
+ }
650
+ }
651
+
652
+ const requiredRoutes = ['interview', 'import', 'recommendations'];
653
+ if (transition) {
654
+ if (transition.schema !== 'ewai.archaeology-future-work-transition/v1') {
655
+ errors.push('Future-work transition must use schema ewai.archaeology-future-work-transition/v1');
656
+ }
657
+ if (transition.offered_to_user !== true) errors.push('The prospective-work routes must be explicitly offered to the user');
658
+ if (!substantive(transition.offer_summary)) errors.push('Future-work transition requires the offer shown to the user');
659
+ const routes = Array.isArray(transition.routes_offered) ? transition.routes_offered : [];
660
+ for (const route of requiredRoutes) {
661
+ if (!routes.includes(route)) errors.push(`Future-work transition did not offer the ${route} route`);
662
+ }
663
+ if (!isoTimestamp(transition.offered_at)) errors.push('Future-work offer requires an ISO-8601 timestamp');
664
+ if (!['interview', 'import', 'recommendations', 'declined'].includes(transition.decision)) {
665
+ errors.push('Future-work transition requires a selected route or explicit decline');
666
+ }
667
+ if (!substantive(transition.decided_by, 2)) errors.push('Future-work transition requires an accountable decision maker');
668
+ if (!isoTimestamp(transition.decided_at)) errors.push('Future-work decision requires an ISO-8601 timestamp');
669
+ }
670
+
671
+ return {
672
+ schema: 'ewai.archaeology-completion-validation/v1',
673
+ valid: errors.length === 0,
674
+ bundleRoot,
675
+ counts: {
676
+ reconstructionRecords: manifestRecords.length,
677
+ canonicalRecords: [...curationById.values()].filter((record) => ['filed', 'already-current'].includes(record.status)).length
678
+ },
679
+ errors
680
+ };
681
+ }
682
+
683
+ const technologyHostingSchemas = Object.freeze({
684
+ preparation: 'ewai.archaeology-technology-hosting-preparation/v1',
685
+ answers: 'ewai.archaeology-technology-hosting-answers/v1',
686
+ profile: 'ewai.archaeology-technology-hosting-profile/v1',
687
+ status: 'ewai.archaeology-technology-hosting-status/v1'
688
+ });
689
+
690
+ const deploymentModels = new Set([
691
+ 'cloud', 'on-premises', 'saas', 'paas', 'iaas', 'managed-service', 'hybrid', 'other', 'unknown'
692
+ ]);
693
+ const operatingModels = new Set([
694
+ 'self-managed', 'provider-managed', 'shared', 'third-party-managed', 'hybrid', 'unknown'
695
+ ]);
696
+ const observationReviewStatuses = new Set(['active', 'inactive', 'contradicted', 'uncertain']);
697
+ const technologyHostingSourceQueries = Object.freeze([
698
+ 'package.json', 'package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'composer.json',
699
+ 'pyproject.toml', 'requirements.txt', 'Pipfile', 'go.mod', 'Cargo.toml', 'Gemfile', 'pom.xml',
700
+ 'build.gradle', '.csproj', 'global.json', 'nuxt.config', 'next.config', 'angular.json', 'vite.config',
701
+ 'schema.prisma', 'Dockerfile', 'docker-compose', 'compose.yaml', 'compose.yml', '.github/workflows',
702
+ '.gitlab-ci', 'azure-pipelines', 'Jenkinsfile', '.tf', 'main.bicep', 'Chart.yaml', 'kustomization', 'deployment.yaml', 'deployment.yml',
703
+ 'azure.yaml', 'vercel.json', 'netlify.toml', 'serverless.yml', 'template.yaml', 'cdk.json',
704
+ 'amplify.yml', 'firebase.json', 'cloudbuild.yaml', 'app.yaml', 'fly.toml', 'render.yaml',
705
+ 'railway.json', 'Procfile', 'sfdx-project.json', 'PowerApps', '.msapp', 'openapi', 'swagger', 'schema.graphql'
706
+ ]);
707
+
708
+ const technologyHostingSignalRules = Object.freeze([
709
+ { test: /(^|\/)package\.json$/i, category: 'runtime', candidate: 'Node.js', detail: 'Node package manifest' },
710
+ { test: /(^|\/)package-lock\.json$/i, category: 'dependency-management', candidate: 'npm', detail: 'npm lockfile' },
711
+ { test: /(^|\/)pnpm-lock\.ya?ml$/i, category: 'dependency-management', candidate: 'pnpm', detail: 'pnpm lockfile' },
712
+ { test: /(^|\/)yarn\.lock$/i, category: 'dependency-management', candidate: 'Yarn', detail: 'Yarn lockfile' },
713
+ { test: /(^|\/)bun\.lockb?$/i, category: 'runtime', candidate: 'Bun', detail: 'Bun lockfile' },
714
+ { test: /(^|\/)composer\.(json|lock)$/i, category: 'dependency-management', candidate: 'Composer', detail: 'Composer project metadata' },
715
+ { test: /(^|\/)(composer\.json|artisan)$/i, category: 'runtime', candidate: 'PHP', detail: 'PHP project signal' },
716
+ { test: /(^|\/)(pyproject\.toml|requirements[^/]*\.txt|Pipfile)$/i, category: 'runtime', candidate: 'Python', detail: 'Python project metadata' },
717
+ { test: /(^|\/)go\.mod$/i, category: 'runtime', candidate: 'Go', detail: 'Go module manifest' },
718
+ { test: /(^|\/)Cargo\.toml$/i, category: 'runtime', candidate: 'Rust', detail: 'Cargo manifest' },
719
+ { test: /(^|\/)Gemfile$/i, category: 'runtime', candidate: 'Ruby', detail: 'Ruby dependency manifest' },
720
+ { test: /(^|\/)(pom\.xml|build\.gradle(?:\.kts)?)$/i, category: 'runtime', candidate: 'JVM', detail: 'JVM build metadata' },
721
+ { test: /\.csproj$/i, category: 'runtime', candidate: '.NET', detail: '.NET project manifest' },
722
+ { test: /(^|\/)nuxt\.config\.[^/]+$/i, category: 'framework', candidate: 'Nuxt', detail: 'Nuxt configuration' },
723
+ { test: /(^|\/)next\.config\.[^/]+$/i, category: 'framework', candidate: 'Next.js', detail: 'Next.js configuration' },
724
+ { test: /(^|\/)angular\.json$/i, category: 'framework', candidate: 'Angular', detail: 'Angular workspace configuration' },
725
+ { test: /(^|\/)vite\.config\.[^/]+$/i, category: 'build-tool', candidate: 'Vite', detail: 'Vite configuration' },
726
+ { test: /(^|\/)prisma\/schema\.prisma$/i, category: 'data-access', candidate: 'Prisma', detail: 'Prisma schema' },
727
+ { test: /(^|\/)Dockerfile(?:\.[^/]+)?$/i, category: 'container', candidate: 'Docker', detail: 'Docker build definition' },
728
+ { test: /(^|\/)(docker-compose[^/]*|compose)\.ya?ml$/i, category: 'container-orchestration', candidate: 'Docker Compose', detail: 'Compose topology' },
729
+ { test: /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/i, category: 'release-automation', candidate: 'GitHub Actions', detail: 'GitHub Actions workflow' },
730
+ { test: /(^|\/)\.gitlab-ci\.ya?ml$/i, category: 'release-automation', candidate: 'GitLab CI/CD', detail: 'GitLab pipeline' },
731
+ { test: /(^|\/)azure-pipelines\.ya?ml$/i, category: 'release-automation', candidate: 'Azure Pipelines', detail: 'Azure pipeline' },
732
+ { test: /(^|\/)Jenkinsfile$/i, category: 'release-automation', candidate: 'Jenkins', detail: 'Jenkins pipeline' },
733
+ { test: /\.tf(?:vars)?$/i, category: 'infrastructure-as-code', candidate: 'Terraform', detail: 'Terraform definition' },
734
+ { test: /\.bicep$/i, category: 'infrastructure-as-code', candidate: 'Azure Bicep', detail: 'Bicep definition' },
735
+ { test: /(^|\/)(Chart\.yaml|values\.ya?ml)$/i, category: 'container-orchestration', candidate: 'Helm', detail: 'Helm chart metadata' },
736
+ { test: /(^|\/)(kustomization\.ya?ml|[^/]*deployment\.ya?ml)$/i, category: 'container-orchestration', candidate: 'Kubernetes', detail: 'Kubernetes deployment metadata' },
737
+ { test: /(^|\/)azure\.ya?ml$/i, category: 'hosting-provider', candidate: 'Microsoft Azure', detail: 'Azure Developer CLI descriptor' },
738
+ { test: /(^|\/)vercel\.json$/i, category: 'hosting-provider', candidate: 'Vercel', detail: 'Vercel deployment descriptor' },
739
+ { test: /(^|\/)netlify\.toml$/i, category: 'hosting-provider', candidate: 'Netlify', detail: 'Netlify deployment descriptor' },
740
+ { test: /(^|\/)(serverless\.ya?ml|template\.ya?ml|cdk\.json|amplify\.ya?ml)$/i, category: 'hosting-provider', candidate: 'Amazon Web Services', detail: 'AWS deployment descriptor' },
741
+ { test: /(^|\/)(firebase\.json|cloudbuild\.ya?ml|app\.yaml)$/i, category: 'hosting-provider', candidate: 'Google Cloud', detail: 'Google Cloud deployment descriptor' },
742
+ { test: /(^|\/)fly\.toml$/i, category: 'hosting-provider', candidate: 'Fly.io', detail: 'Fly.io deployment descriptor' },
743
+ { test: /(^|\/)render\.ya?ml$/i, category: 'hosting-provider', candidate: 'Render', detail: 'Render deployment descriptor' },
744
+ { test: /(^|\/)railway\.json$/i, category: 'hosting-provider', candidate: 'Railway', detail: 'Railway deployment descriptor' },
745
+ { test: /(^|\/)Procfile$/i, category: 'hosting-platform', candidate: 'Heroku-compatible platform', detail: 'Procfile process model' },
746
+ { test: /(^|\/)sfdx-project\.json$/i, category: 'managed-platform', candidate: 'Salesforce', detail: 'Salesforce DX project metadata' },
747
+ { test: /(^|\/)(PowerApps\/|[^/]+\.msapp$)/i, category: 'managed-platform', candidate: 'Microsoft Power Platform', detail: 'Power Apps package evidence' },
748
+ { test: /(^|\/)(openapi|swagger)[^/]*\.(json|ya?ml)$/i, category: 'integration-contract', candidate: 'OpenAPI', detail: 'OpenAPI contract' },
749
+ { test: /(^|\/)schema\.graphqls?$/i, category: 'integration-contract', candidate: 'GraphQL', detail: 'GraphQL schema' }
750
+ ]);
751
+
752
+ function technologyHostingDigest(value) {
753
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
754
+ }
755
+
756
+ function atomicArchaeologyFiles(entries) {
757
+ const transaction = randomUUID();
758
+ const staged = [];
759
+ const backups = [];
760
+ const installed = [];
761
+ try {
762
+ for (const entry of entries) {
763
+ mkdirSync(dirname(entry.path), { recursive: true });
764
+ const temporary = `${entry.path}.ewai-${transaction}.tmp`;
765
+ writeFileSync(temporary, entry.content, 'utf8');
766
+ staged.push({ ...entry, temporary });
767
+ }
768
+ for (const entry of staged) {
769
+ if (!existsSync(entry.path)) continue;
770
+ const backup = `${entry.path}.ewai-${transaction}.bak`;
771
+ renameSync(entry.path, backup);
772
+ backups.push({ path: entry.path, backup });
773
+ }
774
+ for (const entry of staged) {
775
+ renameSync(entry.temporary, entry.path);
776
+ installed.push(entry.path);
777
+ }
778
+ for (const entry of backups) rmSync(entry.backup, { force: true });
779
+ } catch (error) {
780
+ for (const path of installed) rmSync(path, { force: true });
781
+ for (const entry of backups.reverse()) {
782
+ if (existsSync(entry.backup)) renameSync(entry.backup, entry.path);
783
+ }
784
+ throw error;
785
+ } finally {
786
+ for (const entry of staged) rmSync(entry.temporary, { force: true });
787
+ for (const entry of backups) rmSync(entry.backup, { force: true });
788
+ }
789
+ }
790
+
791
+ function readJsonFile(path, label) {
792
+ try {
793
+ return JSON.parse(readFileSync(path, 'utf8'));
794
+ } catch (error) {
795
+ throw new Error(`Invalid ${label}: ${error.message}`);
796
+ }
797
+ }
798
+
799
+ function sourceMapTechnologyEvidence(projectRoot, supplied) {
800
+ if (supplied) return supplied;
801
+ const freshness = repositoryIndexFreshness(projectRoot);
802
+ const coverage = repositorySourceMapCoverage(projectRoot);
803
+ const byFile = new Map();
804
+ const collect = (result) => {
805
+ for (const file of result.files ?? []) {
806
+ byFile.set(`${file.repository}:${file.path}`, file);
807
+ }
808
+ };
809
+ collect(repositorySourceMapFiles(projectRoot, { limit: 200 }));
810
+ for (const query of technologyHostingSourceQueries) {
811
+ collect(repositorySourceMapFiles(projectRoot, { query, limit: 200 }));
812
+ }
813
+ const profiles = repositorySourceMapProfiles(projectRoot, { limit: 200 });
814
+ for (const profile of profiles.profiles ?? []) {
815
+ if (profile.sourceKind === 'core' || !profile.matchCount) continue;
816
+ collect(repositorySourceMapFiles(projectRoot, { profileId: profile.id, limit: 200 }));
817
+ }
818
+ return { runId: coverage.runId, freshness, coverage, files: [...byFile.values()] };
819
+ }
820
+
821
+ function technologyHostingFreshness(freshness, projectRoot, bundleRoot) {
822
+ if (!freshness?.stale) return freshness;
823
+ const generated = new Set([
824
+ 'technology-hosting-brief.json',
825
+ 'technology-hosting-answers.template.json',
826
+ 'technology-hosting-brief.md',
827
+ 'technology-hosting-profile.json',
828
+ 'technology-hosting-profile.md'
829
+ ].map((name) => relative(resolve(projectRoot), resolve(bundleRoot, name)).replaceAll('\\', '/')));
830
+ const samples = freshness.samples ?? {};
831
+ const stripRepository = (value) => {
832
+ const path = String(value ?? '').replaceAll('\\', '/');
833
+ const separator = path.indexOf(':');
834
+ return separator === -1 ? path : path.slice(separator + 1);
835
+ };
836
+ const ownMissing = (samples.missing ?? []).filter((path) => generated.has(stripRepository(path)));
837
+ const ownChanged = (samples.changed ?? []).filter((path) => generated.has(stripRepository(path)));
838
+ const onlyGeneratedChanges = Number(freshness.summary?.deleted ?? 0) === 0
839
+ && Number(freshness.summary?.missing ?? 0) === ownMissing.length
840
+ && Number(freshness.summary?.changed ?? 0) === ownChanged.length;
841
+ if (!onlyGeneratedChanges) return freshness;
842
+ return {
843
+ ...freshness,
844
+ status: 'fresh',
845
+ stale: false,
846
+ reason: 'only-technology-hosting-output-files-changed',
847
+ ignoredGeneratedOutputs: [...generated]
848
+ };
849
+ }
850
+
851
+ function technologyHostingObservations(files) {
852
+ const observations = [];
853
+ for (const file of files ?? []) {
854
+ const path = String(file.path ?? '').replaceAll('\\', '/');
855
+ let matched = false;
856
+ for (const rule of technologyHostingSignalRules) {
857
+ if (!rule.test.test(path)) continue;
858
+ matched = true;
859
+ const key = `${file.repository ?? 'project'}:${path}:${rule.category}:${rule.candidate}`;
860
+ observations.push({
861
+ id: `ATH-OBS-${technologyHostingDigest(key).slice(0, 10).toUpperCase()}`,
862
+ provenance: 'repository-observed',
863
+ repository: file.repository || 'project',
864
+ path,
865
+ category: rule.category,
866
+ candidate: rule.candidate,
867
+ evidence: rule.detail,
868
+ sourceMap: {
869
+ profileId: file.profileId ?? '',
870
+ analyser: file.analyser ?? '',
871
+ analysisOutcome: file.analysisOutcome ?? '',
872
+ classification: file.classification ?? '',
873
+ language: file.language ?? ''
874
+ },
875
+ limitation: 'This repository signal does not prove current runtime use, deployment, hosting location or production state.'
876
+ });
877
+ }
878
+ const profileId = String(file.profileId ?? '').trim();
879
+ if (!matched && profileId && !profileId.startsWith('core-')) {
880
+ const key = `${file.repository ?? 'project'}:${path}:source-map-profile:${profileId}`;
881
+ observations.push({
882
+ id: `ATH-OBS-${technologyHostingDigest(key).slice(0, 10).toUpperCase()}`,
883
+ provenance: 'repository-observed',
884
+ repository: file.repository || 'project',
885
+ path,
886
+ category: 'source-map-profile',
887
+ candidate: profileId,
888
+ evidence: 'An installed technology, stack, organisation or project Source Map profile matched this file.',
889
+ sourceMap: {
890
+ profileId,
891
+ analyser: file.analyser ?? '',
892
+ analysisOutcome: file.analysisOutcome ?? '',
893
+ classification: file.classification ?? '',
894
+ language: file.language ?? ''
895
+ },
896
+ limitation: 'This pack-provided repository signal does not prove current runtime use, deployment, hosting location or production state.'
897
+ });
898
+ }
899
+ }
900
+ return observations.sort((left, right) => (
901
+ left.repository.localeCompare(right.repository)
902
+ || left.path.localeCompare(right.path)
903
+ || left.candidate.localeCompare(right.candidate)
904
+ ));
905
+ }
906
+
907
+ function activeTechnologyHostingPersonas(routing, observations) {
908
+ const inventory = Array.isArray(routing.inventory) ? routing.inventory : [];
909
+ const inventoryById = new Map(inventory.map((persona) => [persona.id, persona]));
910
+ const selectedIds = new Set([
911
+ ...baselinePersonaIds,
912
+ ...(routing.user_review?.status === 'confirmed' ? routing.user_review.selected_personas ?? [] : []),
913
+ ...(routing.assignments ?? []).map((assignment) => assignment.persona)
914
+ ]);
915
+ const signals = [
916
+ 'technology', 'architecture', 'hosting', 'cloud', 'infrastructure', 'deployment', 'operations',
917
+ ...observations.map((observation) => `${observation.category} ${observation.candidate}`)
918
+ ];
919
+ const contextual = selectContextualPersonas({
920
+ personaCatalogue: inventory,
921
+ signals,
922
+ context: { observations },
923
+ contextLabel: 'archaeology technology and hosting discovery',
924
+ limit: 8
925
+ });
926
+ const contextualById = new Map(contextual.map((persona) => [persona.id, persona]));
927
+ for (const persona of contextual) selectedIds.add(persona.id);
928
+ return [...selectedIds].map((id) => {
929
+ const persona = inventoryById.get(id);
930
+ if (!persona) return null;
931
+ const assigned = (routing.assignments ?? []).filter((assignment) => assignment.persona === id);
932
+ const contextualPersona = contextualById.get(id);
933
+ return {
934
+ id,
935
+ name: persona.name || id,
936
+ tier: persona.tier || 'core',
937
+ category: persona.category || '',
938
+ active: true,
939
+ passes: [...new Set(assigned.map((assignment) => assignment.pass))],
940
+ engagementReason: contextualPersona?.engagementReason
941
+ || assigned.map((assignment) => assignment.contribution).filter(Boolean).join(' ')
942
+ || `${persona.name || id} is engaged as a required archaeology evidence lens.`
943
+ };
944
+ }).filter(Boolean);
945
+ }
946
+
947
+ function technologyHostingQuestions(observations) {
948
+ const candidates = [...new Set(observations.map((observation) => observation.candidate))];
949
+ return [
950
+ { id: 'actual-technology', prompt: 'Which languages, frameworks, runtimes, databases, integrations and infrastructure components are actually in current use, and at what versions?', context: candidates },
951
+ { id: 'provider', prompt: 'Who is the actual hosting or managed-platform provider for each live environment?' },
952
+ { id: 'platform-service', prompt: 'Which concrete platform, product or service hosts each workload?' },
953
+ { id: 'locations', prompt: 'Which country, region, data centre, tenant or physical location is used by each environment?' },
954
+ { id: 'environments', prompt: 'What development, test, staging, production, disaster-recovery or other environments exist?' },
955
+ { id: 'deployment-model', prompt: 'Is each workload cloud, on-premises, SaaS, PaaS, IaaS, managed-service, hybrid, other or unknown?' },
956
+ { id: 'operating-model', prompt: 'Is each environment self-managed, provider-managed, shared, third-party-managed, hybrid or unknown, and by whom?' },
957
+ { id: 'data-residency', prompt: 'Where is production data stored, processed, backed up and replicated, and who confirms that statement?' },
958
+ { id: 'release-route', prompt: 'What route takes a change from source control to each environment, including approvals, automation and rollback?' },
959
+ { id: 'inactive-signals', prompt: 'Which repository signals are historical, experimental, generated, inactive or otherwise not representative of the current service?' },
960
+ { id: 'contradictions', prompt: 'Where does repository evidence conflict with owner or operator knowledge, and what evidence can resolve each conflict?' },
961
+ { id: 'unresolved', prompt: 'What remains unknown, who owns the answer and what evidence is needed before relying on it?' }
962
+ ];
963
+ }
964
+
965
+ function renderTechnologyHostingBrief(brief) {
966
+ const personaRows = brief.activePersonas.length
967
+ ? brief.activePersonas.map((persona) => `| ${persona.name} | ${persona.tier} | ${persona.passes.join(', ') || 'contextual'} | ${persona.engagementReason} |`).join('\n')
968
+ : '| None | — | — | No approved persona was available. |';
969
+ const observationRows = brief.observations.length
970
+ ? brief.observations.map((observation) => `| ${observation.id} | ${observation.repository} | \`${observation.path}\` | ${observation.category} | ${observation.candidate} | ${observation.sourceMap.analysisOutcome || 'unknown'} |`).join('\n')
971
+ : '| — | — | — | — | No common technology or hosting signals were detected. | — |';
972
+ return `# Archaeology technology and hosting discovery
973
+
974
+ Status: **${brief.status}**
975
+ Source Map run: **${brief.sourceMap.runId ?? 'missing'}**
976
+ Prepared: **${brief.preparedAt}**
977
+
978
+ Repository evidence is advisory. A configuration file proves only that the signal exists in the indexed repository; it does not prove current runtime use, deployment, live hosting or production state.
979
+
980
+ ## Active personas
981
+
982
+ These are the persona lenses actively engaged for this discovery pass.
983
+
984
+ | Persona | Tier | Passes | Why engaged |
985
+ |---|---|---|---|
986
+ ${personaRows}
987
+
988
+ ## Repository-observed signals
989
+
990
+ | ID | Repository | Path | Category | Candidate | Analysis outcome |
991
+ |---|---|---|---|---|---|
992
+ ${observationRows}
993
+
994
+ ## Owner questions
995
+
996
+ ${brief.questions.map((question) => `- **${question.id}:** ${question.prompt}`).join('\n')}
997
+
998
+ ## Source Map limitations
999
+
1000
+ ${brief.sourceMap.coverageWarnings.length ? brief.sourceMap.coverageWarnings.map((warning) => `- ${warning}`).join('\n') : '- No coverage warning was reported.'}
1001
+
1002
+ ## Required assurance notice
1003
+
1004
+ ${ASSURANCE_NOTICE}
1005
+ `;
1006
+ }
1007
+
1008
+ function emptyTechnologyHostingAnswers(digest) {
1009
+ const answer = () => ({ value: 'unknown', confirmed: false, observationRefs: [], evidence: [], notes: '' });
1010
+ return {
1011
+ schema: technologyHostingSchemas.answers,
1012
+ preparedDigest: digest,
1013
+ technology: [],
1014
+ hosting: {
1015
+ provider: answer(),
1016
+ platformService: answer(),
1017
+ locations: [],
1018
+ environments: [],
1019
+ deploymentModel: answer(),
1020
+ operatingModel: answer(),
1021
+ dataResidency: answer(),
1022
+ releaseRoute: answer(),
1023
+ operatingOwner: answer()
1024
+ },
1025
+ observationReviews: [],
1026
+ contradictions: [],
1027
+ unresolvedQuestions: []
1028
+ };
1029
+ }
1030
+
1031
+ export function prepareArchaeologyTechnologyHosting(projectRoot, bundlePath, options = {}) {
1032
+ const bundleRoot = archaeologyBundleRoot(projectRoot, bundlePath);
1033
+ const personaValidation = validateArchaeologyPersonaGate(projectRoot, bundlePath);
1034
+ if (!personaValidation.valid) {
1035
+ throw new Error(`Technology and hosting discovery requires a valid, user-reviewed persona gate: ${personaValidation.errors.join('; ')}`);
1036
+ }
1037
+ const briefPath = resolve(bundleRoot, 'technology-hosting-brief.json');
1038
+ const templatePath = resolve(bundleRoot, 'technology-hosting-answers.template.json');
1039
+ const markdownPath = resolve(bundleRoot, 'technology-hosting-brief.md');
1040
+ if (!options.force && [briefPath, templatePath, markdownPath].some(existsSync)) {
1041
+ throw new Error('Technology and hosting discovery already exists; use an explicitly approved force refresh to replace the preparation artefacts.');
1042
+ }
1043
+
1044
+ const repositoryEvidence = sourceMapTechnologyEvidence(projectRoot, options.repositoryEvidence);
1045
+ if (repositoryEvidence.freshness?.stale || repositoryEvidence.freshness?.status !== 'fresh') {
1046
+ throw new Error(`Refresh the Repository Source Map before technology and hosting discovery (${repositoryEvidence.freshness?.reason || repositoryEvidence.freshness?.status || 'freshness unknown'}).`);
1047
+ }
1048
+ if (!repositoryEvidence.runId) throw new Error('Refresh the Repository Source Map before technology and hosting discovery; no completed run is available.');
1049
+
1050
+ const routing = YAML.parse(readFileSync(resolve(bundleRoot, 'persona-routing.yaml'), 'utf8')) ?? {};
1051
+ const observations = technologyHostingObservations(repositoryEvidence.files);
1052
+ const activePersonas = activeTechnologyHostingPersonas(routing, observations);
1053
+ const tierCounts = {};
1054
+ for (const persona of routing.inventory ?? []) tierCounts[persona.tier || 'core'] = (tierCounts[persona.tier || 'core'] ?? 0) + 1;
1055
+ const preparedAt = options.now || new Date().toISOString();
1056
+ const body = {
1057
+ schema: technologyHostingSchemas.preparation,
1058
+ status: 'awaiting-owner-answers',
1059
+ preparedAt,
1060
+ sourceMap: {
1061
+ runId: Number(repositoryEvidence.runId),
1062
+ freshness: repositoryEvidence.freshness,
1063
+ outcomes: repositoryEvidence.coverage?.outcomes ?? {},
1064
+ coverageWarnings: repositoryEvidence.coverage?.warnings ?? [],
1065
+ guidance: {
1066
+ repositoryEvidenceIsAdvisory: true,
1067
+ currentRuntimeRequiresHumanConfirmation: true
1068
+ }
1069
+ },
1070
+ observations,
1071
+ personaAvailability: { total: (routing.inventory ?? []).length, byTier: tierCounts },
1072
+ activePersonas,
1073
+ questions: technologyHostingQuestions(observations),
1074
+ notices: [ASSURANCE_NOTICE]
1075
+ };
1076
+ const brief = { ...body, digest: technologyHostingDigest(body) };
1077
+ const template = emptyTechnologyHostingAnswers(brief.digest);
1078
+ atomicArchaeologyFiles([
1079
+ { path: briefPath, content: `${JSON.stringify(brief, null, 2)}\n` },
1080
+ { path: templatePath, content: `${JSON.stringify(template, null, 2)}\n` },
1081
+ { path: markdownPath, content: renderTechnologyHostingBrief(brief) }
1082
+ ]);
1083
+ return {
1084
+ schema: technologyHostingSchemas.preparation,
1085
+ status: brief.status,
1086
+ bundleRoot,
1087
+ briefPath,
1088
+ templatePath,
1089
+ markdownPath,
1090
+ brief
1091
+ };
1092
+ }
1093
+
1094
+ function cleanStringList(value, label) {
1095
+ if (value === undefined) return [];
1096
+ if (!Array.isArray(value)) throw new Error(`${label} must be a list.`);
1097
+ return [...new Set(value.map((item) => String(item ?? '').trim()).filter(Boolean))];
1098
+ }
1099
+
1100
+ function normaliseTechnologyHostingAnswer(value, label, observationIds) {
1101
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error(`${label} must be an answer object.`);
1102
+ const selected = String(value.value ?? '').trim();
1103
+ if (!selected) throw new Error(`${label} requires a value; use unknown when it has not been established.`);
1104
+ const observationRefs = cleanStringList(value.observationRefs, `${label}.observationRefs`);
1105
+ for (const reference of observationRefs) {
1106
+ if (!observationIds.has(reference)) throw new Error(`${label} contains unknown observation reference ${reference}.`);
1107
+ }
1108
+ const evidence = cleanStringList(value.evidence, `${label}.evidence`);
1109
+ if (value.confirmed === true && !evidence.length) throw new Error(`${label} requires evidence before it can be human-confirmed.`);
1110
+ return {
1111
+ value: selected,
1112
+ provenance: value.confirmed === true ? 'human-confirmed' : 'owner-declared',
1113
+ confirmed: value.confirmed === true,
1114
+ observationRefs,
1115
+ evidence,
1116
+ notes: String(value.notes ?? '').trim()
1117
+ };
1118
+ }
1119
+
1120
+ function technologyHostingInputErrors(input, brief) {
1121
+ const errors = [];
1122
+ if (!input || input.schema !== technologyHostingSchemas.answers) errors.push(`Answers must use schema ${technologyHostingSchemas.answers}.`);
1123
+ if (input?.preparedDigest !== brief.digest) errors.push('Answers were not prepared from the current technology and hosting brief digest.');
1124
+ if (!Array.isArray(input?.technology)) errors.push('Technology answers must be a list.');
1125
+ if (!input?.hosting || typeof input.hosting !== 'object' || Array.isArray(input.hosting)) errors.push('Hosting answers must be an object.');
1126
+ if (input?.hosting && !Array.isArray(input.hosting.locations)) errors.push('Hosting locations must be a list.');
1127
+ if (input?.hosting && !Array.isArray(input.hosting.environments)) errors.push('Hosting environments must be a list.');
1128
+ if (!Array.isArray(input?.observationReviews)) errors.push('Observation reviews must be a list.');
1129
+ if (!Array.isArray(input?.contradictions)) errors.push('Contradictions must be a list.');
1130
+ if (!Array.isArray(input?.unresolvedQuestions)) errors.push('Unresolved questions must be a list.');
1131
+ return errors;
1132
+ }
1133
+
1134
+ function normaliseTechnologyItem(item, index, observationIds) {
1135
+ if (!item || typeof item !== 'object' || Array.isArray(item)) throw new Error(`technology[${index}] must be an object.`);
1136
+ const category = String(item.category ?? '').trim();
1137
+ const name = String(item.name ?? '').trim();
1138
+ if (!category || !name) throw new Error(`technology[${index}] requires category and name.`);
1139
+ const answer = normaliseTechnologyHostingAnswer({ ...item, value: name }, `technology[${index}]`, observationIds);
1140
+ return { category, name: answer.value, version: String(item.version ?? '').trim(), ...answer, value: undefined };
1141
+ }
1142
+
1143
+ function cleanUndefined(value) {
1144
+ if (Array.isArray(value)) return value.map(cleanUndefined);
1145
+ if (!value || typeof value !== 'object') return value;
1146
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined).map(([key, entry]) => [key, cleanUndefined(entry)]));
1147
+ }
1148
+
1149
+ function renderTechnologyHostingProfile(profile) {
1150
+ const technologyRows = profile.technology.length
1151
+ ? profile.technology.map((item) => `| ${item.category} | ${item.name} | ${item.version || '—'} | ${item.provenance} | ${item.observationRefs.join(', ') || '—'} |`).join('\n')
1152
+ : '| — | No technology answer recorded | — | — | — |';
1153
+ const environmentRows = profile.hosting.environments.length
1154
+ ? profile.hosting.environments.map((environment) => `| ${environment.name} | ${environment.provider} | ${environment.platformService} | ${environment.location} | ${environment.deploymentModel} | ${environment.operatingModel} | ${environment.provenance} |`).join('\n')
1155
+ : '| — | — | — | — | unknown | unknown | owner-declared |';
1156
+ const hostingRows = ['provider', 'platformService', 'deploymentModel', 'operatingModel', 'dataResidency', 'releaseRoute', 'operatingOwner']
1157
+ .map((field) => `| ${field} | ${profile.hosting[field].value} | ${profile.hosting[field].provenance} | ${profile.hosting[field].observationRefs.join(', ') || '—'} |`).join('\n');
1158
+ return `# Archaeology technology and hosting profile
1159
+
1160
+ Recorded: **${profile.recordedAt}**
1161
+ Reviewed by: **${profile.review.reviewedBy}**
1162
+ Source Map run: **${profile.sourceMap.runId}**
1163
+
1164
+ ## Evidence language
1165
+
1166
+ - **repository-observed** means the indexed repository contains a signal. It is not proof of live use.
1167
+ - **owner-declared** means a person supplied the answer but it still requires accountable confirmation.
1168
+ - **human-confirmed** means the individual answer was explicitly marked confirmed and carries its evidence references.
1169
+
1170
+ ## Technology
1171
+
1172
+ | Category | Name | Version | Provenance | Observation references |
1173
+ |---|---|---|---|---|
1174
+ ${technologyRows}
1175
+
1176
+ ## Hosting and operation
1177
+
1178
+ | Field | Value | Provenance | Observation references |
1179
+ |---|---|---|---|
1180
+ ${hostingRows}
1181
+
1182
+ ### Locations
1183
+
1184
+ ${profile.hosting.locations.length ? profile.hosting.locations.map((location) => `- ${location.value} (${location.provenance})`).join('\n') : '- None recorded.'}
1185
+
1186
+ ### Environments
1187
+
1188
+ | Environment | Provider | Platform/service | Location | Deployment | Operation | Provenance |
1189
+ |---|---|---|---|---|---|---|
1190
+ ${environmentRows}
1191
+
1192
+ ## Repository observations and disposition
1193
+
1194
+ ${profile.observations.length ? profile.observations.map((observation) => `- **${observation.id}** ${observation.repository}/\`${observation.path}\`: ${observation.candidate} — ${observation.review.status}. ${observation.review.notes || observation.limitation}`).join('\n') : '- No common repository signals detected.'}
1195
+
1196
+ ## Contradictions
1197
+
1198
+ ${profile.contradictions.length ? profile.contradictions.map((item) => `- ${item.summary}${item.notes ? ` — ${item.notes}` : ''}`).join('\n') : '- None recorded.'}
1199
+
1200
+ ## Unresolved questions
1201
+
1202
+ ${profile.unresolvedQuestions.length ? profile.unresolvedQuestions.map((question) => `- ${question}`).join('\n') : '- None recorded.'}
1203
+
1204
+ ## Required assurance notice
1205
+
1206
+ ${ASSURANCE_NOTICE}
1207
+ `;
1208
+ }
1209
+
1210
+ export function recordArchaeologyTechnologyHosting(projectRoot, bundlePath, input, options = {}) {
1211
+ const bundleRoot = archaeologyBundleRoot(projectRoot, bundlePath);
1212
+ const briefPath = resolve(bundleRoot, 'technology-hosting-brief.json');
1213
+ const profilePath = resolve(bundleRoot, 'technology-hosting-profile.json');
1214
+ const markdownPath = resolve(bundleRoot, 'technology-hosting-profile.md');
1215
+ if (!existsSync(briefPath)) throw new Error('Prepare technology and hosting discovery before recording answers.');
1216
+ if (existsSync(profilePath) || existsSync(markdownPath)) throw new Error('Technology and hosting profile already exists; preserve the reviewed record and prepare a new Archaeology bundle or explicit refresh.');
1217
+ const reviewedBy = String(options.reviewedBy ?? '').trim();
1218
+ if (reviewedBy.length < 2) throw new Error('Technology and hosting recording requires --reviewed-by NAME.');
1219
+ const brief = readJsonFile(briefPath, 'technology and hosting brief');
1220
+ const errors = technologyHostingInputErrors(input, brief);
1221
+ if (errors.length) throw new Error(errors.join(' '));
1222
+ const observationIds = new Set(brief.observations.map((observation) => observation.id));
1223
+ const technology = input.technology.map((item, index) => cleanUndefined(normaliseTechnologyItem(item, index, observationIds)));
1224
+ const hostingInput = input.hosting;
1225
+ const hosting = {};
1226
+ for (const field of ['provider', 'platformService', 'deploymentModel', 'operatingModel', 'dataResidency', 'releaseRoute', 'operatingOwner']) {
1227
+ hosting[field] = normaliseTechnologyHostingAnswer(hostingInput[field], `hosting.${field}`, observationIds);
1228
+ }
1229
+ if (!deploymentModels.has(hosting.deploymentModel.value.toLowerCase())) throw new Error(`Unsupported deployment model: ${hosting.deploymentModel.value}.`);
1230
+ if (!operatingModels.has(hosting.operatingModel.value.toLowerCase())) throw new Error(`Unsupported operating model: ${hosting.operatingModel.value}.`);
1231
+ hosting.locations = (hostingInput.locations ?? []).map((location, index) => normaliseTechnologyHostingAnswer(location, `hosting.locations[${index}]`, observationIds));
1232
+ hosting.environments = (hostingInput.environments ?? []).map((environment, index) => {
1233
+ if (!environment || typeof environment !== 'object' || Array.isArray(environment)) throw new Error(`hosting.environments[${index}] must be an object.`);
1234
+ const name = String(environment.name ?? '').trim();
1235
+ if (!name) throw new Error(`hosting.environments[${index}] requires a name.`);
1236
+ const deploymentModel = String(environment.deploymentModel ?? 'unknown').trim().toLowerCase();
1237
+ const operatingModel = String(environment.operatingModel ?? 'unknown').trim().toLowerCase();
1238
+ if (!deploymentModels.has(deploymentModel)) throw new Error(`Unsupported deployment model in hosting.environments[${index}]: ${deploymentModel}.`);
1239
+ if (!operatingModels.has(operatingModel)) throw new Error(`Unsupported operating model in hosting.environments[${index}]: ${operatingModel}.`);
1240
+ const answer = normaliseTechnologyHostingAnswer({ ...environment, value: name }, `hosting.environments[${index}]`, observationIds);
1241
+ return cleanUndefined({
1242
+ name,
1243
+ provider: String(environment.provider ?? 'unknown').trim() || 'unknown',
1244
+ platformService: String(environment.platformService ?? 'unknown').trim() || 'unknown',
1245
+ location: String(environment.location ?? 'unknown').trim() || 'unknown',
1246
+ deploymentModel,
1247
+ operatingModel,
1248
+ provenance: answer.provenance,
1249
+ confirmed: answer.confirmed,
1250
+ observationRefs: answer.observationRefs,
1251
+ evidence: answer.evidence,
1252
+ notes: answer.notes
1253
+ });
1254
+ });
1255
+
1256
+ const reviewByObservation = new Map();
1257
+ for (const [index, review] of (input.observationReviews ?? []).entries()) {
1258
+ const observationId = String(review?.observationId ?? '').trim();
1259
+ if (!observationIds.has(observationId)) throw new Error(`observationReviews[${index}] contains unknown observation reference ${observationId}.`);
1260
+ const status = String(review.status ?? '').trim();
1261
+ if (!observationReviewStatuses.has(status)) throw new Error(`observationReviews[${index}] has unsupported status ${status}.`);
1262
+ reviewByObservation.set(observationId, { status, notes: String(review.notes ?? '').trim() });
1263
+ }
1264
+ const observations = brief.observations.map((observation) => ({
1265
+ ...observation,
1266
+ review: reviewByObservation.get(observation.id) ?? { status: 'uncertain', notes: 'No owner disposition was recorded.' }
1267
+ }));
1268
+ const contradictions = (input.contradictions ?? []).map((contradiction, index) => {
1269
+ const summary = String(contradiction?.summary ?? '').trim();
1270
+ if (!summary) throw new Error(`contradictions[${index}] requires a summary.`);
1271
+ const observationRefs = cleanStringList(contradiction.observationRefs, `contradictions[${index}].observationRefs`);
1272
+ for (const reference of observationRefs) {
1273
+ if (!observationIds.has(reference)) throw new Error(`contradictions[${index}] contains unknown observation reference ${reference}.`);
1274
+ }
1275
+ return { summary, observationRefs, notes: String(contradiction.notes ?? '').trim() };
1276
+ });
1277
+ const unresolvedQuestions = cleanStringList(input.unresolvedQuestions, 'unresolvedQuestions');
1278
+ const recordedAt = options.now || new Date().toISOString();
1279
+ const body = {
1280
+ schema: technologyHostingSchemas.profile,
1281
+ status: 'recorded',
1282
+ preparedDigest: brief.digest,
1283
+ recordedAt,
1284
+ sourceMap: brief.sourceMap,
1285
+ review: { reviewedBy, reviewedAt: recordedAt, confirmationScope: 'Individual answers with confirmed=true only' },
1286
+ activePersonas: brief.activePersonas,
1287
+ observations,
1288
+ technology,
1289
+ hosting,
1290
+ contradictions,
1291
+ unresolvedQuestions,
1292
+ notices: [ASSURANCE_NOTICE]
1293
+ };
1294
+ const profile = { ...body, digest: technologyHostingDigest(body) };
1295
+ atomicArchaeologyFiles([
1296
+ { path: profilePath, content: `${JSON.stringify(profile, null, 2)}\n` },
1297
+ { path: markdownPath, content: renderTechnologyHostingProfile(profile) }
1298
+ ]);
1299
+ return { schema: technologyHostingSchemas.profile, status: 'recorded', bundleRoot, profilePath, markdownPath, profile };
1300
+ }
1301
+
1302
+ export function readArchaeologyTechnologyHosting(projectRoot, bundlePath, options = {}) {
1303
+ const bundleRoot = archaeologyBundleRoot(projectRoot, bundlePath);
1304
+ const briefPath = resolve(bundleRoot, 'technology-hosting-brief.json');
1305
+ const profilePath = resolve(bundleRoot, 'technology-hosting-profile.json');
1306
+ if (!existsSync(briefPath)) {
1307
+ return { schema: technologyHostingSchemas.status, status: 'missing', bundleRoot, notices: [ASSURANCE_NOTICE] };
1308
+ }
1309
+ try {
1310
+ const brief = readJsonFile(briefPath, 'technology and hosting brief');
1311
+ const repositoryEvidence = sourceMapTechnologyEvidence(projectRoot, options.repositoryEvidence);
1312
+ const effectiveFreshness = technologyHostingFreshness(repositoryEvidence.freshness, projectRoot, bundleRoot);
1313
+ const stale = effectiveFreshness?.stale
1314
+ || effectiveFreshness?.status !== 'fresh'
1315
+ || Number(repositoryEvidence.runId) !== Number(brief.sourceMap?.runId);
1316
+ const profile = existsSync(profilePath) ? readJsonFile(profilePath, 'technology and hosting profile') : null;
1317
+ if (profile && profile.preparedDigest !== brief.digest) {
1318
+ return { schema: technologyHostingSchemas.status, status: 'stale', reason: 'profile-preparation-digest-mismatch', bundleRoot, brief, profile, notices: [ASSURANCE_NOTICE] };
1319
+ }
1320
+ return {
1321
+ schema: technologyHostingSchemas.status,
1322
+ status: stale ? 'stale' : profile ? 'recorded' : 'prepared',
1323
+ reason: stale ? 'repository-source-map-changed-or-stale' : null,
1324
+ freshness: effectiveFreshness,
1325
+ bundleRoot,
1326
+ brief,
1327
+ profile,
1328
+ notices: [ASSURANCE_NOTICE]
1329
+ };
1330
+ } catch (error) {
1331
+ return { schema: technologyHostingSchemas.status, status: 'invalid', bundleRoot, errors: [error.message], notices: [ASSURANCE_NOTICE] };
1332
+ }
1333
+ }