@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,422 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ import { loadProjectConfig } from "../../../project-config.js";
6
+ import {
7
+ loadTemplatePolicy,
8
+ packageScopeFromSpec,
9
+ templatePolicyDiagnosticsForTemplate,
10
+ writeTemplatePolicy,
11
+ writeTemplatePolicyForProject
12
+ } from "../../../new-project.js";
13
+ import { templateCheckDiagnostic } from "./diagnostics.js";
14
+
15
+ /**
16
+ * @typedef {Object} TemplateCheckDiagnostic
17
+ * @property {string} code
18
+ * @property {"error"|"warning"} severity
19
+ * @property {string} message
20
+ * @property {string|null} path
21
+ * @property {string|null} suggestedFix
22
+ * @property {string|null} step
23
+ */
24
+
25
+ /**
26
+ * @param {ReturnType<typeof loadProjectConfig>} projectConfigInfo
27
+ * @returns {{ requested: string, root: string, manifest: { id: string, version: string, kind: string, topogramVersion: string, includesExecutableImplementation: boolean }, source: "local"|"package", packageSpec: string|null }}
28
+ */
29
+ function currentPolicyTemplate(projectConfigInfo) {
30
+ const template = projectConfigInfo?.config.template || {};
31
+ const source = template.source === "local" || template.source === "package"
32
+ ? template.source
33
+ : "local";
34
+ return {
35
+ requested: typeof template.requested === "string" ? template.requested : String(template.id || "unknown"),
36
+ root: projectConfigInfo?.configDir || process.cwd(),
37
+ manifest: {
38
+ id: typeof template.id === "string" ? template.id : "unknown",
39
+ version: typeof template.version === "string" ? template.version : "unknown",
40
+ kind: "starter",
41
+ topogramVersion: "*",
42
+ includesExecutableImplementation: Boolean(template.includesExecutableImplementation)
43
+ },
44
+ source,
45
+ packageSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {string} projectPath
51
+ * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
52
+ */
53
+ export function buildTemplatePolicyCheckPayload(projectPath) {
54
+ const projectConfigInfo = loadProjectConfig(projectPath);
55
+ if (!projectConfigInfo) {
56
+ const diagnostic = templateCheckDiagnostic({
57
+ code: "template_policy_project_missing",
58
+ message: "Cannot check template policy without topogram.project.json.",
59
+ path: path.resolve(projectPath),
60
+ suggestedFix: "Run this command in a Topogram project.",
61
+ step: "policy"
62
+ });
63
+ return {
64
+ ok: false,
65
+ path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
66
+ exists: false,
67
+ policy: null,
68
+ diagnostics: [diagnostic],
69
+ errors: [diagnostic.message]
70
+ };
71
+ }
72
+ const policyInfo = loadTemplatePolicy(projectConfigInfo.configDir);
73
+ /** @type {TemplateCheckDiagnostic[]} */
74
+ const diagnostics = policyInfo.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
75
+ if (!policyInfo.exists) {
76
+ diagnostics.push(templateCheckDiagnostic({
77
+ code: "template_policy_missing",
78
+ severity: "warning",
79
+ message: "No topogram.template-policy.json found. Template operations are permissive until a policy is defined.",
80
+ path: policyInfo.path,
81
+ suggestedFix: "Run `topogram template policy init` to create a project template policy.",
82
+ step: "policy"
83
+ }));
84
+ } else if (policyInfo.policy) {
85
+ const currentTemplate = currentPolicyTemplate(projectConfigInfo);
86
+ diagnostics.push(...templatePolicyDiagnosticsForTemplate(policyInfo, currentTemplate, "policy")
87
+ .map((diagnostic) => templateCheckDiagnostic(diagnostic)));
88
+ }
89
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
90
+ return {
91
+ ok: errors.length === 0,
92
+ path: policyInfo.path,
93
+ exists: policyInfo.exists,
94
+ policy: policyInfo.policy,
95
+ diagnostics,
96
+ errors
97
+ };
98
+ }
99
+
100
+ /**
101
+ * @param {string} name
102
+ * @param {boolean} ok
103
+ * @param {string} actual
104
+ * @param {string} expected
105
+ * @param {string} message
106
+ * @param {string|null} fix
107
+ * @returns {{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }}
108
+ */
109
+ function templatePolicyRule(name, ok, actual, expected, message, fix = null) {
110
+ return { name, ok, actual, expected, message, fix };
111
+ }
112
+
113
+ /**
114
+ * @param {string} name
115
+ * @returns {string}
116
+ */
117
+ function templatePolicyRuleLabel(name) {
118
+ return ({
119
+ "policy-file": "Policy file",
120
+ "allowed-source": "Allowed source",
121
+ "allowed-template-id": "Allowed template id",
122
+ "allowed-package-scope": "Allowed package scope",
123
+ "pinned-version": "Pinned version",
124
+ "executable-implementation": "Executable implementation"
125
+ })[name] || name;
126
+ }
127
+
128
+ /**
129
+ * @param {string} projectPath
130
+ * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, template: any, catalog: any, package: any, rules: Array<{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }>, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
131
+ */
132
+ export function buildTemplatePolicyExplainPayload(projectPath) {
133
+ const check = buildTemplatePolicyCheckPayload(projectPath);
134
+ const projectConfigInfo = loadProjectConfig(projectPath);
135
+ if (!projectConfigInfo) {
136
+ return {
137
+ ...check,
138
+ template: null,
139
+ catalog: null,
140
+ package: null,
141
+ rules: []
142
+ };
143
+ }
144
+ const templateMetadata = projectConfigInfo.config.template || {};
145
+ const currentTemplate = currentPolicyTemplate(projectConfigInfo);
146
+ const policy = check.policy;
147
+ const packageScope = currentTemplate.source === "package"
148
+ ? packageScopeFromSpec(currentTemplate.packageSpec || currentTemplate.requested)
149
+ : null;
150
+ const rules = [];
151
+ rules.push(templatePolicyRule(
152
+ "policy-file",
153
+ check.exists,
154
+ check.exists ? "present" : "missing",
155
+ "present",
156
+ check.exists
157
+ ? "Project has a template policy file."
158
+ : "Project has no template policy file; template operations are permissive until one is defined.",
159
+ check.exists ? null : "Run `topogram template policy init`."
160
+ ));
161
+ if (policy) {
162
+ rules.push(templatePolicyRule(
163
+ "allowed-source",
164
+ policy.allowedSources.length === 0 || policy.allowedSources.includes(currentTemplate.source),
165
+ currentTemplate.source,
166
+ policy.allowedSources.length > 0 ? policy.allowedSources.join(", ") : "(any)",
167
+ "Current template source must be allowed by allowedSources.",
168
+ `Add '${currentTemplate.source}' to allowedSources after review, or run \`topogram template policy init\`.`
169
+ ));
170
+ rules.push(templatePolicyRule(
171
+ "allowed-template-id",
172
+ policy.allowedTemplateIds.length === 0 || policy.allowedTemplateIds.includes(currentTemplate.manifest.id),
173
+ currentTemplate.manifest.id,
174
+ policy.allowedTemplateIds.length > 0 ? policy.allowedTemplateIds.join(", ") : "(any)",
175
+ "Current template id must be allowed by allowedTemplateIds.",
176
+ `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
177
+ ));
178
+ if (currentTemplate.source === "package") {
179
+ rules.push(templatePolicyRule(
180
+ "allowed-package-scope",
181
+ !policy.allowedPackageScopes ||
182
+ policy.allowedPackageScopes.length === 0 ||
183
+ Boolean(packageScope && policy.allowedPackageScopes.includes(packageScope)),
184
+ packageScope || "(unscoped)",
185
+ policy.allowedPackageScopes && policy.allowedPackageScopes.length > 0 ? policy.allowedPackageScopes.join(", ") : "(any)",
186
+ "Package-backed template source must be in an allowed package scope.",
187
+ `Add '${packageScope || "(unscoped)"}' to allowedPackageScopes after review.`
188
+ ));
189
+ }
190
+ const pinnedVersion = policy.pinnedVersions?.[currentTemplate.manifest.id] || null;
191
+ rules.push(templatePolicyRule(
192
+ "pinned-version",
193
+ !pinnedVersion || pinnedVersion === currentTemplate.manifest.version,
194
+ currentTemplate.manifest.version,
195
+ pinnedVersion || "(unpinned)",
196
+ "Pinned version must match the current template version when a pin exists.",
197
+ `Run \`topogram template policy pin ${currentTemplate.manifest.id}@${currentTemplate.manifest.version}\` after review.`
198
+ ));
199
+ rules.push(templatePolicyRule(
200
+ "executable-implementation",
201
+ !currentTemplate.manifest.includesExecutableImplementation || policy.executableImplementation !== "deny",
202
+ currentTemplate.manifest.includesExecutableImplementation ? "yes" : "no",
203
+ policy.executableImplementation,
204
+ "Executable template implementation must be allowed when implementation/ is present.",
205
+ "Review implementation/, then set executableImplementation to 'allow' or choose a non-executable template."
206
+ ));
207
+ }
208
+ return {
209
+ ...check,
210
+ template: {
211
+ id: currentTemplate.manifest.id,
212
+ version: currentTemplate.manifest.version,
213
+ source: currentTemplate.source,
214
+ requested: currentTemplate.requested,
215
+ sourceSpec: currentTemplate.packageSpec,
216
+ includesExecutableImplementation: currentTemplate.manifest.includesExecutableImplementation
217
+ },
218
+ catalog: templateMetadata.catalog || null,
219
+ package: currentTemplate.source === "package" ? {
220
+ spec: currentTemplate.packageSpec,
221
+ scope: packageScope
222
+ } : null,
223
+ rules
224
+ };
225
+ }
226
+
227
+ /**
228
+ * @param {ReturnType<typeof buildTemplatePolicyExplainPayload>} payload
229
+ * @returns {void}
230
+ */
231
+ export function printTemplatePolicyExplainPayload(payload) {
232
+ console.log(payload.ok ? "Template policy: allowed" : "Template policy: denied");
233
+ console.log(payload.ok
234
+ ? "Decision: the current template is allowed by this project's template policy."
235
+ : "Decision: the current template is blocked by this project's template policy.");
236
+ console.log(`Policy file: ${payload.path}`);
237
+ console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
238
+ if (payload.template) {
239
+ console.log(`Template: ${payload.template.id}@${payload.template.version}`);
240
+ console.log(`Source: ${payload.template.source}`);
241
+ console.log(`Requested: ${payload.template.requested}`);
242
+ if (payload.template.sourceSpec) {
243
+ console.log(`Source spec: ${payload.template.sourceSpec}`);
244
+ }
245
+ console.log(`Executable implementation: ${payload.template.includesExecutableImplementation ? "yes" : "no"}`);
246
+ }
247
+ if (payload.catalog?.id) {
248
+ console.log(`Catalog: ${payload.catalog.id} from ${payload.catalog.source || "unknown"}`);
249
+ console.log(`Catalog package: ${payload.catalog.packageSpec || payload.catalog.package || "unknown"}`);
250
+ }
251
+ if (payload.package) {
252
+ console.log(`Package scope: ${payload.package.scope || "(unscoped)"}`);
253
+ }
254
+ if (payload.rules.length > 0) {
255
+ console.log("");
256
+ console.log("Policy checks:");
257
+ }
258
+ for (const rule of payload.rules) {
259
+ console.log(`${rule.ok ? "PASS" : "FAIL"} ${templatePolicyRuleLabel(rule.name)}: ${rule.message}`);
260
+ console.log(` actual: ${rule.actual}`);
261
+ console.log(` expected: ${rule.expected}`);
262
+ if (!rule.ok && rule.fix) {
263
+ console.log(` fix: ${rule.fix}`);
264
+ }
265
+ }
266
+ for (const diagnostic of payload.diagnostics) {
267
+ const label = diagnostic.severity === "warning" ? "Warning" : "Error";
268
+ console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
269
+ if (diagnostic.suggestedFix) {
270
+ console.log(` fix: ${diagnostic.suggestedFix}`);
271
+ }
272
+ }
273
+ }
274
+
275
+ /**
276
+ * @param {{ ok: boolean, path: string, exists: boolean, policy: any, diagnostics: TemplateCheckDiagnostic[] }} payload
277
+ * @returns {void}
278
+ */
279
+ export function printTemplatePolicyCheckPayload(payload) {
280
+ console.log(payload.ok ? "Template policy check passed" : "Template policy check failed");
281
+ console.log(`Policy: ${payload.path}`);
282
+ console.log(`Exists: ${payload.exists ? "yes" : "no"}`);
283
+ for (const diagnostic of payload.diagnostics) {
284
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
285
+ if (diagnostic.path) {
286
+ console.log(` path: ${diagnostic.path}`);
287
+ }
288
+ if (diagnostic.suggestedFix) {
289
+ console.log(` fix: ${diagnostic.suggestedFix}`);
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * @param {string|null|undefined} spec
296
+ * @returns {{ id: string, version: string }|null}
297
+ */
298
+ function parseTemplateVersionPin(spec) {
299
+ if (!spec) {
300
+ return null;
301
+ }
302
+ const separator = spec.lastIndexOf("@");
303
+ if (separator <= 0 || separator === spec.length - 1) {
304
+ throw new Error("Template policy pin requires a template id and version, for example @scope/template@0.2.0.");
305
+ }
306
+ return {
307
+ id: spec.slice(0, separator),
308
+ version: spec.slice(separator + 1)
309
+ };
310
+ }
311
+
312
+ /**
313
+ * @param {string} projectPath
314
+ * @param {string|null|undefined} spec
315
+ * @returns {{ ok: boolean, path: string, policy: any, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[], errors: string[] }}
316
+ */
317
+ export function buildTemplatePolicyPinPayload(projectPath, spec) {
318
+ const projectConfigInfo = loadProjectConfig(projectPath);
319
+ if (!projectConfigInfo) {
320
+ const diagnostic = templateCheckDiagnostic({
321
+ code: "template_policy_project_missing",
322
+ message: "Cannot pin template policy without topogram.project.json.",
323
+ path: path.resolve(projectPath),
324
+ suggestedFix: "Run this command in a Topogram project.",
325
+ step: "policy"
326
+ });
327
+ return {
328
+ ok: false,
329
+ path: path.join(path.resolve(projectPath), "topogram.template-policy.json"),
330
+ policy: null,
331
+ pinned: { id: "", version: "" },
332
+ diagnostics: [diagnostic],
333
+ errors: [diagnostic.message]
334
+ };
335
+ }
336
+ const parsed = parseTemplateVersionPin(spec);
337
+ const currentTemplate = projectConfigInfo.config.template || {};
338
+ const pin = parsed || {
339
+ id: typeof currentTemplate.id === "string" ? currentTemplate.id : "",
340
+ version: typeof currentTemplate.version === "string" ? currentTemplate.version : ""
341
+ };
342
+ if (!pin.id || !pin.version) {
343
+ const diagnostic = templateCheckDiagnostic({
344
+ code: "template_policy_pin_missing_version",
345
+ message: "Cannot pin a template version without a template id and version.",
346
+ path: projectConfigInfo.configPath,
347
+ suggestedFix: "Pass a pin such as @scope/template@0.2.0, or ensure topogram.project.json records template.id and template.version.",
348
+ step: "policy"
349
+ });
350
+ return {
351
+ ok: false,
352
+ path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
353
+ policy: null,
354
+ pinned: pin,
355
+ diagnostics: [diagnostic],
356
+ errors: [diagnostic.message]
357
+ };
358
+ }
359
+
360
+ const existing = loadTemplatePolicy(projectConfigInfo.configDir);
361
+ const diagnostics = existing.diagnostics.map((diagnostic) => templateCheckDiagnostic(diagnostic));
362
+ if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
363
+ return {
364
+ ok: false,
365
+ path: existing.path,
366
+ policy: existing.policy,
367
+ pinned: pin,
368
+ diagnostics,
369
+ errors: diagnostics.map((diagnostic) => diagnostic.message)
370
+ };
371
+ }
372
+ const policy = existing.policy || writeTemplatePolicyForProject(projectConfigInfo.configDir, projectConfigInfo.config);
373
+ const allowedTemplateIds = policy.allowedTemplateIds.includes(pin.id)
374
+ ? policy.allowedTemplateIds
375
+ : [...policy.allowedTemplateIds, pin.id];
376
+ const allowedPackageScopes = [...(policy.allowedPackageScopes || [])];
377
+ if (pin.id.startsWith("@")) {
378
+ const scope = pin.id.split("/")[0];
379
+ if (scope && !allowedPackageScopes.includes(scope)) {
380
+ allowedPackageScopes.push(scope);
381
+ }
382
+ }
383
+ const nextPolicy = {
384
+ ...policy,
385
+ allowedTemplateIds,
386
+ allowedPackageScopes,
387
+ pinnedVersions: {
388
+ ...(policy.pinnedVersions || {}),
389
+ [pin.id]: pin.version
390
+ }
391
+ };
392
+ writeTemplatePolicy(projectConfigInfo.configDir, nextPolicy);
393
+ return {
394
+ ok: true,
395
+ path: path.join(projectConfigInfo.configDir, "topogram.template-policy.json"),
396
+ policy: nextPolicy,
397
+ pinned: pin,
398
+ diagnostics: [],
399
+ errors: []
400
+ };
401
+ }
402
+
403
+ /**
404
+ * @param {{ ok: boolean, path: string, pinned: { id: string, version: string }, diagnostics: TemplateCheckDiagnostic[] }} payload
405
+ * @returns {void}
406
+ */
407
+ export function printTemplatePolicyPinPayload(payload) {
408
+ console.log(payload.ok ? "Template policy pin updated" : "Template policy pin failed");
409
+ console.log(`Policy: ${payload.path}`);
410
+ if (payload.pinned.id) {
411
+ console.log(`Pinned: ${payload.pinned.id}@${payload.pinned.version || "unknown"}`);
412
+ }
413
+ for (const diagnostic of payload.diagnostics) {
414
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
415
+ if (diagnostic.path) {
416
+ console.log(` path: ${diagnostic.path}`);
417
+ }
418
+ if (diagnostic.suggestedFix) {
419
+ console.log(` fix: ${diagnostic.suggestedFix}`);
420
+ }
421
+ }
422
+ }
@@ -0,0 +1,127 @@
1
+ // @ts-check
2
+
3
+ import childProcess from "node:child_process";
4
+
5
+ import { assertSafeNpmSpec, localNpmrcEnv } from "../../../npm-safety.js";
6
+
7
+ /**
8
+ * @param {unknown} error
9
+ * @returns {string}
10
+ */
11
+ export function messageFromError(error) {
12
+ return error instanceof Error ? error.message : String(error);
13
+ }
14
+
15
+ /**
16
+ * @param {...{ ok: boolean, errors?: any[] }|null|undefined} results
17
+ * @returns {{ ok: boolean, errors: any[] }}
18
+ */
19
+ export function combineProjectValidationResults(...results) {
20
+ const errors = [];
21
+ for (const result of results) {
22
+ errors.push(...(result?.errors || []));
23
+ }
24
+ return {
25
+ ok: errors.length === 0,
26
+ errors
27
+ };
28
+ }
29
+
30
+ /**
31
+ * @param {string} spec
32
+ * @returns {string}
33
+ */
34
+ export function packageNameFromPackageSpec(spec) {
35
+ if (spec.startsWith("@")) {
36
+ const segments = spec.split("/");
37
+ if (segments.length < 2) {
38
+ throw new Error(`Invalid scoped package spec '${spec}'.`);
39
+ }
40
+ const scope = segments[0];
41
+ const nameAndVersion = segments.slice(1).join("/");
42
+ const versionIndex = nameAndVersion.indexOf("@");
43
+ return `${scope}/${versionIndex >= 0 ? nameAndVersion.slice(0, versionIndex) : nameAndVersion}`;
44
+ }
45
+ const versionIndex = spec.indexOf("@");
46
+ return versionIndex >= 0 ? spec.slice(0, versionIndex) : spec;
47
+ }
48
+
49
+ /**
50
+ * @param {Record<string, any>|null|undefined} projectConfig
51
+ * @returns {{ 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 }}
52
+ */
53
+ export function templateMetadataFromProjectConfig(projectConfig) {
54
+ const template = projectConfig?.template || {};
55
+ return {
56
+ id: typeof template.id === "string" ? template.id : null,
57
+ version: typeof template.version === "string" ? template.version : null,
58
+ source: typeof template.source === "string" ? template.source : null,
59
+ sourceSpec: typeof template.sourceSpec === "string" ? template.sourceSpec : null,
60
+ requested: typeof template.requested === "string" ? template.requested : null,
61
+ sourceRoot: typeof template.sourceRoot === "string" ? template.sourceRoot : null,
62
+ catalog: template.catalog && typeof template.catalog === "object" && !Array.isArray(template.catalog)
63
+ ? template.catalog
64
+ : null,
65
+ includesExecutableImplementation: typeof template.includesExecutableImplementation === "boolean"
66
+ ? template.includesExecutableImplementation
67
+ : null
68
+ };
69
+ }
70
+
71
+ /**
72
+ * @param {string} packageName
73
+ * @returns {string}
74
+ */
75
+ function latestVersionForPackage(packageName) {
76
+ assertSafeNpmSpec(packageName);
77
+ const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
78
+ const result = childProcess.spawnSync(npmBin, ["view", "--json", "--", packageName, "version"], {
79
+ encoding: "utf8",
80
+ env: {
81
+ ...process.env,
82
+ ...localNpmrcEnv(process.cwd()),
83
+ PATH: process.env.PATH || ""
84
+ }
85
+ });
86
+ if (result.status !== 0) {
87
+ throw new Error(`Failed to inspect latest version for '${packageName}'.\n${result.stderr || result.stdout}`.trim());
88
+ }
89
+ const raw = (result.stdout || "").trim();
90
+ if (!raw) {
91
+ throw new Error(`npm view returned no version for '${packageName}'.`);
92
+ }
93
+ const parsed = JSON.parse(raw);
94
+ if (typeof parsed !== "string" || !parsed) {
95
+ throw new Error(`npm view returned an invalid version for '${packageName}'.`);
96
+ }
97
+ return parsed;
98
+ }
99
+
100
+ /**
101
+ * @param {ReturnType<typeof templateMetadataFromProjectConfig>} template
102
+ * @returns {{ checked: boolean, supported: boolean, packageName: string|null, version: string|null, isCurrent: boolean|null, candidateSpec: string|null, reason: string|null }}
103
+ */
104
+ export function latestTemplateInfo(template) {
105
+ if (template.source !== "package") {
106
+ return {
107
+ checked: true,
108
+ supported: false,
109
+ packageName: null,
110
+ version: null,
111
+ isCurrent: null,
112
+ candidateSpec: null,
113
+ reason: "Latest-version lookup is only supported for package-backed templates."
114
+ };
115
+ }
116
+ const packageName = packageNameFromPackageSpec(template.sourceSpec || template.requested || template.id || "");
117
+ const version = latestVersionForPackage(packageName);
118
+ return {
119
+ checked: true,
120
+ supported: true,
121
+ packageName,
122
+ version,
123
+ isCurrent: template.version === version,
124
+ candidateSpec: `${packageName}@${version}`,
125
+ reason: null
126
+ };
127
+ }