@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
@@ -113,17 +113,31 @@ export function shapeIdForCapability(record, direction) {
113
113
  return direction === "input" ? `shape_input_${stem}` : `shape_output_${stem}`;
114
114
  }
115
115
 
116
- /** @param {string} shapeId @param {string} label @param {any[]} fields @returns {any} */
117
- export function renderCandidateShape(shapeId, label, fields) {
116
+ /** @param {string} shapeId @param {string} label @param {any[]} fields @param {string|null} [sourceKind] @returns {any} */
117
+ export function renderCandidateShape(shapeId, label, fields, sourceKind = null) {
118
+ /** @param {any} field @returns {{ name: string, fieldType: string, requiredness: string }} */
119
+ function normalizeField(field) {
120
+ if (typeof field === "string") {
121
+ return { name: field, fieldType: "string", requiredness: "optional" };
122
+ }
123
+ return {
124
+ name: field?.name || "id",
125
+ fieldType: field?.field_type || field?.type || "string",
126
+ requiredness: field?.required ? "required" : "optional"
127
+ };
128
+ }
118
129
  const lines = [
119
130
  `shape ${shapeId} {`,
120
131
  ` name "${label}"`,
121
- ` description "Candidate shape imported from brownfield API evidence"`,
132
+ sourceKind === "ui_widget_event"
133
+ ? ` description "Candidate event payload shape imported from brownfield UI interaction evidence"`
134
+ : ` description "Candidate shape imported from brownfield API evidence"`,
122
135
  "",
123
136
  " fields {"
124
137
  ];
125
138
  for (const field of fields) {
126
- lines.push(` ${field} string optional`);
139
+ const normalized = normalizeField(field);
140
+ lines.push(` ${normalized.name} ${normalized.fieldType} ${normalized.requiredness}`);
127
141
  }
128
142
  lines.push(" }", "", " status active", "}");
129
143
  return ensureTrailingNewline(lines.join("\n"));
@@ -236,7 +250,6 @@ export function renderCandidateUiReportDoc(screen, routes, actions) {
236
250
  source_of_truth: "imported",
237
251
  confidence: screen.confidence || "medium",
238
252
  review_required: true,
239
- related_entities: [screen.entity_id].filter(Boolean),
240
253
  provenance: screen.provenance || [],
241
254
  tags: ["import", "ui"]
242
255
  };
@@ -244,11 +257,12 @@ export function renderCandidateUiReportDoc(screen, routes, actions) {
244
257
  "Candidate UI surface imported from brownfield route evidence.",
245
258
  "",
246
259
  `Screen: \`${screen.id_hint}\` (${screen.screen_kind})`,
260
+ screen.entity_id ? `Inferred entity: \`${screen.entity_id}\`` : null,
247
261
  `Routes: ${routes.length ? routes.map((/** @type {any} */ route) => `\`${route.path}\``).join(", ") : "_none_"}`,
248
262
  `Actions: ${actions.length ? actions.map((/** @type {any} */ action) => `\`${action.capability_hint}\``).join(", ") : "_none_"}`,
249
263
  "",
250
264
  "Review this UI surface before promoting it into canonical docs or projections."
251
- ].join("\n");
265
+ ].filter(Boolean).join("\n");
252
266
  return renderMarkdownDoc(metadata, body);
253
267
  }
254
268
 
@@ -257,15 +271,36 @@ export function renderCandidateWidget(widget) {
257
271
  const propName = widget.data_prop || "rows";
258
272
  const pattern = widget.pattern || "search_results";
259
273
  const region = widget.region || "results";
274
+ const inferredEvents = Array.isArray(widget.inferred_events) ? widget.inferred_events : [];
275
+ const eventLines = inferredEvents
276
+ .filter((/** @type {any} */ event) => event.name && event.payload_shape)
277
+ .map((/** @type {any} */ event) => ` ${event.name} ${event.payload_shape}`);
278
+ const selectionEvent = inferredEvents.find((/** @type {any} */ event) => event.name);
279
+ const eventComments = inferredEvents.map((/** @type {any} */ event) =>
280
+ ` # Inferred event: ${event.name || "event"} ${event.action || "action"} ${event.target_screen || event.target || "target"}.`
281
+ );
282
+ const behaviorLines = eventLines.length > 0
283
+ ? [
284
+ " events {",
285
+ ...eventLines,
286
+ " }",
287
+ " behavior [selection]",
288
+ " behaviors {",
289
+ ` selection mode single emits ${selectionEvent?.name || "row_select"}`,
290
+ " }"
291
+ ]
292
+ : [];
260
293
  return ensureTrailingNewline(
261
294
  [
262
295
  `widget ${widget.id_hint} {`,
263
296
  ` name "${widget.label || widget.id_hint}"`,
264
297
  ' description "Candidate reusable widget inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."',
265
298
  " category collection",
299
+ ...eventComments,
266
300
  " props {",
267
301
  ` ${propName} array required`,
268
302
  " }",
303
+ ...behaviorLines,
269
304
  ` patterns [${pattern}]`,
270
305
  ` regions [${region}]`,
271
306
  " status proposed",
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  import { ensureTrailingNewline, titleCase } from "../text-helpers.js";
6
+ import { resolveWorkspaceContext } from "../workspace-paths.js";
6
7
 
7
8
  /** @param {string} startDir @returns {any} */
8
9
  export function findNearestGitRoot(startDir) {
@@ -21,17 +22,10 @@ export function findNearestGitRoot(startDir) {
21
22
 
22
23
  /** @param {string} inputPath @returns {any} */
23
24
  export function normalizeWorkspacePaths(inputPath) {
25
+ const context = resolveWorkspaceContext(inputPath);
24
26
  const absolute = path.resolve(inputPath);
25
- const inputExists = fs.existsSync(absolute);
26
- const hasTopogramChild = fs.existsSync(path.join(absolute, "topogram")) && fs.statSync(path.join(absolute, "topogram")).isDirectory();
27
- const isTopogramDir = path.basename(absolute) === "topogram" && inputExists;
28
- const bootstrapWorkspaceRoot = !isTopogramDir && !hasTopogramChild;
29
- const topogramRoot = isTopogramDir
30
- ? absolute
31
- : hasTopogramChild
32
- ? path.join(absolute, "topogram")
33
- : path.join(absolute, "topogram");
34
- const workspaceRoot = isTopogramDir ? path.dirname(topogramRoot) : absolute;
27
+ const topogramRoot = context.topoRoot;
28
+ const workspaceRoot = context.projectRoot;
35
29
  const repoRoot = findNearestGitRoot(workspaceRoot);
36
30
  return {
37
31
  inputRoot: absolute,
@@ -39,7 +33,7 @@ export function normalizeWorkspacePaths(inputPath) {
39
33
  workspaceRoot,
40
34
  exampleRoot: workspaceRoot,
41
35
  repoRoot,
42
- bootstrappedTopogramRoot: !fs.existsSync(topogramRoot)
36
+ bootstrappedTopogramRoot: context.bootstrappedTopoRoot
43
37
  };
44
38
  }
45
39
 
@@ -0,0 +1,29 @@
1
+ import type { TopogramLocation } from "./topogram-types.js";
2
+
3
+ export const DOC_KINDS: Set<string>;
4
+ export const DOC_STATUSES: Set<string>;
5
+ export const DOC_CONFIDENCE: Set<string>;
6
+ export const DOC_AUDIENCES: Set<string>;
7
+ export const DOC_PRIORITIES: Set<string>;
8
+ export const DOC_ARRAY_FIELDS: Set<string>;
9
+ export const DOC_REFERENCE_FIELDS: Record<string, string>;
10
+ export const DOC_SCALAR_FIELDS: Set<string>;
11
+
12
+ export interface TopogramDocParseError {
13
+ message: string;
14
+ loc: TopogramLocation;
15
+ }
16
+
17
+ export interface TopogramDoc {
18
+ type: "doc";
19
+ file: string;
20
+ relativePath: string;
21
+ metadata: Record<string, any>;
22
+ body: string;
23
+ loc: TopogramLocation;
24
+ parseError: TopogramDocParseError | null;
25
+ }
26
+
27
+ export function collectTopogramDocFiles(inputPath: string): string[];
28
+ export function parseDocFile(filePath: string, workspaceRoot: string): TopogramDoc;
29
+ export function parseDocsPath(inputPath: string): TopogramDoc[];
@@ -0,0 +1,328 @@
1
+ // @ts-check
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+
6
+ export const DEFAULT_TOPO_FOLDER_NAME = "topo";
7
+ export const LEGACY_TOPOGRAM_FOLDER_NAME = "topogram";
8
+ export const DEFAULT_WORKSPACE_PATH = `./${DEFAULT_TOPO_FOLDER_NAME}`;
9
+ export const PROJECT_CONFIG_FILE = "topogram.project.json";
10
+
11
+ const SIGNAL_SCAN_IGNORED_DIRS = new Set([
12
+ ".git",
13
+ ".next",
14
+ ".tmp",
15
+ ".turbo",
16
+ ".yarn",
17
+ "app",
18
+ "build",
19
+ "coverage",
20
+ "dist",
21
+ "expected",
22
+ "node_modules",
23
+ "tmp"
24
+ ]);
25
+ const WORKSPACE_SIGNAL_DIRS = new Set([
26
+ "_archive",
27
+ "acceptance_criteria",
28
+ "actors",
29
+ "bugs",
30
+ "capabilities",
31
+ "decisions",
32
+ "domains",
33
+ "entities",
34
+ "enums",
35
+ "operations",
36
+ "pitches",
37
+ "projections",
38
+ "requirements",
39
+ "rules",
40
+ "shapes",
41
+ "tasks",
42
+ "terms",
43
+ "verifications",
44
+ "widgets",
45
+ "workflows"
46
+ ]);
47
+
48
+ /**
49
+ * @typedef {Object} WorkspaceResolution
50
+ * @property {string} inputRoot
51
+ * @property {string} topoRoot
52
+ * @property {string} projectRoot
53
+ * @property {string|null} configPath
54
+ * @property {boolean} fromConfig
55
+ * @property {boolean} fromSignal
56
+ * @property {boolean} bootstrappedTopoRoot
57
+ */
58
+
59
+ /**
60
+ * @param {string} candidatePath
61
+ * @returns {boolean}
62
+ */
63
+ function isDirectory(candidatePath) {
64
+ try {
65
+ return fs.existsSync(candidatePath) && fs.statSync(candidatePath).isDirectory();
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {string} filePath
73
+ * @returns {any}
74
+ */
75
+ function readJson(filePath) {
76
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
77
+ }
78
+
79
+ /**
80
+ * @param {string} startPath
81
+ * @returns {string}
82
+ */
83
+ function searchStartDirectory(startPath) {
84
+ const absolute = path.resolve(startPath || ".");
85
+ if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
86
+ return path.dirname(absolute);
87
+ }
88
+ if (!fs.existsSync(absolute) && path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME) {
89
+ return path.dirname(absolute);
90
+ }
91
+ return absolute;
92
+ }
93
+
94
+ /**
95
+ * @param {string} startPath
96
+ * @returns {{ config: any, configPath: string, configDir: string }|null}
97
+ */
98
+ export function findProjectRoot(startPath) {
99
+ let current = searchStartDirectory(startPath);
100
+ while (current && current !== path.dirname(current)) {
101
+ const candidate = path.join(current, PROJECT_CONFIG_FILE);
102
+ if (fs.existsSync(candidate)) {
103
+ return {
104
+ config: readJson(candidate),
105
+ configPath: candidate,
106
+ configDir: current
107
+ };
108
+ }
109
+ current = path.dirname(current);
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * @param {string} workspacePath
116
+ * @returns {string}
117
+ */
118
+ export function normalizeWorkspaceConfigPath(workspacePath) {
119
+ const value = String(workspacePath || "").trim();
120
+ if (!value) {
121
+ throw new Error("topogram.project.json workspace must be a non-empty relative path.");
122
+ }
123
+ if (path.isAbsolute(value)) {
124
+ throw new Error("topogram.project.json workspace must be relative to the project root.");
125
+ }
126
+ const normalized = value.replace(/\\/g, "/");
127
+ const resolved = path.posix.normalize(normalized);
128
+ if (resolved === ".." || resolved.startsWith("../")) {
129
+ throw new Error("topogram.project.json workspace must not escape the project root.");
130
+ }
131
+ return normalized;
132
+ }
133
+
134
+ /**
135
+ * @param {any} config
136
+ * @param {string} configDir
137
+ * @returns {string}
138
+ */
139
+ export function resolveProjectWorkspace(config, configDir) {
140
+ if (config && Object.prototype.hasOwnProperty.call(config, "workspaces")) {
141
+ throw new Error("topogram.project.json workspaces[] is not supported yet; use single workspace instead.");
142
+ }
143
+ const configured = config?.workspace == null ? DEFAULT_WORKSPACE_PATH : config.workspace;
144
+ const normalized = normalizeWorkspaceConfigPath(configured);
145
+ return path.resolve(configDir, normalized);
146
+ }
147
+
148
+ /**
149
+ * @param {string} root
150
+ * @param {number} [maxDepth]
151
+ * @returns {boolean}
152
+ */
153
+ export function workspaceHasTgFiles(root, maxDepth = 3) {
154
+ if (!isDirectory(root)) {
155
+ return false;
156
+ }
157
+ const walk = (/** @type {string} */ current, /** @type {number} */ depth) => {
158
+ if (depth > maxDepth) {
159
+ return false;
160
+ }
161
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
162
+ if (SIGNAL_SCAN_IGNORED_DIRS.has(entry.name)) {
163
+ continue;
164
+ }
165
+ const child = path.join(current, entry.name);
166
+ if (entry.isFile() && entry.name.endsWith(".tg")) {
167
+ return true;
168
+ }
169
+ if (entry.isDirectory() && walk(child, depth + 1)) {
170
+ return true;
171
+ }
172
+ }
173
+ return false;
174
+ };
175
+ return walk(root, 0);
176
+ }
177
+
178
+ /**
179
+ * @param {string} root
180
+ * @returns {boolean}
181
+ */
182
+ function isWorkspaceSignalRoot(root) {
183
+ if (!isDirectory(root)) {
184
+ return false;
185
+ }
186
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
187
+ if (entry.isFile() && entry.name.endsWith(".tg")) {
188
+ return true;
189
+ }
190
+ if (entry.isDirectory() && WORKSPACE_SIGNAL_DIRS.has(entry.name) && workspaceHasTgFiles(path.join(root, entry.name), 2)) {
191
+ return true;
192
+ }
193
+ }
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * @param {string} root
199
+ * @returns {string[]}
200
+ */
201
+ function signalWorkspaceCandidates(root) {
202
+ if (!isDirectory(root)) {
203
+ return [];
204
+ }
205
+ /** @type {string[]} */
206
+ const candidates = [];
207
+ if (isWorkspaceSignalRoot(root)) {
208
+ candidates.push(root);
209
+ }
210
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
211
+ if (!entry.isDirectory() || SIGNAL_SCAN_IGNORED_DIRS.has(entry.name)) {
212
+ continue;
213
+ }
214
+ const child = path.join(root, entry.name);
215
+ if (isWorkspaceSignalRoot(child)) {
216
+ candidates.push(child);
217
+ }
218
+ }
219
+ return [...new Set(candidates.map((candidate) => path.resolve(candidate)))].sort();
220
+ }
221
+
222
+ /**
223
+ * @param {string} inputPath
224
+ * @returns {WorkspaceResolution}
225
+ */
226
+ export function resolveWorkspaceContext(inputPath = ".") {
227
+ const absolute = path.resolve(inputPath || ".");
228
+ if (
229
+ isDirectory(absolute) &&
230
+ (
231
+ path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME ||
232
+ (isWorkspaceSignalRoot(absolute) && !isDirectory(path.join(absolute, DEFAULT_TOPO_FOLDER_NAME)))
233
+ )
234
+ ) {
235
+ return {
236
+ inputRoot: absolute,
237
+ topoRoot: absolute,
238
+ projectRoot: path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME ? path.dirname(absolute) : absolute,
239
+ configPath: null,
240
+ fromConfig: false,
241
+ fromSignal: false,
242
+ bootstrappedTopoRoot: false
243
+ };
244
+ }
245
+
246
+ const configInfo = findProjectRoot(absolute);
247
+ if (configInfo) {
248
+ const topoRoot = resolveProjectWorkspace(configInfo.config, configInfo.configDir);
249
+ return {
250
+ inputRoot: absolute,
251
+ topoRoot,
252
+ projectRoot: configInfo.configDir,
253
+ configPath: configInfo.configPath,
254
+ fromConfig: true,
255
+ fromSignal: false,
256
+ bootstrappedTopoRoot: !fs.existsSync(topoRoot)
257
+ };
258
+ }
259
+
260
+ const searchBase = !fs.existsSync(absolute) && path.basename(absolute) === DEFAULT_TOPO_FOLDER_NAME
261
+ ? path.dirname(absolute)
262
+ : absolute;
263
+ const defaultCandidate = path.join(searchBase, DEFAULT_TOPO_FOLDER_NAME);
264
+ if (isDirectory(defaultCandidate)) {
265
+ return {
266
+ inputRoot: absolute,
267
+ topoRoot: defaultCandidate,
268
+ projectRoot: searchBase,
269
+ configPath: null,
270
+ fromConfig: false,
271
+ fromSignal: false,
272
+ bootstrappedTopoRoot: false
273
+ };
274
+ }
275
+
276
+ const signalCandidates = signalWorkspaceCandidates(searchBase);
277
+ if (signalCandidates.length === 1) {
278
+ const topoRoot = signalCandidates[0];
279
+ return {
280
+ inputRoot: absolute,
281
+ topoRoot,
282
+ projectRoot: topoRoot,
283
+ configPath: null,
284
+ fromConfig: false,
285
+ fromSignal: true,
286
+ bootstrappedTopoRoot: false
287
+ };
288
+ }
289
+ if (signalCandidates.length > 1) {
290
+ throw new Error(
291
+ `Multiple Topogram workspace candidates found. Pass one explicitly: ${signalCandidates.join(", ")}`
292
+ );
293
+ }
294
+
295
+ return {
296
+ inputRoot: absolute,
297
+ topoRoot: defaultCandidate,
298
+ projectRoot: searchBase,
299
+ configPath: null,
300
+ fromConfig: false,
301
+ fromSignal: false,
302
+ bootstrappedTopoRoot: true
303
+ };
304
+ }
305
+
306
+ /**
307
+ * @param {string} inputPath
308
+ * @returns {string}
309
+ */
310
+ export function resolveTopoRoot(inputPath = ".") {
311
+ return resolveWorkspaceContext(inputPath).topoRoot;
312
+ }
313
+
314
+ /**
315
+ * @param {string} packageRoot
316
+ * @returns {{ root: string, legacy: boolean }}
317
+ */
318
+ export function resolvePackageWorkspace(packageRoot) {
319
+ const topoRoot = path.join(packageRoot, DEFAULT_TOPO_FOLDER_NAME);
320
+ if (isDirectory(topoRoot)) {
321
+ return { root: topoRoot, legacy: false };
322
+ }
323
+ const legacyRoot = path.join(packageRoot, LEGACY_TOPOGRAM_FOLDER_NAME);
324
+ if (isDirectory(legacyRoot)) {
325
+ return { root: legacyRoot, legacy: true };
326
+ }
327
+ throw new Error(`Package is missing ${DEFAULT_TOPO_FOLDER_NAME}/.`);
328
+ }