@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,512 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import { writeTemplateTrustRecord } from "../template-trust.js";
7
+ import { TEMPLATE_FILES_MANIFEST } from "./constants.js";
8
+ import { currentTemplateMetadata, projectTemplateMetadata } from "./metadata.js";
9
+ import { resolveTemplate } from "./template-resolution.js";
10
+ import { issueMessagesFromDiagnostics, templatePolicyDiagnosticsForProject, templateUpdateDiagnostic } from "./template-policy.js";
11
+ import { candidateTemplateFiles, currentTemplateOwnedFileHashes, currentTemplateOwnedFiles, fileHash, fileMatchesBaseline, fileSnapshot, includesTemplateImplementation, normalizeTemplateUpdateActionPath, readTemplateFilesManifest, unifiedTextDiff, updateTemplateFilesManifestRecord, writeCandidateFile, writeTemplateFilesManifest } from "./template-snapshots.js";
12
+
13
+ /** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
14
+ /** @typedef {import("./types.js").TemplateUpdatePlanOptions} TemplateUpdatePlanOptions */
15
+ /** @typedef {import("./types.js").TemplateUpdateFileActionOptions} TemplateUpdateFileActionOptions */
16
+ /** @typedef {import("./types.js").TemplateOwnedFileRecord} TemplateOwnedFileRecord */
17
+ /** @typedef {import("./types.js").TemplateManifest} TemplateManifest */
18
+ /** @typedef {import("./types.js").TemplateTopologySummary} TemplateTopologySummary */
19
+ /** @typedef {import("./types.js").TemplatePolicy} TemplatePolicy */
20
+ /** @typedef {import("./types.js").TemplatePolicyInfo} TemplatePolicyInfo */
21
+ /** @typedef {import("./types.js").TemplateUpdateDiagnostic} TemplateUpdateDiagnostic */
22
+ /** @typedef {import("./types.js").ResolvedTemplate} ResolvedTemplate */
23
+ /** @typedef {import("./types.js").CatalogTemplateProvenance} CatalogTemplateProvenance */
24
+
25
+ /**
26
+ * @param {TemplateUpdatePlanOptions} options
27
+ * @returns {{ ok: boolean, mode: "plan", writes: false, current: { id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null }, candidate: { id: string, version: string, source: string, sourceSpec: string, requested: string }, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: { added: number, changed: number, currentOnly: number, unchanged: number }, files: Array<{ path: string, kind: "added"|"changed"|"current-only"|"unchanged", current: { sha256: string, size: number }|null, candidate: { sha256: string, size: number }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }> }}
28
+ */
29
+ export function buildTemplateUpdatePlan({
30
+ projectRoot,
31
+ projectConfig,
32
+ templateName = null,
33
+ templatesRoot
34
+ }) {
35
+ const currentTemplate = projectConfig.template || {};
36
+ const templateSpec = templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
37
+ if (!templateSpec || typeof templateSpec !== "string") {
38
+ throw new Error("Cannot plan template update because topogram.project.json has no template source spec.");
39
+ }
40
+ const candidateTemplate = resolveTemplate(templateSpec, templatesRoot);
41
+ const candidateMetadata = projectTemplateMetadata(candidateTemplate);
42
+ /** @type {TemplateUpdateDiagnostic[]} */
43
+ const diagnostics = templatePolicyDiagnosticsForProject(projectRoot, candidateTemplate, "policy");
44
+ if (currentTemplate.id && currentTemplate.id !== candidateMetadata.id) {
45
+ diagnostics.push(templateUpdateDiagnostic({
46
+ code: "template_id_mismatch",
47
+ message: `Candidate template id '${candidateMetadata.id}' does not match current template id '${currentTemplate.id}'.`,
48
+ path: path.join(projectRoot, "topogram.project.json"),
49
+ suggestedFix: "Use a template with the same id, or create a new project from the other template.",
50
+ step: "resolve-candidate"
51
+ }));
52
+ }
53
+ const candidateFiles = candidateTemplateFiles(candidateTemplate, projectConfig);
54
+ const currentFiles = currentTemplateOwnedFiles(
55
+ projectRoot,
56
+ Boolean(includesTemplateImplementation(projectConfig) || candidateMetadata.includesExecutableImplementation),
57
+ projectConfig
58
+ );
59
+ const allPaths = new Set([...candidateFiles.keys(), ...currentFiles.keys()]);
60
+ /** @type {Array<{ path: string, kind: "added"|"changed"|"current-only"|"unchanged", current: { sha256: string, size: number }|null, candidate: { sha256: string, size: number }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }>} */
61
+ const files = [];
62
+
63
+ for (const relativePath of [...allPaths].sort((a, b) => a.localeCompare(b))) {
64
+ const candidateFile = candidateFiles.get(relativePath) || null;
65
+ const currentFile = currentFiles.get(relativePath) || null;
66
+ const candidateSnapshot = candidateFile
67
+ ? fileSnapshot(candidateFile.absolutePath, candidateFile.content)
68
+ : null;
69
+ const currentSnapshot = currentFile
70
+ ? fileSnapshot(currentFile.absolutePath, currentFile.content)
71
+ : null;
72
+ let kind = /** @type {"added"|"changed"|"current-only"|"unchanged"} */ ("unchanged");
73
+ if (!currentSnapshot && candidateSnapshot) {
74
+ kind = "added";
75
+ } else if (currentSnapshot && !candidateSnapshot) {
76
+ kind = "current-only";
77
+ } else if (currentSnapshot && candidateSnapshot && (
78
+ currentSnapshot.sha256 !== candidateSnapshot.sha256 ||
79
+ currentSnapshot.size !== candidateSnapshot.size
80
+ )) {
81
+ kind = "changed";
82
+ }
83
+ const binary = Boolean(currentSnapshot?.binary || candidateSnapshot?.binary);
84
+ const diffOmitted = binary || Boolean(currentSnapshot?.diffOmitted || candidateSnapshot?.diffOmitted);
85
+ files.push({
86
+ path: relativePath,
87
+ kind,
88
+ current: currentSnapshot ? { sha256: currentSnapshot.sha256, size: currentSnapshot.size } : null,
89
+ candidate: candidateSnapshot ? { sha256: candidateSnapshot.sha256, size: candidateSnapshot.size } : null,
90
+ binary,
91
+ diffOmitted,
92
+ unifiedDiff: diffOmitted
93
+ ? null
94
+ : unifiedTextDiff(relativePath, currentSnapshot?.text || null, candidateSnapshot?.text || null)
95
+ });
96
+ }
97
+ const visibleFiles = files.filter((file) => file.kind !== "unchanged");
98
+ const summary = {
99
+ added: visibleFiles.filter((file) => file.kind === "added").length,
100
+ changed: visibleFiles.filter((file) => file.kind === "changed").length,
101
+ currentOnly: visibleFiles.filter((file) => file.kind === "current-only").length,
102
+ unchanged: files.filter((file) => file.kind === "unchanged").length
103
+ };
104
+ const issues = issueMessagesFromDiagnostics(diagnostics);
105
+ return {
106
+ ok: issues.length === 0,
107
+ mode: "plan",
108
+ writes: false,
109
+ current: currentTemplateMetadata(projectConfig),
110
+ candidate: {
111
+ id: candidateMetadata.id,
112
+ version: candidateMetadata.version,
113
+ source: candidateMetadata.source,
114
+ sourceSpec: candidateMetadata.sourceSpec,
115
+ requested: candidateMetadata.requested
116
+ },
117
+ compatible: issues.length === 0,
118
+ issues,
119
+ diagnostics,
120
+ summary,
121
+ files: visibleFiles
122
+ };
123
+ }
124
+
125
+ /**
126
+ * @param {TemplateUpdatePlanOptions} options
127
+ * @returns {{ ok: boolean, mode: "check", writes: false, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
128
+ */
129
+ export function buildTemplateUpdateCheck(options) {
130
+ const plan = buildTemplateUpdatePlan(options);
131
+ const diagnostics = [...plan.diagnostics];
132
+ if (plan.ok && plan.files.length > 0) {
133
+ diagnostics.push(templateUpdateDiagnostic({
134
+ code: "template_update_available",
135
+ message: `Template update has ${plan.files.length} template-owned file change(s).`,
136
+ path: options.projectRoot,
137
+ suggestedFix: "Run `topogram template update --plan` to review, then `topogram template update --apply` after approval.",
138
+ step: "check"
139
+ }));
140
+ }
141
+ const issues = issueMessagesFromDiagnostics(diagnostics);
142
+ return {
143
+ ...plan,
144
+ ok: issues.length === 0,
145
+ mode: "check",
146
+ writes: false,
147
+ issues,
148
+ diagnostics
149
+ };
150
+ }
151
+
152
+ /**
153
+ * @param {TemplateUpdatePlanOptions} options
154
+ * @param {ReturnType<typeof buildTemplateUpdatePlan>} plan
155
+ * @param {"apply"|"status"} mode
156
+ * @returns {{ diagnostics: TemplateUpdateDiagnostic[], issues: string[], skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }> }}
157
+ */
158
+ function analyzeTemplateUpdateApplication(options, plan, mode) {
159
+ /** @type {Array<{ path: string, kind: "current-only", reason: string }>} */
160
+ const skipped = [];
161
+ /** @type {Array<{ path: string, reason: string }>} */
162
+ const conflicts = [];
163
+ /** @type {TemplateUpdateDiagnostic[]} */
164
+ const diagnostics = [...plan.diagnostics];
165
+ if (!plan.ok) {
166
+ return {
167
+ diagnostics,
168
+ issues: issueMessagesFromDiagnostics(diagnostics),
169
+ skipped,
170
+ conflicts
171
+ };
172
+ }
173
+
174
+ const baselineManifest = readTemplateFilesManifest(options.projectRoot);
175
+ if (!baselineManifest) {
176
+ diagnostics.push(templateUpdateDiagnostic({
177
+ code: "template_baseline_missing",
178
+ message: `Cannot apply template update because ${TEMPLATE_FILES_MANIFEST} is missing. Review current template-owned files, then run 'topogram trust template' to record the baseline before applying template updates.`,
179
+ path: path.join(options.projectRoot, TEMPLATE_FILES_MANIFEST),
180
+ suggestedFix: "Review current template-owned files, then run `topogram trust template` to record the baseline before applying template updates.",
181
+ step: "baseline"
182
+ }));
183
+ }
184
+ const baselineByPath = new Map((baselineManifest?.files || []).map((file) => [file.path, file]));
185
+ const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
186
+ for (const file of plan.files) {
187
+ if (file.kind === "current-only") {
188
+ skipped.push({
189
+ path: file.path,
190
+ kind: "current-only",
191
+ reason: "Deletes are not applied by template update --apply in this milestone."
192
+ });
193
+ diagnostics.push(templateUpdateDiagnostic({
194
+ code: "template_current_only_skipped",
195
+ severity: "warning",
196
+ message: `Current-only file '${file.path}' needs manual delete review. Deletes are not applied by template update --apply in this milestone.`,
197
+ path: path.join(options.projectRoot, file.path),
198
+ suggestedFix: "Delete the file manually after review if it should be removed from this project.",
199
+ step: mode
200
+ }));
201
+ continue;
202
+ }
203
+ if (file.kind !== "added" && file.kind !== "changed") {
204
+ continue;
205
+ }
206
+ const baseline = baselineByPath.get(file.path) || null;
207
+ const currentHash = currentHashes.get(file.path) || null;
208
+ if (!fileMatchesBaseline(baseline, currentHash)) {
209
+ const reason = baseline
210
+ ? "Current file differs from the last trusted template-owned baseline."
211
+ : "Current file is not part of the trusted template-owned baseline.";
212
+ conflicts.push({
213
+ path: file.path,
214
+ reason
215
+ });
216
+ diagnostics.push(templateUpdateDiagnostic({
217
+ code: "template_update_conflict",
218
+ message: `Template update conflict in '${file.path}': ${reason}`,
219
+ path: path.join(options.projectRoot, file.path),
220
+ suggestedFix: "Review local edits; keep them manually or refresh the baseline with `topogram trust template` after review.",
221
+ step: "conflict-check"
222
+ }));
223
+ }
224
+ }
225
+ return {
226
+ diagnostics,
227
+ issues: issueMessagesFromDiagnostics(diagnostics),
228
+ skipped,
229
+ conflicts
230
+ };
231
+ }
232
+
233
+ /**
234
+ * @param {TemplateUpdatePlanOptions} options
235
+ * @returns {{ ok: boolean, mode: "status", writes: false, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
236
+ */
237
+ export function buildTemplateUpdateStatus(options) {
238
+ const plan = buildTemplateUpdatePlan(options);
239
+ const analysis = analyzeTemplateUpdateApplication(options, plan, "status");
240
+ const diagnostics = [...analysis.diagnostics];
241
+ if (plan.ok && plan.files.length > 0) {
242
+ diagnostics.push(templateUpdateDiagnostic({
243
+ code: "template_update_available",
244
+ message: `Template update has ${plan.files.length} template-owned file change(s).`,
245
+ path: options.projectRoot,
246
+ suggestedFix: "Run `topogram template update --plan` to review, then `topogram template update --apply` after approval.",
247
+ step: "status"
248
+ }));
249
+ }
250
+ const issues = issueMessagesFromDiagnostics(diagnostics);
251
+ return {
252
+ ...plan,
253
+ ok: issues.length === 0,
254
+ mode: "status",
255
+ writes: false,
256
+ issues,
257
+ diagnostics,
258
+ applied: [],
259
+ skipped: analysis.skipped,
260
+ conflicts: analysis.conflicts
261
+ };
262
+ }
263
+
264
+ /**
265
+ * @param {string} projectRoot
266
+ * @param {string} action
267
+ * @returns {TemplateUpdateDiagnostic}
268
+ */
269
+ function templateBaselineMissingDiagnostic(projectRoot, action) {
270
+ return templateUpdateDiagnostic({
271
+ code: "template_baseline_missing",
272
+ message: `Cannot ${action} because ${TEMPLATE_FILES_MANIFEST} is missing. Review current template-owned files, then run 'topogram trust template' to record the baseline before applying template updates.`,
273
+ path: path.join(projectRoot, TEMPLATE_FILES_MANIFEST),
274
+ suggestedFix: "Review current template-owned files, then run `topogram trust template` to record the baseline before applying template updates.",
275
+ step: "baseline"
276
+ });
277
+ }
278
+
279
+ /**
280
+ * @param {TemplateUpdateDiagnostic[]} diagnostics
281
+ * @param {ReturnType<typeof buildTemplateUpdatePlan>|null} plan
282
+ * @param {TemplateUpdateFileActionOptions["action"]} action
283
+ * @param {string} relativePath
284
+ * @param {Array<{ path: string, kind: "added"|"changed" }>} applied
285
+ * @param {Array<{ path: string, kind: "accepted-current" }>} accepted
286
+ * @param {Array<{ path: string, kind: "current-only" }>} deleted
287
+ * @param {Array<{ path: string, reason: string }>} conflicts
288
+ * @param {ReturnType<typeof currentTemplateMetadata>} [current]
289
+ * @returns {{ ok: boolean, mode: TemplateUpdateFileActionOptions["action"], writes: boolean, current: ReturnType<typeof currentTemplateMetadata>, candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"]|null, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, accepted: Array<{ path: string, kind: "accepted-current" }>, deleted: Array<{ path: string, kind: "current-only" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"], action: TemplateUpdateFileActionOptions["action"], path: string }}
290
+ */
291
+ function templateUpdateFileActionResult(diagnostics, plan, action, relativePath, applied, accepted, deleted, conflicts, current = { id: null, version: null, source: null, sourceSpec: null, requested: null }) {
292
+ const issues = issueMessagesFromDiagnostics(diagnostics);
293
+ return {
294
+ ...(plan || {}),
295
+ ok: issues.length === 0,
296
+ mode: action,
297
+ writes: applied.length > 0 || accepted.length > 0 || deleted.length > 0,
298
+ current: plan?.current || current,
299
+ candidate: plan?.candidate || null,
300
+ compatible: plan?.compatible || issues.length === 0,
301
+ issues,
302
+ diagnostics,
303
+ summary: plan?.summary || { added: 0, changed: 0, currentOnly: 0, unchanged: 0 },
304
+ applied,
305
+ accepted,
306
+ deleted,
307
+ skipped: [],
308
+ conflicts,
309
+ files: plan?.files || [],
310
+ action,
311
+ path: relativePath
312
+ };
313
+ }
314
+
315
+ /**
316
+ * @param {TemplateUpdateFileActionOptions} options
317
+ * @returns {{ ok: boolean, mode: "accept-current"|"accept-candidate"|"delete-current", writes: boolean, current: ReturnType<typeof currentTemplateMetadata>, candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"]|null, compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, accepted: Array<{ path: string, kind: "accepted-current" }>, deleted: Array<{ path: string, kind: "current-only" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"], action: "accept-current"|"accept-candidate"|"delete-current", path: string }}
318
+ */
319
+ export function applyTemplateUpdateFileAction(options) {
320
+ const relativePath = normalizeTemplateUpdateActionPath(options.filePath);
321
+ /** @type {TemplateUpdateDiagnostic[]} */
322
+ const diagnostics = [];
323
+ /** @type {Array<{ path: string, kind: "added"|"changed" }>} */
324
+ const applied = [];
325
+ /** @type {Array<{ path: string, kind: "accepted-current" }>} */
326
+ const accepted = [];
327
+ /** @type {Array<{ path: string, kind: "current-only" }>} */
328
+ const deleted = [];
329
+ /** @type {Array<{ path: string, reason: string }>} */
330
+ const conflicts = [];
331
+ const baselineManifest = readTemplateFilesManifest(options.projectRoot);
332
+ const current = currentTemplateMetadata(options.projectConfig);
333
+ if (!baselineManifest) {
334
+ diagnostics.push(templateBaselineMissingDiagnostic(options.projectRoot, options.action));
335
+ return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
336
+ }
337
+
338
+ if (options.action === "accept-current") {
339
+ const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
340
+ const currentHash = currentHashes.get(relativePath) || null;
341
+ if (!currentHash) {
342
+ diagnostics.push(templateUpdateDiagnostic({
343
+ code: "template_file_not_current",
344
+ message: `Cannot accept current file '${relativePath}' because it is not a current template-owned file.`,
345
+ path: path.join(options.projectRoot, relativePath),
346
+ suggestedFix: "Pass a file under topogram/, topogram.project.json, or trusted implementation/.",
347
+ step: "accept-current"
348
+ }));
349
+ return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
350
+ }
351
+ updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, currentHash);
352
+ accepted.push({ path: relativePath, kind: "accepted-current" });
353
+ return templateUpdateFileActionResult(diagnostics, null, options.action, relativePath, applied, accepted, deleted, conflicts, current);
354
+ }
355
+
356
+ const plan = buildTemplateUpdatePlan(options);
357
+ diagnostics.push(...plan.diagnostics);
358
+ if (!plan.ok) {
359
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
360
+ }
361
+ const file = plan.files.find((item) => item.path === relativePath) || null;
362
+ if (!file) {
363
+ diagnostics.push(templateUpdateDiagnostic({
364
+ code: "template_file_unchanged",
365
+ message: `Template-owned file '${relativePath}' has no candidate update action.`,
366
+ path: path.join(options.projectRoot, relativePath),
367
+ suggestedFix: "Run `topogram template update --status` to see files that need adoption.",
368
+ step: options.action
369
+ }));
370
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
371
+ }
372
+
373
+ const baselineByPath = new Map(baselineManifest.files.map((record) => [record.path, record]));
374
+ const currentHashes = currentTemplateOwnedFileHashes(options.projectRoot, options.projectConfig);
375
+ const baseline = baselineByPath.get(relativePath) || null;
376
+ const currentHash = currentHashes.get(relativePath) || null;
377
+
378
+ if (options.action === "delete-current") {
379
+ if (file.kind !== "current-only") {
380
+ diagnostics.push(templateUpdateDiagnostic({
381
+ code: "template_delete_not_current_only",
382
+ message: `Cannot delete '${relativePath}' because it is not a current-only template-owned file.`,
383
+ path: path.join(options.projectRoot, relativePath),
384
+ suggestedFix: "Use delete-current only for files the candidate template removed.",
385
+ step: "delete-current"
386
+ }));
387
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
388
+ }
389
+ if (!fileMatchesBaseline(baseline, currentHash)) {
390
+ const reason = baseline
391
+ ? "Current file differs from the last trusted template-owned baseline."
392
+ : "Current file is not part of the trusted template-owned baseline.";
393
+ conflicts.push({ path: relativePath, reason });
394
+ diagnostics.push(templateUpdateDiagnostic({
395
+ code: "template_update_conflict",
396
+ message: `Template delete conflict in '${relativePath}': ${reason}`,
397
+ path: path.join(options.projectRoot, relativePath),
398
+ suggestedFix: "Review local edits before deleting, or accept current as the new baseline.",
399
+ step: "delete-current"
400
+ }));
401
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
402
+ }
403
+ fs.rmSync(path.join(options.projectRoot, relativePath));
404
+ updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, null);
405
+ deleted.push({ path: relativePath, kind: "current-only" });
406
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
407
+ }
408
+
409
+ if (file.kind !== "added" && file.kind !== "changed") {
410
+ diagnostics.push(templateUpdateDiagnostic({
411
+ code: "template_candidate_not_applicable",
412
+ message: `Cannot accept candidate for '${relativePath}' because the candidate has no added or changed file.`,
413
+ path: path.join(options.projectRoot, relativePath),
414
+ suggestedFix: "Use accept-candidate only for added or changed candidate files.",
415
+ step: "accept-candidate"
416
+ }));
417
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
418
+ }
419
+ if (file.kind === "changed" && !fileMatchesBaseline(baseline, currentHash)) {
420
+ const reason = baseline
421
+ ? "Current file differs from the last trusted template-owned baseline."
422
+ : "Current file is not part of the trusted template-owned baseline.";
423
+ conflicts.push({ path: relativePath, reason });
424
+ diagnostics.push(templateUpdateDiagnostic({
425
+ code: "template_update_conflict",
426
+ message: `Template candidate conflict in '${relativePath}': ${reason}`,
427
+ path: path.join(options.projectRoot, relativePath),
428
+ suggestedFix: "Review local edits before accepting the candidate file.",
429
+ step: "accept-candidate"
430
+ }));
431
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
432
+ }
433
+ const currentTemplate = options.projectConfig.template || {};
434
+ const templateSpec = options.templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
435
+ const candidateTemplate = resolveTemplate(templateSpec, options.templatesRoot);
436
+ const candidateFile = candidateTemplateFiles(candidateTemplate, options.projectConfig).get(relativePath);
437
+ if (!candidateFile) {
438
+ throw new Error(`Cannot accept missing candidate template file: ${relativePath}`);
439
+ }
440
+ writeCandidateFile(candidateFile, path.join(options.projectRoot, relativePath));
441
+ const nextHash = fileHash({
442
+ absolutePath: path.join(options.projectRoot, relativePath),
443
+ content: null
444
+ });
445
+ updateTemplateFilesManifestRecord(options.projectRoot, baselineManifest, relativePath, {
446
+ path: relativePath,
447
+ sha256: nextHash.sha256,
448
+ size: nextHash.size
449
+ });
450
+ applied.push({ path: relativePath, kind: file.kind });
451
+ return templateUpdateFileActionResult(diagnostics, plan, options.action, relativePath, applied, accepted, deleted, conflicts);
452
+ }
453
+
454
+ /**
455
+ * @param {TemplateUpdatePlanOptions} options
456
+ * @returns {{ ok: boolean, mode: "apply", writes: boolean, current: ReturnType<typeof buildTemplateUpdatePlan>["current"], candidate: ReturnType<typeof buildTemplateUpdatePlan>["candidate"], compatible: boolean, issues: string[], diagnostics: TemplateUpdateDiagnostic[], summary: ReturnType<typeof buildTemplateUpdatePlan>["summary"], applied: Array<{ path: string, kind: "added"|"changed" }>, skipped: Array<{ path: string, kind: "current-only", reason: string }>, conflicts: Array<{ path: string, reason: string }>, files: ReturnType<typeof buildTemplateUpdatePlan>["files"] }}
457
+ */
458
+ export function applyTemplateUpdate(options) {
459
+ const plan = buildTemplateUpdatePlan(options);
460
+ /** @type {Array<{ path: string, kind: "added"|"changed" }>} */
461
+ const applied = [];
462
+ const analysis = analyzeTemplateUpdateApplication(options, plan, "apply");
463
+ const { diagnostics, issues, skipped, conflicts } = analysis;
464
+ if (!plan.ok || issues.length > 0) {
465
+ return {
466
+ ...plan,
467
+ ok: false,
468
+ mode: "apply",
469
+ writes: false,
470
+ applied,
471
+ skipped,
472
+ conflicts,
473
+ issues,
474
+ diagnostics
475
+ };
476
+ }
477
+
478
+ const currentTemplate = options.projectConfig.template || {};
479
+ const templateSpec = options.templateName || currentTemplate.sourceSpec || currentTemplate.requested || currentTemplate.id;
480
+ const candidateTemplate = resolveTemplate(templateSpec, options.templatesRoot);
481
+ const candidateFiles = candidateTemplateFiles(candidateTemplate, options.projectConfig);
482
+ for (const file of plan.files) {
483
+ if (file.kind !== "added" && file.kind !== "changed") {
484
+ continue;
485
+ }
486
+ const candidateFile = candidateFiles.get(file.path);
487
+ if (!candidateFile) {
488
+ throw new Error(`Cannot apply missing candidate template file: ${file.path}`);
489
+ }
490
+ writeCandidateFile(candidateFile, path.join(options.projectRoot, file.path));
491
+ applied.push({ path: file.path, kind: file.kind });
492
+ }
493
+
494
+ if (applied.length > 0) {
495
+ const nextProjectConfig = JSON.parse(fs.readFileSync(path.join(options.projectRoot, "topogram.project.json"), "utf8"));
496
+ writeTemplateFilesManifest(options.projectRoot, nextProjectConfig);
497
+ if (nextProjectConfig.implementation) {
498
+ writeTemplateTrustRecord(options.projectRoot, nextProjectConfig);
499
+ }
500
+ }
501
+ return {
502
+ ...plan,
503
+ ok: true,
504
+ mode: "apply",
505
+ writes: applied.length > 0,
506
+ issues,
507
+ diagnostics,
508
+ applied,
509
+ skipped,
510
+ conflicts
511
+ };
512
+ }
@@ -0,0 +1,83 @@
1
+ export interface CreateNewProjectOptions {
2
+ targetPath: string;
3
+ templateName?: string;
4
+ engineRoot: string;
5
+ templatesRoot: string;
6
+ templateProvenance?: CatalogTemplateProvenance | null;
7
+ }
8
+
9
+ export interface TemplateUpdatePlanOptions {
10
+ projectRoot: string;
11
+ projectConfig: Record<string, any>;
12
+ templateName?: string | null;
13
+ templatesRoot: string;
14
+ }
15
+
16
+ export type TemplateUpdateFileActionOptions = TemplateUpdatePlanOptions & {
17
+ filePath: string;
18
+ action: "accept-current" | "accept-candidate" | "delete-current";
19
+ };
20
+
21
+ export interface TemplateOwnedFileRecord {
22
+ path: string;
23
+ sha256: string;
24
+ size: number;
25
+ }
26
+
27
+ export interface TemplateManifest {
28
+ id: string;
29
+ version: string;
30
+ kind: string;
31
+ topogramVersion: string;
32
+ includesExecutableImplementation?: boolean;
33
+ description?: string;
34
+ starterScripts?: Record<string, string>;
35
+ }
36
+
37
+ export interface TemplateTopologySummary {
38
+ surfaces: string[];
39
+ generators: string[];
40
+ stack: string;
41
+ }
42
+
43
+ export interface TemplatePolicy {
44
+ version: string;
45
+ allowedSources: Array<"local" | "package">;
46
+ allowedTemplateIds: string[];
47
+ allowedPackageScopes?: string[];
48
+ executableImplementation: "allow" | "warn" | "deny";
49
+ pinnedVersions?: Record<string, string>;
50
+ }
51
+
52
+ export interface TemplatePolicyInfo {
53
+ path: string;
54
+ policy: TemplatePolicy | null;
55
+ exists: boolean;
56
+ diagnostics: TemplateUpdateDiagnostic[];
57
+ }
58
+
59
+ export interface TemplateUpdateDiagnostic {
60
+ code: string;
61
+ severity: "error" | "warning";
62
+ message: string;
63
+ path: string | null;
64
+ suggestedFix: string | null;
65
+ step: string | null;
66
+ }
67
+
68
+ export interface ResolvedTemplate {
69
+ requested: string;
70
+ root: string;
71
+ manifest: TemplateManifest;
72
+ source: "local" | "package";
73
+ packageSpec: string | null;
74
+ }
75
+
76
+ export interface CatalogTemplateProvenance {
77
+ id: string;
78
+ source: string;
79
+ package: string;
80
+ version: string;
81
+ packageSpec: string;
82
+ includesExecutableImplementation?: boolean;
83
+ }