@topogram/cli 0.3.64 → 0.3.66

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 (278) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +716 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/adoption/reporting.js +1 -1
  5. package/src/agent-brief.js +7 -21
  6. package/src/agent-ops/query-builders/auth.js +375 -0
  7. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  8. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  9. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  10. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  12. package/src/agent-ops/query-builders/change-risk.js +25 -0
  13. package/src/agent-ops/query-builders/common.js +149 -0
  14. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  15. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  16. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  17. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  18. package/src/agent-ops/query-builders/work-packets.js +417 -0
  19. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  20. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  21. package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
  22. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  23. package/src/agent-ops/query-builders.d.ts +26 -26
  24. package/src/agent-ops/query-builders.js +42 -5021
  25. package/src/archive/jsonl.js +2 -2
  26. package/src/archive/resolver-bridge.js +1 -1
  27. package/src/archive/unarchive.js +2 -1
  28. package/src/catalog/constants.js +10 -0
  29. package/src/catalog/copy.js +65 -0
  30. package/src/catalog/diagnostics.js +15 -0
  31. package/src/catalog/entries.js +42 -0
  32. package/src/catalog/files.js +67 -0
  33. package/src/catalog/provenance.js +123 -0
  34. package/src/catalog/source.js +150 -0
  35. package/src/catalog/validation.js +252 -0
  36. package/src/catalog.d.ts +2 -0
  37. package/src/catalog.js +18 -746
  38. package/src/cli/command-parsers/project.js +3 -0
  39. package/src/cli/command-parsers/shared.js +1 -1
  40. package/src/cli/commands/agent.js +2 -2
  41. package/src/cli/commands/catalog/check.js +31 -0
  42. package/src/cli/commands/catalog/copy.js +59 -0
  43. package/src/cli/commands/catalog/doctor.js +248 -0
  44. package/src/cli/commands/catalog/help.js +21 -0
  45. package/src/cli/commands/catalog/list.js +52 -0
  46. package/src/cli/commands/catalog/runner.js +92 -0
  47. package/src/cli/commands/catalog/shared.js +17 -0
  48. package/src/cli/commands/catalog/show.js +134 -0
  49. package/src/cli/commands/catalog.js +30 -615
  50. package/src/cli/commands/check.js +3 -3
  51. package/src/cli/commands/doctor.js +2 -9
  52. package/src/cli/commands/generator-policy/package-info.js +162 -0
  53. package/src/cli/commands/generator-policy/payloads.js +372 -0
  54. package/src/cli/commands/generator-policy/printers.js +159 -0
  55. package/src/cli/commands/generator-policy/runner.js +81 -0
  56. package/src/cli/commands/generator-policy/shared.js +39 -0
  57. package/src/cli/commands/generator-policy.js +15 -783
  58. package/src/cli/commands/import/adopt.js +170 -0
  59. package/src/cli/commands/import/check.js +91 -0
  60. package/src/cli/commands/import/diff.js +84 -0
  61. package/src/cli/commands/import/help.js +47 -0
  62. package/src/cli/commands/import/paths.js +269 -0
  63. package/src/cli/commands/import/plan.js +292 -0
  64. package/src/cli/commands/import/refresh.js +471 -0
  65. package/src/cli/commands/import/status-history.js +196 -0
  66. package/src/cli/commands/import/workspace.js +233 -0
  67. package/src/cli/commands/import.js +33 -1732
  68. package/src/cli/commands/migrate.js +153 -0
  69. package/src/cli/commands/package/constants.js +17 -0
  70. package/src/cli/commands/package/doctor.js +240 -0
  71. package/src/cli/commands/package/help.js +27 -0
  72. package/src/cli/commands/package/lockfile.js +135 -0
  73. package/src/cli/commands/package/npm.js +97 -0
  74. package/src/cli/commands/package/reporting.js +35 -0
  75. package/src/cli/commands/package/runner.js +33 -0
  76. package/src/cli/commands/package/shared.js +9 -0
  77. package/src/cli/commands/package/update-cli.js +252 -0
  78. package/src/cli/commands/package/versions.js +35 -0
  79. package/src/cli/commands/package.js +29 -813
  80. package/src/cli/commands/query/change-plan.js +68 -0
  81. package/src/cli/commands/query/definitions.js +202 -0
  82. package/src/cli/commands/query/import-adopt.js +121 -0
  83. package/src/cli/commands/query/runner/artifacts.js +102 -0
  84. package/src/cli/commands/query/runner/boundaries.js +211 -0
  85. package/src/cli/commands/query/runner/change.js +182 -0
  86. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  87. package/src/cli/commands/query/runner/index.js +31 -0
  88. package/src/cli/commands/query/runner/output.js +12 -0
  89. package/src/cli/commands/query/runner/workflow.js +241 -0
  90. package/src/cli/commands/query/runner.js +3 -0
  91. package/src/cli/commands/query/workflow-context.js +5 -0
  92. package/src/cli/commands/query/workspace.js +270 -0
  93. package/src/cli/commands/query.js +9 -1300
  94. package/src/cli/commands/source.js +3 -12
  95. package/src/cli/commands/template/baseline.js +100 -0
  96. package/src/cli/commands/template/check.js +467 -0
  97. package/src/cli/commands/template/constants.js +8 -0
  98. package/src/cli/commands/template/diagnostics.js +26 -0
  99. package/src/cli/commands/template/help.js +28 -0
  100. package/src/cli/commands/template/lifecycle.js +404 -0
  101. package/src/cli/commands/template/list-show.js +287 -0
  102. package/src/cli/commands/template/policy.js +422 -0
  103. package/src/cli/commands/template/shared.js +127 -0
  104. package/src/cli/commands/template/updates.js +352 -0
  105. package/src/cli/commands/template-runner.js +6 -6
  106. package/src/cli/commands/template.js +41 -2143
  107. package/src/cli/commands/trust.js +1 -1
  108. package/src/cli/commands/workflow.js +6 -1
  109. package/src/cli/dispatcher.js +6 -1
  110. package/src/cli/help.js +15 -14
  111. package/src/cli/migration-guidance.js +1 -1
  112. package/src/cli/output-safety.js +2 -1
  113. package/src/cli/path-normalization.js +3 -13
  114. package/src/generator/api/contracts.js +497 -0
  115. package/src/generator/api/metadata.js +221 -0
  116. package/src/generator/api/openapi.js +559 -0
  117. package/src/generator/api/schema.js +124 -0
  118. package/src/generator/api/types.d.ts +98 -0
  119. package/src/generator/api.js +3 -1195
  120. package/src/generator/context/domain-page.js +1 -1
  121. package/src/generator/context/shared/domain-sdlc.js +282 -0
  122. package/src/generator/context/shared/maintained-boundary.js +665 -0
  123. package/src/generator/context/shared/metrics.js +85 -0
  124. package/src/generator/context/shared/primitives.js +64 -0
  125. package/src/generator/context/shared/relationships.js +453 -0
  126. package/src/generator/context/shared/summaries.js +263 -0
  127. package/src/generator/context/shared/types.d.ts +207 -0
  128. package/src/generator/context/shared.d.ts +42 -0
  129. package/src/generator/context/shared.js +80 -1390
  130. package/src/generator/context/slice/core.js +397 -0
  131. package/src/generator/context/slice/sdlc.js +417 -0
  132. package/src/generator/context/slice/ui-packets.js +183 -0
  133. package/src/generator/context/slice.js +2 -859
  134. package/src/generator/context/task-mode.js +2 -2
  135. package/src/generator/registry/index.js +507 -0
  136. package/src/generator/registry.js +18 -504
  137. package/src/generator/runtime/environment/index.js +666 -0
  138. package/src/generator/runtime/environment.js +4 -666
  139. package/src/generator/runtime/runtime-check/index.js +554 -0
  140. package/src/generator/runtime/runtime-check.js +4 -554
  141. package/src/generator/runtime/shared/index.js +572 -0
  142. package/src/generator/runtime/shared.js +19 -570
  143. package/src/generator/sdlc/doc-page.js +1 -1
  144. package/src/generator/shared.d.ts +2 -0
  145. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  146. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  147. package/src/generator/surfaces/shared.d.ts +3 -0
  148. package/src/generator/widget-conformance/behavior-report.js +258 -0
  149. package/src/generator/widget-conformance/checks.js +371 -0
  150. package/src/generator/widget-conformance/projection-context.js +200 -0
  151. package/src/generator/widget-conformance/report.js +166 -0
  152. package/src/generator/widget-conformance/types.d.ts +121 -0
  153. package/src/generator/widget-conformance.js +3 -824
  154. package/src/import/core/context.d.ts +3 -0
  155. package/src/import/core/context.js +5 -7
  156. package/src/import/core/contracts.d.ts +1 -0
  157. package/src/import/core/registry.d.ts +4 -0
  158. package/src/import/core/runner/candidates.js +337 -0
  159. package/src/import/core/runner/options.js +22 -0
  160. package/src/import/core/runner/reports.js +51 -0
  161. package/src/import/core/runner/run.js +79 -0
  162. package/src/import/core/runner/tracks.js +150 -0
  163. package/src/import/core/runner/ui-drafts.js +393 -0
  164. package/src/import/core/runner.js +3 -698
  165. package/src/import/core/shared/api-routes.js +221 -0
  166. package/src/import/core/shared/candidates.js +97 -0
  167. package/src/import/core/shared/files.js +177 -0
  168. package/src/import/core/shared/next-app.js +389 -0
  169. package/src/import/core/shared/types.d.ts +51 -0
  170. package/src/import/core/shared/ui-routes.js +230 -0
  171. package/src/import/core/shared.js +60 -861
  172. package/src/new-project/constants.js +128 -0
  173. package/src/new-project/create.js +90 -0
  174. package/src/new-project/json.js +28 -0
  175. package/src/new-project/metadata.js +96 -0
  176. package/src/new-project/package-spec.js +161 -0
  177. package/src/new-project/project-files.js +351 -0
  178. package/src/new-project/template-policy.js +269 -0
  179. package/src/new-project/template-resolution.js +370 -0
  180. package/src/new-project/template-snapshots.js +442 -0
  181. package/src/new-project/template-updates.js +512 -0
  182. package/src/new-project/types.d.ts +83 -0
  183. package/src/new-project.js +6 -2277
  184. package/src/parser.d.ts +87 -1
  185. package/src/parser.js +118 -0
  186. package/src/policy/review-boundaries.d.ts +15 -0
  187. package/src/project-config/index.js +591 -0
  188. package/src/project-config.js +19 -561
  189. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  190. package/src/resolver/enrich/bug.js +2 -0
  191. package/src/resolver/enrich/pitch.js +2 -0
  192. package/src/resolver/enrich/requirement.js +2 -0
  193. package/src/resolver/enrich/task.js +2 -0
  194. package/src/resolver/index.js +19 -2089
  195. package/src/resolver/normalize.js +384 -1
  196. package/src/resolver/plans.js +168 -0
  197. package/src/resolver/projections-api.js +494 -0
  198. package/src/resolver/projections-db.js +133 -0
  199. package/src/resolver/projections-ui.js +317 -0
  200. package/src/resolver/shapes.js +251 -0
  201. package/src/resolver/shared.js +278 -0
  202. package/src/resolver/widgets.js +132 -0
  203. package/src/sdlc/adopt.js +6 -5
  204. package/src/sdlc/paths.js +3 -5
  205. package/src/sdlc/scaffold.js +2 -1
  206. package/src/template-trust/constants.js +62 -0
  207. package/src/template-trust/content.js +258 -0
  208. package/src/template-trust/diff.js +92 -0
  209. package/src/template-trust/policy.js +61 -0
  210. package/src/template-trust/record.js +90 -0
  211. package/src/template-trust/status.js +182 -0
  212. package/src/template-trust.js +24 -687
  213. package/src/text-helpers.d.ts +1 -0
  214. package/src/topogram-types.d.ts +69 -0
  215. package/src/validator/common.js +488 -0
  216. package/src/validator/data-model.js +237 -0
  217. package/src/validator/docs.js +167 -0
  218. package/src/validator/expressions.js +146 -1
  219. package/src/validator/index.d.ts +23 -0
  220. package/src/validator/index.js +32 -3585
  221. package/src/validator/kinds.d.ts +41 -0
  222. package/src/validator/kinds.js +2 -0
  223. package/src/validator/model-helpers.js +46 -0
  224. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  225. package/src/validator/per-kind/bug.js +6 -0
  226. package/src/validator/per-kind/domain.js +15 -2
  227. package/src/validator/per-kind/pitch.js +7 -0
  228. package/src/validator/per-kind/requirement.js +5 -0
  229. package/src/validator/per-kind/task.js +7 -0
  230. package/src/validator/per-kind/widget.js +14 -0
  231. package/src/validator/projections/api-http-async.js +410 -0
  232. package/src/validator/projections/api-http-authz.js +88 -0
  233. package/src/validator/projections/api-http-core.js +205 -0
  234. package/src/validator/projections/api-http-policies.js +339 -0
  235. package/src/validator/projections/api-http-responses.js +233 -0
  236. package/src/validator/projections/api-http.js +44 -0
  237. package/src/validator/projections/db.js +353 -0
  238. package/src/validator/projections/generator-defaults.js +45 -0
  239. package/src/validator/projections/helpers.js +87 -0
  240. package/src/validator/projections/ui-helpers.js +214 -0
  241. package/src/validator/projections/ui-navigation.js +344 -0
  242. package/src/validator/projections/ui-structure.js +364 -0
  243. package/src/validator/projections/ui-widgets.js +493 -0
  244. package/src/validator/projections/ui.js +46 -0
  245. package/src/validator/registry.js +48 -1
  246. package/src/validator/utils.d.ts +20 -0
  247. package/src/validator/utils.js +115 -12
  248. package/src/widget-behavior.d.ts +1 -0
  249. package/src/workflows/import-app/api/collect.js +221 -0
  250. package/src/workflows/import-app/api/openapi.js +257 -0
  251. package/src/workflows/import-app/api/routes.js +327 -0
  252. package/src/workflows/import-app/api/sources.js +22 -0
  253. package/src/workflows/import-app/api.js +2 -797
  254. package/src/workflows/reconcile/adoption-plan/build.js +212 -0
  255. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  256. package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
  257. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  258. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  259. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  260. package/src/workflows/reconcile/adoption-plan.js +30 -740
  261. package/src/workflows/reconcile/auth/closures.js +115 -0
  262. package/src/workflows/reconcile/auth/formatters.js +142 -0
  263. package/src/workflows/reconcile/auth/inference.js +330 -0
  264. package/src/workflows/reconcile/auth/roles.js +122 -0
  265. package/src/workflows/reconcile/auth.js +35 -690
  266. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  267. package/src/workflows/reconcile/bundle-core.js +12 -598
  268. package/src/workflows/reconcile/candidate-model.js +18 -2
  269. package/src/workflows/reconcile/canonical-surface.js +1 -1
  270. package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
  271. package/src/workflows/reconcile/impacts/indexes.js +105 -0
  272. package/src/workflows/reconcile/impacts/patches.js +252 -0
  273. package/src/workflows/reconcile/impacts/reports.js +80 -0
  274. package/src/workflows/reconcile/impacts.js +14 -623
  275. package/src/workflows/reconcile/renderers.js +41 -6
  276. package/src/workflows/shared.js +5 -11
  277. package/src/workspace-docs.d.ts +29 -0
  278. package/src/workspace-paths.js +328 -0
@@ -0,0 +1,442 @@
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
+ import { DEFAULT_TOPO_FOLDER_NAME, resolvePackageWorkspace } from "../workspace-paths.js";
11
+
12
+ /** @typedef {import("./types.js").CreateNewProjectOptions} CreateNewProjectOptions */
13
+ /** @typedef {import("./types.js").TemplateUpdatePlanOptions} TemplateUpdatePlanOptions */
14
+ /** @typedef {import("./types.js").TemplateUpdateFileActionOptions} TemplateUpdateFileActionOptions */
15
+ /** @typedef {import("./types.js").TemplateOwnedFileRecord} TemplateOwnedFileRecord */
16
+ /** @typedef {import("./types.js").TemplateManifest} TemplateManifest */
17
+ /** @typedef {import("./types.js").TemplateTopologySummary} TemplateTopologySummary */
18
+ /** @typedef {import("./types.js").TemplatePolicy} TemplatePolicy */
19
+ /** @typedef {import("./types.js").TemplatePolicyInfo} TemplatePolicyInfo */
20
+ /** @typedef {import("./types.js").TemplateUpdateDiagnostic} TemplateUpdateDiagnostic */
21
+ /** @typedef {import("./types.js").ResolvedTemplate} ResolvedTemplate */
22
+ /** @typedef {import("./types.js").CatalogTemplateProvenance} CatalogTemplateProvenance */
23
+
24
+ /**
25
+ * @param {string} filePath
26
+ * @returns {string}
27
+ */
28
+ export function normalizeTemplateUpdateActionPath(filePath) {
29
+ const normalized = path.posix.normalize(filePath.replace(/\\/g, "/"));
30
+ if (
31
+ !filePath ||
32
+ path.isAbsolute(filePath) ||
33
+ normalized === "." ||
34
+ normalized.startsWith("../") ||
35
+ normalized === ".."
36
+ ) {
37
+ throw new Error(`Template update action requires a relative template-owned file path: ${filePath || "(missing)"}`);
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ /**
43
+ * @param {any} bytes
44
+ * @returns {boolean}
45
+ */
46
+ function isLikelyText(bytes) {
47
+ if (bytes.includes(0)) {
48
+ return false;
49
+ }
50
+ const length = Math.min(bytes.length, 4096);
51
+ let suspicious = 0;
52
+ for (let index = 0; index < length; index += 1) {
53
+ const byte = bytes[index];
54
+ if (byte === 9 || byte === 10 || byte === 13) {
55
+ continue;
56
+ }
57
+ if (byte < 32 || byte === 127) {
58
+ suspicious += 1;
59
+ }
60
+ }
61
+ return length === 0 || suspicious / length < 0.02;
62
+ }
63
+
64
+ /**
65
+ * @param {string} text
66
+ * @returns {string[]}
67
+ */
68
+ function linesForDiff(text) {
69
+ const lines = text.split("\n");
70
+ if (lines.at(-1) === "") {
71
+ lines.pop();
72
+ }
73
+ return lines;
74
+ }
75
+
76
+ /**
77
+ * @param {string[]} before
78
+ * @param {string[]} after
79
+ * @returns {Array<{ type: "same"|"added"|"removed", text: string }>}
80
+ */
81
+ function diffLines(before, after) {
82
+ const rows = before.length;
83
+ const columns = after.length;
84
+ /** @type {number[][]} */
85
+ const table = Array.from({ length: rows + 1 }, () => Array(columns + 1).fill(0));
86
+ for (let row = rows - 1; row >= 0; row -= 1) {
87
+ for (let column = columns - 1; column >= 0; column -= 1) {
88
+ table[row][column] = before[row] === after[column]
89
+ ? table[row + 1][column + 1] + 1
90
+ : Math.max(table[row + 1][column], table[row][column + 1]);
91
+ }
92
+ }
93
+ /** @type {Array<{ type: "same"|"added"|"removed", text: string }>} */
94
+ const changes = [];
95
+ let row = 0;
96
+ let column = 0;
97
+ while (row < rows && column < columns) {
98
+ if (before[row] === after[column]) {
99
+ changes.push({ type: "same", text: before[row] });
100
+ row += 1;
101
+ column += 1;
102
+ } else if (table[row + 1][column] >= table[row][column + 1]) {
103
+ changes.push({ type: "removed", text: before[row] });
104
+ row += 1;
105
+ } else {
106
+ changes.push({ type: "added", text: after[column] });
107
+ column += 1;
108
+ }
109
+ }
110
+ while (row < rows) {
111
+ changes.push({ type: "removed", text: before[row] });
112
+ row += 1;
113
+ }
114
+ while (column < columns) {
115
+ changes.push({ type: "added", text: after[column] });
116
+ column += 1;
117
+ }
118
+ return changes;
119
+ }
120
+
121
+ /**
122
+ * @param {string} relativePath
123
+ * @param {string|null} beforeText
124
+ * @param {string|null} afterText
125
+ * @returns {string|null}
126
+ */
127
+ export function unifiedTextDiff(relativePath, beforeText, afterText) {
128
+ if (beforeText === null && afterText === null) {
129
+ return null;
130
+ }
131
+ const beforeLines = beforeText === null ? [] : linesForDiff(beforeText);
132
+ const afterLines = afterText === null ? [] : linesForDiff(afterText);
133
+ const changes = diffLines(beforeLines, afterLines);
134
+ const lines = [
135
+ `--- current/${relativePath}`,
136
+ `+++ candidate/${relativePath}`,
137
+ `@@ -1,${beforeLines.length} +1,${afterLines.length} @@`
138
+ ];
139
+ for (const change of changes) {
140
+ const prefix = change.type === "added" ? "+" : change.type === "removed" ? "-" : " ";
141
+ lines.push(`${prefix}${change.text}`);
142
+ }
143
+ return `${lines.join("\n")}\n`;
144
+ }
145
+
146
+ /**
147
+ * @param {string} value
148
+ * @returns {number}
149
+ */
150
+ function utf8ByteLength(value) {
151
+ let length = 0;
152
+ for (const char of value) {
153
+ const codePoint = char.codePointAt(0) || 0;
154
+ if (codePoint <= 0x7f) {
155
+ length += 1;
156
+ } else if (codePoint <= 0x7ff) {
157
+ length += 2;
158
+ } else if (codePoint <= 0xffff) {
159
+ length += 3;
160
+ } else {
161
+ length += 4;
162
+ }
163
+ }
164
+ return length;
165
+ }
166
+
167
+ /**
168
+ * @param {string} root
169
+ * @param {string} currentDir
170
+ * @param {string[]} files
171
+ * @returns {void}
172
+ */
173
+ function collectFiles(root, currentDir, files) {
174
+ if (!fs.existsSync(currentDir)) {
175
+ return;
176
+ }
177
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
178
+ if (entry.name === ".DS_Store" || entry.name === "node_modules" || entry.name === ".tmp") {
179
+ continue;
180
+ }
181
+ const entryPath = path.join(currentDir, entry.name);
182
+ if (entry.isSymbolicLink()) {
183
+ 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.`);
184
+ }
185
+ if (entry.isDirectory()) {
186
+ collectFiles(root, entryPath, files);
187
+ continue;
188
+ }
189
+ if (entry.isFile()) {
190
+ files.push(path.relative(root, entryPath).replace(/\\/g, "/"));
191
+ }
192
+ }
193
+ }
194
+
195
+ /**
196
+ * @param {string|null} absolutePath
197
+ * @param {string|null} content
198
+ * @returns {{ sha256: string, size: number, text: string|null, binary: boolean, diffOmitted: boolean }|null}
199
+ */
200
+ export function fileSnapshot(absolutePath, content = null) {
201
+ if (!absolutePath && content === null) {
202
+ return null;
203
+ }
204
+ if (content !== null) {
205
+ return {
206
+ sha256: crypto.createHash("sha256").update(content, "utf8").digest("hex"),
207
+ size: utf8ByteLength(content),
208
+ text: content,
209
+ binary: false,
210
+ diffOmitted: false
211
+ };
212
+ }
213
+ const bytes = fs.readFileSync(absolutePath || "");
214
+ const sha256 = crypto.createHash("sha256").update(bytes).digest("hex");
215
+ if (bytes.length > MAX_TEXT_DIFF_BYTES) {
216
+ return { sha256, size: bytes.length, text: null, binary: false, diffOmitted: true };
217
+ }
218
+ if (!isLikelyText(bytes)) {
219
+ return { sha256, size: bytes.length, text: null, binary: true, diffOmitted: false };
220
+ }
221
+ return { sha256, size: bytes.length, text: bytes.toString("utf8"), binary: false, diffOmitted: false };
222
+ }
223
+
224
+ /**
225
+ * @param {{ absolutePath: string|null, content: string|null }} file
226
+ * @returns {{ sha256: string, size: number }}
227
+ */
228
+ export function fileHash(file) {
229
+ const snapshot = fileSnapshot(file.absolutePath, file.content);
230
+ if (!snapshot) {
231
+ throw new Error("Cannot hash missing template-owned file.");
232
+ }
233
+ return {
234
+ sha256: snapshot.sha256,
235
+ size: snapshot.size
236
+ };
237
+ }
238
+
239
+ /**
240
+ * @param {ResolvedTemplate} template
241
+ * @param {Record<string, any>|null} [currentProjectConfig]
242
+ * @returns {Map<string, { path: string, content: string|null, absolutePath: string|null }>}
243
+ */
244
+ export function candidateTemplateFiles(template, currentProjectConfig = null) {
245
+ const files = new Map();
246
+ const templateWorkspace = resolvePackageWorkspace(template.root);
247
+ /** @type {string[]} */
248
+ const workspaceFiles = [];
249
+ collectFiles(template.root, templateWorkspace.root, workspaceFiles);
250
+ for (const sourceRelativePath of workspaceFiles) {
251
+ const workspaceRelative = path.relative(templateWorkspace.root, path.join(template.root, sourceRelativePath)).replace(/\\/g, "/");
252
+ const targetRelativePath = path.posix.join(DEFAULT_TOPO_FOLDER_NAME, workspaceRelative);
253
+ files.set(targetRelativePath, {
254
+ path: targetRelativePath,
255
+ content: null,
256
+ absolutePath: path.join(template.root, sourceRelativePath)
257
+ });
258
+ }
259
+ const implementationRoot = path.join(template.root, "implementation");
260
+ if (fs.existsSync(implementationRoot)) {
261
+ /** @type {string[]} */
262
+ const relativeFiles = [];
263
+ collectFiles(template.root, implementationRoot, relativeFiles);
264
+ for (const relativePath of relativeFiles) {
265
+ files.set(relativePath, {
266
+ path: relativePath,
267
+ content: null,
268
+ absolutePath: path.join(template.root, relativePath)
269
+ });
270
+ }
271
+ }
272
+ const candidateProjectConfig = JSON.parse(fs.readFileSync(path.join(template.root, "topogram.project.json"), "utf8"));
273
+ candidateProjectConfig.workspace = `./${DEFAULT_TOPO_FOLDER_NAME}`;
274
+ candidateProjectConfig.template = candidateProjectTemplateMetadata(template, currentProjectConfig);
275
+ files.set("topogram.project.json", {
276
+ path: "topogram.project.json",
277
+ content: `${stableJsonStringify(candidateProjectConfig)}\n`,
278
+ absolutePath: null
279
+ });
280
+ return files;
281
+ }
282
+
283
+ /**
284
+ * @param {string} projectRoot
285
+ * @param {boolean} includeImplementation
286
+ * @param {Record<string, any>} projectConfig
287
+ * @returns {Map<string, { path: string, absolutePath: string|null, content: string|null }>}
288
+ */
289
+ export function currentTemplateOwnedFiles(projectRoot, includeImplementation, projectConfig) {
290
+ const files = new Map();
291
+ for (const rootName of includeImplementation ? [DEFAULT_TOPO_FOLDER_NAME, "implementation"] : [DEFAULT_TOPO_FOLDER_NAME]) {
292
+ const root = path.join(projectRoot, rootName);
293
+ if (!fs.existsSync(root)) {
294
+ continue;
295
+ }
296
+ /** @type {string[]} */
297
+ const relativeFiles = [];
298
+ collectFiles(projectRoot, root, relativeFiles);
299
+ for (const relativePath of relativeFiles) {
300
+ files.set(relativePath, {
301
+ path: relativePath,
302
+ absolutePath: path.join(projectRoot, relativePath),
303
+ content: null
304
+ });
305
+ }
306
+ }
307
+ const projectConfigPath = path.join(projectRoot, "topogram.project.json");
308
+ if (fs.existsSync(projectConfigPath)) {
309
+ files.set("topogram.project.json", {
310
+ path: "topogram.project.json",
311
+ absolutePath: projectConfigPath,
312
+ content: null
313
+ });
314
+ }
315
+ return files;
316
+ }
317
+
318
+ /**
319
+ * @param {Record<string, any>} projectConfig
320
+ * @returns {boolean}
321
+ */
322
+ export function includesTemplateImplementation(projectConfig) {
323
+ const template = projectConfig.template || {};
324
+ return Boolean(
325
+ projectConfig.implementation ||
326
+ template.includesExecutableImplementation
327
+ );
328
+ }
329
+
330
+ /**
331
+ * @param {string} projectRoot
332
+ * @param {Record<string, any>} projectConfig
333
+ * @returns {Map<string, TemplateOwnedFileRecord>}
334
+ */
335
+ export function currentTemplateOwnedFileHashes(projectRoot, projectConfig) {
336
+ const files = currentTemplateOwnedFiles(projectRoot, includesTemplateImplementation(projectConfig), projectConfig);
337
+ return new Map([...files.entries()].map(([relativePath, file]) => {
338
+ const hash = fileHash(file);
339
+ return [relativePath, { path: relativePath, ...hash }];
340
+ }));
341
+ }
342
+
343
+ /**
344
+ * @param {string} projectRoot
345
+ * @returns {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }|null}
346
+ */
347
+ export function readTemplateFilesManifest(projectRoot) {
348
+ const manifestPath = path.join(projectRoot, TEMPLATE_FILES_MANIFEST);
349
+ if (!fs.existsSync(manifestPath)) {
350
+ return null;
351
+ }
352
+ return JSON.parse(fs.readFileSync(manifestPath, "utf8"));
353
+ }
354
+
355
+ /**
356
+ * @param {string} projectRoot
357
+ * @param {Record<string, any>} projectConfig
358
+ * @returns {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }}
359
+ */
360
+ export function writeTemplateFilesManifest(projectRoot, projectConfig) {
361
+ const fileRecords = [...currentTemplateOwnedFileHashes(projectRoot, projectConfig).values()]
362
+ .sort((left, right) => left.path.localeCompare(right.path));
363
+ const manifest = {
364
+ version: "0.1",
365
+ template: {
366
+ id: projectConfig.template?.id || null,
367
+ version: projectConfig.template?.version || null,
368
+ source: projectConfig.template?.source || null,
369
+ sourceSpec: projectConfig.template?.sourceSpec || null,
370
+ requested: projectConfig.template?.requested || null,
371
+ catalog: projectConfig.template?.catalog || null
372
+ },
373
+ files: fileRecords
374
+ };
375
+ fs.writeFileSync(path.join(projectRoot, TEMPLATE_FILES_MANIFEST), `${stableJsonStringify(manifest)}\n`, "utf8");
376
+ return manifest;
377
+ }
378
+
379
+ /**
380
+ * @param {string} projectRoot
381
+ * @param {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }} manifest
382
+ * @returns {void}
383
+ */
384
+ export function writeTemplateFilesManifestData(projectRoot, manifest) {
385
+ const sortedManifest = {
386
+ ...manifest,
387
+ files: [...manifest.files].sort((left, right) => left.path.localeCompare(right.path))
388
+ };
389
+ fs.writeFileSync(path.join(projectRoot, TEMPLATE_FILES_MANIFEST), `${stableJsonStringify(sortedManifest)}\n`, "utf8");
390
+ }
391
+
392
+ /**
393
+ * @param {string} projectRoot
394
+ * @param {{ version: string, template: Record<string, any>, files: TemplateOwnedFileRecord[] }} manifest
395
+ * @param {string} relativePath
396
+ * @param {TemplateOwnedFileRecord|null} record
397
+ * @returns {void}
398
+ */
399
+ export function updateTemplateFilesManifestRecord(projectRoot, manifest, relativePath, record) {
400
+ const byPath = new Map(manifest.files.map((file) => [file.path, file]));
401
+ if (record) {
402
+ byPath.set(relativePath, record);
403
+ } else {
404
+ byPath.delete(relativePath);
405
+ }
406
+ writeTemplateFilesManifestData(projectRoot, {
407
+ ...manifest,
408
+ files: [...byPath.values()]
409
+ });
410
+ }
411
+
412
+ /**
413
+ * @param {{ absolutePath: string|null, content: string|null }} candidateFile
414
+ * @param {string} destinationPath
415
+ * @returns {void}
416
+ */
417
+ export function writeCandidateFile(candidateFile, destinationPath) {
418
+ fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
419
+ if (candidateFile.content !== null) {
420
+ fs.writeFileSync(destinationPath, candidateFile.content, "utf8");
421
+ return;
422
+ }
423
+ if (!candidateFile.absolutePath) {
424
+ throw new Error(`Cannot apply template file without content or source path: ${destinationPath}`);
425
+ }
426
+ fs.cpSync(candidateFile.absolutePath, destinationPath);
427
+ }
428
+
429
+ /**
430
+ * @param {TemplateOwnedFileRecord|null} baseline
431
+ * @param {{ sha256: string, size: number }|null} currentHash
432
+ * @returns {boolean}
433
+ */
434
+ export function fileMatchesBaseline(baseline, currentHash) {
435
+ if (!baseline && !currentHash) {
436
+ return true;
437
+ }
438
+ if (!baseline || !currentHash) {
439
+ return false;
440
+ }
441
+ return baseline.sha256 === currentHash.sha256 && baseline.size === currentHash.size;
442
+ }