@topogram/cli 0.3.64 → 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 (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. 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
+ }