@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,258 @@
1
+ // @ts-check
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ import {
8
+ IGNORED_IMPLEMENTATION_ENTRIES,
9
+ MAX_TEXT_DIFF_BYTES,
10
+ normalizeRelativePath,
11
+ normalizeRoot,
12
+ unsupportedImplementationSymlinkMessage
13
+ } from "./constants.js";
14
+
15
+ /**
16
+ * @param {string} value
17
+ * @returns {string}
18
+ */
19
+ function escapeDiffPath(value) {
20
+ return value.replace(/\t/g, "\\t").replace(/\n/g, "\\n");
21
+ }
22
+
23
+ /**
24
+ * @param {any} bytes
25
+ * @returns {boolean}
26
+ */
27
+ function isLikelyText(bytes) {
28
+ if (bytes.includes(0)) {
29
+ return false;
30
+ }
31
+ const length = Math.min(bytes.length, 4096);
32
+ let suspicious = 0;
33
+ for (let index = 0; index < length; index += 1) {
34
+ const byte = bytes[index];
35
+ if (byte === 9 || byte === 10 || byte === 13) {
36
+ continue;
37
+ }
38
+ if (byte < 32 || byte === 127) {
39
+ suspicious += 1;
40
+ }
41
+ }
42
+ return length === 0 || suspicious / length < 0.02;
43
+ }
44
+
45
+ /**
46
+ * @param {string} text
47
+ * @returns {string[]}
48
+ */
49
+ function linesForDiff(text) {
50
+ const lines = text.split("\n");
51
+ if (lines.at(-1) === "") {
52
+ lines.pop();
53
+ }
54
+ return lines;
55
+ }
56
+
57
+ /**
58
+ * @param {string[]} before
59
+ * @param {string[]} after
60
+ * @returns {Array<{ type: "same"|"added"|"removed", text: string }>}
61
+ */
62
+ function diffLines(before, after) {
63
+ const rows = before.length;
64
+ const columns = after.length;
65
+ /** @type {number[][]} */
66
+ const table = Array.from({ length: rows + 1 }, () => Array(columns + 1).fill(0));
67
+ for (let row = rows - 1; row >= 0; row -= 1) {
68
+ for (let column = columns - 1; column >= 0; column -= 1) {
69
+ table[row][column] = before[row] === after[column]
70
+ ? table[row + 1][column + 1] + 1
71
+ : Math.max(table[row + 1][column], table[row][column + 1]);
72
+ }
73
+ }
74
+ /** @type {Array<{ type: "same"|"added"|"removed", text: string }>} */
75
+ const changes = [];
76
+ let row = 0;
77
+ let column = 0;
78
+ while (row < rows && column < columns) {
79
+ if (before[row] === after[column]) {
80
+ changes.push({ type: "same", text: before[row] });
81
+ row += 1;
82
+ column += 1;
83
+ } else if (table[row + 1][column] >= table[row][column + 1]) {
84
+ changes.push({ type: "removed", text: before[row] });
85
+ row += 1;
86
+ } else {
87
+ changes.push({ type: "added", text: after[column] });
88
+ column += 1;
89
+ }
90
+ }
91
+ while (row < rows) {
92
+ changes.push({ type: "removed", text: before[row] });
93
+ row += 1;
94
+ }
95
+ while (column < columns) {
96
+ changes.push({ type: "added", text: after[column] });
97
+ column += 1;
98
+ }
99
+ return changes;
100
+ }
101
+
102
+ /**
103
+ * @param {string} relativePath
104
+ * @param {string|null} beforeText
105
+ * @param {string|null} afterText
106
+ * @returns {string|null}
107
+ */
108
+ export function unifiedTextDiff(relativePath, beforeText, afterText) {
109
+ if (beforeText === null && afterText === null) {
110
+ return null;
111
+ }
112
+ const beforeLines = beforeText === null ? [] : linesForDiff(beforeText);
113
+ const afterLines = afterText === null ? [] : linesForDiff(afterText);
114
+ const changes = diffLines(beforeLines, afterLines);
115
+ const lines = [
116
+ `--- a/implementation/${escapeDiffPath(relativePath)}`,
117
+ `+++ b/implementation/${escapeDiffPath(relativePath)}`,
118
+ `@@ -1,${beforeLines.length} +1,${afterLines.length} @@`
119
+ ];
120
+ for (const change of changes) {
121
+ const prefix = change.type === "added" ? "+" : change.type === "removed" ? "-" : " ";
122
+ lines.push(`${prefix}${change.text}`);
123
+ }
124
+ return `${lines.join("\n")}\n`;
125
+ }
126
+
127
+ /**
128
+ * @param {string} filePath
129
+ * @returns {{ text: string|null, binary: boolean, omitted: boolean }}
130
+ */
131
+ function readReviewText(filePath) {
132
+ const bytes = fs.readFileSync(filePath);
133
+ if (bytes.length > MAX_TEXT_DIFF_BYTES) {
134
+ return { text: null, binary: false, omitted: true };
135
+ }
136
+ if (!isLikelyText(bytes)) {
137
+ return { text: null, binary: true, omitted: false };
138
+ }
139
+ return { text: bytes.toString("utf8"), binary: false, omitted: false };
140
+ }
141
+
142
+ /**
143
+ * @param {string} implementationRoot
144
+ * @param {string} currentDir
145
+ * @param {string[]} files
146
+ * @returns {void}
147
+ */
148
+ function collectImplementationFiles(implementationRoot, currentDir, files) {
149
+ for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
150
+ if (IGNORED_IMPLEMENTATION_ENTRIES.has(entry.name)) {
151
+ continue;
152
+ }
153
+ const entryPath = path.join(currentDir, entry.name);
154
+ const relativePath = normalizeRelativePath(path.relative(implementationRoot, entryPath));
155
+ if (entry.isSymbolicLink()) {
156
+ throw new Error(unsupportedImplementationSymlinkMessage(relativePath));
157
+ }
158
+ if (entry.isDirectory()) {
159
+ collectImplementationFiles(implementationRoot, entryPath, files);
160
+ continue;
161
+ }
162
+ if (entry.isFile()) {
163
+ files.push(relativePath);
164
+ }
165
+ }
166
+ }
167
+
168
+ /**
169
+ * @param {string} configDir
170
+ * @returns {{ algorithm: "sha256", root: string, digest: string, files: Array<{ path: string, sha256: string, size: number }> }}
171
+ */
172
+ export function hashImplementationContent(configDir) {
173
+ const implementationRoot = path.join(configDir, "implementation");
174
+ if (!fs.existsSync(implementationRoot) || !fs.statSync(implementationRoot).isDirectory()) {
175
+ throw new Error(`Cannot trust template implementation because ${normalizeRoot(implementationRoot)} does not exist.`);
176
+ }
177
+ /** @type {string[]} */
178
+ const relativePaths = [];
179
+ collectImplementationFiles(implementationRoot, implementationRoot, relativePaths);
180
+ relativePaths.sort((a, b) => a.localeCompare(b));
181
+ const files = relativePaths.map((relativePath) => {
182
+ const filePath = path.join(implementationRoot, relativePath);
183
+ const bytes = fs.readFileSync(filePath);
184
+ return {
185
+ path: relativePath,
186
+ sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
187
+ size: bytes.length
188
+ };
189
+ });
190
+ const aggregate = crypto.createHash("sha256");
191
+ for (const file of files) {
192
+ aggregate.update(file.path);
193
+ aggregate.update("\0");
194
+ aggregate.update(file.sha256);
195
+ aggregate.update("\0");
196
+ aggregate.update(String(file.size));
197
+ aggregate.update("\0");
198
+ }
199
+ return {
200
+ algorithm: "sha256",
201
+ root: "implementation",
202
+ digest: aggregate.digest("hex"),
203
+ files
204
+ };
205
+ }
206
+
207
+ /**
208
+ * @param {Map<string, { path: string, sha256: string, size: number }>} trustedByPath
209
+ * @param {Map<string, { path: string, sha256: string, size: number }>} currentByPath
210
+ * @returns {{ added: string[], removed: string[], changed: string[] }}
211
+ */
212
+ export function diffContentFiles(trustedByPath, currentByPath) {
213
+ /** @type {string[]} */
214
+ const added = [];
215
+ /** @type {string[]} */
216
+ const removed = [];
217
+ /** @type {string[]} */
218
+ const changed = [];
219
+ for (const [filePath, current] of currentByPath) {
220
+ const trusted = trustedByPath.get(filePath);
221
+ if (!trusted) {
222
+ added.push(filePath);
223
+ } else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
224
+ changed.push(filePath);
225
+ }
226
+ }
227
+ for (const filePath of trustedByPath.keys()) {
228
+ if (!currentByPath.has(filePath)) {
229
+ removed.push(filePath);
230
+ }
231
+ }
232
+ return {
233
+ added: added.sort((a, b) => a.localeCompare(b)),
234
+ removed: removed.sort((a, b) => a.localeCompare(b)),
235
+ changed: changed.sort((a, b) => a.localeCompare(b))
236
+ };
237
+ }
238
+
239
+ /**
240
+ * @param {string} configDir
241
+ * @param {string} relativePath
242
+ * @param {{ path: string, sha256: string, size: number }|null} file
243
+ * @returns {{ path: string, sha256: string|null, size: number|null, binary: boolean, diffOmitted: boolean, text: string|null }}
244
+ */
245
+ export function implementationReviewFile(configDir, relativePath, file) {
246
+ if (!file) {
247
+ return { path: relativePath, sha256: null, size: null, binary: false, diffOmitted: false, text: null };
248
+ }
249
+ const reviewText = readReviewText(path.join(configDir, "implementation", relativePath));
250
+ return {
251
+ path: relativePath,
252
+ sha256: file.sha256,
253
+ size: file.size,
254
+ binary: reviewText.binary,
255
+ diffOmitted: reviewText.omitted,
256
+ text: reviewText.text
257
+ };
258
+ }
@@ -0,0 +1,92 @@
1
+ // @ts-check
2
+
3
+ import {
4
+ hashImplementationContent,
5
+ implementationReviewFile,
6
+ unifiedTextDiff
7
+ } from "./content.js";
8
+ import { getTemplateTrustStatus } from "./status.js";
9
+
10
+ /**
11
+ * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} implementationInfo
12
+ * @param {Record<string, any>|null} projectConfig
13
+ * @returns {{ ok: boolean, requiresTrust: boolean, status: ReturnType<typeof getTemplateTrustStatus>, files: Array<{ path: string, kind: "added"|"removed"|"changed", trusted: { path: string, sha256: string|null, size: number|null }|null, current: { path: string, sha256: string|null, size: number|null, binary: boolean, diffOmitted: boolean }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }> }}
14
+ */
15
+ export function getTemplateTrustDiff(implementationInfo, projectConfig = null) {
16
+ const status = getTemplateTrustStatus(implementationInfo, projectConfig);
17
+ if (!status.requiresTrust || !status.trustRecord?.content) {
18
+ return { ok: status.ok, requiresTrust: status.requiresTrust, status, files: [] };
19
+ }
20
+ let currentContent;
21
+ try {
22
+ currentContent = hashImplementationContent(implementationInfo.configDir);
23
+ } catch (_error) {
24
+ return { ok: false, requiresTrust: status.requiresTrust, status, files: [] };
25
+ }
26
+ const trustedByPath = new Map((status.trustRecord.content.files || []).map((file) => [file.path, file]));
27
+ const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
28
+ /** @type {Array<{ path: string, kind: "added"|"removed"|"changed", trusted: { path: string, sha256: string|null, size: number|null }|null, current: { path: string, sha256: string|null, size: number|null, binary: boolean, diffOmitted: boolean }|null, binary: boolean, diffOmitted: boolean, unifiedDiff: string|null }>} */
29
+ const files = [];
30
+
31
+ for (const relativePath of status.content.changed) {
32
+ const trusted = trustedByPath.get(relativePath) || null;
33
+ const current = currentByPath.get(relativePath) || null;
34
+ const currentReview = implementationReviewFile(implementationInfo.configDir, relativePath, current);
35
+ files.push({
36
+ path: relativePath,
37
+ kind: "changed",
38
+ trusted: trusted ? { path: relativePath, sha256: trusted.sha256, size: trusted.size } : null,
39
+ current: {
40
+ path: relativePath,
41
+ sha256: currentReview.sha256,
42
+ size: currentReview.size,
43
+ binary: currentReview.binary,
44
+ diffOmitted: currentReview.diffOmitted
45
+ },
46
+ binary: currentReview.binary,
47
+ diffOmitted: true,
48
+ unifiedDiff: null
49
+ });
50
+ }
51
+ for (const relativePath of status.content.added) {
52
+ const current = currentByPath.get(relativePath) || null;
53
+ const currentReview = implementationReviewFile(implementationInfo.configDir, relativePath, current);
54
+ files.push({
55
+ path: relativePath,
56
+ kind: "added",
57
+ trusted: null,
58
+ current: {
59
+ path: relativePath,
60
+ sha256: currentReview.sha256,
61
+ size: currentReview.size,
62
+ binary: currentReview.binary,
63
+ diffOmitted: currentReview.diffOmitted
64
+ },
65
+ binary: currentReview.binary,
66
+ diffOmitted: currentReview.binary || currentReview.diffOmitted,
67
+ unifiedDiff: currentReview.binary || currentReview.diffOmitted
68
+ ? null
69
+ : unifiedTextDiff(relativePath, null, currentReview.text)
70
+ });
71
+ }
72
+ for (const relativePath of status.content.removed) {
73
+ const trusted = trustedByPath.get(relativePath) || null;
74
+ files.push({
75
+ path: relativePath,
76
+ kind: "removed",
77
+ trusted: trusted ? { path: relativePath, sha256: trusted.sha256, size: trusted.size } : null,
78
+ current: null,
79
+ binary: false,
80
+ diffOmitted: true,
81
+ unifiedDiff: null
82
+ });
83
+ }
84
+
85
+ files.sort((a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
86
+ return {
87
+ ok: status.ok,
88
+ requiresTrust: status.requiresTrust,
89
+ status,
90
+ files
91
+ };
92
+ }
@@ -0,0 +1,61 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * @param {string} parent
7
+ * @param {string} child
8
+ * @returns {boolean}
9
+ */
10
+ export function isSameOrInside(parent, child) {
11
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
12
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
13
+ }
14
+
15
+ /**
16
+ * @param {Record<string, any>} config
17
+ * @returns {{ id: string|null, module: string, export: string }}
18
+ */
19
+ export function implementationTrustFingerprint(config) {
20
+ const implementationModule = config.implementation_module || config.module;
21
+ if (!implementationModule || typeof implementationModule !== "string") {
22
+ throw new Error("Topogram implementation config is missing implementation module.");
23
+ }
24
+ return {
25
+ id: config.implementation_id || config.id || null,
26
+ module: implementationModule,
27
+ export: config.implementation_export || config.export || "default"
28
+ };
29
+ }
30
+
31
+ /**
32
+ * @param {Record<string, any>|null} projectConfig
33
+ * @returns {boolean}
34
+ */
35
+ export function projectHasTemplateAttachment(projectConfig) {
36
+ const template = projectConfig?.template || null;
37
+ return Boolean(template?.id || template?.sourceSpec || template?.requested);
38
+ }
39
+
40
+ /**
41
+ * @param {{ config: Record<string, any>, configDir: string }} implementationInfo
42
+ * @param {Record<string, any>|null} [projectConfig]
43
+ * @returns {boolean}
44
+ */
45
+ export function implementationRequiresTrust(implementationInfo, projectConfig = null) {
46
+ const fingerprint = implementationTrustFingerprint(implementationInfo.config);
47
+ const modulePath = path.resolve(implementationInfo.configDir, fingerprint.module);
48
+ const implementationRoot = path.resolve(implementationInfo.configDir, "implementation");
49
+ return isSameOrInside(implementationRoot, modulePath) || projectHasTemplateAttachment(projectConfig);
50
+ }
51
+
52
+ /**
53
+ * @param {{ config: Record<string, any>, configDir: string }} implementationInfo
54
+ * @returns {boolean}
55
+ */
56
+ export function implementationModuleIsUnderRoot(implementationInfo) {
57
+ const fingerprint = implementationTrustFingerprint(implementationInfo.config);
58
+ const modulePath = path.resolve(implementationInfo.configDir, fingerprint.module);
59
+ const implementationRoot = path.resolve(implementationInfo.configDir, "implementation");
60
+ return isSameOrInside(implementationRoot, modulePath);
61
+ }
@@ -0,0 +1,90 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ import {
7
+ implementationOutsideRootMessage,
8
+ TEMPLATE_TRUST_FILE,
9
+ TEMPLATE_TRUST_POLICY
10
+ } from "./constants.js";
11
+ import { hashImplementationContent } from "./content.js";
12
+ import {
13
+ implementationModuleIsUnderRoot,
14
+ implementationTrustFingerprint
15
+ } from "./policy.js";
16
+
17
+ /**
18
+ * @typedef {Object} TemplateTrustRecord
19
+ * @property {string} version
20
+ * @property {string} trustPolicy
21
+ * @property {string} trustedAt
22
+ * @property {{ id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null, sourceRoot: string|null, catalog?: Record<string, any>|null }} template
23
+ * @property {{ id: string|null, module: string, export: string }} implementation
24
+ * @property {{ algorithm: "sha256", root: string, digest: string, files: Array<{ path: string, sha256: string, size: number }> }} content
25
+ */
26
+
27
+ /**
28
+ * @param {string} configDir
29
+ * @returns {TemplateTrustRecord|null}
30
+ */
31
+ export function readTemplateTrustRecord(configDir) {
32
+ const trustPath = path.join(configDir, TEMPLATE_TRUST_FILE);
33
+ if (!fs.existsSync(trustPath)) {
34
+ return null;
35
+ }
36
+ return /** @type {TemplateTrustRecord} */ (JSON.parse(fs.readFileSync(trustPath, "utf8")));
37
+ }
38
+
39
+ /**
40
+ * @param {string} configDir
41
+ * @param {Record<string, any>} projectConfig
42
+ * @param {{ id: string|null, module: string, export: string }} implementation
43
+ * @returns {TemplateTrustRecord}
44
+ */
45
+ function buildTrustRecord(configDir, projectConfig, implementation) {
46
+ const template = projectConfig.template || {};
47
+ const content = hashImplementationContent(configDir);
48
+ return {
49
+ version: "1",
50
+ trustPolicy: TEMPLATE_TRUST_POLICY,
51
+ trustedAt: new Date().toISOString(),
52
+ template: {
53
+ id: typeof template.id === "string" ? template.id : null,
54
+ version: typeof template.version === "string" ? template.version : null,
55
+ source: typeof template.source === "string" ? template.source : null,
56
+ sourceSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null,
57
+ requested: typeof template.requested === "string" ? template.requested : null,
58
+ sourceRoot: typeof template.sourceRoot === "string" ? template.sourceRoot : null,
59
+ catalog: template.catalog && typeof template.catalog === "object" && !Array.isArray(template.catalog)
60
+ ? template.catalog
61
+ : null
62
+ },
63
+ implementation,
64
+ content
65
+ };
66
+ }
67
+
68
+ /**
69
+ * @param {string} configDir
70
+ * @param {Record<string, any>} projectConfig
71
+ * @returns {TemplateTrustRecord}
72
+ */
73
+ export function writeTemplateTrustRecord(configDir, projectConfig) {
74
+ const implementationConfig = projectConfig.implementation;
75
+ if (!implementationConfig) {
76
+ throw new Error("Cannot trust template implementation because topogram.project.json has no implementation config.");
77
+ }
78
+ const implementationInfo = {
79
+ config: implementationConfig,
80
+ configDir
81
+ };
82
+ if (!implementationModuleIsUnderRoot(implementationInfo)) {
83
+ const implementation = implementationTrustFingerprint(implementationConfig);
84
+ throw new Error(implementationOutsideRootMessage(implementation.module));
85
+ }
86
+ const implementation = implementationTrustFingerprint(implementationConfig);
87
+ const record = buildTrustRecord(configDir, projectConfig, implementation);
88
+ fs.writeFileSync(path.join(configDir, TEMPLATE_TRUST_FILE), `${JSON.stringify(record, null, 2)}\n`, "utf8");
89
+ return record;
90
+ }
@@ -0,0 +1,182 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ import {
6
+ implementationOutsideRootMessage,
7
+ normalizeRoot,
8
+ TEMPLATE_TRUST_FILE,
9
+ TEMPLATE_TRUST_POLICY,
10
+ templateTrustRecoveryGuidance
11
+ } from "./constants.js";
12
+ import {
13
+ diffContentFiles,
14
+ hashImplementationContent
15
+ } from "./content.js";
16
+ import {
17
+ implementationModuleIsUnderRoot,
18
+ implementationRequiresTrust,
19
+ implementationTrustFingerprint,
20
+ projectHasTemplateAttachment
21
+ } from "./policy.js";
22
+ import { readTemplateTrustRecord } from "./record.js";
23
+
24
+ /**
25
+ * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} implementationInfo
26
+ * @param {Record<string, any>|null} projectConfig
27
+ * @returns {void}
28
+ */
29
+ export function assertTrustedImplementation(implementationInfo, projectConfig = null) {
30
+ const status = getTemplateTrustStatus(implementationInfo, projectConfig);
31
+ if (!status.requiresTrust || status.ok) {
32
+ return;
33
+ }
34
+ const firstIssue = status.issues[0] || "implementation trust is invalid";
35
+ const guidance = templateTrustRecoveryGuidance(firstIssue);
36
+ throw new Error(
37
+ guidance ? `${firstIssue}. ${guidance}` : firstIssue
38
+ );
39
+ }
40
+
41
+ /**
42
+ * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }} implementationInfo
43
+ * @param {Record<string, any>|null} projectConfig
44
+ * @returns {{ ok: boolean, requiresTrust: boolean, trustPath: string, trustRecord: import("./record.js").TemplateTrustRecord|null, template: { id: string|null, version: string|null, source: string|null, sourceSpec: string|null, requested: string|null, sourceRoot: string|null, catalog?: Record<string, any>|null, includesExecutableImplementation: boolean|null }, implementation: { id: string|null, module: string|null, export: string|null }, content: { trustedDigest: string|null, currentDigest: string|null, added: string[], removed: string[], changed: string[] }, issues: string[] }}
45
+ */
46
+ export function getTemplateTrustStatus(implementationInfo, projectConfig = null) {
47
+ const templateAttached = projectHasTemplateAttachment(projectConfig);
48
+ if (!implementationRequiresTrust(implementationInfo, projectConfig)) {
49
+ return {
50
+ ok: true,
51
+ requiresTrust: false,
52
+ trustPath: path.join(implementationInfo.configDir, TEMPLATE_TRUST_FILE),
53
+ trustRecord: null,
54
+ template: { id: null, version: null, source: null, sourceSpec: null, requested: null, sourceRoot: null, catalog: null, includesExecutableImplementation: null },
55
+ implementation: { id: null, module: null, export: null },
56
+ content: { trustedDigest: null, currentDigest: null, added: [], removed: [], changed: [] },
57
+ issues: []
58
+ };
59
+ }
60
+ const fingerprint = implementationTrustFingerprint(implementationInfo.config);
61
+ const moduleInsideImplementation = implementationModuleIsUnderRoot(implementationInfo);
62
+ const trustRecord = readTemplateTrustRecord(implementationInfo.configDir);
63
+ const configLabel = implementationInfo.configPath || "topogram.project.json";
64
+ const trustPath = path.join(implementationInfo.configDir, TEMPLATE_TRUST_FILE);
65
+ const projectTemplate = projectConfig?.template || null;
66
+ /** @type {string[]} */
67
+ const issues = [];
68
+ /** @type {{ trustedDigest: string|null, currentDigest: string|null, added: string[], removed: string[], changed: string[] }} */
69
+ const contentStatus = { trustedDigest: null, currentDigest: null, added: [], removed: [], changed: [] };
70
+
71
+ if (templateAttached && !moduleInsideImplementation) {
72
+ issues.push(implementationOutsideRootMessage(fingerprint.module));
73
+ }
74
+
75
+ if (!trustRecord) {
76
+ issues.push(
77
+ `Refusing to load executable implementation '${fingerprint.module}' from ${normalizeRoot(configLabel)} without ${TEMPLATE_TRUST_FILE}`
78
+ );
79
+ } else {
80
+ if (trustRecord.trustPolicy !== TEMPLATE_TRUST_POLICY) {
81
+ issues.push(`${TEMPLATE_TRUST_FILE} uses unsupported trust policy '${trustRecord.trustPolicy}'`);
82
+ }
83
+ if (trustRecord.implementation?.module !== fingerprint.module) {
84
+ issues.push(`${TEMPLATE_TRUST_FILE} trusts implementation module '${trustRecord.implementation?.module}', but ${normalizeRoot(configLabel)} uses '${fingerprint.module}'`);
85
+ }
86
+ if (trustRecord.implementation?.export !== fingerprint.export) {
87
+ issues.push(`${TEMPLATE_TRUST_FILE} trusts implementation export '${trustRecord.implementation?.export}', but ${normalizeRoot(configLabel)} uses '${fingerprint.export}'`);
88
+ }
89
+ const trustedId = trustRecord.implementation?.id || null;
90
+ if (trustedId !== (fingerprint.id || null)) {
91
+ issues.push(`${TEMPLATE_TRUST_FILE} trusts implementation id '${trustedId || ""}', but ${normalizeRoot(configLabel)} uses '${fingerprint.id || ""}'`);
92
+ }
93
+
94
+ if (projectTemplate?.id && trustRecord.template?.id !== projectTemplate.id) {
95
+ issues.push(`${TEMPLATE_TRUST_FILE} trusts template '${trustRecord.template?.id}', but topogram.project.json declares '${projectTemplate.id}'`);
96
+ }
97
+ if (projectTemplate?.version && trustRecord.template?.version !== projectTemplate.version) {
98
+ issues.push(`${TEMPLATE_TRUST_FILE} trusts template version '${trustRecord.template?.version}', but topogram.project.json declares '${projectTemplate.version}'`);
99
+ }
100
+
101
+ if (!moduleInsideImplementation) {
102
+ // The module itself is outside the only supported trust root. Do not
103
+ // pretend the implementation/ content digest covers the executable code.
104
+ } else if (!trustRecord.content) {
105
+ issues.push(`${TEMPLATE_TRUST_FILE} is missing implementation content hashes`);
106
+ } else if (trustRecord.content.algorithm !== "sha256") {
107
+ issues.push(`${TEMPLATE_TRUST_FILE} uses unsupported content hash algorithm '${trustRecord.content.algorithm}'`);
108
+ } else {
109
+ try {
110
+ const currentContent = hashImplementationContent(implementationInfo.configDir);
111
+ contentStatus.trustedDigest = trustRecord.content.digest;
112
+ contentStatus.currentDigest = currentContent.digest;
113
+ const trustedByPath = new Map((trustRecord.content.files || []).map((file) => [file.path, file]));
114
+ const currentByPath = new Map(currentContent.files.map((file) => [file.path, file]));
115
+ const diff = diffContentFiles(trustedByPath, currentByPath);
116
+ contentStatus.added = diff.added;
117
+ contentStatus.removed = diff.removed;
118
+ contentStatus.changed = diff.changed;
119
+ if (trustRecord.content.digest !== currentContent.digest) {
120
+ issues.push(`${TEMPLATE_TRUST_FILE} implementation content changed since it was last trusted`);
121
+ }
122
+ } catch (error) {
123
+ issues.push(error instanceof Error ? error.message : String(error));
124
+ }
125
+ }
126
+ }
127
+
128
+ return {
129
+ ok: issues.length === 0,
130
+ requiresTrust: true,
131
+ trustPath,
132
+ trustRecord,
133
+ template: {
134
+ id: projectTemplate?.id || trustRecord?.template?.id || null,
135
+ version: projectTemplate?.version || trustRecord?.template?.version || null,
136
+ source: projectTemplate?.source || trustRecord?.template?.source || null,
137
+ sourceSpec: projectTemplate?.sourceSpec || trustRecord?.template?.sourceSpec || null,
138
+ requested: projectTemplate?.requested || trustRecord?.template?.requested || null,
139
+ sourceRoot: projectTemplate?.sourceRoot || trustRecord?.template?.sourceRoot || null,
140
+ catalog: projectTemplate?.catalog || trustRecord?.template?.catalog || null,
141
+ includesExecutableImplementation: typeof projectTemplate?.includesExecutableImplementation === "boolean"
142
+ ? projectTemplate.includesExecutableImplementation
143
+ : null
144
+ },
145
+ implementation: fingerprint,
146
+ content: contentStatus,
147
+ issues
148
+ };
149
+ }
150
+
151
+ /**
152
+ * @param {{ config: Record<string, any>, configPath: string|null, configDir: string }|null} projectConfigInfo
153
+ * @returns {{ ok: boolean, errors: Array<{ message: string, loc: any }> }}
154
+ */
155
+ export function validateProjectImplementationTrust(projectConfigInfo) {
156
+ if (!projectConfigInfo?.config?.implementation) {
157
+ return { ok: true, errors: [] };
158
+ }
159
+ const implementationModule =
160
+ projectConfigInfo.config.implementation.implementation_module ||
161
+ projectConfigInfo.config.implementation.module;
162
+ if (!implementationModule) {
163
+ return { ok: true, errors: [] };
164
+ }
165
+ const implementationInfo = {
166
+ config: projectConfigInfo.config.implementation,
167
+ configPath: projectConfigInfo.configPath,
168
+ configDir: projectConfigInfo.configDir
169
+ };
170
+ try {
171
+ assertTrustedImplementation(implementationInfo, projectConfigInfo.config);
172
+ return { ok: true, errors: [] };
173
+ } catch (error) {
174
+ return {
175
+ ok: false,
176
+ errors: [{
177
+ message: error instanceof Error ? error.message : String(error),
178
+ loc: null
179
+ }]
180
+ };
181
+ }
182
+ }