@topogram/cli 0.3.63 → 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 (344) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.d.ts +6 -0
  4. package/src/adoption/plan.js +12 -703
  5. package/src/adoption/reporting.d.ts +10 -0
  6. package/src/adoption/review-groups.d.ts +6 -0
  7. package/src/agent-brief.d.ts +3 -0
  8. package/src/agent-brief.js +495 -0
  9. package/src/agent-ops/query-builders/auth.js +375 -0
  10. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  12. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  13. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  14. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  15. package/src/agent-ops/query-builders/change-risk.js +25 -0
  16. package/src/agent-ops/query-builders/common.js +149 -0
  17. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  18. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  19. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  20. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  21. package/src/agent-ops/query-builders/work-packets.js +417 -0
  22. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  23. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  24. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  25. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  26. package/src/agent-ops/query-builders.d.ts +26 -0
  27. package/src/agent-ops/query-builders.js +42 -5021
  28. package/src/archive/archive.d.ts +2 -0
  29. package/src/archive/compact.d.ts +1 -0
  30. package/src/archive/unarchive.d.ts +1 -0
  31. package/src/catalog/constants.js +10 -0
  32. package/src/catalog/copy.js +60 -0
  33. package/src/catalog/diagnostics.js +15 -0
  34. package/src/catalog/entries.js +42 -0
  35. package/src/catalog/files.js +67 -0
  36. package/src/catalog/provenance.js +122 -0
  37. package/src/catalog/source.js +150 -0
  38. package/src/catalog/validation.js +252 -0
  39. package/src/catalog.d.ts +12 -0
  40. package/src/catalog.js +18 -750
  41. package/src/cli/catalog-alias.d.ts +1 -0
  42. package/src/cli/command-parser.js +38 -0
  43. package/src/cli/command-parsers/core.js +102 -0
  44. package/src/cli/command-parsers/generator.js +39 -0
  45. package/src/cli/command-parsers/import.js +44 -0
  46. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  47. package/src/cli/command-parsers/project.js +47 -0
  48. package/src/cli/command-parsers/sdlc.js +47 -0
  49. package/src/cli/command-parsers/shared.js +51 -0
  50. package/src/cli/command-parsers/template.js +48 -0
  51. package/src/cli/commands/agent.js +47 -0
  52. package/src/cli/commands/catalog/check.js +31 -0
  53. package/src/cli/commands/catalog/copy.js +59 -0
  54. package/src/cli/commands/catalog/doctor.js +248 -0
  55. package/src/cli/commands/catalog/help.js +21 -0
  56. package/src/cli/commands/catalog/list.js +52 -0
  57. package/src/cli/commands/catalog/runner.js +92 -0
  58. package/src/cli/commands/catalog/shared.js +17 -0
  59. package/src/cli/commands/catalog/show.js +134 -0
  60. package/src/cli/commands/catalog.js +32 -0
  61. package/src/cli/commands/check.js +268 -0
  62. package/src/cli/commands/doctor.js +268 -0
  63. package/src/cli/commands/emit.js +149 -0
  64. package/src/cli/commands/generate.js +96 -0
  65. package/src/cli/commands/generator-policy/package-info.js +162 -0
  66. package/src/cli/commands/generator-policy/payloads.js +372 -0
  67. package/src/cli/commands/generator-policy/printers.js +159 -0
  68. package/src/cli/commands/generator-policy/runner.js +81 -0
  69. package/src/cli/commands/generator-policy/shared.js +39 -0
  70. package/src/cli/commands/generator-policy.js +17 -0
  71. package/src/cli/commands/generator.js +443 -0
  72. package/src/cli/commands/import/adopt.js +170 -0
  73. package/src/cli/commands/import/check.js +91 -0
  74. package/src/cli/commands/import/diff.js +84 -0
  75. package/src/cli/commands/import/help.js +47 -0
  76. package/src/cli/commands/import/paths.js +277 -0
  77. package/src/cli/commands/import/plan.js +284 -0
  78. package/src/cli/commands/import/refresh.js +470 -0
  79. package/src/cli/commands/import/status-history.js +196 -0
  80. package/src/cli/commands/import/workspace.js +230 -0
  81. package/src/cli/commands/import-runner.js +157 -0
  82. package/src/cli/commands/import.js +35 -0
  83. package/src/cli/commands/inspect.js +55 -0
  84. package/src/cli/commands/new.js +94 -0
  85. package/src/cli/commands/package/constants.js +17 -0
  86. package/src/cli/commands/package/doctor.js +240 -0
  87. package/src/cli/commands/package/help.js +27 -0
  88. package/src/cli/commands/package/lockfile.js +135 -0
  89. package/src/cli/commands/package/npm.js +97 -0
  90. package/src/cli/commands/package/reporting.js +35 -0
  91. package/src/cli/commands/package/runner.js +33 -0
  92. package/src/cli/commands/package/shared.js +9 -0
  93. package/src/cli/commands/package/update-cli.js +252 -0
  94. package/src/cli/commands/package/versions.js +35 -0
  95. package/src/cli/commands/package.js +31 -0
  96. package/src/cli/commands/query/change-plan.js +68 -0
  97. package/src/cli/commands/query/definitions.js +202 -0
  98. package/src/cli/commands/query/import-adopt.js +121 -0
  99. package/src/cli/commands/query/runner/artifacts.js +102 -0
  100. package/src/cli/commands/query/runner/boundaries.js +211 -0
  101. package/src/cli/commands/query/runner/change.js +182 -0
  102. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  103. package/src/cli/commands/query/runner/index.js +31 -0
  104. package/src/cli/commands/query/runner/output.js +12 -0
  105. package/src/cli/commands/query/runner/workflow.js +241 -0
  106. package/src/cli/commands/query/runner.js +3 -0
  107. package/src/cli/commands/query/workflow-context.js +5 -0
  108. package/src/cli/commands/query/workspace.js +274 -0
  109. package/src/cli/commands/query.js +11 -0
  110. package/src/cli/commands/release-rollout.js +257 -0
  111. package/src/cli/commands/release-shared.js +528 -0
  112. package/src/cli/commands/release-status.js +429 -0
  113. package/src/cli/commands/release.js +107 -0
  114. package/src/cli/commands/sdlc.js +168 -0
  115. package/src/cli/commands/setup.js +76 -0
  116. package/src/cli/commands/source.js +291 -0
  117. package/src/cli/commands/template/baseline.js +100 -0
  118. package/src/cli/commands/template/check.js +466 -0
  119. package/src/cli/commands/template/constants.js +8 -0
  120. package/src/cli/commands/template/diagnostics.js +26 -0
  121. package/src/cli/commands/template/help.js +28 -0
  122. package/src/cli/commands/template/lifecycle.js +404 -0
  123. package/src/cli/commands/template/list-show.js +287 -0
  124. package/src/cli/commands/template/policy.js +422 -0
  125. package/src/cli/commands/template/shared.js +127 -0
  126. package/src/cli/commands/template/updates.js +352 -0
  127. package/src/cli/commands/template-runner.js +198 -0
  128. package/src/cli/commands/template.js +43 -0
  129. package/src/cli/commands/trust.js +219 -0
  130. package/src/cli/commands/version.js +40 -0
  131. package/src/cli/commands/widget.js +168 -0
  132. package/src/cli/commands/workflow.js +63 -0
  133. package/src/cli/dispatcher.js +392 -0
  134. package/src/cli/help-dispatch.js +188 -0
  135. package/src/cli/help.js +296 -0
  136. package/src/cli/migration-guidance.js +59 -0
  137. package/src/cli/options.js +96 -0
  138. package/src/cli/output-safety.js +107 -0
  139. package/src/cli/path-normalization.js +29 -0
  140. package/src/cli.js +47 -11711
  141. package/src/example-implementation.d.ts +2 -0
  142. package/src/format.d.ts +1 -0
  143. package/src/generator/api/contracts.js +497 -0
  144. package/src/generator/api/metadata.js +221 -0
  145. package/src/generator/api/openapi.js +559 -0
  146. package/src/generator/api/schema.js +124 -0
  147. package/src/generator/api/types.d.ts +98 -0
  148. package/src/generator/api.js +3 -1195
  149. package/src/generator/check.d.ts +1 -0
  150. package/src/generator/context/bundle.d.ts +1 -0
  151. package/src/generator/context/shared/domain-sdlc.js +282 -0
  152. package/src/generator/context/shared/maintained-boundary.js +665 -0
  153. package/src/generator/context/shared/metrics.js +85 -0
  154. package/src/generator/context/shared/primitives.js +64 -0
  155. package/src/generator/context/shared/relationships.js +453 -0
  156. package/src/generator/context/shared/summaries.js +263 -0
  157. package/src/generator/context/shared/types.d.ts +207 -0
  158. package/src/generator/context/shared.d.ts +44 -0
  159. package/src/generator/context/shared.js +80 -1390
  160. package/src/generator/context/slice/core.js +397 -0
  161. package/src/generator/context/slice/sdlc.js +417 -0
  162. package/src/generator/context/slice/ui-packets.js +183 -0
  163. package/src/generator/context/slice.js +2 -859
  164. package/src/generator/native/parity-bundle.js +2 -1
  165. package/src/generator/registry/index.js +507 -0
  166. package/src/generator/registry.js +18 -504
  167. package/src/generator/runtime/environment/index.js +666 -0
  168. package/src/generator/runtime/environment.js +4 -666
  169. package/src/generator/runtime/runtime-check/index.js +554 -0
  170. package/src/generator/runtime/runtime-check.js +4 -554
  171. package/src/generator/runtime/shared/index.js +572 -0
  172. package/src/generator/runtime/shared.js +19 -570
  173. package/src/generator/shared.d.ts +2 -0
  174. package/src/generator/surfaces/shared.d.ts +3 -0
  175. package/src/generator/surfaces/web/html-escape.js +22 -0
  176. package/src/generator/surfaces/web/react.js +10 -8
  177. package/src/generator/surfaces/web/sveltekit.js +7 -5
  178. package/src/generator/surfaces/web/vanilla.js +8 -4
  179. package/src/generator/widget-conformance/behavior-report.js +258 -0
  180. package/src/generator/widget-conformance/checks.js +371 -0
  181. package/src/generator/widget-conformance/projection-context.js +200 -0
  182. package/src/generator/widget-conformance/report.js +166 -0
  183. package/src/generator/widget-conformance/types.d.ts +121 -0
  184. package/src/generator/widget-conformance.js +3 -824
  185. package/src/generator.d.ts +2 -0
  186. package/src/github-client.js +520 -0
  187. package/src/import/core/context.d.ts +3 -0
  188. package/src/import/core/contracts.d.ts +1 -0
  189. package/src/import/core/registry.d.ts +4 -0
  190. package/src/import/core/runner/candidates.js +217 -0
  191. package/src/import/core/runner/options.js +22 -0
  192. package/src/import/core/runner/reports.js +50 -0
  193. package/src/import/core/runner/run.js +79 -0
  194. package/src/import/core/runner/tracks.js +150 -0
  195. package/src/import/core/runner/ui-drafts.js +337 -0
  196. package/src/import/core/runner.js +3 -698
  197. package/src/import/core/shared/api-routes.js +221 -0
  198. package/src/import/core/shared/candidates.js +97 -0
  199. package/src/import/core/shared/files.js +177 -0
  200. package/src/import/core/shared/next-app.js +389 -0
  201. package/src/import/core/shared/types.d.ts +51 -0
  202. package/src/import/core/shared/ui-routes.js +230 -0
  203. package/src/import/core/shared.js +67 -910
  204. package/src/import/extractors/api/flutter-dio.js +4 -8
  205. package/src/import/extractors/api/react-native-repository.js +4 -8
  206. package/src/import/index.d.ts +4 -0
  207. package/src/import/provenance.d.ts +4 -0
  208. package/src/new-project/constants.js +128 -0
  209. package/src/new-project/create.js +83 -0
  210. package/src/new-project/json.js +28 -0
  211. package/src/new-project/metadata.js +96 -0
  212. package/src/new-project/package-spec.js +161 -0
  213. package/src/new-project/project-files.js +348 -0
  214. package/src/new-project/template-policy.js +269 -0
  215. package/src/new-project/template-resolution.js +368 -0
  216. package/src/new-project/template-snapshots.js +430 -0
  217. package/src/new-project/template-updates.js +512 -0
  218. package/src/new-project/types.d.ts +83 -0
  219. package/src/new-project.js +6 -2188
  220. package/src/npm-safety.js +79 -0
  221. package/src/parser.d.ts +87 -0
  222. package/src/parser.js +118 -0
  223. package/src/path-helpers.d.ts +1 -0
  224. package/src/path-helpers.js +20 -0
  225. package/src/policy/review-boundaries.d.ts +15 -0
  226. package/src/project-config/index.js +564 -0
  227. package/src/project-config.js +19 -560
  228. package/src/reconcile/docs.d.ts +8 -0
  229. package/src/reconcile/journeys.d.ts +1 -0
  230. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  231. package/src/resolver/enrich/bug.js +2 -0
  232. package/src/resolver/enrich/pitch.js +2 -0
  233. package/src/resolver/enrich/requirement.js +2 -0
  234. package/src/resolver/enrich/task.js +2 -0
  235. package/src/resolver/index.js +19 -2089
  236. package/src/resolver/normalize.js +384 -1
  237. package/src/resolver/plans.js +168 -0
  238. package/src/resolver/projections-api.js +494 -0
  239. package/src/resolver/projections-db.js +133 -0
  240. package/src/resolver/projections-ui.js +317 -0
  241. package/src/resolver/shapes.js +251 -0
  242. package/src/resolver/shared.js +278 -0
  243. package/src/resolver/widgets.js +132 -0
  244. package/src/resolver.d.ts +1 -0
  245. package/src/runtime-support.js +29 -0
  246. package/src/sdlc/adopt.d.ts +1 -0
  247. package/src/sdlc/check.d.ts +1 -0
  248. package/src/sdlc/explain.d.ts +1 -0
  249. package/src/sdlc/release.d.ts +1 -0
  250. package/src/sdlc/scaffold.d.ts +1 -0
  251. package/src/sdlc/transition.d.ts +1 -0
  252. package/src/template-trust/constants.js +62 -0
  253. package/src/template-trust/content.js +258 -0
  254. package/src/template-trust/diff.js +92 -0
  255. package/src/template-trust/policy.js +61 -0
  256. package/src/template-trust/record.js +90 -0
  257. package/src/template-trust/status.js +182 -0
  258. package/src/template-trust.js +24 -687
  259. package/src/text-helpers.d.ts +7 -0
  260. package/src/text-helpers.js +245 -0
  261. package/src/topogram-config.js +306 -0
  262. package/src/topogram-types.d.ts +69 -0
  263. package/src/validator/common.js +488 -0
  264. package/src/validator/data-model.js +237 -0
  265. package/src/validator/docs.js +167 -0
  266. package/src/validator/expressions.js +146 -1
  267. package/src/validator/index.d.ts +23 -0
  268. package/src/validator/index.js +32 -3585
  269. package/src/validator/kinds.d.ts +41 -0
  270. package/src/validator/kinds.js +2 -0
  271. package/src/validator/model-helpers.js +46 -0
  272. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  273. package/src/validator/per-kind/bug.js +6 -0
  274. package/src/validator/per-kind/domain.js +15 -2
  275. package/src/validator/per-kind/pitch.js +7 -0
  276. package/src/validator/per-kind/requirement.js +5 -0
  277. package/src/validator/per-kind/task.js +7 -0
  278. package/src/validator/per-kind/widget.js +14 -0
  279. package/src/validator/projections/api-http-async.js +410 -0
  280. package/src/validator/projections/api-http-authz.js +88 -0
  281. package/src/validator/projections/api-http-core.js +205 -0
  282. package/src/validator/projections/api-http-policies.js +339 -0
  283. package/src/validator/projections/api-http-responses.js +233 -0
  284. package/src/validator/projections/api-http.js +44 -0
  285. package/src/validator/projections/db.js +353 -0
  286. package/src/validator/projections/generator-defaults.js +45 -0
  287. package/src/validator/projections/helpers.js +87 -0
  288. package/src/validator/projections/ui-helpers.js +214 -0
  289. package/src/validator/projections/ui-navigation.js +344 -0
  290. package/src/validator/projections/ui-structure.js +364 -0
  291. package/src/validator/projections/ui-widgets.js +493 -0
  292. package/src/validator/projections/ui.js +46 -0
  293. package/src/validator/registry.js +48 -1
  294. package/src/validator/utils.d.ts +20 -0
  295. package/src/validator/utils.js +115 -12
  296. package/src/validator.d.ts +2 -0
  297. package/src/widget-behavior.d.ts +1 -0
  298. package/src/workflows/adoption/index.js +26 -0
  299. package/src/workflows/docs-generate.js +262 -0
  300. package/src/workflows/docs-scan.js +703 -0
  301. package/src/workflows/docs.js +15 -0
  302. package/src/workflows/import-app/api/collect.js +221 -0
  303. package/src/workflows/import-app/api/openapi.js +257 -0
  304. package/src/workflows/import-app/api/routes.js +327 -0
  305. package/src/workflows/import-app/api/sources.js +22 -0
  306. package/src/workflows/import-app/api.js +4 -0
  307. package/src/workflows/import-app/db.js +538 -0
  308. package/src/workflows/import-app/index.js +30 -0
  309. package/src/workflows/import-app/shared.js +218 -0
  310. package/src/workflows/import-app/ui.js +443 -0
  311. package/src/workflows/import-app/workflow.js +159 -0
  312. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  313. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  314. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  315. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  316. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  317. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  318. package/src/workflows/reconcile/adoption-plan.js +32 -0
  319. package/src/workflows/reconcile/auth/closures.js +115 -0
  320. package/src/workflows/reconcile/auth/formatters.js +142 -0
  321. package/src/workflows/reconcile/auth/inference.js +330 -0
  322. package/src/workflows/reconcile/auth/roles.js +122 -0
  323. package/src/workflows/reconcile/auth.js +37 -0
  324. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  325. package/src/workflows/reconcile/bundle-core.js +14 -0
  326. package/src/workflows/reconcile/bundle-shared.js +75 -0
  327. package/src/workflows/reconcile/candidate-model.js +477 -0
  328. package/src/workflows/reconcile/canonical-surface.js +264 -0
  329. package/src/workflows/reconcile/gap-report.js +333 -0
  330. package/src/workflows/reconcile/ids.js +6 -0
  331. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  332. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  333. package/src/workflows/reconcile/impacts/patches.js +252 -0
  334. package/src/workflows/reconcile/impacts/reports.js +80 -0
  335. package/src/workflows/reconcile/impacts.js +16 -0
  336. package/src/workflows/reconcile/index.js +7 -0
  337. package/src/workflows/reconcile/renderers.js +461 -0
  338. package/src/workflows/reconcile/summary.js +90 -0
  339. package/src/workflows/reconcile/workflow.js +309 -0
  340. package/src/workflows/shared.js +189 -0
  341. package/src/workflows/types.d.ts +93 -0
  342. package/src/workflows.d.ts +1 -0
  343. package/src/workflows.js +10 -7652
  344. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,703 @@
1
+ // @ts-check
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { stableStringify } from "../format.js";
6
+ import { relativeTo } from "../path-helpers.js";
7
+ import { canonicalCandidateTerm, ensureTrailingNewline, extractRankedTerms, idHintify, slugify, titleCase } from "../text-helpers.js";
8
+ import { listFilesRecursive, markdownTitle, normalizeWorkspacePaths, readTextIfExists, renderMarkdownDoc } from "./shared.js";
9
+ import { tryLoadResolvedGraph } from "./docs-generate.js";
10
+ import { renderCandidateMetadataComments } from "./reconcile/renderers.js";
11
+
12
+ /** @param {WorkspacePaths} paths @returns {any} */
13
+ function discoverDocSources(paths) {
14
+ const candidates = new Set();
15
+ const pushIfExists = (/** @type {any} */ filePath) => {
16
+ if (filePath && fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
17
+ candidates.add(filePath);
18
+ }
19
+ };
20
+
21
+ pushIfExists(path.join(paths.exampleRoot, "README.md"));
22
+ pushIfExists(path.join(paths.exampleRoot, "apps", "README.md"));
23
+ pushIfExists(path.join(paths.exampleRoot, "artifacts", "README.md"));
24
+ pushIfExists(path.join(paths.exampleRoot, "implementation", "README.md"));
25
+
26
+ for (const filePath of listFilesRecursive(path.join(paths.exampleRoot, "artifacts", "docs"), (/** @type {any} */ child) => child.endsWith(".md"))) {
27
+ candidates.add(filePath);
28
+ }
29
+ for (const filePath of listFilesRecursive(path.join(paths.exampleRoot, "apps"), (/** @type {any} */ child) => path.basename(child).toLowerCase().startsWith("readme"))) {
30
+ candidates.add(filePath);
31
+ }
32
+
33
+ return [...candidates].sort();
34
+ }
35
+
36
+ /** @param {string} markdown @returns {any} */
37
+ function extractTerms(markdown) {
38
+ return extractRankedTerms(markdown, { technicalStopwords: true });
39
+ }
40
+
41
+ /** @param {string} markdown @returns {any} */
42
+ function extractWorkflowSignals(markdown) {
43
+ const text = markdown.toLowerCase();
44
+ /** @type {any[]} */
45
+ const signals = [];
46
+ if (/(workflow|review|approve|reject|revision|resubmit)/.test(text)) {
47
+ signals.push("review_workflow");
48
+ }
49
+ if (/(create|edit|update|close|resolve|archive|export)/.test(text)) {
50
+ signals.push("lifecycle_flow");
51
+ }
52
+ return [...new Set(signals)];
53
+ }
54
+
55
+ /** @param {string} capabilityId @returns {any} */
56
+ function tokenizeCapabilityId(capabilityId) {
57
+ return capabilityId
58
+ .replace(/^cap_/, "")
59
+ .split(/[_-]+/)
60
+ .filter(Boolean);
61
+ }
62
+
63
+ /** @param {string} token @returns {any} */
64
+ function expandTokenVariants(token) {
65
+ const variants = new Set([token]);
66
+ if (token.endsWith("y")) {
67
+ variants.add(`${token.slice(0, -1)}ies`);
68
+ } else {
69
+ variants.add(`${token}s`);
70
+ }
71
+ if (token === "close") {
72
+ variants.add("closed");
73
+ variants.add("closing");
74
+ }
75
+ if (token === "complete") {
76
+ variants.add("completed");
77
+ variants.add("completion");
78
+ }
79
+ if (token === "create") {
80
+ variants.add("created");
81
+ variants.add("creating");
82
+ variants.add("new");
83
+ }
84
+ if (token === "update") {
85
+ variants.add("updated");
86
+ variants.add("updating");
87
+ variants.add("edit");
88
+ variants.add("edited");
89
+ variants.add("editing");
90
+ }
91
+ if (token === "export") {
92
+ variants.add("exports");
93
+ variants.add("download");
94
+ variants.add("downloaded");
95
+ }
96
+ if (token === "request") {
97
+ variants.add("requested");
98
+ variants.add("requesting");
99
+ }
100
+ if (token === "reject") {
101
+ variants.add("rejected");
102
+ variants.add("rejecting");
103
+ }
104
+ if (token === "approve") {
105
+ variants.add("approved");
106
+ variants.add("approving");
107
+ }
108
+ if (token === "revision") {
109
+ variants.add("revise");
110
+ variants.add("revisions");
111
+ }
112
+ return [...variants];
113
+ }
114
+
115
+ /** @param {string} markdown @param {string} token @returns {any} */
116
+ function includesAnyVariant(markdown, token) {
117
+ const text = markdown.toLowerCase();
118
+ return expandTokenVariants(token).some((/** @type {any} */ variant) => includesTerm(text, variant));
119
+ }
120
+
121
+ /** @param {ResolvedGraph} graph @returns {any} */
122
+ function buildCapabilityWorkflowHints(graph) {
123
+ const capabilities = graph?.byKind.capability || [];
124
+ return capabilities
125
+ .filter((/** @type {any} */ capability) => {
126
+ const id = capability.id.replace(/^cap_/, "");
127
+ const tokens = tokenizeCapabilityId(capability.id);
128
+ if (tokens[0] === "get" || tokens[0] === "list") {
129
+ return false;
130
+ }
131
+ return (
132
+ capability.creates.length > 0 ||
133
+ capability.updates.length > 0 ||
134
+ capability.deletes.length > 0 ||
135
+ /(close|complete|export|download|approve|reject|request|revision)/.test(id)
136
+ );
137
+ })
138
+ .map((/** @type {any} */ capability) => {
139
+ const id = capability.id.replace(/^cap_/, "");
140
+ const tokens = tokenizeCapabilityId(capability.id);
141
+ const actionTokens = tokens.filter((/** @type {any} */ token) => !["task", "tasks", "issue", "issues", "user", "users", "project", "projects", "article", "articles", "board", "boards"].includes(token));
142
+ const nounTokens = tokens.filter((/** @type {any} */ token) => !actionTokens.includes(token));
143
+ return {
144
+ id,
145
+ capabilityId: capability.id,
146
+ title: capability.name || titleCase(id),
147
+ actionTokens,
148
+ nounTokens
149
+ };
150
+ });
151
+ }
152
+
153
+ /** @param {string} markdown @param {any[]} workflowHints @returns {any} */
154
+ function inferCapabilityWorkflowSignals(markdown, workflowHints) {
155
+ /** @type {any[]} */
156
+ const matches = [];
157
+ for (const hint of workflowHints) {
158
+ const actionMatched = hint.actionTokens.length === 0 || hint.actionTokens.some((/** @type {any} */ token) => includesAnyVariant(markdown, token));
159
+ const nounMatched = hint.nounTokens.length === 0 || hint.nounTokens.some((/** @type {any} */ token) => includesAnyVariant(markdown, token));
160
+ if (actionMatched && nounMatched) {
161
+ matches.push(hint);
162
+ }
163
+ }
164
+ return matches;
165
+ }
166
+
167
+ /** @param {any} signal @returns {any} */
168
+ function workflowPriority(signal) {
169
+ if (/export|download/.test(signal)) {
170
+ return 7;
171
+ }
172
+ if (/request|revision|approve|reject/.test(signal)) {
173
+ return 6;
174
+ }
175
+ if (/close|complete/.test(signal)) {
176
+ return 5;
177
+ }
178
+ if (/create/.test(signal)) {
179
+ return 4;
180
+ }
181
+ if (/update/.test(signal)) {
182
+ return 3;
183
+ }
184
+ if (/delete/.test(signal)) {
185
+ return 2;
186
+ }
187
+ if (signal === "review_workflow") {
188
+ return 1;
189
+ }
190
+ if (signal === "lifecycle_flow") {
191
+ return 0;
192
+ }
193
+ return 0;
194
+ }
195
+
196
+ /** @param {string} markdown @param {string} term @returns {any} */
197
+ function includesTerm(markdown, term) {
198
+ if (!markdown || !term) {
199
+ return false;
200
+ }
201
+ const pattern = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}s?\\b`, "i");
202
+ return pattern.test(markdown);
203
+ }
204
+
205
+ const DOC_ACTOR_HINTS = [
206
+ {
207
+ id: "actor_user",
208
+ title: "User",
209
+ phrases: ["user", "users", "workspace member", "workspace members"],
210
+ participantPatterns: [/\busers?\s+(?:(?:can|still)\s+)?(?:browse|view|open|return|sign\s+in|complete|create|update|see)\b/i, /\bworkspace members?\s+(?:browse|view|open|return|complete|see)\b/i]
211
+ },
212
+ {
213
+ id: "actor_author",
214
+ title: "Author",
215
+ phrases: ["author", "authors"],
216
+ participantPatterns: [/\bauthors?\s+(?:returns?|edits?|opens?|submits?|resubmits?|drafts?|updates?|sees?|receives?|understands?)\b/i]
217
+ },
218
+ {
219
+ id: "actor_reviewer",
220
+ title: "Reviewer",
221
+ phrases: ["reviewer", "reviewers"],
222
+ participantPatterns: [/\breviewers?\s+(?:reviews?|requests?|opens?|sees?|receives?|assigns?|returns?)\b/i]
223
+ },
224
+ {
225
+ id: "actor_manager",
226
+ title: "Manager",
227
+ phrases: ["manager", "managers"],
228
+ participantPatterns: [/\bmanagers?\s+(?:reviews?|assigns?|sees?|opens?|returns?)\b/i]
229
+ },
230
+ {
231
+ id: "actor_admin",
232
+ title: "Admin",
233
+ phrases: ["admin", "admins", "administrator", "administrators"],
234
+ participantPatterns: [/\badmins?\s+(?:reviews?|opens?|sees?|assigns?|returns?)\b/i, /\badministrators?\s+(?:reviews?|opens?|sees?|assigns?|returns?)\b/i]
235
+ },
236
+ {
237
+ id: "actor_system_job",
238
+ title: "System Job",
239
+ phrases: ["system job", "background job", "worker process"],
240
+ participantPatterns: [/\b(system job|background job|worker process)\s+(?:runs|retries|processes|updates)\b/i]
241
+ }
242
+ ];
243
+
244
+ const DOC_ROLE_HINTS = [
245
+ {
246
+ id: "role_author",
247
+ title: "Author",
248
+ exactPatterns: [/\brole[_\s-]?author\b/i, /\bauthors?\s+may\b/i, /\bauthors?\s+can\b/i]
249
+ },
250
+ {
251
+ id: "role_reviewer",
252
+ title: "Reviewer",
253
+ exactPatterns: [/\brole[_\s-]?reviewer\b/i, /\breviewers?\s+may\b/i, /\breviewers?\s+can\b/i]
254
+ },
255
+ {
256
+ id: "role_manager",
257
+ title: "Manager",
258
+ exactPatterns: [/\brole[_\s-]?manager\b/i, /\bonly\s+managers?\s+may\b/i, /\bmanagers?\s+may\b/i, /\bmanagers?\s+can\b/i]
259
+ },
260
+ {
261
+ id: "role_admin",
262
+ title: "Admin",
263
+ exactPatterns: [/\brole[_\s-]?admin\b/i, /\bonly\s+admins?\s+may\b/i, /\badmins?\s+may\b/i, /\badmins?\s+can\b/i]
264
+ },
265
+ {
266
+ id: "role_owner",
267
+ title: "Owner",
268
+ exactPatterns: [/\brole[_\s-]?owner\b/i, /\bowners?\s+may\b/i, /\bowners?\s+can\b/i]
269
+ },
270
+ {
271
+ id: "role_assignee",
272
+ title: "Assignee",
273
+ exactPatterns: [/\brole[_\s-]?assignee\b/i, /\bassignees?\s+may\b/i, /\bassignees?\s+can\b/i]
274
+ }
275
+ ];
276
+
277
+ /** @param {string} markdown @param {any} phrase @returns {any} */
278
+ function countPhrase(markdown, phrase) {
279
+ if (!markdown || !phrase) {
280
+ return 0;
281
+ }
282
+ const pattern = new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
283
+ return markdown.match(pattern)?.length || 0;
284
+ }
285
+
286
+ /** @param {string} markdown @param {any[]} patterns @returns {any} */
287
+ function countPatternMatches(markdown, patterns = []) {
288
+ return patterns.reduce((/** @type {any} */ total, /** @type {any} */ pattern) => total + (markdown.match(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))?.length || 0), 0);
289
+ }
290
+
291
+ /** @param {string} value @returns {any} */
292
+ export function confidenceRank(value) {
293
+ if (value === "high") {
294
+ return 3;
295
+ }
296
+ if (value === "medium") {
297
+ return 2;
298
+ }
299
+ return 1;
300
+ }
301
+
302
+ /** @param {any} a @param {any} b @returns {any} */
303
+ function maxConfidence(a, b) {
304
+ return confidenceRank(a) >= confidenceRank(b) ? a : b;
305
+ }
306
+
307
+ /** @param {string} markdown @returns {any} */
308
+ function inferRoleSignals(markdown) {
309
+ /** @type {any[]} */
310
+ const hits = [];
311
+ for (const hint of DOC_ROLE_HINTS) {
312
+ const explicitRolePatternCount = countPatternMatches(markdown, hint.exactPatterns.filter((/** @type {any} */ pattern) => pattern.source.includes("role[")));
313
+ const restrictivePatternCount = countPatternMatches(markdown, hint.exactPatterns.filter((/** @type {any} */ pattern) => pattern.source.includes("only\\s+")));
314
+ const permissionPatternCount = countPatternMatches(markdown, hint.exactPatterns);
315
+ if (permissionPatternCount === 0) {
316
+ continue;
317
+ }
318
+ hits.push({
319
+ ...hint,
320
+ confidence: explicitRolePatternCount > 0 || restrictivePatternCount > 0 ? "high" : "medium",
321
+ evidence: {
322
+ permission_pattern_count: permissionPatternCount,
323
+ restrictive_pattern_count: restrictivePatternCount,
324
+ explicit_role_pattern_count: explicitRolePatternCount
325
+ }
326
+ });
327
+ }
328
+ return hits;
329
+ }
330
+
331
+ /** @param {string} markdown @param {any[]} roleSignals @returns {any} */
332
+ function inferActorSignals(markdown, roleSignals = []) {
333
+ /** @type {any[]} */
334
+ const hits = [];
335
+ const roleSignalsByTitle = new Map(roleSignals.map((/** @type {any} */ signal) => [signal.title.toLowerCase(), signal]));
336
+ for (const hint of DOC_ACTOR_HINTS) {
337
+ const genericPhraseCount = hint.phrases.reduce((/** @type {any} */ total, /** @type {any} */ phrase) => total + countPhrase(markdown, phrase), 0);
338
+ const participantPatternCount = countPatternMatches(markdown, hint.participantPatterns || []);
339
+ if (genericPhraseCount === 0 && participantPatternCount === 0) {
340
+ continue;
341
+ }
342
+ const relatedRoleSignal = roleSignalsByTitle.get(hint.title.toLowerCase());
343
+ const permissionOverlapCount = relatedRoleSignal?.evidence?.permission_pattern_count || 0;
344
+ if (participantPatternCount === 0 && permissionOverlapCount > 0 && genericPhraseCount <= permissionOverlapCount) {
345
+ continue;
346
+ }
347
+ hits.push({
348
+ ...hint,
349
+ confidence: participantPatternCount > 0 ? "medium" : genericPhraseCount >= 2 ? "medium" : "low",
350
+ evidence: {
351
+ phrase_count: genericPhraseCount,
352
+ participant_pattern_count: participantPatternCount,
353
+ permission_overlap_count: permissionOverlapCount
354
+ }
355
+ });
356
+ }
357
+ return hits;
358
+ }
359
+
360
+ /** @param {WorkflowRecord} record @returns {any} */
361
+ export function renderCandidateActor(record) {
362
+ const metadataLines = [renderCandidateMetadataComments(record)];
363
+ for (const docId of (record.related_docs || []).slice(0, 3)) {
364
+ metadataLines.push(`# imported related_doc: ${docId}`);
365
+ }
366
+ for (const capabilityId of (record.related_capabilities || []).slice(0, 3)) {
367
+ metadataLines.push(`# imported related_capability: ${capabilityId}`);
368
+ }
369
+ return ensureTrailingNewline(
370
+ `${metadataLines.join("\n")}\n` +
371
+ `actor ${record.id_hint} {\n` +
372
+ ` name "${record.label || titleCase(record.id_hint.replace(/^actor_/, ""))}"\n` +
373
+ ` description "Candidate actor inferred from existing documentation"\n\n` +
374
+ ` status proposed\n` +
375
+ `}\n`
376
+ );
377
+ }
378
+
379
+ /** @param {WorkflowRecord} record @returns {any} */
380
+ export function renderCandidateRole(record) {
381
+ const metadataLines = [renderCandidateMetadataComments(record)];
382
+ for (const docId of (record.related_docs || []).slice(0, 3)) {
383
+ metadataLines.push(`# imported related_doc: ${docId}`);
384
+ }
385
+ for (const capabilityId of (record.related_capabilities || []).slice(0, 3)) {
386
+ metadataLines.push(`# imported related_capability: ${capabilityId}`);
387
+ }
388
+ return ensureTrailingNewline(
389
+ `${metadataLines.join("\n")}\n` +
390
+ `role ${record.id_hint} {\n` +
391
+ ` name "${record.label || titleCase(record.id_hint.replace(/^role_/, ""))}"\n` +
392
+ ` description "Candidate role inferred from existing documentation"\n\n` +
393
+ ` status proposed\n` +
394
+ `}\n`
395
+ );
396
+ }
397
+
398
+ /** @param {string} inputPath @returns {any} */
399
+ export function scanDocsWorkflow(inputPath) {
400
+ const paths = normalizeWorkspacePaths(inputPath);
401
+ const graph = tryLoadResolvedGraph(paths.topogramRoot);
402
+ const sources = discoverDocSources(paths);
403
+ /** @type {any[]} */
404
+ const findings = [];
405
+ const glossaryCandidates = new Map();
406
+ const workflowCandidates = new Map();
407
+ const actorCandidates = new Map();
408
+ const roleCandidates = new Map();
409
+ const exampleName = path.basename(paths.exampleRoot).toLowerCase();
410
+ const exampleTerms = new Set(
411
+ exampleName
412
+ .split(/[^a-z0-9]+/)
413
+ .filter(Boolean)
414
+ .flatMap((/** @type {any} */ term) => [term, canonicalCandidateTerm(term)])
415
+ );
416
+ const preferredTerms = new Set([
417
+ ...((graph?.byKind.entity || []).map((/** @type {any} */ entity) => entity.id.replace(/^entity_/, ""))),
418
+ ...((graph?.byKind.term || []).map((/** @type {any} */ term) => term.id))
419
+ ]);
420
+ const workflowHints = buildCapabilityWorkflowHints(graph);
421
+
422
+ for (const filePath of sources) {
423
+ const markdown = readTextIfExists(filePath) || "";
424
+ const title = markdownTitle(filePath, markdown);
425
+ const terms = extractTerms(markdown).slice(0, 8);
426
+ const workflowSignals = [
427
+ ...extractWorkflowSignals(markdown),
428
+ ...inferCapabilityWorkflowSignals(markdown, workflowHints).map((/** @type {any} */ hint) => hint.id)
429
+ ];
430
+ const workflowHintsForFile = inferCapabilityWorkflowSignals(markdown, workflowHints);
431
+ const relatedDocIdsForFile = [...new Set(workflowSignals.map((/** @type {any} */ signal) => slugify(signal)))];
432
+ const relatedCapabilityIdsForFile = [...new Set(workflowHintsForFile.map((/** @type {any} */ hint) => hint.capabilityId).filter(Boolean))];
433
+ const roleSignals = inferRoleSignals(markdown);
434
+ const actorSignals = inferActorSignals(markdown, roleSignals);
435
+ findings.push({
436
+ file: filePath,
437
+ relative_path: relativeTo(paths.repoRoot, filePath),
438
+ title,
439
+ term_candidates: terms,
440
+ workflow_signals: workflowSignals,
441
+ actor_signals: actorSignals.map((/** @type {any} */ signal) => signal.id),
442
+ role_signals: roleSignals.map((/** @type {any} */ signal) => signal.id)
443
+ });
444
+
445
+ for (const term of terms) {
446
+ if (!preferredTerms.has(term)) {
447
+ continue;
448
+ }
449
+ if (!glossaryCandidates.has(term)) {
450
+ glossaryCandidates.set(term, []);
451
+ }
452
+ glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
453
+ }
454
+
455
+ for (const term of preferredTerms) {
456
+ if (!includesTerm(markdown, term)) {
457
+ continue;
458
+ }
459
+ if (!glossaryCandidates.has(term)) {
460
+ glossaryCandidates.set(term, []);
461
+ }
462
+ glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
463
+ }
464
+
465
+ for (const term of terms.slice(0, 4)) {
466
+ if (!preferredTerms.has(term) && (exampleTerms.has(term) || exampleTerms.has(canonicalCandidateTerm(term)))) {
467
+ continue;
468
+ }
469
+ if (!glossaryCandidates.has(term)) {
470
+ glossaryCandidates.set(term, []);
471
+ }
472
+ glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
473
+ }
474
+
475
+ for (const signal of workflowSignals) {
476
+ if (!workflowCandidates.has(signal)) {
477
+ workflowCandidates.set(signal, []);
478
+ }
479
+ workflowCandidates.get(signal).push(relativeTo(paths.repoRoot, filePath));
480
+ }
481
+
482
+ for (const signal of actorSignals) {
483
+ if (!actorCandidates.has(signal.id)) {
484
+ actorCandidates.set(signal.id, {
485
+ id_hint: signal.id,
486
+ label: signal.title,
487
+ confidence: signal.confidence,
488
+ source_kind: "docs",
489
+ provenance: [],
490
+ related_docs: [],
491
+ related_capabilities: [],
492
+ evidence: []
493
+ });
494
+ }
495
+ const candidate = actorCandidates.get(signal.id);
496
+ candidate.confidence = maxConfidence(candidate.confidence, signal.confidence);
497
+ candidate.provenance.push(relativeTo(paths.repoRoot, filePath));
498
+ candidate.related_docs.push(...relatedDocIdsForFile);
499
+ candidate.related_capabilities.push(...relatedCapabilityIdsForFile);
500
+ candidate.evidence.push(signal.evidence);
501
+ }
502
+
503
+ for (const signal of roleSignals) {
504
+ if (!roleCandidates.has(signal.id)) {
505
+ roleCandidates.set(signal.id, {
506
+ id_hint: signal.id,
507
+ label: signal.title,
508
+ confidence: signal.confidence,
509
+ source_kind: "docs",
510
+ provenance: [],
511
+ related_docs: [],
512
+ related_capabilities: [],
513
+ evidence: []
514
+ });
515
+ }
516
+ const candidate = roleCandidates.get(signal.id);
517
+ candidate.confidence = maxConfidence(candidate.confidence, signal.confidence);
518
+ candidate.provenance.push(relativeTo(paths.repoRoot, filePath));
519
+ candidate.related_docs.push(...relatedDocIdsForFile);
520
+ candidate.related_capabilities.push(...relatedCapabilityIdsForFile);
521
+ candidate.evidence.push(signal.evidence);
522
+ }
523
+ }
524
+
525
+ /** @type {WorkflowFiles} */
526
+
527
+ /** @type {WorkflowFiles} */
528
+
529
+ const files = {};
530
+ /** @type {any[]} */
531
+ const candidateDocs = [];
532
+ const orderedGlossaryCandidates = [...glossaryCandidates.entries()]
533
+ .filter((/** @type {any} */ [term]) => preferredTerms.has(term) || (!exampleTerms.has(term) && !exampleTerms.has(canonicalCandidateTerm(term))))
534
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => {
535
+ const aPreferred = preferredTerms.has(a[0]) ? 1 : 0;
536
+ const bPreferred = preferredTerms.has(b[0]) ? 1 : 0;
537
+ if (aPreferred !== bPreferred) {
538
+ return bPreferred - aPreferred;
539
+ }
540
+ if (a[1].length !== b[1].length) {
541
+ return b[1].length - a[1].length;
542
+ }
543
+ return a[0].localeCompare(b[0]);
544
+ });
545
+ const preferredGlossaryCandidates = orderedGlossaryCandidates.filter((/** @type {any} */ [term]) => preferredTerms.has(term));
546
+ const fallbackGlossaryCandidates = orderedGlossaryCandidates.filter((/** @type {any} */ [term]) => !preferredTerms.has(term));
547
+ const seenCanonicalTerms = new Set();
548
+ for (const [term, provenance] of [...preferredGlossaryCandidates, ...fallbackGlossaryCandidates]) {
549
+ const canonicalTerm = canonicalCandidateTerm(term);
550
+ if (seenCanonicalTerms.has(canonicalTerm)) {
551
+ continue;
552
+ }
553
+ seenCanonicalTerms.add(canonicalTerm);
554
+ /** @type {WorkflowRecord} */
555
+ const metadata = {
556
+ id: slugify(term),
557
+ kind: "glossary",
558
+ title: titleCase(term),
559
+ status: "inferred",
560
+ source_of_truth: "imported",
561
+ confidence: "low",
562
+ review_required: true,
563
+ provenance,
564
+ tags: ["import", "glossary"]
565
+ };
566
+ const body = `Candidate glossary term inferred from existing documentation.\n\nObserved term: \`${term}\`\n\nThis entry should be reviewed and either promoted, renamed, merged, or discarded.`;
567
+ const relativePath = `candidates/docs/glossary/${slugify(term)}.md`;
568
+ files[relativePath] = renderMarkdownDoc(metadata, body);
569
+ candidateDocs.push({
570
+ id: metadata.id,
571
+ kind: metadata.kind,
572
+ title: metadata.title,
573
+ path: relativePath,
574
+ confidence: metadata.confidence,
575
+ provenance: metadata.provenance,
576
+ related_entities: metadata.related_entities || [],
577
+ related_capabilities: metadata.related_capabilities || [],
578
+ source_of_truth: metadata.source_of_truth
579
+ });
580
+ if (candidateDocs.filter((/** @type {any} */ doc) => doc.kind === "glossary").length >= 6) {
581
+ break;
582
+ }
583
+ }
584
+
585
+ const genericWorkflowSignals = new Set(["review_workflow", "lifecycle_flow"]);
586
+ const orderedWorkflowCandidates = [...workflowCandidates.entries()].sort((/** @type {any} */ a, /** @type {any} */ b) => {
587
+ const aGeneric = genericWorkflowSignals.has(a[0]) ? 1 : 0;
588
+ const bGeneric = genericWorkflowSignals.has(b[0]) ? 1 : 0;
589
+ if (aGeneric !== bGeneric) {
590
+ return aGeneric - bGeneric;
591
+ }
592
+ const aPriority = workflowPriority(a[0]);
593
+ const bPriority = workflowPriority(b[0]);
594
+ if (aPriority !== bPriority) {
595
+ return bPriority - aPriority;
596
+ }
597
+ if (a[1].length !== b[1].length) {
598
+ return b[1].length - a[1].length;
599
+ }
600
+ return a[0].localeCompare(b[0]);
601
+ });
602
+ let specificWorkflowCount = 0;
603
+ let genericWorkflowCount = 0;
604
+ for (const [signal, provenance] of orderedWorkflowCandidates) {
605
+ const workflowHint = workflowHints.find((/** @type {any} */ hint) => hint.id === signal);
606
+ const isGeneric = genericWorkflowSignals.has(signal);
607
+ if (isGeneric && genericWorkflowCount >= 2) {
608
+ continue;
609
+ }
610
+ if (!isGeneric && specificWorkflowCount >= 4) {
611
+ continue;
612
+ }
613
+ const title = workflowHint?.title || (signal === "review_workflow" ? "Review Workflow" : "Lifecycle Flow");
614
+ /** @type {WorkflowRecord} */
615
+ const metadata = {
616
+ id: slugify(signal),
617
+ kind: "workflow",
618
+ title,
619
+ status: "inferred",
620
+ source_of_truth: "imported",
621
+ confidence: genericWorkflowSignals.has(signal) ? "low" : "medium",
622
+ review_required: true,
623
+ related_capabilities: workflowHint ? [workflowHint.capabilityId] : [],
624
+ provenance,
625
+ tags: ["import", "workflow"]
626
+ };
627
+ const body = workflowHint
628
+ ? `Candidate workflow inferred from existing documentation.\n\nMatched capability: \`${workflowHint.capabilityId}\`\n\nThis entry should be reconciled with the eventual capability and UI model before promotion.`
629
+ : `Candidate workflow inferred from existing documentation.\n\nWorkflow signal: \`${signal}\`\n\nThis entry should be reconciled with the eventual capability and UI model before promotion.`;
630
+ const relativePath = `candidates/docs/workflows/${slugify(signal)}.md`;
631
+ files[relativePath] = renderMarkdownDoc(metadata, body);
632
+ candidateDocs.push({
633
+ id: metadata.id,
634
+ kind: metadata.kind,
635
+ title: metadata.title,
636
+ path: relativePath,
637
+ confidence: metadata.confidence,
638
+ provenance: metadata.provenance,
639
+ related_entities: metadata.related_entities || [],
640
+ related_capabilities: metadata.related_capabilities || [],
641
+ source_of_truth: metadata.source_of_truth
642
+ });
643
+ if (isGeneric) {
644
+ genericWorkflowCount += 1;
645
+ } else {
646
+ specificWorkflowCount += 1;
647
+ }
648
+ }
649
+
650
+ const candidateActors = [...actorCandidates.values()]
651
+ .map((/** @type {any} */ record) => ({
652
+ ...record,
653
+ provenance: [...new Set(record.provenance)].sort(),
654
+ related_docs: [...new Set(record.related_docs || [])].sort(),
655
+ related_capabilities: [...new Set(record.related_capabilities || [])].sort(),
656
+ inference_summary: `phrases=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.phrase_count || 0), 0)}, participant_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.participant_pattern_count || 0), 0)}, permission_overlap=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.permission_overlap_count || 0), 0)}`
657
+ }))
658
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint))
659
+ .slice(0, 6);
660
+ for (const record of candidateActors) {
661
+ const relativePath = `candidates/docs/actors/${record.id_hint.replace(/^actor_/, "").replaceAll("_", "-")}.tg`;
662
+ files[relativePath] = renderCandidateActor(record);
663
+ }
664
+
665
+ const candidateRoles = [...roleCandidates.values()]
666
+ .map((/** @type {any} */ record) => ({
667
+ ...record,
668
+ provenance: [...new Set(record.provenance)].sort(),
669
+ related_docs: [...new Set(record.related_docs || [])].sort(),
670
+ related_capabilities: [...new Set(record.related_capabilities || [])].sort(),
671
+ inference_summary: `permission_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.permission_pattern_count || 0), 0)}, restrictive_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.restrictive_pattern_count || 0), 0)}, explicit_role_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.explicit_role_pattern_count || 0), 0)}`
672
+ }))
673
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint))
674
+ .slice(0, 6);
675
+ for (const record of candidateRoles) {
676
+ const relativePath = `candidates/docs/roles/${record.id_hint.replace(/^role_/, "").replaceAll("_", "-")}.tg`;
677
+ files[relativePath] = renderCandidateRole(record);
678
+ }
679
+
680
+ /** @type {WorkflowRecord} */
681
+ const report = {
682
+ type: "scan_docs_report",
683
+ workspace: paths.topogramRoot,
684
+ bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
685
+ source_count: sources.length,
686
+ sources: sources.map((/** @type {any} */ filePath) => relativeTo(paths.repoRoot, filePath)),
687
+ findings,
688
+ candidate_docs: candidateDocs,
689
+ candidate_actors: candidateActors,
690
+ candidate_roles: candidateRoles
691
+ };
692
+ files["candidates/docs/findings.json"] = `${stableStringify(findings)}\n`;
693
+ files["candidates/docs/import-report.json"] = `${stableStringify(report)}\n`;
694
+ files["candidates/docs/import-report.md"] = ensureTrailingNewline(
695
+ `# Docs Import Report\n\nScanned ${sources.length} source document(s).\n\n## Candidate Docs\n\n${candidateDocs.length === 0 ? "- None" : candidateDocs.map((/** @type {any} */ doc) => `- \`${doc.kind}\` ${doc.title}`).join("\n")}\n\n## Candidate Actors\n\n${candidateActors.length === 0 ? "- None" : candidateActors.map((/** @type {any} */ actor) => `- \`${actor.id_hint}\` (${actor.confidence})`).join("\n")}\n\n## Candidate Roles\n\n${candidateRoles.length === 0 ? "- None" : candidateRoles.map((/** @type {any} */ role) => `- \`${role.id_hint}\` (${role.confidence})`).join("\n")}\n`
696
+ );
697
+
698
+ return {
699
+ summary: report,
700
+ files,
701
+ defaultOutDir: paths.topogramRoot
702
+ };
703
+ }