@topogram/cli 0.3.63 → 0.3.65

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 (344) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.d.ts +6 -0
  4. package/src/adoption/plan.js +12 -703
  5. package/src/adoption/reporting.d.ts +10 -0
  6. package/src/adoption/review-groups.d.ts +6 -0
  7. package/src/agent-brief.d.ts +3 -0
  8. package/src/agent-brief.js +495 -0
  9. package/src/agent-ops/query-builders/auth.js +375 -0
  10. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  12. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  13. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  14. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  15. package/src/agent-ops/query-builders/change-risk.js +25 -0
  16. package/src/agent-ops/query-builders/common.js +149 -0
  17. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  18. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  19. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  20. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  21. package/src/agent-ops/query-builders/work-packets.js +417 -0
  22. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  23. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  24. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  25. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  26. package/src/agent-ops/query-builders.d.ts +26 -0
  27. package/src/agent-ops/query-builders.js +42 -5021
  28. package/src/archive/archive.d.ts +2 -0
  29. package/src/archive/compact.d.ts +1 -0
  30. package/src/archive/unarchive.d.ts +1 -0
  31. package/src/catalog/constants.js +10 -0
  32. package/src/catalog/copy.js +60 -0
  33. package/src/catalog/diagnostics.js +15 -0
  34. package/src/catalog/entries.js +42 -0
  35. package/src/catalog/files.js +67 -0
  36. package/src/catalog/provenance.js +122 -0
  37. package/src/catalog/source.js +150 -0
  38. package/src/catalog/validation.js +252 -0
  39. package/src/catalog.d.ts +12 -0
  40. package/src/catalog.js +18 -750
  41. package/src/cli/catalog-alias.d.ts +1 -0
  42. package/src/cli/command-parser.js +38 -0
  43. package/src/cli/command-parsers/core.js +102 -0
  44. package/src/cli/command-parsers/generator.js +39 -0
  45. package/src/cli/command-parsers/import.js +44 -0
  46. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  47. package/src/cli/command-parsers/project.js +47 -0
  48. package/src/cli/command-parsers/sdlc.js +47 -0
  49. package/src/cli/command-parsers/shared.js +51 -0
  50. package/src/cli/command-parsers/template.js +48 -0
  51. package/src/cli/commands/agent.js +47 -0
  52. package/src/cli/commands/catalog/check.js +31 -0
  53. package/src/cli/commands/catalog/copy.js +59 -0
  54. package/src/cli/commands/catalog/doctor.js +248 -0
  55. package/src/cli/commands/catalog/help.js +21 -0
  56. package/src/cli/commands/catalog/list.js +52 -0
  57. package/src/cli/commands/catalog/runner.js +92 -0
  58. package/src/cli/commands/catalog/shared.js +17 -0
  59. package/src/cli/commands/catalog/show.js +134 -0
  60. package/src/cli/commands/catalog.js +32 -0
  61. package/src/cli/commands/check.js +268 -0
  62. package/src/cli/commands/doctor.js +268 -0
  63. package/src/cli/commands/emit.js +149 -0
  64. package/src/cli/commands/generate.js +96 -0
  65. package/src/cli/commands/generator-policy/package-info.js +162 -0
  66. package/src/cli/commands/generator-policy/payloads.js +372 -0
  67. package/src/cli/commands/generator-policy/printers.js +159 -0
  68. package/src/cli/commands/generator-policy/runner.js +81 -0
  69. package/src/cli/commands/generator-policy/shared.js +39 -0
  70. package/src/cli/commands/generator-policy.js +17 -0
  71. package/src/cli/commands/generator.js +443 -0
  72. package/src/cli/commands/import/adopt.js +170 -0
  73. package/src/cli/commands/import/check.js +91 -0
  74. package/src/cli/commands/import/diff.js +84 -0
  75. package/src/cli/commands/import/help.js +47 -0
  76. package/src/cli/commands/import/paths.js +277 -0
  77. package/src/cli/commands/import/plan.js +284 -0
  78. package/src/cli/commands/import/refresh.js +470 -0
  79. package/src/cli/commands/import/status-history.js +196 -0
  80. package/src/cli/commands/import/workspace.js +230 -0
  81. package/src/cli/commands/import-runner.js +157 -0
  82. package/src/cli/commands/import.js +35 -0
  83. package/src/cli/commands/inspect.js +55 -0
  84. package/src/cli/commands/new.js +94 -0
  85. package/src/cli/commands/package/constants.js +17 -0
  86. package/src/cli/commands/package/doctor.js +240 -0
  87. package/src/cli/commands/package/help.js +27 -0
  88. package/src/cli/commands/package/lockfile.js +135 -0
  89. package/src/cli/commands/package/npm.js +97 -0
  90. package/src/cli/commands/package/reporting.js +35 -0
  91. package/src/cli/commands/package/runner.js +33 -0
  92. package/src/cli/commands/package/shared.js +9 -0
  93. package/src/cli/commands/package/update-cli.js +252 -0
  94. package/src/cli/commands/package/versions.js +35 -0
  95. package/src/cli/commands/package.js +31 -0
  96. package/src/cli/commands/query/change-plan.js +68 -0
  97. package/src/cli/commands/query/definitions.js +202 -0
  98. package/src/cli/commands/query/import-adopt.js +121 -0
  99. package/src/cli/commands/query/runner/artifacts.js +102 -0
  100. package/src/cli/commands/query/runner/boundaries.js +211 -0
  101. package/src/cli/commands/query/runner/change.js +182 -0
  102. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  103. package/src/cli/commands/query/runner/index.js +31 -0
  104. package/src/cli/commands/query/runner/output.js +12 -0
  105. package/src/cli/commands/query/runner/workflow.js +241 -0
  106. package/src/cli/commands/query/runner.js +3 -0
  107. package/src/cli/commands/query/workflow-context.js +5 -0
  108. package/src/cli/commands/query/workspace.js +274 -0
  109. package/src/cli/commands/query.js +11 -0
  110. package/src/cli/commands/release-rollout.js +257 -0
  111. package/src/cli/commands/release-shared.js +528 -0
  112. package/src/cli/commands/release-status.js +429 -0
  113. package/src/cli/commands/release.js +107 -0
  114. package/src/cli/commands/sdlc.js +168 -0
  115. package/src/cli/commands/setup.js +76 -0
  116. package/src/cli/commands/source.js +291 -0
  117. package/src/cli/commands/template/baseline.js +100 -0
  118. package/src/cli/commands/template/check.js +466 -0
  119. package/src/cli/commands/template/constants.js +8 -0
  120. package/src/cli/commands/template/diagnostics.js +26 -0
  121. package/src/cli/commands/template/help.js +28 -0
  122. package/src/cli/commands/template/lifecycle.js +404 -0
  123. package/src/cli/commands/template/list-show.js +287 -0
  124. package/src/cli/commands/template/policy.js +422 -0
  125. package/src/cli/commands/template/shared.js +127 -0
  126. package/src/cli/commands/template/updates.js +352 -0
  127. package/src/cli/commands/template-runner.js +198 -0
  128. package/src/cli/commands/template.js +43 -0
  129. package/src/cli/commands/trust.js +219 -0
  130. package/src/cli/commands/version.js +40 -0
  131. package/src/cli/commands/widget.js +168 -0
  132. package/src/cli/commands/workflow.js +63 -0
  133. package/src/cli/dispatcher.js +392 -0
  134. package/src/cli/help-dispatch.js +188 -0
  135. package/src/cli/help.js +296 -0
  136. package/src/cli/migration-guidance.js +59 -0
  137. package/src/cli/options.js +96 -0
  138. package/src/cli/output-safety.js +107 -0
  139. package/src/cli/path-normalization.js +29 -0
  140. package/src/cli.js +47 -11711
  141. package/src/example-implementation.d.ts +2 -0
  142. package/src/format.d.ts +1 -0
  143. package/src/generator/api/contracts.js +497 -0
  144. package/src/generator/api/metadata.js +221 -0
  145. package/src/generator/api/openapi.js +559 -0
  146. package/src/generator/api/schema.js +124 -0
  147. package/src/generator/api/types.d.ts +98 -0
  148. package/src/generator/api.js +3 -1195
  149. package/src/generator/check.d.ts +1 -0
  150. package/src/generator/context/bundle.d.ts +1 -0
  151. package/src/generator/context/shared/domain-sdlc.js +282 -0
  152. package/src/generator/context/shared/maintained-boundary.js +665 -0
  153. package/src/generator/context/shared/metrics.js +85 -0
  154. package/src/generator/context/shared/primitives.js +64 -0
  155. package/src/generator/context/shared/relationships.js +453 -0
  156. package/src/generator/context/shared/summaries.js +263 -0
  157. package/src/generator/context/shared/types.d.ts +207 -0
  158. package/src/generator/context/shared.d.ts +44 -0
  159. package/src/generator/context/shared.js +80 -1390
  160. package/src/generator/context/slice/core.js +397 -0
  161. package/src/generator/context/slice/sdlc.js +417 -0
  162. package/src/generator/context/slice/ui-packets.js +183 -0
  163. package/src/generator/context/slice.js +2 -859
  164. package/src/generator/native/parity-bundle.js +2 -1
  165. package/src/generator/registry/index.js +507 -0
  166. package/src/generator/registry.js +18 -504
  167. package/src/generator/runtime/environment/index.js +666 -0
  168. package/src/generator/runtime/environment.js +4 -666
  169. package/src/generator/runtime/runtime-check/index.js +554 -0
  170. package/src/generator/runtime/runtime-check.js +4 -554
  171. package/src/generator/runtime/shared/index.js +572 -0
  172. package/src/generator/runtime/shared.js +19 -570
  173. package/src/generator/shared.d.ts +2 -0
  174. package/src/generator/surfaces/shared.d.ts +3 -0
  175. package/src/generator/surfaces/web/html-escape.js +22 -0
  176. package/src/generator/surfaces/web/react.js +10 -8
  177. package/src/generator/surfaces/web/sveltekit.js +7 -5
  178. package/src/generator/surfaces/web/vanilla.js +8 -4
  179. package/src/generator/widget-conformance/behavior-report.js +258 -0
  180. package/src/generator/widget-conformance/checks.js +371 -0
  181. package/src/generator/widget-conformance/projection-context.js +200 -0
  182. package/src/generator/widget-conformance/report.js +166 -0
  183. package/src/generator/widget-conformance/types.d.ts +121 -0
  184. package/src/generator/widget-conformance.js +3 -824
  185. package/src/generator.d.ts +2 -0
  186. package/src/github-client.js +520 -0
  187. package/src/import/core/context.d.ts +3 -0
  188. package/src/import/core/contracts.d.ts +1 -0
  189. package/src/import/core/registry.d.ts +4 -0
  190. package/src/import/core/runner/candidates.js +217 -0
  191. package/src/import/core/runner/options.js +22 -0
  192. package/src/import/core/runner/reports.js +50 -0
  193. package/src/import/core/runner/run.js +79 -0
  194. package/src/import/core/runner/tracks.js +150 -0
  195. package/src/import/core/runner/ui-drafts.js +337 -0
  196. package/src/import/core/runner.js +3 -698
  197. package/src/import/core/shared/api-routes.js +221 -0
  198. package/src/import/core/shared/candidates.js +97 -0
  199. package/src/import/core/shared/files.js +177 -0
  200. package/src/import/core/shared/next-app.js +389 -0
  201. package/src/import/core/shared/types.d.ts +51 -0
  202. package/src/import/core/shared/ui-routes.js +230 -0
  203. package/src/import/core/shared.js +67 -910
  204. package/src/import/extractors/api/flutter-dio.js +4 -8
  205. package/src/import/extractors/api/react-native-repository.js +4 -8
  206. package/src/import/index.d.ts +4 -0
  207. package/src/import/provenance.d.ts +4 -0
  208. package/src/new-project/constants.js +128 -0
  209. package/src/new-project/create.js +83 -0
  210. package/src/new-project/json.js +28 -0
  211. package/src/new-project/metadata.js +96 -0
  212. package/src/new-project/package-spec.js +161 -0
  213. package/src/new-project/project-files.js +348 -0
  214. package/src/new-project/template-policy.js +269 -0
  215. package/src/new-project/template-resolution.js +368 -0
  216. package/src/new-project/template-snapshots.js +430 -0
  217. package/src/new-project/template-updates.js +512 -0
  218. package/src/new-project/types.d.ts +83 -0
  219. package/src/new-project.js +6 -2188
  220. package/src/npm-safety.js +79 -0
  221. package/src/parser.d.ts +87 -0
  222. package/src/parser.js +118 -0
  223. package/src/path-helpers.d.ts +1 -0
  224. package/src/path-helpers.js +20 -0
  225. package/src/policy/review-boundaries.d.ts +15 -0
  226. package/src/project-config/index.js +564 -0
  227. package/src/project-config.js +19 -560
  228. package/src/reconcile/docs.d.ts +8 -0
  229. package/src/reconcile/journeys.d.ts +1 -0
  230. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  231. package/src/resolver/enrich/bug.js +2 -0
  232. package/src/resolver/enrich/pitch.js +2 -0
  233. package/src/resolver/enrich/requirement.js +2 -0
  234. package/src/resolver/enrich/task.js +2 -0
  235. package/src/resolver/index.js +19 -2089
  236. package/src/resolver/normalize.js +384 -1
  237. package/src/resolver/plans.js +168 -0
  238. package/src/resolver/projections-api.js +494 -0
  239. package/src/resolver/projections-db.js +133 -0
  240. package/src/resolver/projections-ui.js +317 -0
  241. package/src/resolver/shapes.js +251 -0
  242. package/src/resolver/shared.js +278 -0
  243. package/src/resolver/widgets.js +132 -0
  244. package/src/resolver.d.ts +1 -0
  245. package/src/runtime-support.js +29 -0
  246. package/src/sdlc/adopt.d.ts +1 -0
  247. package/src/sdlc/check.d.ts +1 -0
  248. package/src/sdlc/explain.d.ts +1 -0
  249. package/src/sdlc/release.d.ts +1 -0
  250. package/src/sdlc/scaffold.d.ts +1 -0
  251. package/src/sdlc/transition.d.ts +1 -0
  252. package/src/template-trust/constants.js +62 -0
  253. package/src/template-trust/content.js +258 -0
  254. package/src/template-trust/diff.js +92 -0
  255. package/src/template-trust/policy.js +61 -0
  256. package/src/template-trust/record.js +90 -0
  257. package/src/template-trust/status.js +182 -0
  258. package/src/template-trust.js +24 -687
  259. package/src/text-helpers.d.ts +7 -0
  260. package/src/text-helpers.js +245 -0
  261. package/src/topogram-config.js +306 -0
  262. package/src/topogram-types.d.ts +69 -0
  263. package/src/validator/common.js +488 -0
  264. package/src/validator/data-model.js +237 -0
  265. package/src/validator/docs.js +167 -0
  266. package/src/validator/expressions.js +146 -1
  267. package/src/validator/index.d.ts +23 -0
  268. package/src/validator/index.js +32 -3585
  269. package/src/validator/kinds.d.ts +41 -0
  270. package/src/validator/kinds.js +2 -0
  271. package/src/validator/model-helpers.js +46 -0
  272. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  273. package/src/validator/per-kind/bug.js +6 -0
  274. package/src/validator/per-kind/domain.js +15 -2
  275. package/src/validator/per-kind/pitch.js +7 -0
  276. package/src/validator/per-kind/requirement.js +5 -0
  277. package/src/validator/per-kind/task.js +7 -0
  278. package/src/validator/per-kind/widget.js +14 -0
  279. package/src/validator/projections/api-http-async.js +410 -0
  280. package/src/validator/projections/api-http-authz.js +88 -0
  281. package/src/validator/projections/api-http-core.js +205 -0
  282. package/src/validator/projections/api-http-policies.js +339 -0
  283. package/src/validator/projections/api-http-responses.js +233 -0
  284. package/src/validator/projections/api-http.js +44 -0
  285. package/src/validator/projections/db.js +353 -0
  286. package/src/validator/projections/generator-defaults.js +45 -0
  287. package/src/validator/projections/helpers.js +87 -0
  288. package/src/validator/projections/ui-helpers.js +214 -0
  289. package/src/validator/projections/ui-navigation.js +344 -0
  290. package/src/validator/projections/ui-structure.js +364 -0
  291. package/src/validator/projections/ui-widgets.js +493 -0
  292. package/src/validator/projections/ui.js +46 -0
  293. package/src/validator/registry.js +48 -1
  294. package/src/validator/utils.d.ts +20 -0
  295. package/src/validator/utils.js +115 -12
  296. package/src/validator.d.ts +2 -0
  297. package/src/widget-behavior.d.ts +1 -0
  298. package/src/workflows/adoption/index.js +26 -0
  299. package/src/workflows/docs-generate.js +262 -0
  300. package/src/workflows/docs-scan.js +703 -0
  301. package/src/workflows/docs.js +15 -0
  302. package/src/workflows/import-app/api/collect.js +221 -0
  303. package/src/workflows/import-app/api/openapi.js +257 -0
  304. package/src/workflows/import-app/api/routes.js +327 -0
  305. package/src/workflows/import-app/api/sources.js +22 -0
  306. package/src/workflows/import-app/api.js +4 -0
  307. package/src/workflows/import-app/db.js +538 -0
  308. package/src/workflows/import-app/index.js +30 -0
  309. package/src/workflows/import-app/shared.js +218 -0
  310. package/src/workflows/import-app/ui.js +443 -0
  311. package/src/workflows/import-app/workflow.js +159 -0
  312. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  313. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  314. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  315. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  316. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  317. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  318. package/src/workflows/reconcile/adoption-plan.js +32 -0
  319. package/src/workflows/reconcile/auth/closures.js +115 -0
  320. package/src/workflows/reconcile/auth/formatters.js +142 -0
  321. package/src/workflows/reconcile/auth/inference.js +330 -0
  322. package/src/workflows/reconcile/auth/roles.js +122 -0
  323. package/src/workflows/reconcile/auth.js +37 -0
  324. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  325. package/src/workflows/reconcile/bundle-core.js +14 -0
  326. package/src/workflows/reconcile/bundle-shared.js +75 -0
  327. package/src/workflows/reconcile/candidate-model.js +477 -0
  328. package/src/workflows/reconcile/canonical-surface.js +264 -0
  329. package/src/workflows/reconcile/gap-report.js +333 -0
  330. package/src/workflows/reconcile/ids.js +6 -0
  331. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  332. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  333. package/src/workflows/reconcile/impacts/patches.js +252 -0
  334. package/src/workflows/reconcile/impacts/reports.js +80 -0
  335. package/src/workflows/reconcile/impacts.js +16 -0
  336. package/src/workflows/reconcile/index.js +7 -0
  337. package/src/workflows/reconcile/renderers.js +461 -0
  338. package/src/workflows/reconcile/summary.js +90 -0
  339. package/src/workflows/reconcile/workflow.js +309 -0
  340. package/src/workflows/shared.js +189 -0
  341. package/src/workflows/types.d.ts +93 -0
  342. package/src/workflows.d.ts +1 -0
  343. package/src/workflows.js +10 -7652
  344. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,470 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ import { stableStringify } from "../../../format.js";
8
+ import {
9
+ buildTopogramImportStatus,
10
+ collectImportSourceFileRecords,
11
+ TOPOGRAM_IMPORT_FILE,
12
+ writeTopogramImportRecord
13
+ } from "../../../import/provenance.js";
14
+ import { runWorkflow } from "../../../workflows.js";
15
+ import {
16
+ countByField,
17
+ importProjectCommandPath,
18
+ normalizeProjectRoot,
19
+ normalizeTopogramPath,
20
+ readImportAdoptionReceipts
21
+ } from "./paths.js";
22
+ import { countFilesRecursive, importCandidateCounts, writeRelativeFiles } from "./workspace.js";
23
+ import { verifyImportAdoptionReceipts } from "./status-history.js";
24
+
25
+ /**
26
+ * @typedef {Record<string, any>} AnyRecord
27
+ */
28
+
29
+ /**
30
+ * @param {string} projectRoot
31
+ * @returns {{ path: string, record: Record<string, any> }}
32
+ */
33
+ export function readTopogramImportRecord(projectRoot) {
34
+ const importPath = path.join(normalizeProjectRoot(projectRoot), TOPOGRAM_IMPORT_FILE);
35
+ if (!fs.existsSync(importPath)) {
36
+ throw new Error(`No brownfield import provenance found at '${importPath}'. Run 'topogram import <app-path> --out <target>' first.`);
37
+ }
38
+ try {
39
+ return { path: importPath, record: JSON.parse(fs.readFileSync(importPath, "utf8")) };
40
+ } catch (error) {
41
+ throw new Error(`Invalid brownfield import provenance JSON at '${importPath}'.`);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * @param {Record<string, any>} importRecord
47
+ * @returns {string|null}
48
+ */
49
+ export function importTrackValueFromRecord(importRecord) {
50
+ const tracks = Array.isArray(importRecord.import?.tracks)
51
+ ? importRecord.import.tracks.map((/** @type {any} */ track) => String(track).trim()).filter(Boolean)
52
+ : [];
53
+ return tracks.length ? [...new Set(tracks)].join(",") : null;
54
+ }
55
+
56
+ /**
57
+ * @param {string} topogramRoot
58
+ * @returns {{ rawCandidateFiles: number, reconcileFiles: number }}
59
+ */
60
+ export function clearImportRefreshCandidateArtifacts(topogramRoot) {
61
+ const appCandidatesRoot = path.join(topogramRoot, "candidates", "app");
62
+ const reconcileRoot = path.join(topogramRoot, "candidates", "reconcile");
63
+ const removed = {
64
+ rawCandidateFiles: countFilesRecursive(appCandidatesRoot),
65
+ reconcileFiles: countFilesRecursive(reconcileRoot)
66
+ };
67
+ fs.rmSync(appCandidatesRoot, { recursive: true, force: true });
68
+ fs.rmSync(reconcileRoot, { recursive: true, force: true });
69
+ return removed;
70
+ }
71
+
72
+ /**
73
+ * @param {{ changed?: any[], added?: any[], removed?: any[] }} [content]
74
+ * @returns {{ changed: number, added: number, removed: number }}
75
+ */
76
+ export function sourceDiffCounts(content = {}) {
77
+ return {
78
+ changed: content.changed?.length || 0,
79
+ added: content.added?.length || 0,
80
+ removed: content.removed?.length || 0
81
+ };
82
+ }
83
+
84
+ /**
85
+ * @param {string} projectRoot
86
+ * @param {AnyRecord} importRecord
87
+ * @param {string} sourceRoot
88
+ * @returns {AnyRecord}
89
+ */
90
+ export function compareImportRecordToSource(projectRoot, importRecord, sourceRoot) {
91
+ const trustedFiles = Array.isArray(importRecord.files) ? importRecord.files : [];
92
+ const trustedByPath = new Map(trustedFiles.map((/** @type {AnyRecord} */ file) => [String(file.path), file]));
93
+ const currentFiles = collectImportSourceFileRecords(sourceRoot, { excludeRoots: [projectRoot] });
94
+ const currentByPath = new Map(currentFiles.map((/** @type {AnyRecord} */ file) => [file.path, file]));
95
+ /** @type {string[]} */
96
+ const changed = [];
97
+ /** @type {string[]} */
98
+ const added = [];
99
+ /** @type {string[]} */
100
+ const removed = [];
101
+ for (const [filePath, current] of currentByPath) {
102
+ const trusted = trustedByPath.get(filePath);
103
+ if (!trusted) {
104
+ added.push(filePath);
105
+ } else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
106
+ changed.push(filePath);
107
+ }
108
+ }
109
+ for (const filePath of trustedByPath.keys()) {
110
+ if (!currentByPath.has(filePath)) {
111
+ removed.push(filePath);
112
+ }
113
+ }
114
+ const content = {
115
+ changed: changed.sort((a, b) => a.localeCompare(b)),
116
+ added: added.sort((a, b) => a.localeCompare(b)),
117
+ removed: removed.sort((a, b) => a.localeCompare(b))
118
+ };
119
+ const counts = sourceDiffCounts(content);
120
+ const clean = counts.changed === 0 && counts.added === 0 && counts.removed === 0;
121
+ return {
122
+ ok: clean,
123
+ status: clean ? "clean" : "changed",
124
+ content,
125
+ counts,
126
+ files: currentFiles
127
+ };
128
+ }
129
+
130
+ /**
131
+ * @param {Record<string, number>} [previous]
132
+ * @param {Record<string, number>} [next]
133
+ * @returns {AnyRecord}
134
+ */
135
+ export function buildCountDeltas(previous = {}, next = {}) {
136
+ const keys = [...new Set([...Object.keys(previous || {}), ...Object.keys(next || {})])].sort((a, b) => a.localeCompare(b));
137
+ /** @type {Record<string, { previous: number, next: number, delta: number }>} */
138
+ const deltas = {};
139
+ /** @type {Array<{ key: string, previous: number, next: number, delta: number }>} */
140
+ const changed = [];
141
+ for (const key of keys) {
142
+ const previousCount = Number(previous?.[key] || 0);
143
+ const nextCount = Number(next?.[key] || 0);
144
+ const delta = nextCount - previousCount;
145
+ deltas[key] = { previous: previousCount, next: nextCount, delta };
146
+ if (delta !== 0) {
147
+ changed.push({ key, previous: previousCount, next: nextCount, delta });
148
+ }
149
+ }
150
+ return {
151
+ previous,
152
+ next,
153
+ deltas,
154
+ changed
155
+ };
156
+ }
157
+
158
+ /**
159
+ * @param {AnyRecord} item
160
+ * @returns {string}
161
+ */
162
+ export function adoptionSurfaceKey(item) {
163
+ return `${item?.bundle || "unbundled"}:${item?.kind || "unknown"}:${item?.item || item?.id || "unknown"}`;
164
+ }
165
+
166
+ /**
167
+ * @param {AnyRecord} item
168
+ * @returns {AnyRecord}
169
+ */
170
+ export function summarizeAdoptionSurface(item) {
171
+ return {
172
+ key: adoptionSurfaceKey(item),
173
+ bundle: item?.bundle || "unbundled",
174
+ kind: item?.kind || "unknown",
175
+ item: item?.item || item?.id || "unknown",
176
+ currentState: item?.current_state || null
177
+ };
178
+ }
179
+
180
+ /**
181
+ * @param {AnyRecord[]} [currentSurfaces]
182
+ * @param {AnyRecord[]} [nextSurfaces]
183
+ * @returns {AnyRecord}
184
+ */
185
+ export function summarizeAdoptionPlanDeltas(currentSurfaces = [], nextSurfaces = []) {
186
+ const currentByKey = new Map((currentSurfaces || []).map((item) => [adoptionSurfaceKey(item), item]));
187
+ const nextByKey = new Map((nextSurfaces || []).map((item) => [adoptionSurfaceKey(item), item]));
188
+ /** @type {AnyRecord[]} */
189
+ const added = [];
190
+ /** @type {AnyRecord[]} */
191
+ const removed = [];
192
+ /** @type {AnyRecord[]} */
193
+ const changed = [];
194
+ for (const [key, next] of nextByKey) {
195
+ const current = currentByKey.get(key);
196
+ if (!current) {
197
+ added.push(summarizeAdoptionSurface(next));
198
+ } else if (stableStringify(current) !== stableStringify(next)) {
199
+ changed.push({
200
+ ...summarizeAdoptionSurface(next),
201
+ previousState: current.current_state || null,
202
+ nextState: next.current_state || null
203
+ });
204
+ }
205
+ }
206
+ for (const [key, current] of currentByKey) {
207
+ if (!nextByKey.has(key)) {
208
+ removed.push(summarizeAdoptionSurface(current));
209
+ }
210
+ }
211
+ const currentByBundle = countByField(currentSurfaces, "bundle");
212
+ const nextByBundle = countByField(nextSurfaces, "bundle");
213
+ return {
214
+ added: added.sort((left, right) => left.key.localeCompare(right.key)),
215
+ removed: removed.sort((left, right) => left.key.localeCompare(right.key)),
216
+ changed: changed.sort((left, right) => left.key.localeCompare(right.key)),
217
+ byBundle: buildCountDeltas(currentByBundle, nextByBundle)
218
+ };
219
+ }
220
+
221
+ /**
222
+ * @param {string|null|undefined} fileContents
223
+ * @returns {AnyRecord[]}
224
+ */
225
+ export function adoptionSurfacesFromPlanFile(fileContents) {
226
+ if (!fileContents) {
227
+ return [];
228
+ }
229
+ const parsed = JSON.parse(fileContents);
230
+ return parsed.imported_proposal_surfaces || [];
231
+ }
232
+
233
+ /**
234
+ * @param {string} projectRoot
235
+ * @param {string} topogramRoot
236
+ * @param {Record<string, any>} importFiles
237
+ * @returns {AnyRecord}
238
+ */
239
+ export function buildRefreshPreviewReconcile(projectRoot, topogramRoot, importFiles) {
240
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "topogram-import-refresh-preview."));
241
+ try {
242
+ const tempProjectRoot = path.join(tempRoot, "workspace");
243
+ const tempTopogramRoot = path.join(tempProjectRoot, "topogram");
244
+ fs.mkdirSync(tempProjectRoot, { recursive: true });
245
+ fs.cpSync(topogramRoot, tempTopogramRoot, { recursive: true });
246
+ const projectConfigPath = path.join(projectRoot, "topogram.project.json");
247
+ if (fs.existsSync(projectConfigPath)) {
248
+ fs.cpSync(projectConfigPath, path.join(tempProjectRoot, "topogram.project.json"));
249
+ }
250
+ clearImportRefreshCandidateArtifacts(tempTopogramRoot);
251
+ writeRelativeFiles(tempTopogramRoot, importFiles || {});
252
+ const reconcileResult = runWorkflow("reconcile", tempProjectRoot, {});
253
+ return {
254
+ reconcileFileCount: Object.keys(reconcileResult.files || {}).length,
255
+ reconcileFilePaths: Object.keys(reconcileResult.files || {}).sort((a, b) => a.localeCompare(b)),
256
+ adoptionSurfaces: adoptionSurfacesFromPlanFile(reconcileResult.files?.["candidates/reconcile/adoption-plan.agent.json"]),
257
+ summary: reconcileResult.summary || {}
258
+ };
259
+ } finally {
260
+ fs.rmSync(tempRoot, { recursive: true, force: true });
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @param {string} topogramRoot
266
+ * @returns {AnyRecord[]}
267
+ */
268
+ export function readCurrentAdoptionSurfaces(topogramRoot) {
269
+ const planPath = path.join(topogramRoot, "candidates", "reconcile", "adoption-plan.agent.json");
270
+ if (!fs.existsSync(planPath)) {
271
+ return [];
272
+ }
273
+ return adoptionSurfacesFromPlanFile(fs.readFileSync(planPath, "utf8"));
274
+ }
275
+
276
+ /**
277
+ * @param {string} inputPath
278
+ * @param {{ sourcePath?: string|null }} [options]
279
+ * @returns {AnyRecord}
280
+ */
281
+ export function buildBrownfieldImportRefreshAnalysis(inputPath, options = {}) {
282
+ const projectRoot = normalizeProjectRoot(inputPath);
283
+ const topogramRoot = normalizeTopogramPath(projectRoot);
284
+ if (!fs.existsSync(topogramRoot) || !fs.statSync(topogramRoot).isDirectory()) {
285
+ throw new Error(`No topogram directory found for imported workspace '${inputPath}'.`);
286
+ }
287
+
288
+ const { record: importRecord } = readTopogramImportRecord(projectRoot);
289
+ const sourcePath = options.sourcePath && !String(options.sourcePath).startsWith("-")
290
+ ? options.sourcePath
291
+ : importRecord.source?.path;
292
+ if (!sourcePath) {
293
+ throw new Error("No brownfield source path was provided or recorded. Use 'topogram import refresh <workspace> --from <app-path>'.");
294
+ }
295
+ const sourceRoot = path.resolve(sourcePath);
296
+ if (!fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
297
+ throw new Error(`Cannot refresh from missing app directory '${sourcePath}'.`);
298
+ }
299
+ if (sourceRoot === projectRoot) {
300
+ throw new Error("Refusing to refresh import from the imported Topogram workspace itself.");
301
+ }
302
+
303
+ const sourceComparison = compareImportRecordToSource(projectRoot, importRecord, sourceRoot);
304
+ const trackValue = importTrackValueFromRecord(importRecord);
305
+ const importResult = runWorkflow("import-app", sourceRoot, { from: trackValue });
306
+ const candidateCounts = importCandidateCounts(importResult.summary);
307
+ const candidateCountDeltas = buildCountDeltas(importRecord.import?.candidateCounts || {}, candidateCounts);
308
+ const removedCandidateFiles = {
309
+ rawCandidateFiles: countFilesRecursive(path.join(topogramRoot, "candidates", "app")),
310
+ reconcileFiles: countFilesRecursive(path.join(topogramRoot, "candidates", "reconcile"))
311
+ };
312
+ const previewReconcile = buildRefreshPreviewReconcile(projectRoot, topogramRoot, importResult.files || {});
313
+ const currentAdoptionSurfaces = readCurrentAdoptionSurfaces(topogramRoot);
314
+ const adoptionPlanDeltas = summarizeAdoptionPlanDeltas(currentAdoptionSurfaces, previewReconcile.adoptionSurfaces);
315
+ const receiptVerification = verifyImportAdoptionReceipts(projectRoot, readImportAdoptionReceipts(projectRoot));
316
+ const plannedFiles = [
317
+ TOPOGRAM_IMPORT_FILE,
318
+ ...Object.keys(importResult.files || {}).map((filePath) => `topogram/${filePath}`),
319
+ ...previewReconcile.reconcileFilePaths.map((/** @type {string} */ filePath) => `topogram/${filePath}`)
320
+ ].sort((a, b) => a.localeCompare(b));
321
+ const analysis = /** @type {AnyRecord} */ ({
322
+ projectRoot,
323
+ topogramRoot,
324
+ sourcePath: sourceRoot,
325
+ provenancePath: path.join(projectRoot, TOPOGRAM_IMPORT_FILE),
326
+ importedAt: importRecord.importedAt || null,
327
+ previousImportStatus: sourceComparison.status,
328
+ sourceDiff: {
329
+ status: sourceComparison.status,
330
+ counts: sourceComparison.counts,
331
+ changed: sourceComparison.content.changed,
332
+ added: sourceComparison.content.added,
333
+ removed: sourceComparison.content.removed
334
+ },
335
+ tracks: importResult.summary.tracks || [],
336
+ sourceFiles: sourceComparison.files.length,
337
+ removedCandidateFiles,
338
+ rawCandidateFiles: Object.keys(importResult.files || {}).length,
339
+ reconcileFiles: previewReconcile.reconcileFileCount,
340
+ candidateCounts,
341
+ candidateCountDeltas,
342
+ adoptionPlanDeltas,
343
+ receiptVerification,
344
+ plannedFiles
345
+ });
346
+ Object.defineProperty(analysis, "importResult", {
347
+ value: importResult,
348
+ enumerable: false
349
+ });
350
+ return analysis;
351
+ }
352
+
353
+ /**
354
+ * @param {string} inputPath
355
+ * @param {{ sourcePath?: string|null, dryRun?: boolean }} [options]
356
+ * @returns {{ ok: boolean, dryRun: boolean, projectRoot: string, topogramRoot: string, sourcePath: string, provenancePath: string, previousImportStatus: string, currentImportStatus: string, tracks: string[], sourceFiles: number, sourceDiff: Record<string, any>, removedCandidateFiles: Record<string, number>, rawCandidateFiles: number, reconcileFiles: number, writtenFiles: string[], plannedFiles: string[], candidateCounts: Record<string, number>, candidateCountDeltas: Record<string, any>, adoptionPlanDeltas: Record<string, any>, receiptVerification: Record<string, any>, refreshMetadata: Record<string, any>|null, nextCommands: string[] }}
357
+ */
358
+ export function buildBrownfieldImportRefreshPayload(inputPath, options = {}) {
359
+ const analysis = buildBrownfieldImportRefreshAnalysis(inputPath, options);
360
+ const dryRun = Boolean(options.dryRun);
361
+ let provenancePath = analysis.provenancePath;
362
+ let currentImportStatus = dryRun ? analysis.previousImportStatus : "unknown";
363
+ /** @type {string[]} */
364
+ let writtenFiles = [];
365
+ /** @type {AnyRecord|null} */
366
+ let refreshMetadata = null;
367
+ if (!dryRun) {
368
+ const removedCandidateFiles = clearImportRefreshCandidateArtifacts(analysis.topogramRoot);
369
+ const rawCandidateFiles = writeRelativeFiles(analysis.topogramRoot, analysis.importResult.files || {});
370
+ const reconcileResult = runWorkflow("reconcile", analysis.projectRoot, {});
371
+ const reconcileFiles = writeRelativeFiles(analysis.topogramRoot, reconcileResult.files || {});
372
+ const refreshedAt = new Date().toISOString();
373
+ refreshMetadata = {
374
+ refreshedAt,
375
+ previousSourceStatus: analysis.previousImportStatus,
376
+ sourceDiffCounts: analysis.sourceDiff.counts
377
+ };
378
+ const provenance = writeTopogramImportRecord(analysis.projectRoot, {
379
+ sourceRoot: analysis.sourcePath,
380
+ ignoredRoots: [analysis.projectRoot],
381
+ importedAt: analysis.importedAt || undefined,
382
+ refreshedAt,
383
+ refresh: {
384
+ previousSourceStatus: analysis.previousImportStatus,
385
+ sourceDiffCounts: analysis.sourceDiff.counts
386
+ },
387
+ tracks: analysis.importResult.summary.tracks || [],
388
+ findingsCount: analysis.importResult.summary.findings_count || 0,
389
+ candidateCounts: analysis.candidateCounts,
390
+ files: collectImportSourceFileRecords(analysis.sourcePath, { excludeRoots: [analysis.projectRoot] })
391
+ });
392
+ provenancePath = provenance.path;
393
+ currentImportStatus = buildTopogramImportStatus(analysis.projectRoot).status;
394
+ writtenFiles = [
395
+ TOPOGRAM_IMPORT_FILE,
396
+ ...rawCandidateFiles.map((filePath) => `topogram/${filePath}`),
397
+ ...reconcileFiles.map((filePath) => `topogram/${filePath}`)
398
+ ].sort((a, b) => a.localeCompare(b));
399
+ analysis.removedCandidateFiles = removedCandidateFiles;
400
+ analysis.rawCandidateFiles = rawCandidateFiles.length;
401
+ analysis.reconcileFiles = reconcileFiles.length;
402
+ }
403
+ return {
404
+ ok: dryRun || currentImportStatus === "clean",
405
+ dryRun,
406
+ projectRoot: analysis.projectRoot,
407
+ topogramRoot: analysis.topogramRoot,
408
+ sourcePath: analysis.sourcePath,
409
+ provenancePath,
410
+ previousImportStatus: analysis.previousImportStatus,
411
+ currentImportStatus,
412
+ tracks: analysis.tracks,
413
+ sourceFiles: analysis.sourceFiles,
414
+ sourceDiff: analysis.sourceDiff,
415
+ removedCandidateFiles: analysis.removedCandidateFiles,
416
+ rawCandidateFiles: analysis.rawCandidateFiles,
417
+ reconcileFiles: analysis.reconcileFiles,
418
+ writtenFiles,
419
+ plannedFiles: analysis.plannedFiles,
420
+ candidateCounts: analysis.candidateCounts,
421
+ candidateCountDeltas: analysis.candidateCountDeltas,
422
+ adoptionPlanDeltas: analysis.adoptionPlanDeltas,
423
+ receiptVerification: analysis.receiptVerification,
424
+ refreshMetadata,
425
+ nextCommands: [
426
+ dryRun
427
+ ? `topogram import refresh ${importProjectCommandPath(analysis.projectRoot)}`
428
+ : `topogram import check ${importProjectCommandPath(analysis.projectRoot)}`,
429
+ `topogram import plan ${importProjectCommandPath(analysis.projectRoot)}`,
430
+ `topogram import status ${importProjectCommandPath(analysis.projectRoot)}`,
431
+ `topogram import history ${importProjectCommandPath(analysis.projectRoot)} --verify`
432
+ ]
433
+ };
434
+ }
435
+
436
+ /**
437
+ * @param {ReturnType<typeof buildBrownfieldImportRefreshPayload>} payload
438
+ * @returns {void}
439
+ */
440
+ export function printBrownfieldImportRefresh(payload) {
441
+ console.log(`${payload.dryRun ? "Previewed" : "Refreshed"} brownfield import candidates for ${payload.projectRoot}.`);
442
+ console.log(`Source: ${payload.sourcePath}`);
443
+ console.log(`Topogram: ${payload.topogramRoot}`);
444
+ console.log(`Import provenance: ${payload.provenancePath}`);
445
+ console.log(`Previous source status: ${payload.previousImportStatus}`);
446
+ console.log(`Current source status: ${payload.currentImportStatus}`);
447
+ console.log(`Source diff: changed=${payload.sourceDiff.counts.changed}, added=${payload.sourceDiff.counts.added}, removed=${payload.sourceDiff.counts.removed}`);
448
+ console.log(`Tracked source files: ${payload.sourceFiles}`);
449
+ console.log(`Raw candidate files: ${payload.rawCandidateFiles}`);
450
+ console.log(`Reconcile proposal files: ${payload.reconcileFiles}`);
451
+ console.log(`Replaced candidate files: ${payload.removedCandidateFiles.rawCandidateFiles + payload.removedCandidateFiles.reconcileFiles}`);
452
+ const candidateChanges = payload.candidateCountDeltas.changed || [];
453
+ console.log(`Candidate count changes: ${candidateChanges.length}`);
454
+ for (const item of candidateChanges.slice(0, 8)) {
455
+ const sign = item.delta > 0 ? "+" : "";
456
+ console.log(`- ${item.key}: ${item.previous} -> ${item.next} (${sign}${item.delta})`);
457
+ }
458
+ const adoptionDeltas = payload.adoptionPlanDeltas;
459
+ console.log(`Adoption plan changes: added=${adoptionDeltas.added.length}, removed=${adoptionDeltas.removed.length}, changed=${adoptionDeltas.changed.length}`);
460
+ console.log(`Receipt verification: ${payload.receiptVerification.status}`);
461
+ if (payload.dryRun) {
462
+ console.log("No files were written. Re-run without --dry-run to refresh candidates and source provenance.");
463
+ }
464
+ console.log("Canonical Topogram files were not overwritten. Adopt refreshed candidates explicitly after review.");
465
+ console.log("");
466
+ console.log("Next steps:");
467
+ for (const command of payload.nextCommands) {
468
+ console.log(` ${command}`);
469
+ }
470
+ }
@@ -0,0 +1,196 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ importAdoptionsPath,
8
+ normalizeProjectRoot,
9
+ normalizeTopogramPath,
10
+ projectFileHash,
11
+ readImportAdoptionReceipts
12
+ } from "./paths.js";
13
+ import { buildBrownfieldImportCheckPayload } from "./check.js";
14
+ import { readImportAdoptionArtifacts, summarizeImportAdoption } from "./plan.js";
15
+ import { runWorkflow } from "../../../workflows.js";
16
+
17
+ /**
18
+ * @typedef {Record<string, any>} AnyRecord
19
+ */
20
+
21
+ /**
22
+ * @param {string} inputPath
23
+ * @returns {AnyRecord}
24
+ */
25
+ export function buildBrownfieldImportStatusPayload(inputPath) {
26
+ const artifacts = readImportAdoptionArtifacts(inputPath);
27
+ const importCheck = buildBrownfieldImportCheckPayload(artifacts.projectRoot);
28
+ const adoptionStatus = runWorkflow("adoption-status", artifacts.projectRoot).summary || artifacts.adoptionStatus || {};
29
+ const adoption = summarizeImportAdoption(artifacts.adoptionPlan, adoptionStatus, artifacts.projectRoot);
30
+ const history = buildBrownfieldImportHistoryPayload(artifacts.projectRoot);
31
+ return {
32
+ ok: importCheck.ok,
33
+ projectRoot: artifacts.projectRoot,
34
+ topogramRoot: artifacts.topogramRoot,
35
+ import: importCheck.import,
36
+ topogram: importCheck.topogram,
37
+ adoption: {
38
+ status: adoptionStatus,
39
+ summary: adoption.summary,
40
+ bundles: adoption.bundles,
41
+ risks: adoption.risks,
42
+ nextCommand: adoption.nextCommand,
43
+ history: history.summary
44
+ },
45
+ errors: importCheck.errors
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {AnyRecord} payload
51
+ * @returns {void}
52
+ */
53
+ export function printBrownfieldImportStatus(payload) {
54
+ console.log(`Import status: ${payload.import.status}`);
55
+ console.log(`Topogram check: ${payload.topogram.ok ? "passed" : "failed"}`);
56
+ console.log(`Adoption: ${payload.adoption.summary.appliedItemCount} applied, ${payload.adoption.summary.pendingItemCount} pending, ${payload.adoption.summary.blockedItemCount} blocked`);
57
+ const next = payload.adoption.nextCommand;
58
+ if (next) {
59
+ console.log(`Next: ${next}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @param {string} projectRoot
65
+ * @param {AnyRecord[]} receipts
66
+ * @returns {AnyRecord}
67
+ */
68
+ export function verifyImportAdoptionReceipts(projectRoot, receipts) {
69
+ const topogramRoot = normalizeTopogramPath(projectRoot);
70
+ const files = [];
71
+ for (const receipt of receipts || []) {
72
+ const hashedFiles = Array.isArray(receipt.writtenFileHashes) ? receipt.writtenFileHashes : [];
73
+ const hashedPaths = new Set(hashedFiles.map((/** @type {AnyRecord} */ item) => item.path));
74
+ for (const item of hashedFiles) {
75
+ const relativePath = item.path;
76
+ const filePath = path.join(topogramRoot, relativePath);
77
+ if (!fs.existsSync(filePath)) {
78
+ files.push({
79
+ receiptTimestamp: receipt.timestamp || null,
80
+ selector: receipt.selector || null,
81
+ path: relativePath,
82
+ status: "removed",
83
+ expectedSha256: item.sha256 || null,
84
+ currentSha256: null,
85
+ expectedSize: item.size ?? null,
86
+ currentSize: null
87
+ });
88
+ continue;
89
+ }
90
+ const currentHash = projectFileHash(filePath);
91
+ const matches = item.sha256 === currentHash.sha256 && item.size === currentHash.size;
92
+ files.push({
93
+ receiptTimestamp: receipt.timestamp || null,
94
+ selector: receipt.selector || null,
95
+ path: relativePath,
96
+ status: matches ? "matched" : "changed",
97
+ expectedSha256: item.sha256 || null,
98
+ currentSha256: currentHash.sha256,
99
+ expectedSize: item.size ?? null,
100
+ currentSize: currentHash.size
101
+ });
102
+ }
103
+ for (const relativePath of receipt.writtenFiles || []) {
104
+ if (hashedPaths.has(relativePath)) {
105
+ continue;
106
+ }
107
+ files.push({
108
+ receiptTimestamp: receipt.timestamp || null,
109
+ selector: receipt.selector || null,
110
+ path: relativePath,
111
+ status: "unverifiable",
112
+ expectedSha256: null,
113
+ currentSha256: null,
114
+ expectedSize: null,
115
+ currentSize: null
116
+ });
117
+ }
118
+ }
119
+ const summary = {
120
+ checkedFileCount: files.length,
121
+ matchedFileCount: files.filter((item) => item.status === "matched").length,
122
+ changedFileCount: files.filter((item) => item.status === "changed").length,
123
+ removedFileCount: files.filter((item) => item.status === "removed").length,
124
+ unverifiableFileCount: files.filter((item) => item.status === "unverifiable").length
125
+ };
126
+ const status = summary.changedFileCount > 0 || summary.removedFileCount > 0
127
+ ? "changed"
128
+ : summary.unverifiableFileCount > 0
129
+ ? "unverifiable"
130
+ : "matched";
131
+ return {
132
+ status,
133
+ summary,
134
+ files,
135
+ auditOnly: true,
136
+ note: "History verification is audit-only. Imported/adopted Topogram files are project-owned, and edits do not make the workspace invalid."
137
+ };
138
+ }
139
+
140
+ /**
141
+ * @param {string} inputPath
142
+ * @param {{ verify?: boolean }} [options]
143
+ * @returns {AnyRecord}
144
+ */
145
+ export function buildBrownfieldImportHistoryPayload(inputPath, options = {}) {
146
+ const projectRoot = normalizeProjectRoot(inputPath);
147
+ const historyPath = importAdoptionsPath(projectRoot);
148
+ const receipts = readImportAdoptionReceipts(projectRoot);
149
+ const forcedWrites = receipts.filter((receipt) => receipt.forced);
150
+ const verification = options.verify ? verifyImportAdoptionReceipts(projectRoot, receipts) : null;
151
+ return {
152
+ ok: true,
153
+ projectRoot,
154
+ path: historyPath,
155
+ exists: fs.existsSync(historyPath),
156
+ verified: Boolean(options.verify),
157
+ summary: {
158
+ receiptCount: receipts.length,
159
+ writeCount: receipts.filter((receipt) => receipt.mode === "write").length,
160
+ forcedWriteCount: forcedWrites.length,
161
+ lastTimestamp: receipts[receipts.length - 1]?.timestamp || null,
162
+ lastSelector: receipts[receipts.length - 1]?.selector || null
163
+ },
164
+ verification,
165
+ receipts
166
+ };
167
+ }
168
+
169
+ /**
170
+ * @param {AnyRecord} payload
171
+ * @returns {void}
172
+ */
173
+ export function printBrownfieldImportHistory(payload) {
174
+ console.log(`Import adoption history for ${payload.projectRoot}`);
175
+ console.log(`Receipts: ${payload.summary.receiptCount}`);
176
+ console.log(`Forced writes: ${payload.summary.forcedWriteCount}`);
177
+ if (!payload.exists) {
178
+ console.log(`No history file found at ${payload.path}.`);
179
+ return;
180
+ }
181
+ for (const receipt of payload.receipts) {
182
+ const forced = receipt.forced ? " forced" : "";
183
+ const reason = receipt.reason ? ` reason="${receipt.reason}"` : "";
184
+ console.log(`- ${receipt.timestamp}: ${receipt.selector}${forced}, ${receipt.writtenFiles?.length || 0} file(s), source=${receipt.sourceProvenance?.status || "unknown"}${reason}`);
185
+ }
186
+ if (payload.verification) {
187
+ const summary = payload.verification.summary;
188
+ console.log("");
189
+ console.log(`Verification: ${payload.verification.status}`);
190
+ console.log(`Matched: ${summary.matchedFileCount}; changed: ${summary.changedFileCount}; removed: ${summary.removedFileCount}; unverifiable: ${summary.unverifiableFileCount}`);
191
+ for (const file of payload.verification.files.filter((/** @type {AnyRecord} */ item) => item.status !== "matched")) {
192
+ console.log(`- ${file.path}: ${file.status}`);
193
+ }
194
+ console.log(payload.verification.note);
195
+ }
196
+ }