@topogram/cli 0.3.64 → 0.3.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/agent-ops/query-builders/auth.js +375 -0
  5. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  6. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  7. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  8. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  9. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  10. package/src/agent-ops/query-builders/change-risk.js +25 -0
  11. package/src/agent-ops/query-builders/common.js +149 -0
  12. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  13. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  14. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  15. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  16. package/src/agent-ops/query-builders/work-packets.js +417 -0
  17. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  18. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  19. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  20. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  21. package/src/agent-ops/query-builders.d.ts +26 -26
  22. package/src/agent-ops/query-builders.js +42 -5021
  23. package/src/catalog/constants.js +10 -0
  24. package/src/catalog/copy.js +60 -0
  25. package/src/catalog/diagnostics.js +15 -0
  26. package/src/catalog/entries.js +42 -0
  27. package/src/catalog/files.js +67 -0
  28. package/src/catalog/provenance.js +122 -0
  29. package/src/catalog/source.js +150 -0
  30. package/src/catalog/validation.js +252 -0
  31. package/src/catalog.d.ts +2 -0
  32. package/src/catalog.js +18 -746
  33. package/src/cli/commands/catalog/check.js +31 -0
  34. package/src/cli/commands/catalog/copy.js +59 -0
  35. package/src/cli/commands/catalog/doctor.js +248 -0
  36. package/src/cli/commands/catalog/help.js +21 -0
  37. package/src/cli/commands/catalog/list.js +52 -0
  38. package/src/cli/commands/catalog/runner.js +92 -0
  39. package/src/cli/commands/catalog/shared.js +17 -0
  40. package/src/cli/commands/catalog/show.js +134 -0
  41. package/src/cli/commands/catalog.js +30 -615
  42. package/src/cli/commands/generator-policy/package-info.js +162 -0
  43. package/src/cli/commands/generator-policy/payloads.js +372 -0
  44. package/src/cli/commands/generator-policy/printers.js +159 -0
  45. package/src/cli/commands/generator-policy/runner.js +81 -0
  46. package/src/cli/commands/generator-policy/shared.js +39 -0
  47. package/src/cli/commands/generator-policy.js +15 -783
  48. package/src/cli/commands/import/adopt.js +170 -0
  49. package/src/cli/commands/import/check.js +91 -0
  50. package/src/cli/commands/import/diff.js +84 -0
  51. package/src/cli/commands/import/help.js +47 -0
  52. package/src/cli/commands/import/paths.js +277 -0
  53. package/src/cli/commands/import/plan.js +284 -0
  54. package/src/cli/commands/import/refresh.js +470 -0
  55. package/src/cli/commands/import/status-history.js +196 -0
  56. package/src/cli/commands/import/workspace.js +230 -0
  57. package/src/cli/commands/import.js +33 -1732
  58. package/src/cli/commands/package/constants.js +17 -0
  59. package/src/cli/commands/package/doctor.js +240 -0
  60. package/src/cli/commands/package/help.js +27 -0
  61. package/src/cli/commands/package/lockfile.js +135 -0
  62. package/src/cli/commands/package/npm.js +97 -0
  63. package/src/cli/commands/package/reporting.js +35 -0
  64. package/src/cli/commands/package/runner.js +33 -0
  65. package/src/cli/commands/package/shared.js +9 -0
  66. package/src/cli/commands/package/update-cli.js +252 -0
  67. package/src/cli/commands/package/versions.js +35 -0
  68. package/src/cli/commands/package.js +29 -813
  69. package/src/cli/commands/query/change-plan.js +68 -0
  70. package/src/cli/commands/query/definitions.js +202 -0
  71. package/src/cli/commands/query/import-adopt.js +121 -0
  72. package/src/cli/commands/query/runner/artifacts.js +102 -0
  73. package/src/cli/commands/query/runner/boundaries.js +211 -0
  74. package/src/cli/commands/query/runner/change.js +182 -0
  75. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  76. package/src/cli/commands/query/runner/index.js +31 -0
  77. package/src/cli/commands/query/runner/output.js +12 -0
  78. package/src/cli/commands/query/runner/workflow.js +241 -0
  79. package/src/cli/commands/query/runner.js +3 -0
  80. package/src/cli/commands/query/workflow-context.js +5 -0
  81. package/src/cli/commands/query/workspace.js +274 -0
  82. package/src/cli/commands/query.js +9 -1300
  83. package/src/cli/commands/template/baseline.js +100 -0
  84. package/src/cli/commands/template/check.js +466 -0
  85. package/src/cli/commands/template/constants.js +8 -0
  86. package/src/cli/commands/template/diagnostics.js +26 -0
  87. package/src/cli/commands/template/help.js +28 -0
  88. package/src/cli/commands/template/lifecycle.js +404 -0
  89. package/src/cli/commands/template/list-show.js +287 -0
  90. package/src/cli/commands/template/policy.js +422 -0
  91. package/src/cli/commands/template/shared.js +127 -0
  92. package/src/cli/commands/template/updates.js +352 -0
  93. package/src/cli/commands/template.js +41 -2143
  94. package/src/generator/api/contracts.js +497 -0
  95. package/src/generator/api/metadata.js +221 -0
  96. package/src/generator/api/openapi.js +559 -0
  97. package/src/generator/api/schema.js +124 -0
  98. package/src/generator/api/types.d.ts +98 -0
  99. package/src/generator/api.js +3 -1195
  100. package/src/generator/context/shared/domain-sdlc.js +282 -0
  101. package/src/generator/context/shared/maintained-boundary.js +665 -0
  102. package/src/generator/context/shared/metrics.js +85 -0
  103. package/src/generator/context/shared/primitives.js +64 -0
  104. package/src/generator/context/shared/relationships.js +453 -0
  105. package/src/generator/context/shared/summaries.js +263 -0
  106. package/src/generator/context/shared/types.d.ts +207 -0
  107. package/src/generator/context/shared.d.ts +42 -0
  108. package/src/generator/context/shared.js +80 -1390
  109. package/src/generator/context/slice/core.js +397 -0
  110. package/src/generator/context/slice/sdlc.js +417 -0
  111. package/src/generator/context/slice/ui-packets.js +183 -0
  112. package/src/generator/context/slice.js +2 -859
  113. package/src/generator/registry/index.js +507 -0
  114. package/src/generator/registry.js +18 -504
  115. package/src/generator/runtime/environment/index.js +666 -0
  116. package/src/generator/runtime/environment.js +4 -666
  117. package/src/generator/runtime/runtime-check/index.js +554 -0
  118. package/src/generator/runtime/runtime-check.js +4 -554
  119. package/src/generator/runtime/shared/index.js +572 -0
  120. package/src/generator/runtime/shared.js +19 -570
  121. package/src/generator/shared.d.ts +2 -0
  122. package/src/generator/surfaces/shared.d.ts +3 -0
  123. package/src/generator/widget-conformance/behavior-report.js +258 -0
  124. package/src/generator/widget-conformance/checks.js +371 -0
  125. package/src/generator/widget-conformance/projection-context.js +200 -0
  126. package/src/generator/widget-conformance/report.js +166 -0
  127. package/src/generator/widget-conformance/types.d.ts +121 -0
  128. package/src/generator/widget-conformance.js +3 -824
  129. package/src/import/core/context.d.ts +3 -0
  130. package/src/import/core/contracts.d.ts +1 -0
  131. package/src/import/core/registry.d.ts +4 -0
  132. package/src/import/core/runner/candidates.js +217 -0
  133. package/src/import/core/runner/options.js +22 -0
  134. package/src/import/core/runner/reports.js +50 -0
  135. package/src/import/core/runner/run.js +79 -0
  136. package/src/import/core/runner/tracks.js +150 -0
  137. package/src/import/core/runner/ui-drafts.js +337 -0
  138. package/src/import/core/runner.js +3 -698
  139. package/src/import/core/shared/api-routes.js +221 -0
  140. package/src/import/core/shared/candidates.js +97 -0
  141. package/src/import/core/shared/files.js +177 -0
  142. package/src/import/core/shared/next-app.js +389 -0
  143. package/src/import/core/shared/types.d.ts +51 -0
  144. package/src/import/core/shared/ui-routes.js +230 -0
  145. package/src/import/core/shared.js +60 -861
  146. package/src/new-project/constants.js +128 -0
  147. package/src/new-project/create.js +83 -0
  148. package/src/new-project/json.js +28 -0
  149. package/src/new-project/metadata.js +96 -0
  150. package/src/new-project/package-spec.js +161 -0
  151. package/src/new-project/project-files.js +348 -0
  152. package/src/new-project/template-policy.js +269 -0
  153. package/src/new-project/template-resolution.js +368 -0
  154. package/src/new-project/template-snapshots.js +430 -0
  155. package/src/new-project/template-updates.js +512 -0
  156. package/src/new-project/types.d.ts +83 -0
  157. package/src/new-project.js +6 -2277
  158. package/src/parser.d.ts +87 -1
  159. package/src/parser.js +118 -0
  160. package/src/policy/review-boundaries.d.ts +15 -0
  161. package/src/project-config/index.js +564 -0
  162. package/src/project-config.js +19 -561
  163. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  164. package/src/resolver/enrich/bug.js +2 -0
  165. package/src/resolver/enrich/pitch.js +2 -0
  166. package/src/resolver/enrich/requirement.js +2 -0
  167. package/src/resolver/enrich/task.js +2 -0
  168. package/src/resolver/index.js +19 -2089
  169. package/src/resolver/normalize.js +384 -1
  170. package/src/resolver/plans.js +168 -0
  171. package/src/resolver/projections-api.js +494 -0
  172. package/src/resolver/projections-db.js +133 -0
  173. package/src/resolver/projections-ui.js +317 -0
  174. package/src/resolver/shapes.js +251 -0
  175. package/src/resolver/shared.js +278 -0
  176. package/src/resolver/widgets.js +132 -0
  177. package/src/template-trust/constants.js +62 -0
  178. package/src/template-trust/content.js +258 -0
  179. package/src/template-trust/diff.js +92 -0
  180. package/src/template-trust/policy.js +61 -0
  181. package/src/template-trust/record.js +90 -0
  182. package/src/template-trust/status.js +182 -0
  183. package/src/template-trust.js +24 -687
  184. package/src/text-helpers.d.ts +1 -0
  185. package/src/topogram-types.d.ts +69 -0
  186. package/src/validator/common.js +488 -0
  187. package/src/validator/data-model.js +237 -0
  188. package/src/validator/docs.js +167 -0
  189. package/src/validator/expressions.js +146 -1
  190. package/src/validator/index.d.ts +23 -0
  191. package/src/validator/index.js +32 -3585
  192. package/src/validator/kinds.d.ts +41 -0
  193. package/src/validator/kinds.js +2 -0
  194. package/src/validator/model-helpers.js +46 -0
  195. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  196. package/src/validator/per-kind/bug.js +6 -0
  197. package/src/validator/per-kind/domain.js +15 -2
  198. package/src/validator/per-kind/pitch.js +7 -0
  199. package/src/validator/per-kind/requirement.js +5 -0
  200. package/src/validator/per-kind/task.js +7 -0
  201. package/src/validator/per-kind/widget.js +14 -0
  202. package/src/validator/projections/api-http-async.js +410 -0
  203. package/src/validator/projections/api-http-authz.js +88 -0
  204. package/src/validator/projections/api-http-core.js +205 -0
  205. package/src/validator/projections/api-http-policies.js +339 -0
  206. package/src/validator/projections/api-http-responses.js +233 -0
  207. package/src/validator/projections/api-http.js +44 -0
  208. package/src/validator/projections/db.js +353 -0
  209. package/src/validator/projections/generator-defaults.js +45 -0
  210. package/src/validator/projections/helpers.js +87 -0
  211. package/src/validator/projections/ui-helpers.js +214 -0
  212. package/src/validator/projections/ui-navigation.js +344 -0
  213. package/src/validator/projections/ui-structure.js +364 -0
  214. package/src/validator/projections/ui-widgets.js +493 -0
  215. package/src/validator/projections/ui.js +46 -0
  216. package/src/validator/registry.js +48 -1
  217. package/src/validator/utils.d.ts +20 -0
  218. package/src/validator/utils.js +115 -12
  219. package/src/widget-behavior.d.ts +1 -0
  220. package/src/workflows/import-app/api/collect.js +221 -0
  221. package/src/workflows/import-app/api/openapi.js +257 -0
  222. package/src/workflows/import-app/api/routes.js +327 -0
  223. package/src/workflows/import-app/api/sources.js +22 -0
  224. package/src/workflows/import-app/api.js +2 -797
  225. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  226. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  227. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  228. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  229. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  230. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  231. package/src/workflows/reconcile/adoption-plan.js +30 -740
  232. package/src/workflows/reconcile/auth/closures.js +115 -0
  233. package/src/workflows/reconcile/auth/formatters.js +142 -0
  234. package/src/workflows/reconcile/auth/inference.js +330 -0
  235. package/src/workflows/reconcile/auth/roles.js +122 -0
  236. package/src/workflows/reconcile/auth.js +35 -690
  237. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  238. package/src/workflows/reconcile/bundle-core.js +12 -598
  239. package/src/workflows/reconcile/canonical-surface.js +1 -1
  240. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  241. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  242. package/src/workflows/reconcile/impacts/patches.js +252 -0
  243. package/src/workflows/reconcile/impacts/reports.js +80 -0
  244. package/src/workflows/reconcile/impacts.js +14 -623
  245. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,162 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ /**
7
+ * @param {string} filePath
8
+ * @returns {any|null}
9
+ */
10
+ export function readJsonIfPresent(filePath) {
11
+ if (!fs.existsSync(filePath)) {
12
+ return null;
13
+ }
14
+ try {
15
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ /**
22
+ * @param {Record<string, any>|null} projectPackage
23
+ * @param {string} packageName
24
+ * @returns {{ field: string|null, spec: string|null }}
25
+ */
26
+ function dependencySpecForPackage(projectPackage, packageName) {
27
+ const dependencyFields = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
28
+ for (const field of dependencyFields) {
29
+ const dependencies = projectPackage?.[field];
30
+ if (dependencies && typeof dependencies === "object" && typeof dependencies[packageName] === "string") {
31
+ return {
32
+ field,
33
+ spec: dependencies[packageName]
34
+ };
35
+ }
36
+ }
37
+ return {
38
+ field: null,
39
+ spec: null
40
+ };
41
+ }
42
+
43
+ /**
44
+ * @param {Record<string, any>|null} lockfile
45
+ * @param {string} packageName
46
+ * @returns {{ version: string|null, resolved: string|null, integrity: string|null, entryPath: string|null }}
47
+ */
48
+ function npmLockfileInfoForPackage(lockfile, packageName) {
49
+ const packageEntryPath = `node_modules/${packageName}`;
50
+ const packageEntry = lockfile?.packages?.[packageEntryPath];
51
+ if (packageEntry && typeof packageEntry === "object") {
52
+ return {
53
+ version: typeof packageEntry.version === "string" ? packageEntry.version : null,
54
+ resolved: typeof packageEntry.resolved === "string" ? packageEntry.resolved : null,
55
+ integrity: typeof packageEntry.integrity === "string" ? packageEntry.integrity : null,
56
+ entryPath: packageEntryPath
57
+ };
58
+ }
59
+ const dependencyEntry = lockfile?.dependencies?.[packageName];
60
+ if (dependencyEntry && typeof dependencyEntry === "object") {
61
+ return {
62
+ version: typeof dependencyEntry.version === "string" ? dependencyEntry.version : null,
63
+ resolved: typeof dependencyEntry.resolved === "string" ? dependencyEntry.resolved : null,
64
+ integrity: typeof dependencyEntry.integrity === "string" ? dependencyEntry.integrity : null,
65
+ entryPath: packageName
66
+ };
67
+ }
68
+ return {
69
+ version: null,
70
+ resolved: null,
71
+ integrity: null,
72
+ entryPath: null
73
+ };
74
+ }
75
+
76
+ /**
77
+ * @param {string} projectRoot
78
+ * @returns {{ kind: "npm"|"pnpm"|"yarn"|"bun"|null, path: string|null, data: Record<string, any>|null, note: string|null }}
79
+ */
80
+ function lockfileMetadataForProject(projectRoot) {
81
+ const npmLockfilePath = path.join(projectRoot, "package-lock.json");
82
+ if (fs.existsSync(npmLockfilePath)) {
83
+ return {
84
+ kind: "npm",
85
+ path: npmLockfilePath,
86
+ data: readJsonIfPresent(npmLockfilePath),
87
+ note: null
88
+ };
89
+ }
90
+ const candidates = [
91
+ { kind: "pnpm", file: "pnpm-lock.yaml" },
92
+ { kind: "yarn", file: "yarn.lock" },
93
+ { kind: "bun", file: "bun.lock" },
94
+ { kind: "bun", file: "bun.lockb" }
95
+ ];
96
+ for (const candidate of candidates) {
97
+ const lockfilePath = path.join(projectRoot, candidate.file);
98
+ if (fs.existsSync(lockfilePath)) {
99
+ return {
100
+ kind: /** @type {"pnpm"|"yarn"|"bun"} */ (candidate.kind),
101
+ path: lockfilePath,
102
+ data: null,
103
+ note: `${candidate.file} found; package versions are not inspected by this command.`
104
+ };
105
+ }
106
+ }
107
+ return {
108
+ kind: null,
109
+ path: null,
110
+ data: null,
111
+ note: null
112
+ };
113
+ }
114
+
115
+ /**
116
+ * @param {string} projectRoot
117
+ * @param {string} packageName
118
+ * @returns {{ dependencyField: string|null, dependencySpec: string|null, installedVersion: string|null, installedPackageJsonPath: string|null, lockfileKind: "npm"|"pnpm"|"yarn"|"bun"|null, lockfilePath: string|null, lockfileVersion: string|null, lockfileResolved: string|null, lockfileIntegrity: string|null, lockfileEntryPath: string|null, lockfileNote: string|null }}
119
+ */
120
+ export function packageInfoForGenerator(projectRoot, packageName) {
121
+ const projectPackage = readJsonIfPresent(path.join(projectRoot, "package.json"));
122
+ const dependency = dependencySpecForPackage(projectPackage, packageName);
123
+ const lockfile = lockfileMetadataForProject(projectRoot);
124
+ const lockfileInfo = lockfile.kind === "npm"
125
+ ? npmLockfileInfoForPackage(lockfile.data, packageName)
126
+ : {
127
+ version: null,
128
+ resolved: null,
129
+ integrity: null,
130
+ entryPath: null
131
+ };
132
+ const installedPackageJsonPath = path.join(projectRoot, "node_modules", ...packageName.split("/"), "package.json");
133
+ const installedPackage = readJsonIfPresent(installedPackageJsonPath);
134
+ return {
135
+ dependencyField: dependency.field,
136
+ dependencySpec: dependency.spec,
137
+ installedVersion: typeof installedPackage?.version === "string" ? installedPackage.version : null,
138
+ installedPackageJsonPath: installedPackage ? installedPackageJsonPath : null,
139
+ lockfileKind: lockfile.kind,
140
+ lockfilePath: lockfile.path,
141
+ lockfileVersion: lockfileInfo.version,
142
+ lockfileResolved: lockfileInfo.resolved,
143
+ lockfileIntegrity: lockfileInfo.integrity,
144
+ lockfileEntryPath: lockfileInfo.entryPath,
145
+ lockfileNote: lockfile.note
146
+ };
147
+ }
148
+
149
+ /**
150
+ * @param {ReturnType<typeof packageInfoForGenerator>} packageInfo
151
+ * @returns {string}
152
+ */
153
+ export function formatGeneratorPackageLockfile(packageInfo) {
154
+ if (!packageInfo.lockfileKind || !packageInfo.lockfilePath) {
155
+ return "(not found)";
156
+ }
157
+ const label = path.basename(packageInfo.lockfilePath);
158
+ if (packageInfo.lockfileVersion) {
159
+ return `${packageInfo.lockfileKind} ${packageInfo.lockfileVersion}`;
160
+ }
161
+ return `${label} (version not inspected)`;
162
+ }
@@ -0,0 +1,372 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ import {
6
+ defaultGeneratorPolicy,
7
+ GENERATOR_POLICY_FILE,
8
+ generatorPackageAllowed,
9
+ generatorPolicyDiagnosticsForBindings,
10
+ loadGeneratorPolicy,
11
+ packageBackedGeneratorBindings,
12
+ packageScopeFromName,
13
+ parseGeneratorPolicyPin,
14
+ writeGeneratorPolicy
15
+ } from "../../../generator-policy.js";
16
+ import { loadProjectConfig } from "../../../project-config.js";
17
+ import { packageInfoForGenerator } from "./package-info.js";
18
+ import { effectiveGeneratorPolicy, generatorPolicyRule } from "./shared.js";
19
+
20
+ /**
21
+ * @param {string} projectRoot
22
+ * @param {any} policy
23
+ * @param {ReturnType<typeof packageBackedGeneratorBindings>[number]} binding
24
+ * @returns {ReturnType<typeof packageBackedGeneratorBindings>[number] & { allowed: boolean, packageInfo: ReturnType<typeof packageInfoForGenerator>, pin: { key: string|null, version: string|null, matches: boolean|null } }}
25
+ */
26
+ function generatorPolicyBindingStatus(projectRoot, policy, binding) {
27
+ const packagePin = policy.pinnedVersions[binding.packageName] || null;
28
+ const generatorPin = policy.pinnedVersions[binding.generatorId] || null;
29
+ const pinnedVersion = packagePin || generatorPin;
30
+ return {
31
+ ...binding,
32
+ allowed: generatorPackageAllowed(policy, binding.packageName),
33
+ packageInfo: packageInfoForGenerator(projectRoot, binding.packageName),
34
+ pin: {
35
+ key: packagePin ? binding.packageName : generatorPin ? binding.generatorId : null,
36
+ version: pinnedVersion,
37
+ matches: pinnedVersion ? pinnedVersion === binding.version : null
38
+ }
39
+ };
40
+ }
41
+
42
+ /**
43
+ * @param {any[]} diagnostics
44
+ * @param {Array<ReturnType<typeof generatorPolicyBindingStatus>>} bindings
45
+ * @returns {any[]}
46
+ */
47
+ function annotateGeneratorPolicyDiagnostics(diagnostics, bindings) {
48
+ return diagnostics.map((diagnostic) => {
49
+ const binding = bindings.find((item) => (
50
+ item.packageName === diagnostic.packageName &&
51
+ (!diagnostic.runtimeId || item.runtimeId === diagnostic.runtimeId)
52
+ ));
53
+ if (!binding) {
54
+ return diagnostic;
55
+ }
56
+ return {
57
+ ...diagnostic,
58
+ packageVersion: binding.packageInfo.installedVersion || binding.packageInfo.lockfileVersion || null,
59
+ packageDependencyField: binding.packageInfo.dependencyField,
60
+ packageDependencySpec: binding.packageInfo.dependencySpec,
61
+ packageLockfileKind: binding.packageInfo.lockfileKind,
62
+ packageLockfilePath: binding.packageInfo.lockfilePath,
63
+ packageLockVersion: binding.packageInfo.lockfileVersion
64
+ };
65
+ });
66
+ }
67
+
68
+ /**
69
+ * @param {Array<ReturnType<typeof generatorPolicyBindingStatus>>} bindings
70
+ * @returns {any[]}
71
+ */
72
+ function generatorPolicyPackageMetadataDiagnostics(bindings) {
73
+ const diagnostics = [];
74
+ for (const binding of bindings) {
75
+ if (!binding.packageInfo.dependencySpec) {
76
+ diagnostics.push({
77
+ code: "generator_package_dependency_missing",
78
+ severity: "warning",
79
+ message: `Runtime '${binding.runtimeId}' generator package '${binding.packageName}' is not declared in package.json dependencies.`,
80
+ path: binding.packageInfo.installedPackageJsonPath,
81
+ suggestedFix: `Declare '${binding.packageName}' in package.json devDependencies so generator adoption is visible in package review.`,
82
+ step: "generator-policy",
83
+ runtimeId: binding.runtimeId,
84
+ generatorId: binding.generatorId,
85
+ packageName: binding.packageName,
86
+ version: binding.version,
87
+ packageVersion: binding.packageInfo.installedVersion || binding.packageInfo.lockfileVersion || null,
88
+ packageDependencyField: binding.packageInfo.dependencyField,
89
+ packageDependencySpec: binding.packageInfo.dependencySpec,
90
+ packageLockfileKind: binding.packageInfo.lockfileKind,
91
+ packageLockfilePath: binding.packageInfo.lockfilePath,
92
+ packageLockVersion: binding.packageInfo.lockfileVersion
93
+ });
94
+ }
95
+ if (
96
+ binding.packageInfo.installedVersion &&
97
+ binding.packageInfo.lockfileVersion &&
98
+ binding.packageInfo.installedVersion !== binding.packageInfo.lockfileVersion
99
+ ) {
100
+ diagnostics.push({
101
+ code: "generator_package_version_drift",
102
+ severity: "warning",
103
+ message: `Runtime '${binding.runtimeId}' generator package '${binding.packageName}' is installed at '${binding.packageInfo.installedVersion}', but package-lock records '${binding.packageInfo.lockfileVersion}'.`,
104
+ path: binding.packageInfo.lockfilePath,
105
+ suggestedFix: "Run the package manager install command and review the resulting lockfile before pinning generator policy.",
106
+ step: "generator-policy",
107
+ runtimeId: binding.runtimeId,
108
+ generatorId: binding.generatorId,
109
+ packageName: binding.packageName,
110
+ version: binding.version,
111
+ packageVersion: binding.packageInfo.installedVersion,
112
+ packageDependencyField: binding.packageInfo.dependencyField,
113
+ packageDependencySpec: binding.packageInfo.dependencySpec,
114
+ packageLockfileKind: binding.packageInfo.lockfileKind,
115
+ packageLockfilePath: binding.packageInfo.lockfilePath,
116
+ packageLockVersion: binding.packageInfo.lockfileVersion
117
+ });
118
+ }
119
+ }
120
+ return diagnostics;
121
+ }
122
+
123
+ /**
124
+ * @param {string} projectPath
125
+ * @returns {{ ok: boolean, path: string, exists: boolean, policy: any, defaulted: boolean, bindings: Array<ReturnType<typeof generatorPolicyBindingStatus>>, diagnostics: any[], errors: string[] }}
126
+ */
127
+ export function buildGeneratorPolicyCheckPayload(projectPath) {
128
+ const projectConfigInfo = loadProjectConfig(projectPath);
129
+ if (!projectConfigInfo) {
130
+ const diagnostic = {
131
+ code: "generator_policy_project_missing",
132
+ severity: "error",
133
+ message: "Cannot check generator policy without topogram.project.json.",
134
+ path: path.resolve(projectPath),
135
+ suggestedFix: "Run this command in a Topogram project.",
136
+ step: "generator-policy"
137
+ };
138
+ return {
139
+ ok: false,
140
+ path: path.join(path.resolve(projectPath), GENERATOR_POLICY_FILE),
141
+ exists: false,
142
+ policy: null,
143
+ defaulted: false,
144
+ bindings: [],
145
+ diagnostics: [diagnostic],
146
+ errors: [diagnostic.message]
147
+ };
148
+ }
149
+ const policyInfo = loadGeneratorPolicy(projectConfigInfo.configDir);
150
+ const rawBindings = packageBackedGeneratorBindings(projectConfigInfo.config);
151
+ const policy = policyInfo.policy || effectiveGeneratorPolicy(policyInfo);
152
+ const bindings = rawBindings.map((binding) => generatorPolicyBindingStatus(projectConfigInfo.configDir, policy, binding));
153
+ const diagnostics = [];
154
+ if (!policyInfo.exists) {
155
+ diagnostics.push({
156
+ code: "generator_policy_missing",
157
+ severity: "warning",
158
+ message: `No ${GENERATOR_POLICY_FILE} found. Default generator policy allows @topogram/* package-backed generators and blocks other package scopes.`,
159
+ path: policyInfo.path,
160
+ suggestedFix: "Run `topogram generator policy init` to write an explicit project generator policy after review.",
161
+ step: "generator-policy"
162
+ });
163
+ }
164
+ diagnostics.push(...generatorPolicyDiagnosticsForBindings(policyInfo, rawBindings, "generator-policy"));
165
+ diagnostics.push(...generatorPolicyPackageMetadataDiagnostics(bindings));
166
+ const annotatedDiagnostics = annotateGeneratorPolicyDiagnostics(diagnostics, bindings);
167
+ const errors = annotatedDiagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
168
+ return {
169
+ ok: errors.length === 0,
170
+ path: policyInfo.path,
171
+ exists: policyInfo.exists,
172
+ policy,
173
+ defaulted: !policyInfo.exists,
174
+ bindings,
175
+ diagnostics: annotatedDiagnostics,
176
+ errors
177
+ };
178
+ }
179
+
180
+ /**
181
+ * @param {string} projectPath
182
+ * @returns {ReturnType<typeof buildGeneratorPolicyCheckPayload> & { rules: Array<{ name: string, ok: boolean, actual: string, expected: string, message: string, fix: string|null }> }}
183
+ */
184
+ export function buildGeneratorPolicyExplainPayload(projectPath) {
185
+ const check = buildGeneratorPolicyCheckPayload(projectPath);
186
+ const policy = check.policy || effectiveGeneratorPolicy({ path: check.path, exists: false, policy: null, diagnostics: [] });
187
+ const rules = [];
188
+ rules.push(generatorPolicyRule(
189
+ "policy-file",
190
+ check.exists,
191
+ check.exists ? "present" : "missing",
192
+ "present",
193
+ check.exists
194
+ ? "Project has a generator policy file."
195
+ : "Project is using the default generator policy.",
196
+ check.exists ? null : "Run `topogram generator policy init` after review."
197
+ ));
198
+ for (const binding of check.bindings) {
199
+ const scope = packageScopeFromName(binding.packageName);
200
+ rules.push(generatorPolicyRule(
201
+ "allowed-package",
202
+ generatorPackageAllowed(policy, binding.packageName),
203
+ `${binding.packageName}${scope ? ` (${scope})` : ""}`,
204
+ [
205
+ `scopes=${policy.allowedPackageScopes.join(", ") || "(none)"}`,
206
+ `packages=${policy.allowedPackages.join(", ") || "(none)"}`
207
+ ].join("; "),
208
+ `Runtime '${binding.runtimeId}' package-backed generator must be from an allowed package or scope.`,
209
+ `Run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` after reviewing the generator package.`
210
+ ));
211
+ const pinnedVersion = policy.pinnedVersions[binding.packageName] || policy.pinnedVersions[binding.generatorId] || null;
212
+ rules.push(generatorPolicyRule(
213
+ "pinned-version",
214
+ !pinnedVersion || pinnedVersion === binding.version,
215
+ binding.version,
216
+ pinnedVersion || "(unpinned)",
217
+ `Runtime '${binding.runtimeId}' generator version must match its policy pin when one exists.`,
218
+ `Run \`topogram generator policy pin ${binding.packageName}@${binding.version}\` after review.`
219
+ ));
220
+ }
221
+ return {
222
+ ...check,
223
+ rules
224
+ };
225
+ }
226
+
227
+ /**
228
+ * @param {string} projectPath
229
+ * @returns {ReturnType<typeof buildGeneratorPolicyExplainPayload> & { summary: { packageBackedGenerators: number, allowed: number, denied: number, pinned: number, unpinned: number, pinMismatches: number } }}
230
+ */
231
+ export function buildGeneratorPolicyStatusPayload(projectPath) {
232
+ const explain = buildGeneratorPolicyExplainPayload(projectPath);
233
+ return {
234
+ ...explain,
235
+ summary: {
236
+ packageBackedGenerators: explain.bindings.length,
237
+ allowed: explain.bindings.filter((binding) => binding.allowed).length,
238
+ denied: explain.bindings.filter((binding) => !binding.allowed).length,
239
+ pinned: explain.bindings.filter((binding) => Boolean(binding.pin.version)).length,
240
+ unpinned: explain.bindings.filter((binding) => !binding.pin.version).length,
241
+ pinMismatches: explain.bindings.filter((binding) => binding.pin.matches === false).length
242
+ }
243
+ };
244
+ }
245
+
246
+ /**
247
+ * @param {string} projectPath
248
+ * @returns {{ ok: boolean, path: string, policy: any, diagnostics: any[], errors: string[] }}
249
+ */
250
+ export function buildGeneratorPolicyInitPayload(projectPath) {
251
+ const projectConfigInfo = loadProjectConfig(projectPath);
252
+ if (!projectConfigInfo) {
253
+ throw new Error("Cannot initialize generator policy without topogram.project.json.");
254
+ }
255
+ const policy = writeGeneratorPolicy(projectConfigInfo.configDir, defaultGeneratorPolicy());
256
+ return {
257
+ ok: true,
258
+ path: path.join(projectConfigInfo.configDir, GENERATOR_POLICY_FILE),
259
+ policy,
260
+ diagnostics: [],
261
+ errors: []
262
+ };
263
+ }
264
+
265
+ /**
266
+ * @param {string} projectPath
267
+ * @param {string|null|undefined} spec
268
+ * @returns {{ ok: boolean, path: string, policy: any, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[], errors: string[] }}
269
+ */
270
+ export function buildGeneratorPolicyPinPayload(projectPath, spec) {
271
+ const projectConfigInfo = loadProjectConfig(projectPath);
272
+ if (!projectConfigInfo) {
273
+ const diagnostic = {
274
+ code: "generator_policy_project_missing",
275
+ severity: "error",
276
+ message: "Cannot pin generator policy without topogram.project.json.",
277
+ path: path.resolve(projectPath),
278
+ suggestedFix: "Run this command in a Topogram project.",
279
+ step: "generator-policy"
280
+ };
281
+ return {
282
+ ok: false,
283
+ path: path.join(path.resolve(projectPath), GENERATOR_POLICY_FILE),
284
+ policy: null,
285
+ pinned: [],
286
+ diagnostics: [diagnostic],
287
+ errors: [diagnostic.message]
288
+ };
289
+ }
290
+ const policyInfo = loadGeneratorPolicy(projectConfigInfo.configDir);
291
+ const policyDiagnostics = /** @type {any[]} */ (policyInfo.diagnostics || []);
292
+ if (policyDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
293
+ const errors = policyDiagnostics.filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.message);
294
+ return {
295
+ ok: false,
296
+ path: policyInfo.path,
297
+ policy: policyInfo.policy,
298
+ pinned: [],
299
+ diagnostics: policyDiagnostics,
300
+ errors
301
+ };
302
+ }
303
+ let pins = [];
304
+ try {
305
+ pins = spec
306
+ ? [parseGeneratorPolicyPin(spec)]
307
+ : packageBackedGeneratorBindings(projectConfigInfo.config).map((binding) => ({
308
+ packageName: binding.packageName,
309
+ version: binding.version
310
+ }));
311
+ } catch (error) {
312
+ const diagnostic = {
313
+ code: "generator_policy_pin_invalid",
314
+ severity: "error",
315
+ message: error instanceof Error ? error.message : String(error),
316
+ path: policyInfo.path,
317
+ suggestedFix: "Pass a pin such as @topogram/generator-react-web@1.",
318
+ step: "generator-policy"
319
+ };
320
+ return {
321
+ ok: false,
322
+ path: policyInfo.path,
323
+ policy: policyInfo.policy,
324
+ pinned: [],
325
+ diagnostics: [diagnostic],
326
+ errors: [diagnostic.message]
327
+ };
328
+ }
329
+ if (pins.length === 0) {
330
+ const diagnostic = {
331
+ code: "generator_policy_pin_no_generators",
332
+ severity: "error",
333
+ message: "No package-backed topology generator bindings are available to pin.",
334
+ path: projectConfigInfo.configPath,
335
+ suggestedFix: "Pass an explicit pin such as @topogram/generator-react-web@1, or use bundled generators.",
336
+ step: "generator-policy"
337
+ };
338
+ return {
339
+ ok: false,
340
+ path: policyInfo.path,
341
+ policy: policyInfo.policy,
342
+ pinned: [],
343
+ diagnostics: [diagnostic],
344
+ errors: [diagnostic.message]
345
+ };
346
+ }
347
+ const policy = policyInfo.policy || defaultGeneratorPolicy();
348
+ const allowedPackages = [...policy.allowedPackages];
349
+ const allowedPackageScopes = [...policy.allowedPackageScopes];
350
+ const pinnedVersions = { ...policy.pinnedVersions };
351
+ for (const pin of pins) {
352
+ if (!allowedPackages.includes(pin.packageName)) {
353
+ allowedPackages.push(pin.packageName);
354
+ }
355
+ pinnedVersions[pin.packageName] = pin.version;
356
+ }
357
+ const nextPolicy = {
358
+ ...policy,
359
+ allowedPackageScopes,
360
+ allowedPackages,
361
+ pinnedVersions
362
+ };
363
+ writeGeneratorPolicy(projectConfigInfo.configDir, nextPolicy);
364
+ return {
365
+ ok: true,
366
+ path: path.join(projectConfigInfo.configDir, GENERATOR_POLICY_FILE),
367
+ policy: nextPolicy,
368
+ pinned: pins,
369
+ diagnostics: [],
370
+ errors: []
371
+ };
372
+ }
@@ -0,0 +1,159 @@
1
+ // @ts-check
2
+
3
+ import path from "node:path";
4
+
5
+ import { formatGeneratorPackageLockfile } from "./package-info.js";
6
+ import { generatorPolicyRuleLabel } from "./shared.js";
7
+
8
+ /**
9
+ * @param {{ ok: boolean, path: string, policy: any }} payload
10
+ * @returns {void}
11
+ */
12
+ export function printGeneratorPolicyInitPayload(payload) {
13
+ console.log(`Wrote generator policy: ${payload.path}`);
14
+ console.log(`Allowed package scopes: ${payload.policy.allowedPackageScopes.join(", ") || "(none)"}`);
15
+ console.log(`Allowed packages: ${payload.policy.allowedPackages.join(", ") || "(none)"}`);
16
+ }
17
+
18
+ /**
19
+ * @param {any} payload
20
+ * @returns {void}
21
+ */
22
+ export function printGeneratorPolicyCheckPayload(payload) {
23
+ console.log(payload.ok ? "Generator policy check passed" : "Generator policy check failed");
24
+ console.log(`Policy: ${payload.path}`);
25
+ console.log(`Exists: ${payload.exists ? "yes" : "no"}`);
26
+ console.log(`Defaulted: ${payload.defaulted ? "yes" : "no"}`);
27
+ console.log(`Package-backed generators: ${payload.bindings.length}`);
28
+ for (const binding of payload.bindings) {
29
+ console.log(`- ${binding.runtimeId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
30
+ console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
31
+ if (binding.packageInfo.dependencySpec) {
32
+ console.log(` dependency: ${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}`);
33
+ }
34
+ if (binding.packageInfo.lockfileVersion) {
35
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
36
+ } else if (binding.packageInfo.lockfileKind) {
37
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
38
+ }
39
+ }
40
+ for (const diagnostic of payload.diagnostics) {
41
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
42
+ if (diagnostic.path) {
43
+ console.log(` path: ${diagnostic.path}`);
44
+ }
45
+ if (diagnostic.suggestedFix) {
46
+ console.log(` fix: ${diagnostic.suggestedFix}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * @param {any} payload
53
+ * @returns {void}
54
+ */
55
+ export function printGeneratorPolicyStatusPayload(payload) {
56
+ console.log(payload.ok ? "Generator policy status: allowed" : "Generator policy status: denied");
57
+ console.log(`Policy file: ${payload.path}`);
58
+ console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
59
+ console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
60
+ console.log(`Package-backed generators: ${payload.summary.packageBackedGenerators}`);
61
+ console.log(`Allowed packages: ${payload.summary.allowed}`);
62
+ console.log(`Denied packages: ${payload.summary.denied}`);
63
+ console.log(`Pinned generators: ${payload.summary.pinned}`);
64
+ console.log(`Unpinned generators: ${payload.summary.unpinned}`);
65
+ console.log(`Pin mismatches: ${payload.summary.pinMismatches}`);
66
+ if (payload.bindings.length > 0) {
67
+ console.log("");
68
+ console.log("Generator packages:");
69
+ }
70
+ for (const binding of payload.bindings) {
71
+ console.log(`- ${binding.runtimeId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
72
+ console.log(` allowed: ${binding.allowed ? "yes" : "no"}`);
73
+ console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
74
+ console.log(` dependency: ${binding.packageInfo.dependencyField && binding.packageInfo.dependencySpec ? `${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}` : "(not declared)"}`);
75
+ console.log(` lockfile: ${formatGeneratorPackageLockfile(binding.packageInfo)}`);
76
+ console.log(` policy pin: ${binding.pin.version ? `${binding.pin.key}@${binding.pin.version}` : "(none)"}`);
77
+ }
78
+ for (const diagnostic of payload.diagnostics) {
79
+ const label = diagnostic.severity === "warning" ? "Warning" : "Error";
80
+ console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
81
+ if (diagnostic.packageVersion) {
82
+ console.log(` package version: ${diagnostic.packageVersion}`);
83
+ }
84
+ if (diagnostic.packageDependencySpec) {
85
+ console.log(` dependency: ${diagnostic.packageDependencyField} ${diagnostic.packageDependencySpec}`);
86
+ }
87
+ if (diagnostic.packageLockfilePath) {
88
+ console.log(` lockfile: ${path.basename(diagnostic.packageLockfilePath)}${diagnostic.packageLockVersion ? ` ${diagnostic.packageLockVersion}` : ""}`);
89
+ }
90
+ if (diagnostic.suggestedFix) {
91
+ console.log(` fix: ${diagnostic.suggestedFix}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ /**
97
+ * @param {any} payload
98
+ * @returns {void}
99
+ */
100
+ export function printGeneratorPolicyExplainPayload(payload) {
101
+ console.log(payload.ok ? "Generator policy: allowed" : "Generator policy: denied");
102
+ console.log(payload.ok
103
+ ? "Decision: package-backed generators are allowed by this project's generator policy."
104
+ : "Decision: one or more package-backed generators are blocked by this project's generator policy.");
105
+ console.log(`Policy file: ${payload.path}`);
106
+ console.log(`Policy file exists: ${payload.exists ? "yes" : "no"}`);
107
+ console.log(`Default policy active: ${payload.defaulted ? "yes" : "no"}`);
108
+ if (payload.bindings.length > 0) {
109
+ console.log("");
110
+ console.log("Package-backed generators:");
111
+ for (const binding of payload.bindings) {
112
+ console.log(`- ${binding.runtimeId}: ${binding.generatorId}@${binding.version} via ${binding.packageName}`);
113
+ console.log(` npm package: ${binding.packageInfo.installedVersion || "(not installed)"}`);
114
+ if (binding.packageInfo.dependencySpec) {
115
+ console.log(` dependency: ${binding.packageInfo.dependencyField} ${binding.packageInfo.dependencySpec}`);
116
+ }
117
+ }
118
+ }
119
+ if (payload.rules.length > 0) {
120
+ console.log("");
121
+ console.log("Policy checks:");
122
+ }
123
+ for (const rule of payload.rules) {
124
+ console.log(`${rule.ok ? "PASS" : "FAIL"} ${generatorPolicyRuleLabel(rule.name)}: ${rule.message}`);
125
+ console.log(` actual: ${rule.actual}`);
126
+ console.log(` expected: ${rule.expected}`);
127
+ if (!rule.ok && rule.fix) {
128
+ console.log(` fix: ${rule.fix}`);
129
+ }
130
+ }
131
+ for (const diagnostic of payload.diagnostics) {
132
+ const label = diagnostic.severity === "warning" ? "Warning" : "Error";
133
+ console.log(`${label}: ${diagnostic.code}: ${diagnostic.message}`);
134
+ if (diagnostic.suggestedFix) {
135
+ console.log(` fix: ${diagnostic.suggestedFix}`);
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * @param {{ ok: boolean, path: string, pinned: Array<{ packageName: string, version: string }>, diagnostics: any[] }} payload
142
+ * @returns {void}
143
+ */
144
+ export function printGeneratorPolicyPinPayload(payload) {
145
+ console.log(payload.ok ? "Generator policy pin updated" : "Generator policy pin failed");
146
+ console.log(`Policy: ${payload.path}`);
147
+ for (const pin of payload.pinned) {
148
+ console.log(`Pinned: ${pin.packageName}@${pin.version}`);
149
+ }
150
+ for (const diagnostic of payload.diagnostics) {
151
+ console.log(`[${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.message}`);
152
+ if (diagnostic.path) {
153
+ console.log(` path: ${diagnostic.path}`);
154
+ }
155
+ if (diagnostic.suggestedFix) {
156
+ console.log(` fix: ${diagnostic.suggestedFix}`);
157
+ }
158
+ }
159
+ }