@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,290 @@
1
+ import {
2
+ canonicalCandidateTerm,
3
+ dedupeCandidateRecords,
4
+ findImportFiles,
5
+ idHintify,
6
+ makeCandidateRecord,
7
+ normalizeImportRelativePath,
8
+ pluralizeCandidateTerm,
9
+ readJsonIfExists,
10
+ readTextIfExists,
11
+ slugify,
12
+ titleCase
13
+ } from "../../core/shared.js";
14
+
15
+ const JS_TEST_PATTERN = /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs)$/i;
16
+ const PYTHON_TEST_PATTERN = /(^|\/)(test_[^/]+|[^/]+_test)\.py$/i;
17
+ const RUBY_TEST_PATTERN = /(^|\/)[^/]+_spec\.rb$/i;
18
+ const SWIFT_TEST_PATTERN = /(^|\/)[^/]*Tests?\.swift$/i;
19
+
20
+ function frameworkInfo(context) {
21
+ const packageFiles = findImportFiles(context.paths, (filePath) => /package\.json$/i.test(filePath));
22
+ const frameworks = new Set();
23
+ const scripts = [];
24
+
25
+ for (const packagePath of packageFiles) {
26
+ const pkg = readJsonIfExists(packagePath);
27
+ if (!pkg) continue;
28
+ const deps = {
29
+ ...(pkg.dependencies || {}),
30
+ ...(pkg.devDependencies || {})
31
+ };
32
+ if (deps["@playwright/test"] || deps.playwright) frameworks.add("playwright");
33
+ if (deps.vitest) frameworks.add("vitest");
34
+ if (deps.jest) frameworks.add("jest");
35
+ if (deps.cypress) frameworks.add("cypress");
36
+
37
+ for (const [name, command] of Object.entries(pkg.scripts || {})) {
38
+ if (!/^test($|[-:])/.test(name) && !/(playwright|vitest|jest|cypress|pytest|rspec)/i.test(String(command || ""))) {
39
+ continue;
40
+ }
41
+ scripts.push({
42
+ name,
43
+ command,
44
+ file: normalizeImportRelativePath(context.paths, packagePath)
45
+ });
46
+ }
47
+ }
48
+
49
+ const configFiles = findImportFiles(
50
+ context.paths,
51
+ (filePath) => /(playwright\.config|vitest\.config|jest\.config|cypress\.config)\.(ts|js|mjs|cjs)$/i.test(filePath)
52
+ );
53
+ for (const filePath of configFiles) {
54
+ const lower = filePath.toLowerCase();
55
+ if (lower.includes("playwright")) frameworks.add("playwright");
56
+ if (lower.includes("vitest")) frameworks.add("vitest");
57
+ if (lower.includes("jest")) frameworks.add("jest");
58
+ if (lower.includes("cypress")) frameworks.add("cypress");
59
+ }
60
+
61
+ return {
62
+ frameworks: [...frameworks].sort(),
63
+ scripts: scripts.sort((left, right) => left.name.localeCompare(right.name) || left.file.localeCompare(right.file))
64
+ };
65
+ }
66
+
67
+ function findTestFiles(context) {
68
+ return findImportFiles(context.paths, (filePath) => {
69
+ const normalized = filePath.replaceAll("\\", "/");
70
+ return (
71
+ JS_TEST_PATTERN.test(normalized) ||
72
+ PYTHON_TEST_PATTERN.test(normalized) ||
73
+ RUBY_TEST_PATTERN.test(normalized) ||
74
+ SWIFT_TEST_PATTERN.test(normalized) ||
75
+ /\/playwright\/.+\.(ts|tsx|js|jsx)$/i.test(normalized) ||
76
+ /\/cypress\/.+\.(ts|tsx|js|jsx)$/i.test(normalized)
77
+ );
78
+ });
79
+ }
80
+
81
+ function inferFramework(filePath, frameworks) {
82
+ const normalized = filePath.replaceAll("\\", "/").toLowerCase();
83
+ if (normalized.includes("/playwright/")) return "playwright";
84
+ if (normalized.includes("/cypress/")) return "cypress";
85
+ if (normalized.endsWith(".py")) return "pytest";
86
+ if (normalized.endsWith("_spec.rb")) return "rspec";
87
+ if (normalized.endsWith(".swift")) return "xctest";
88
+ if (frameworks.includes("vitest")) return "vitest";
89
+ if (frameworks.includes("jest")) return "jest";
90
+ return "test";
91
+ }
92
+
93
+ function inferMethod(filePath, framework) {
94
+ const normalized = filePath.replaceAll("\\", "/").toLowerCase();
95
+ if (framework === "playwright" || framework === "cypress") {
96
+ return normalized.includes("smoke") ? "smoke" : "runtime";
97
+ }
98
+ if (normalized.includes("/e2e/") || normalized.includes("/integration/")) {
99
+ return "runtime";
100
+ }
101
+ return "contract";
102
+ }
103
+
104
+ function extractScenarioTitles(source, filePath) {
105
+ const normalized = filePath.replaceAll("\\", "/").toLowerCase();
106
+ const titles = [];
107
+
108
+ if (/\.(ts|tsx|js|jsx|mjs|cjs)$/.test(normalized)) {
109
+ for (const match of source.matchAll(/\b(?:test|it|describe)\(\s*['"`]([^'"`]+)['"`]/g)) {
110
+ titles.push(match[1]);
111
+ }
112
+ } else if (normalized.endsWith(".py")) {
113
+ for (const match of source.matchAll(/\bdef\s+(test_[a-zA-Z0-9_]+)\s*\(/g)) {
114
+ titles.push(match[1].replace(/^test_/, "").replaceAll("_", " "));
115
+ }
116
+ } else if (normalized.endsWith(".rb")) {
117
+ for (const match of source.matchAll(/\bit\s+['"]([^'"]+)['"]/g)) {
118
+ titles.push(match[1]);
119
+ }
120
+ } else if (normalized.endsWith(".swift")) {
121
+ for (const match of source.matchAll(/\bfunc\s+(test[A-Za-z0-9_]+)\s*\(/g)) {
122
+ titles.push(match[1].replace(/^test/, "").replace(/([A-Z])/g, " $1").trim());
123
+ }
124
+ }
125
+
126
+ return [...new Set(titles)];
127
+ }
128
+
129
+ function capabilityMatchers(apiCandidates = []) {
130
+ return (apiCandidates.capabilities || []).map((capability) => {
131
+ const entityStem = canonicalCandidateTerm(
132
+ String(capability.entity_id || capability.id_hint || "").replace(/^entity_/, "").replace(/^cap_[a-z]+_/, "")
133
+ );
134
+ const terms = new Set([
135
+ entityStem,
136
+ pluralizeCandidateTerm(entityStem),
137
+ ...String(capability.label || "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean),
138
+ ...String(capability.id_hint || "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean)
139
+ ]);
140
+ return { capability, terms: [...terms].filter(Boolean) };
141
+ });
142
+ }
143
+
144
+ function inferRelatedCapabilities(text, matchers) {
145
+ const normalized = ` ${String(text || "").toLowerCase().replace(/[^a-z0-9]+/g, " ")} `;
146
+ return matchers
147
+ .filter(({ terms }) => terms.some((term) => term && normalized.includes(` ${term} `)))
148
+ .map(({ capability }) => capability.id_hint)
149
+ .sort();
150
+ }
151
+
152
+ function scenarioAction(title) {
153
+ const normalized = String(title || "").toLowerCase();
154
+ for (const [needle, action] of [
155
+ ["create", "create"],
156
+ ["add ", "create"],
157
+ ["new ", "create"],
158
+ ["list", "list"],
159
+ ["index", "list"],
160
+ ["get ", "get"],
161
+ ["view", "get"],
162
+ ["show", "get"],
163
+ ["update", "update"],
164
+ ["edit", "update"],
165
+ ["delete", "delete"],
166
+ ["remove", "delete"],
167
+ ["approve", "approve"],
168
+ ["reject", "reject"],
169
+ ["close", "close"],
170
+ ["complete", "complete"],
171
+ ["export", "export"],
172
+ ["download", "download"],
173
+ ["revision", "request_revision"]
174
+ ]) {
175
+ if (normalized.includes(needle)) return action;
176
+ }
177
+ return null;
178
+ }
179
+
180
+ function refineScenarioCapabilities(title, capabilityIds) {
181
+ const action = scenarioAction(title);
182
+ if (!action) {
183
+ return capabilityIds;
184
+ }
185
+ const matching = capabilityIds.filter((id) => id.startsWith(`cap_${action}_`));
186
+ return matching.length > 0 ? matching : capabilityIds;
187
+ }
188
+
189
+ export const genericVerificationExtractor = {
190
+ id: "verification.generic",
191
+ track: "verification",
192
+ detect(context) {
193
+ const { frameworks } = frameworkInfo(context);
194
+ const testFiles = findTestFiles(context);
195
+ if (frameworks.length === 0 && testFiles.length === 0) {
196
+ return { score: 0, reasons: [] };
197
+ }
198
+ return {
199
+ score: Math.min(95, 40 + frameworks.length * 10 + Math.min(testFiles.length, 8) * 4),
200
+ reasons: [
201
+ frameworks.length > 0 ? `detected frameworks: ${frameworks.join(", ")}` : null,
202
+ testFiles.length > 0 ? `detected ${testFiles.length} test file(s)` : null
203
+ ].filter(Boolean)
204
+ };
205
+ },
206
+ extract(context) {
207
+ const { frameworks, scripts } = frameworkInfo(context);
208
+ const files = findTestFiles(context);
209
+ const apiImport = context.priorResults.api || { candidates: { capabilities: [] } };
210
+ const matchers = capabilityMatchers(apiImport.candidates);
211
+ const findings = [];
212
+ const candidates = {
213
+ verifications: [],
214
+ scenarios: [],
215
+ frameworks,
216
+ scripts
217
+ };
218
+
219
+ for (const filePath of files) {
220
+ const source = readTextIfExists(filePath) || "";
221
+ const relativePath = normalizeImportRelativePath(context.paths, filePath);
222
+ const framework = inferFramework(filePath, frameworks);
223
+ const method = inferMethod(filePath, framework);
224
+ const titles = extractScenarioTitles(source, filePath);
225
+ const fileLabel = titleCase(pathStem(filePath));
226
+ const relatedCapabilities = inferRelatedCapabilities(`${relativePath}\n${source}`, matchers);
227
+ const verificationId = `verification_${idHintify(`${framework}_${relativePath}`)}`;
228
+ const scenarioIds = [];
229
+
230
+ for (const [index, title] of (titles.length > 0 ? titles : [fileLabel]).entries()) {
231
+ const scenarioId = `verification_scenario_${idHintify(`${framework}_${relativePath}_${title}`) || `${verificationId}_${index + 1}`}`;
232
+ const scenarioCapabilities = refineScenarioCapabilities(title, relatedCapabilities);
233
+ scenarioIds.push(scenarioId);
234
+ candidates.scenarios.push(makeCandidateRecord({
235
+ kind: "verification_scenario",
236
+ idHint: scenarioId,
237
+ label: title,
238
+ confidence: scenarioCapabilities.length > 0 ? "medium" : "low",
239
+ sourceKind: "test_suite",
240
+ provenance: relativePath,
241
+ track: "verification",
242
+ framework,
243
+ method,
244
+ file_path: relativePath,
245
+ verification_id: verificationId,
246
+ related_capabilities: scenarioCapabilities
247
+ }));
248
+ }
249
+
250
+ candidates.verifications.push(makeCandidateRecord({
251
+ kind: "verification",
252
+ idHint: verificationId,
253
+ label: fileLabel,
254
+ confidence: relatedCapabilities.length > 0 ? "medium" : "low",
255
+ sourceKind: "test_suite",
256
+ provenance: relativePath,
257
+ track: "verification",
258
+ framework,
259
+ method,
260
+ file_path: relativePath,
261
+ scenario_ids: scenarioIds,
262
+ related_capabilities: relatedCapabilities,
263
+ runner_scripts: scripts
264
+ .filter((script) => /(test|playwright|vitest|jest|cypress|pytest|rspec)/i.test(script.name) || /(playwright|vitest|jest|cypress|pytest|rspec)/i.test(script.command))
265
+ .map((script) => script.name)
266
+ }));
267
+ }
268
+
269
+ candidates.verifications = dedupeCandidateRecords(candidates.verifications, (record) => record.id_hint);
270
+ candidates.scenarios = dedupeCandidateRecords(candidates.scenarios, (record) => record.id_hint);
271
+
272
+ findings.push({
273
+ kind: "verification_import",
274
+ framework_count: frameworks.length,
275
+ test_file_count: files.length,
276
+ verification_count: candidates.verifications.length,
277
+ scenario_count: candidates.scenarios.length
278
+ });
279
+
280
+ return { findings, candidates };
281
+ }
282
+ };
283
+
284
+ function pathStem(filePath) {
285
+ const base = String(filePath || "").replaceAll("\\", "/").split("/").pop() || "verification";
286
+ return base
287
+ .replace(/\.(test|spec)\.[^.]+$/i, "")
288
+ .replace(/\.[^.]+$/i, "")
289
+ .replace(/[_-]+/g, " ");
290
+ }
@@ -0,0 +1,137 @@
1
+ import { dedupeCandidateRecords, idHintify, inferCapabilityEntityId, makeCandidateRecord, titleCase } from "../../core/shared.js";
2
+
3
+ function findEntityStatusFields(entity, enumCandidatesById) {
4
+ return (entity?.fields || []).filter((field) =>
5
+ ["status", "state"].includes(field.name) && enumCandidatesById.has(idHintify(field.field_type))
6
+ );
7
+ }
8
+
9
+ function targetStateForCapability(capability, knownStates) {
10
+ if (capability.target_state) {
11
+ const explicitState = idHintify(capability.target_state);
12
+ if (knownStates.length === 0 || knownStates.includes(explicitState)) return explicitState;
13
+ }
14
+ const id = capability.id_hint || "";
15
+ const method = String(capability.endpoint?.method || "").toUpperCase();
16
+ for (const [needle, state] of [
17
+ ["sign_in", "authenticated"],
18
+ ["login", "authenticated"],
19
+ ["authenticate", "authenticated"],
20
+ ["register", "registered"],
21
+ ["approve", "approved"],
22
+ ["reject", "rejected"],
23
+ ["revision", "needs_revision"],
24
+ ["request_revision", "needs_revision"],
25
+ ["submit", "submitted"],
26
+ ["close", "closed"],
27
+ ["complete", "completed"],
28
+ ["archive", "archived"],
29
+ ["delete", "deleted"],
30
+ [method === "POST" && id.startsWith("cap_create_") ? "create" : "", knownStates[0] || "created"]
31
+ ].filter(([needle]) => needle)) {
32
+ if (id.includes(needle)) {
33
+ const canonicalState = idHintify(state);
34
+ if (knownStates.length === 0 || knownStates.includes(canonicalState)) return canonicalState;
35
+ }
36
+ }
37
+ return null;
38
+ }
39
+
40
+ export const genericWorkflowExtractor = {
41
+ id: "workflows.generic",
42
+ track: "workflows",
43
+ detect() {
44
+ return { score: 50, reasons: ["Workflow inference runs over imported DB/API evidence"] };
45
+ },
46
+ extract(context) {
47
+ const dbImport = context.priorResults.db || { findings: [], candidates: { entities: [], enums: [] } };
48
+ const apiImport = context.priorResults.api || { findings: [], candidates: { capabilities: [] } };
49
+ const docScan = context.scanDocsSummary ? context.scanDocsSummary() : { candidate_docs: [] };
50
+ const findings = [];
51
+ const candidates = { workflows: [], workflow_states: [], workflow_transitions: [] };
52
+ const enumCandidatesById = new Map((dbImport.candidates.enums || []).map((entry) => [entry.id_hint, entry]));
53
+ const entityCandidatesById = new Map((dbImport.candidates.entities || []).map((entry) => [entry.id_hint, entry]));
54
+ const workflowDocs = (docScan.candidate_docs || []).filter((doc) => doc.kind === "workflow");
55
+ const workflows = new Map();
56
+
57
+ for (const capability of apiImport.candidates.capabilities || []) {
58
+ const entityId = inferCapabilityEntityId(capability);
59
+ const workflowId = `workflow_${entityId.replace(/^entity_/, "")}`;
60
+ const capabilityActors =
61
+ capability.auth_hint === "secured" ? ["user"] :
62
+ capability.auth_hint === "public" ? ["anonymous"] :
63
+ [];
64
+ if (!workflows.has(workflowId)) {
65
+ const entity = entityCandidatesById.get(entityId);
66
+ const statusFields = findEntityStatusFields(entity, enumCandidatesById);
67
+ const states = statusFields.flatMap((field) => enumCandidatesById.get(idHintify(field.field_type))?.values || []).map(idHintify);
68
+ workflows.set(workflowId, {
69
+ workflow: makeCandidateRecord({
70
+ kind: "workflow",
71
+ idHint: workflowId,
72
+ label: `${titleCase(entityId.replace(/^entity_/, ""))} Workflow`,
73
+ confidence: "medium",
74
+ sourceKind: "generated_artifact",
75
+ provenance: capability.provenance || [],
76
+ track: "workflows",
77
+ entity_id: entityId,
78
+ actor_hints: capabilityActors,
79
+ related_capabilities: []
80
+ }),
81
+ states: states.map((state) => makeCandidateRecord({
82
+ kind: "workflow_state",
83
+ idHint: `${workflowId}_${state}`,
84
+ label: titleCase(state),
85
+ confidence: "medium",
86
+ sourceKind: "schema",
87
+ provenance: entity?.provenance || capability.provenance || [],
88
+ track: "workflows",
89
+ workflow_id: workflowId,
90
+ entity_id: entityId,
91
+ state_id: state
92
+ })),
93
+ transitions: []
94
+ });
95
+ }
96
+ const workflow = workflows.get(workflowId);
97
+ workflow.workflow.related_capabilities.push(capability.id_hint);
98
+ workflow.workflow.actor_hints = [...new Set([...(workflow.workflow.actor_hints || []), ...capabilityActors])].sort();
99
+ const knownStates = workflow.states.map((entry) => entry.state_id);
100
+ const targetState = targetStateForCapability(capability, knownStates);
101
+ if (targetState) {
102
+ workflow.transitions.push(makeCandidateRecord({
103
+ kind: "workflow_transition",
104
+ idHint: `${workflowId}_${idHintify(capability.id_hint)}`,
105
+ label: titleCase(capability.id_hint.replace(/^cap_/, "")),
106
+ confidence: "low",
107
+ sourceKind: capability.source_kind || "generated_artifact",
108
+ provenance: capability.provenance || [],
109
+ track: "workflows",
110
+ workflow_id: workflowId,
111
+ entity_id: entityId,
112
+ capability_id: capability.id_hint,
113
+ actor_hints: capabilityActors,
114
+ to_state: targetState
115
+ }));
116
+ }
117
+ }
118
+
119
+ for (const workflow of workflows.values()) {
120
+ workflow.workflow.related_capabilities = [...new Set(workflow.workflow.related_capabilities)].sort();
121
+ candidates.workflows.push(workflow.workflow);
122
+ candidates.workflow_states.push(...workflow.states);
123
+ candidates.workflow_transitions.push(...workflow.transitions);
124
+ }
125
+
126
+ findings.push({
127
+ kind: "workflow_inference",
128
+ workflow_count: candidates.workflows.length,
129
+ workflow_doc_signals: workflowDocs.map((doc) => doc.id)
130
+ });
131
+
132
+ candidates.workflows = dedupeCandidateRecords(candidates.workflows, (record) => record.id_hint);
133
+ candidates.workflow_states = dedupeCandidateRecords(candidates.workflow_states, (record) => record.id_hint);
134
+ candidates.workflow_transitions = dedupeCandidateRecords(candidates.workflow_transitions, (record) => record.id_hint);
135
+ return { findings, candidates };
136
+ }
137
+ };
@@ -0,0 +1,7 @@
1
+ import { runImportApp, parseImportTracks } from "./core/runner.js";
2
+ import { createImportContext, normalizeWorkspacePaths } from "./core/context.js";
3
+
4
+ export { createImportContext, normalizeWorkspacePaths, parseImportTracks };
5
+ export function runImportAppWorkflow(inputPath, options = {}) {
6
+ return runImportApp(inputPath, options);
7
+ }
@@ -0,0 +1,158 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { listFilesRecursive, relativeTo } from "./core/shared.js";
6
+
7
+ export const TOPOGRAM_IMPORT_FILE = ".topogram-import.json";
8
+
9
+ function fileHash(filePath) {
10
+ const bytes = fs.readFileSync(filePath);
11
+ return {
12
+ sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
13
+ size: bytes.length
14
+ };
15
+ }
16
+
17
+ function isSameOrInside(parent, child) {
18
+ const relative = path.relative(path.resolve(parent), path.resolve(child));
19
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
20
+ }
21
+
22
+ function normalizeExcludeRoots(sourceRoot, excludeRoots = []) {
23
+ return excludeRoots
24
+ .filter(Boolean)
25
+ .map((item) => path.resolve(item))
26
+ .filter((item) => isSameOrInside(sourceRoot, item));
27
+ }
28
+
29
+ export function collectImportSourceFileRecords(sourceRoot, options = {}) {
30
+ const resolvedSourceRoot = path.resolve(sourceRoot);
31
+ const excludeRoots = normalizeExcludeRoots(resolvedSourceRoot, options.excludeRoots || []);
32
+ return listFilesRecursive(resolvedSourceRoot, (filePath) => {
33
+ return !excludeRoots.some((excludeRoot) => isSameOrInside(excludeRoot, filePath));
34
+ }).map((filePath) => ({
35
+ path: relativeTo(resolvedSourceRoot, filePath),
36
+ ...fileHash(filePath)
37
+ }));
38
+ }
39
+
40
+ export function writeTopogramImportRecord(projectRoot, input) {
41
+ const resolvedProjectRoot = path.resolve(projectRoot);
42
+ const timestamp = input.timestamp || new Date().toISOString();
43
+ const record = {
44
+ version: "0.1",
45
+ kind: "brownfield-import",
46
+ importedAt: input.importedAt || timestamp,
47
+ ...(input.refreshedAt ? { refreshedAt: input.refreshedAt } : {}),
48
+ source: {
49
+ path: path.resolve(input.sourceRoot),
50
+ hashAlgorithm: "sha256",
51
+ ignoredRoots: (input.ignoredRoots || []).map((item) => path.resolve(item))
52
+ },
53
+ import: {
54
+ tracks: input.tracks || [],
55
+ findingsCount: input.findingsCount || 0,
56
+ candidateCounts: input.candidateCounts || {}
57
+ },
58
+ ownership: {
59
+ importedArtifacts: "project-owned",
60
+ note: "Topogram artifacts created by import are editable after import. Source hashes record the brownfield app evidence trusted at import time."
61
+ },
62
+ ...(input.refresh ? { refresh: input.refresh } : {}),
63
+ files: input.files || []
64
+ };
65
+ const importPath = path.join(resolvedProjectRoot, TOPOGRAM_IMPORT_FILE);
66
+ fs.writeFileSync(importPath, `${JSON.stringify(record, null, 2)}\n`, "utf8");
67
+ return { path: importPath, record };
68
+ }
69
+
70
+ export function buildTopogramImportStatus(projectRoot) {
71
+ const resolvedProjectRoot = path.resolve(projectRoot);
72
+ const importPath = path.join(resolvedProjectRoot, TOPOGRAM_IMPORT_FILE);
73
+ if (!fs.existsSync(importPath)) {
74
+ return {
75
+ ok: false,
76
+ exists: false,
77
+ path: importPath,
78
+ status: "missing",
79
+ source: null,
80
+ content: { changed: [], added: [], removed: [] },
81
+ diagnostics: [{
82
+ code: "topogram_import_missing",
83
+ severity: "error",
84
+ message: `${TOPOGRAM_IMPORT_FILE} was not found. This workspace does not have brownfield import provenance.`,
85
+ path: importPath,
86
+ suggestedFix: "Run `topogram import <app-path> --out <target>` to create an imported Topogram workspace."
87
+ }],
88
+ errors: [`${TOPOGRAM_IMPORT_FILE} was not found.`]
89
+ };
90
+ }
91
+
92
+ const source = JSON.parse(fs.readFileSync(importPath, "utf8"));
93
+ const sourceRoot = path.resolve(source.source?.path || "");
94
+ if (!sourceRoot || !fs.existsSync(sourceRoot) || !fs.statSync(sourceRoot).isDirectory()) {
95
+ const message = `Imported source path was not found: ${source.source?.path || "unknown"}`;
96
+ return {
97
+ ok: false,
98
+ exists: true,
99
+ path: importPath,
100
+ status: "missing-source",
101
+ source,
102
+ content: { changed: [], added: [], removed: [] },
103
+ diagnostics: [{
104
+ code: "topogram_import_source_missing",
105
+ severity: "error",
106
+ message,
107
+ path: source.source?.path || null,
108
+ suggestedFix: "Restore the imported source path or rerun import from the current brownfield app location."
109
+ }],
110
+ errors: [message]
111
+ };
112
+ }
113
+
114
+ const trustedFiles = Array.isArray(source.files) ? source.files : [];
115
+ const trustedByPath = new Map(trustedFiles.map((file) => [String(file.path), file]));
116
+ const currentFiles = collectImportSourceFileRecords(sourceRoot, {
117
+ excludeRoots: source.source?.ignoredRoots || [resolvedProjectRoot]
118
+ });
119
+ const currentByPath = new Map(currentFiles.map((file) => [file.path, file]));
120
+ const changed = [];
121
+ const added = [];
122
+ const removed = [];
123
+ for (const [filePath, current] of currentByPath) {
124
+ const trusted = trustedByPath.get(filePath);
125
+ if (!trusted) {
126
+ added.push(filePath);
127
+ } else if (trusted.sha256 !== current.sha256 || trusted.size !== current.size) {
128
+ changed.push(filePath);
129
+ }
130
+ }
131
+ for (const filePath of trustedByPath.keys()) {
132
+ if (!currentByPath.has(filePath)) {
133
+ removed.push(filePath);
134
+ }
135
+ }
136
+ const content = {
137
+ changed: changed.sort((a, b) => a.localeCompare(b)),
138
+ added: added.sort((a, b) => a.localeCompare(b)),
139
+ removed: removed.sort((a, b) => a.localeCompare(b))
140
+ };
141
+ const clean = content.changed.length === 0 && content.added.length === 0 && content.removed.length === 0;
142
+ return {
143
+ ok: clean,
144
+ exists: true,
145
+ path: importPath,
146
+ status: clean ? "clean" : "changed",
147
+ source,
148
+ content,
149
+ diagnostics: clean ? [] : [{
150
+ code: "topogram_import_source_changed",
151
+ severity: "error",
152
+ message: "Imported source files changed since they were trusted for this import.",
153
+ path: sourceRoot,
154
+ suggestedFix: "Review the source changes. If they should drive Topogram changes, rerun import or update the Topogram artifacts manually."
155
+ }],
156
+ errors: clean ? [] : ["Imported source files changed since import."]
157
+ };
158
+ }