@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,430 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import crypto from "node:crypto";
5
+ import path from "node:path";
6
+
7
+ import { MAX_TEXT_DIFF_BYTES, TEMPLATE_FILES_MANIFEST } from "./constants.js";
8
+ import { stableJsonStringify } from "./json.js";
9
+ import { candidateProjectTemplateMetadata } from "./metadata.js";
10
+
11
+ /** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
12
+ /** @typedef {import("./types.js").TemplateUpdatePlanOptions} TemplateUpdatePlanOptions */
13
+ /** @typedef {import("./types.js").TemplateUpdateFileActionOptions} TemplateUpdateFileActionOptions */
14
+ /** @typedef {import("./types.js").TemplateOwnedFileRecord} TemplateOwnedFileRecord */
15
+ /** @typedef {import("./types.js").TemplateManifest} TemplateManifest */
16
+ /** @typedef {import("./types.js").TemplateTopologySummary} TemplateTopologySummary */
17
+ /** @typedef {import("./types.js").TemplatePolicy} TemplatePolicy */
18
+ /** @typedef {import("./types.js").TemplatePolicyInfo} TemplatePolicyInfo */
19
+ /** @typedef {import("./types.js").TemplateUpdateDiagnostic} TemplateUpdateDiagnostic */
20
+ /** @typedef {import("./types.js").ResolvedTemplate} ResolvedTemplate */
21
+ /** @typedef {import("./types.js").CatalogTemplateProvenance} CatalogTemplateProvenance */
22
+
23
+ /**
24
+ * @param {string} filePath
25
+ * @returns {string}
26
+ */
27
+ export function normalizeTemplateUpdateActionPath(filePath) {
28
+ const normalized = path.posix.normalize(filePath.replace(/\\/g, "/"));
29
+ if (
30
+ !filePath ||
31
+ path.isAbsolute(filePath) ||
32
+ normalized === "." ||
33
+ normalized.startsWith("../") ||
34
+ normalized === ".."
35
+ ) {
36
+ throw new Error(`Template update action requires a relative template-owned file path: ${filePath || "(missing)"}`);
37
+ }
38
+ return normalized;
39
+ }
40
+
41
+ /**
42
+ * @param {any} bytes
43
+ * @returns {boolean}
44
+ */
45
+ function isLikelyText(bytes) {
46
+ if (bytes.includes(0)) {
47
+ return false;
48
+ }
49
+ const length = Math.min(bytes.length, 4096);
50
+ let suspicious = 0;
51
+ for (let index = 0; index < length; index += 1) {
52
+ const byte = bytes[index];
53
+ if (byte === 9 || byte === 10 || byte === 13) {
54
+ continue;
55
+ }
56
+ if (byte < 32 || byte === 127) {
57
+ suspicious += 1;
58
+ }
59
+ }
60
+ return length === 0 || suspicious / length < 0.02;
61
+ }
62
+
63
+ /**
64
+ * @param {string} text
65
+ * @returns {string[]}
66
+ */
67
+ function linesForDiff(text) {
68
+ const lines = text.split("\n");
69
+ if (lines.at(-1) === "") {
70
+ lines.pop();
71
+ }
72
+ return lines;
73
+ }
74
+
75
+ /**
76
+ * @param {string[]} before
77
+ * @param {string[]} after
78
+ * @returns {Array<{ type: "same"|"added"|"removed", text: string }>}
79
+ */
80
+ function diffLines(before, after) {
81
+ const rows = before.length;
82
+ const columns = after.length;
83
+ /** @type {number[][]} */
84
+ const table = Array.from({ length: rows + 1 }, () => Array(columns + 1).fill(0));
85
+ for (let row = rows - 1; row >= 0; row -= 1) {
86
+ for (let column = columns - 1; column >= 0; column -= 1) {
87
+ table[row][column] = before[row] === after[column]
88
+ ? table[row + 1][column + 1] + 1
89
+ : Math.max(table[row + 1][column], table[row][column + 1]);
90
+ }
91
+ }
92
+ /** @type {Array<{ type: "same"|"added"|"removed", text: string }>} */
93
+ const changes = [];
94
+ let row = 0;
95
+ let column = 0;
96
+ while (row < rows && column < columns) {
97
+ if (before[row] === after[column]) {
98
+ changes.push({ type: "same", text: before[row] });
99
+ row += 1;
100
+ column += 1;
101
+ } else if (table[row + 1][column] >= table[row][column + 1]) {
102
+ changes.push({ type: "removed", text: before[row] });
103
+ row += 1;
104
+ } else {
105
+ changes.push({ type: "added", text: after[column] });
106
+ column += 1;
107
+ }
108
+ }
109
+ while (row < rows) {
110
+ changes.push({ type: "removed", text: before[row] });
111
+ row += 1;
112
+ }
113
+ while (column < columns) {
114
+ changes.push({ type: "added", text: after[column] });
115
+ column += 1;
116
+ }
117
+ return changes;
118
+ }
119
+
120
+ /**
121
+ * @param {string} relativePath
122
+ * @param {string|null} beforeText
123
+ * @param {string|null} afterText
124
+ * @returns {string|null}
125
+ */
126
+ export function unifiedTextDiff(relativePath, beforeText, afterText) {
127
+ if (beforeText === null && afterText === null) {
128
+ return null;
129
+ }
130
+ const beforeLines = beforeText === null ? [] : linesForDiff(beforeText);
131
+ const afterLines = afterText === null ? [] : linesForDiff(afterText);
132
+ const changes = diffLines(beforeLines, afterLines);
133
+ const lines = [
134
+ `--- current/${relativePath}`,
135
+ `+++ candidate/${relativePath}`,
136
+ `@@ -1,${beforeLines.length} +1,${afterLines.length} @@`
137
+ ];
138
+ for (const change of changes) {
139
+ const prefix = change.type === "added" ? "+" : change.type === "removed" ? "-" : " ";
140
+ lines.push(`${prefix}${change.text}`);
141
+ }
142
+ return `${lines.join("\n")}\n`;
143
+ }
144
+
145
+ /**
146
+ * @param {string} value
147
+ * @returns {number}
148
+ */
149
+ function utf8ByteLength(value) {
150
+ let length = 0;
151
+ for (const char of value) {
152
+ const codePoint = char.codePointAt(0) || 0;
153
+ if (codePoint <= 0x7f) {
154
+ length += 1;
155
+ } else if (codePoint <= 0x7ff) {
156
+ length += 2;
157
+ } else if (codePoint <= 0xffff) {
158
+ length += 3;
159
+ } else {
160
+ length += 4;
161
+ }
162
+ }
163
+ return length;
164
+ }
165
+
166
+ /**
167
+ * @param {string} root
168
+ * @param {string} currentDir
169
+ * @param {string[]} files
170
+ * @returns {void}
171
+ */
172
+ function collectFiles(root, currentDir, files) {
173
+ if (!fs.existsSync(currentDir)) {
174
+ return;
175
+ }
176
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
177
+ if (entry.name === ".DS_Store" || entry.name === "node_modules" || entry.name === ".tmp") {
178
+ continue;
179
+ }
180
+ const entryPath = path.join(currentDir, entry.name);
181
+ if (entry.isSymbolicLink()) {
182
+ throw new Error(`Template-owned files cannot include symlink '${path.relative(root, entryPath).replace(/\\/g, "/")}'. Template-owned files must be real files so Topogram can hash the exact content being trusted. Replace the symlink with a real file, then run topogram trust status, topogram trust diff, and topogram trust template after review.`);
183
+ }
184
+ if (entry.isDirectory()) {
185
+ collectFiles(root, entryPath, files);
186
+ continue;
187
+ }
188
+ if (entry.isFile()) {
189
+ files.push(path.relative(root, entryPath).replace(/\\/g, "/"));
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * @param {string|null} absolutePath
196
+ * @param {string|null} content
197
+ * @returns {{ sha256: string, size: number, text: string|null, binary: boolean, diffOmitted: boolean }|null}
198
+ */
199
+ export function fileSnapshot(absolutePath, content = null) {
200
+ if (!absolutePath && content === null) {
201
+ return null;
202
+ }
203
+ if (content !== null) {
204
+ return {
205
+ sha256: crypto.createHash("sha256").update(content, "utf8").digest("hex"),
206
+ size: utf8ByteLength(content),
207
+ text: content,
208
+ binary: false,
209
+ diffOmitted: false
210
+ };
211
+ }
212
+ const bytes = fs.readFileSync(absolutePath || "");
213
+ const sha256 = crypto.createHash("sha256").update(bytes).digest("hex");
214
+ if (bytes.length > MAX_TEXT_DIFF_BYTES) {
215
+ return { sha256, size: bytes.length, text: null, binary: false, diffOmitted: true };
216
+ }
217
+ if (!isLikelyText(bytes)) {
218
+ return { sha256, size: bytes.length, text: null, binary: true, diffOmitted: false };
219
+ }
220
+ return { sha256, size: bytes.length, text: bytes.toString("utf8"), binary: false, diffOmitted: false };
221
+ }
222
+
223
+ /**
224
+ * @param {{ absolutePath: string|null, content: string|null }} file
225
+ * @returns {{ sha256: string, size: number }}
226
+ */
227
+ export function fileHash(file) {
228
+ const snapshot = fileSnapshot(file.absolutePath, file.content);
229
+ if (!snapshot) {
230
+ throw new Error("Cannot hash missing template-owned file.");
231
+ }
232
+ return {
233
+ sha256: snapshot.sha256,
234
+ size: snapshot.size
235
+ };
236
+ }
237
+
238
+ /**
239
+ * @param {ResolvedTemplate} template
240
+ * @param {Record<string, any>|null} [currentProjectConfig]
241
+ * @returns {Map<string, { path: string, content: string|null, absolutePath: string|null }>}
242
+ */
243
+ export function candidateTemplateFiles(template, currentProjectConfig = null) {
244
+ const files = new Map();
245
+ for (const rootName of ["topogram", "implementation"]) {
246
+ const root = path.join(template.root, rootName);
247
+ if (!fs.existsSync(root)) {
248
+ continue;
249
+ }
250
+ /** @type {string[]} */
251
+ const relativeFiles = [];
252
+ collectFiles(template.root, root, relativeFiles);
253
+ for (const relativePath of relativeFiles) {
254
+ files.set(relativePath, {
255
+ path: relativePath,
256
+ content: null,
257
+ absolutePath: path.join(template.root, relativePath)
258
+ });
259
+ }
260
+ }
261
+ const candidateProjectConfig = JSON.parse(fs.readFileSync(path.join(template.root, "topogram.project.json"), "utf8"));
262
+ candidateProjectConfig.template = candidateProjectTemplateMetadata(template, currentProjectConfig);
263
+ files.set("topogram.project.json", {
264
+ path: "topogram.project.json",
265
+ content: `${stableJsonStringify(candidateProjectConfig)}\n`,
266
+ absolutePath: null
267
+ });
268
+ return files;
269
+ }
270
+
271
+ /**
272
+ * @param {string} projectRoot
273
+ * @param {boolean} includeImplementation
274
+ * @param {Record<string, any>} projectConfig
275
+ * @returns {Map<string, { path: string, absolutePath: string|null, content: string|null }>}
276
+ */
277
+ export function currentTemplateOwnedFiles(projectRoot, includeImplementation, projectConfig) {
278
+ const files = new Map();
279
+ for (const rootName of includeImplementation ? ["topogram", "implementation"] : ["topogram"]) {
280
+ const root = path.join(projectRoot, rootName);
281
+ if (!fs.existsSync(root)) {
282
+ continue;
283
+ }
284
+ /** @type {string[]} */
285
+ const relativeFiles = [];
286
+ collectFiles(projectRoot, root, relativeFiles);
287
+ for (const relativePath of relativeFiles) {
288
+ files.set(relativePath, {
289
+ path: relativePath,
290
+ absolutePath: path.join(projectRoot, relativePath),
291
+ content: null
292
+ });
293
+ }
294
+ }
295
+ const projectConfigPath = path.join(projectRoot, "topogram.project.json");
296
+ if (fs.existsSync(projectConfigPath)) {
297
+ files.set("topogram.project.json", {
298
+ path: "topogram.project.json",
299
+ absolutePath: projectConfigPath,
300
+ content: null
301
+ });
302
+ }
303
+ return files;
304
+ }
305
+
306
+ /**
307
+ * @param {Record<string, any>} projectConfig
308
+ * @returns {boolean}
309
+ */
310
+ export function includesTemplateImplementation(projectConfig) {
311
+ const template = projectConfig.template || {};
312
+ return Boolean(
313
+ projectConfig.implementation ||
314
+ template.includesExecutableImplementation
315
+ );
316
+ }
317
+
318
+ /**
319
+ * @param {string} projectRoot
320
+ * @param {Record<string, any>} projectConfig
321
+ * @returns {Map<string, TemplateOwnedFileRecord>}
322
+ */
323
+ export function currentTemplateOwnedFileHashes(projectRoot, projectConfig) {
324
+ const files = currentTemplateOwnedFiles(projectRoot, includesTemplateImplementation(projectConfig), projectConfig);
325
+ return new Map([...files.entries()].map(([relativePath, file]) => {
326
+ const hash = fileHash(file);
327
+ return [relativePath, { path: relativePath, ...hash }];
328
+ }));
329
+ }
330
+
331
+ /**
332
+ * @param {string} projectRoot
333
+ * @returns {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }|null}
334
+ */
335
+ export function readTemplateFilesManifest(projectRoot) {
336
+ const manifestPath = path.join(projectRoot, TEMPLATE_FILES_MANIFEST);
337
+ if (!fs.existsSync(manifestPath)) {
338
+ return null;
339
+ }
340
+ return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
341
+ }
342
+
343
+ /**
344
+ * @param {string} projectRoot
345
+ * @param {Record<string, any>} projectConfig
346
+ * @returns {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }}
347
+ */
348
+ export function writeTemplateFilesManifest(projectRoot, projectConfig) {
349
+ const fileRecords = [...currentTemplateOwnedFileHashes(projectRoot, projectConfig).values()]
350
+ .sort((left, right) => left.path.localeCompare(right.path));
351
+ const manifest = {
352
+ version: "0.1",
353
+ template: {
354
+ id: projectConfig.template?.id || null,
355
+ version: projectConfig.template?.version || null,
356
+ source: projectConfig.template?.source || null,
357
+ sourceSpec: projectConfig.template?.sourceSpec || null,
358
+ requested: projectConfig.template?.requested || null,
359
+ catalog: projectConfig.template?.catalog || null
360
+ },
361
+ files: fileRecords
362
+ };
363
+ fs.writeFileSync(path.join(projectRoot, TEMPLATE_FILES_MANIFEST), `${stableJsonStringify(manifest)}\n`, "utf8");
364
+ return manifest;
365
+ }
366
+
367
+ /**
368
+ * @param {string} projectRoot
369
+ * @param {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }} manifest
370
+ * @returns {void}
371
+ */
372
+ export function writeTemplateFilesManifestData(projectRoot, manifest) {
373
+ const sortedManifest = {
374
+ ...manifest,
375
+ files: [...manifest.files].sort((left, right) => left.path.localeCompare(right.path))
376
+ };
377
+ fs.writeFileSync(path.join(projectRoot, TEMPLATE_FILES_MANIFEST), `${stableJsonStringify(sortedManifest)}\n`, "utf8");
378
+ }
379
+
380
+ /**
381
+ * @param {string} projectRoot
382
+ * @param {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }} manifest
383
+ * @param {string} relativePath
384
+ * @param {TemplateOwnedFileRecord|null} record
385
+ * @returns {void}
386
+ */
387
+ export function updateTemplateFilesManifestRecord(projectRoot, manifest, relativePath, record) {
388
+ const byPath = new Map(manifest.files.map((file) => [file.path, file]));
389
+ if (record) {
390
+ byPath.set(relativePath, record);
391
+ } else {
392
+ byPath.delete(relativePath);
393
+ }
394
+ writeTemplateFilesManifestData(projectRoot, {
395
+ ...manifest,
396
+ files: [...byPath.values()]
397
+ });
398
+ }
399
+
400
+ /**
401
+ * @param {{ absolutePath: string|null, content: string|null }} candidateFile
402
+ * @param {string} destinationPath
403
+ * @returns {void}
404
+ */
405
+ export function writeCandidateFile(candidateFile, destinationPath) {
406
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
407
+ if (candidateFile.content !== null) {
408
+ fs.writeFileSync(destinationPath, candidateFile.content, "utf8");
409
+ return;
410
+ }
411
+ if (!candidateFile.absolutePath) {
412
+ throw new Error(`Cannot apply template file without content or source path: ${destinationPath}`);
413
+ }
414
+ fs.cpSync(candidateFile.absolutePath, destinationPath);
415
+ }
416
+
417
+ /**
418
+ * @param {TemplateOwnedFileRecord|null} baseline
419
+ * @param {{ sha256: string, size: number }|null} currentHash
420
+ * @returns {boolean}
421
+ */
422
+ export function fileMatchesBaseline(baseline, currentHash) {
423
+ if (!baseline && !currentHash) {
424
+ return true;
425
+ }
426
+ if (!baseline || !currentHash) {
427
+ return false;
428
+ }
429
+ return baseline.sha256 === currentHash.sha256 && baseline.size === currentHash.size;
430
+ }