@topogram/cli 0.3.64 → 0.3.66

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +716 -0
  3. package/src/adoption/plan.js +12 -703
  4. package/src/adoption/reporting.js +1 -1
  5. package/src/agent-brief.js +7 -21
  6. package/src/agent-ops/query-builders/auth.js +375 -0
  7. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  8. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  9. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  10. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  12. package/src/agent-ops/query-builders/change-risk.js +25 -0
  13. package/src/agent-ops/query-builders/common.js +149 -0
  14. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  15. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  16. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  17. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  18. package/src/agent-ops/query-builders/work-packets.js +417 -0
  19. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  20. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  21. package/src/agent-ops/query-builders/workflow-presets-core.js +677 -0
  22. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  23. package/src/agent-ops/query-builders.d.ts +26 -26
  24. package/src/agent-ops/query-builders.js +42 -5021
  25. package/src/archive/jsonl.js +2 -2
  26. package/src/archive/resolver-bridge.js +1 -1
  27. package/src/archive/unarchive.js +2 -1
  28. package/src/catalog/constants.js +10 -0
  29. package/src/catalog/copy.js +65 -0
  30. package/src/catalog/diagnostics.js +15 -0
  31. package/src/catalog/entries.js +42 -0
  32. package/src/catalog/files.js +67 -0
  33. package/src/catalog/provenance.js +123 -0
  34. package/src/catalog/source.js +150 -0
  35. package/src/catalog/validation.js +252 -0
  36. package/src/catalog.d.ts +2 -0
  37. package/src/catalog.js +18 -746
  38. package/src/cli/command-parsers/project.js +3 -0
  39. package/src/cli/command-parsers/shared.js +1 -1
  40. package/src/cli/commands/agent.js +2 -2
  41. package/src/cli/commands/catalog/check.js +31 -0
  42. package/src/cli/commands/catalog/copy.js +59 -0
  43. package/src/cli/commands/catalog/doctor.js +248 -0
  44. package/src/cli/commands/catalog/help.js +21 -0
  45. package/src/cli/commands/catalog/list.js +52 -0
  46. package/src/cli/commands/catalog/runner.js +92 -0
  47. package/src/cli/commands/catalog/shared.js +17 -0
  48. package/src/cli/commands/catalog/show.js +134 -0
  49. package/src/cli/commands/catalog.js +30 -615
  50. package/src/cli/commands/check.js +3 -3
  51. package/src/cli/commands/doctor.js +2 -9
  52. package/src/cli/commands/generator-policy/package-info.js +162 -0
  53. package/src/cli/commands/generator-policy/payloads.js +372 -0
  54. package/src/cli/commands/generator-policy/printers.js +159 -0
  55. package/src/cli/commands/generator-policy/runner.js +81 -0
  56. package/src/cli/commands/generator-policy/shared.js +39 -0
  57. package/src/cli/commands/generator-policy.js +15 -783
  58. package/src/cli/commands/import/adopt.js +170 -0
  59. package/src/cli/commands/import/check.js +91 -0
  60. package/src/cli/commands/import/diff.js +84 -0
  61. package/src/cli/commands/import/help.js +47 -0
  62. package/src/cli/commands/import/paths.js +269 -0
  63. package/src/cli/commands/import/plan.js +292 -0
  64. package/src/cli/commands/import/refresh.js +471 -0
  65. package/src/cli/commands/import/status-history.js +196 -0
  66. package/src/cli/commands/import/workspace.js +233 -0
  67. package/src/cli/commands/import.js +33 -1732
  68. package/src/cli/commands/migrate.js +153 -0
  69. package/src/cli/commands/package/constants.js +17 -0
  70. package/src/cli/commands/package/doctor.js +240 -0
  71. package/src/cli/commands/package/help.js +27 -0
  72. package/src/cli/commands/package/lockfile.js +135 -0
  73. package/src/cli/commands/package/npm.js +97 -0
  74. package/src/cli/commands/package/reporting.js +35 -0
  75. package/src/cli/commands/package/runner.js +33 -0
  76. package/src/cli/commands/package/shared.js +9 -0
  77. package/src/cli/commands/package/update-cli.js +252 -0
  78. package/src/cli/commands/package/versions.js +35 -0
  79. package/src/cli/commands/package.js +29 -813
  80. package/src/cli/commands/query/change-plan.js +68 -0
  81. package/src/cli/commands/query/definitions.js +202 -0
  82. package/src/cli/commands/query/import-adopt.js +121 -0
  83. package/src/cli/commands/query/runner/artifacts.js +102 -0
  84. package/src/cli/commands/query/runner/boundaries.js +211 -0
  85. package/src/cli/commands/query/runner/change.js +182 -0
  86. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  87. package/src/cli/commands/query/runner/index.js +31 -0
  88. package/src/cli/commands/query/runner/output.js +12 -0
  89. package/src/cli/commands/query/runner/workflow.js +241 -0
  90. package/src/cli/commands/query/runner.js +3 -0
  91. package/src/cli/commands/query/workflow-context.js +5 -0
  92. package/src/cli/commands/query/workspace.js +270 -0
  93. package/src/cli/commands/query.js +9 -1300
  94. package/src/cli/commands/source.js +3 -12
  95. package/src/cli/commands/template/baseline.js +100 -0
  96. package/src/cli/commands/template/check.js +467 -0
  97. package/src/cli/commands/template/constants.js +8 -0
  98. package/src/cli/commands/template/diagnostics.js +26 -0
  99. package/src/cli/commands/template/help.js +28 -0
  100. package/src/cli/commands/template/lifecycle.js +404 -0
  101. package/src/cli/commands/template/list-show.js +287 -0
  102. package/src/cli/commands/template/policy.js +422 -0
  103. package/src/cli/commands/template/shared.js +127 -0
  104. package/src/cli/commands/template/updates.js +352 -0
  105. package/src/cli/commands/template-runner.js +6 -6
  106. package/src/cli/commands/template.js +41 -2143
  107. package/src/cli/commands/trust.js +1 -1
  108. package/src/cli/commands/workflow.js +6 -1
  109. package/src/cli/dispatcher.js +6 -1
  110. package/src/cli/help.js +15 -14
  111. package/src/cli/migration-guidance.js +1 -1
  112. package/src/cli/output-safety.js +2 -1
  113. package/src/cli/path-normalization.js +3 -13
  114. package/src/generator/api/contracts.js +497 -0
  115. package/src/generator/api/metadata.js +221 -0
  116. package/src/generator/api/openapi.js +559 -0
  117. package/src/generator/api/schema.js +124 -0
  118. package/src/generator/api/types.d.ts +98 -0
  119. package/src/generator/api.js +3 -1195
  120. package/src/generator/context/domain-page.js +1 -1
  121. package/src/generator/context/shared/domain-sdlc.js +282 -0
  122. package/src/generator/context/shared/maintained-boundary.js +665 -0
  123. package/src/generator/context/shared/metrics.js +85 -0
  124. package/src/generator/context/shared/primitives.js +64 -0
  125. package/src/generator/context/shared/relationships.js +453 -0
  126. package/src/generator/context/shared/summaries.js +263 -0
  127. package/src/generator/context/shared/types.d.ts +207 -0
  128. package/src/generator/context/shared.d.ts +42 -0
  129. package/src/generator/context/shared.js +80 -1390
  130. package/src/generator/context/slice/core.js +397 -0
  131. package/src/generator/context/slice/sdlc.js +417 -0
  132. package/src/generator/context/slice/ui-packets.js +183 -0
  133. package/src/generator/context/slice.js +2 -859
  134. package/src/generator/context/task-mode.js +2 -2
  135. package/src/generator/registry/index.js +507 -0
  136. package/src/generator/registry.js +18 -504
  137. package/src/generator/runtime/environment/index.js +666 -0
  138. package/src/generator/runtime/environment.js +4 -666
  139. package/src/generator/runtime/runtime-check/index.js +554 -0
  140. package/src/generator/runtime/runtime-check.js +4 -554
  141. package/src/generator/runtime/shared/index.js +572 -0
  142. package/src/generator/runtime/shared.js +19 -570
  143. package/src/generator/sdlc/doc-page.js +1 -1
  144. package/src/generator/shared.d.ts +2 -0
  145. package/src/generator/surfaces/databases/lifecycle-shared.js +1 -1
  146. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +1 -1
  147. package/src/generator/surfaces/shared.d.ts +3 -0
  148. package/src/generator/widget-conformance/behavior-report.js +258 -0
  149. package/src/generator/widget-conformance/checks.js +371 -0
  150. package/src/generator/widget-conformance/projection-context.js +200 -0
  151. package/src/generator/widget-conformance/report.js +166 -0
  152. package/src/generator/widget-conformance/types.d.ts +121 -0
  153. package/src/generator/widget-conformance.js +3 -824
  154. package/src/import/core/context.d.ts +3 -0
  155. package/src/import/core/context.js +5 -7
  156. package/src/import/core/contracts.d.ts +1 -0
  157. package/src/import/core/registry.d.ts +4 -0
  158. package/src/import/core/runner/candidates.js +337 -0
  159. package/src/import/core/runner/options.js +22 -0
  160. package/src/import/core/runner/reports.js +51 -0
  161. package/src/import/core/runner/run.js +79 -0
  162. package/src/import/core/runner/tracks.js +150 -0
  163. package/src/import/core/runner/ui-drafts.js +393 -0
  164. package/src/import/core/runner.js +3 -698
  165. package/src/import/core/shared/api-routes.js +221 -0
  166. package/src/import/core/shared/candidates.js +97 -0
  167. package/src/import/core/shared/files.js +177 -0
  168. package/src/import/core/shared/next-app.js +389 -0
  169. package/src/import/core/shared/types.d.ts +51 -0
  170. package/src/import/core/shared/ui-routes.js +230 -0
  171. package/src/import/core/shared.js +60 -861
  172. package/src/new-project/constants.js +128 -0
  173. package/src/new-project/create.js +90 -0
  174. package/src/new-project/json.js +28 -0
  175. package/src/new-project/metadata.js +96 -0
  176. package/src/new-project/package-spec.js +161 -0
  177. package/src/new-project/project-files.js +351 -0
  178. package/src/new-project/template-policy.js +269 -0
  179. package/src/new-project/template-resolution.js +370 -0
  180. package/src/new-project/template-snapshots.js +442 -0
  181. package/src/new-project/template-updates.js +512 -0
  182. package/src/new-project/types.d.ts +83 -0
  183. package/src/new-project.js +6 -2277
  184. package/src/parser.d.ts +87 -1
  185. package/src/parser.js +118 -0
  186. package/src/policy/review-boundaries.d.ts +15 -0
  187. package/src/project-config/index.js +591 -0
  188. package/src/project-config.js +19 -561
  189. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  190. package/src/resolver/enrich/bug.js +2 -0
  191. package/src/resolver/enrich/pitch.js +2 -0
  192. package/src/resolver/enrich/requirement.js +2 -0
  193. package/src/resolver/enrich/task.js +2 -0
  194. package/src/resolver/index.js +19 -2089
  195. package/src/resolver/normalize.js +384 -1
  196. package/src/resolver/plans.js +168 -0
  197. package/src/resolver/projections-api.js +494 -0
  198. package/src/resolver/projections-db.js +133 -0
  199. package/src/resolver/projections-ui.js +317 -0
  200. package/src/resolver/shapes.js +251 -0
  201. package/src/resolver/shared.js +278 -0
  202. package/src/resolver/widgets.js +132 -0
  203. package/src/sdlc/adopt.js +6 -5
  204. package/src/sdlc/paths.js +3 -5
  205. package/src/sdlc/scaffold.js +2 -1
  206. package/src/template-trust/constants.js +62 -0
  207. package/src/template-trust/content.js +258 -0
  208. package/src/template-trust/diff.js +92 -0
  209. package/src/template-trust/policy.js +61 -0
  210. package/src/template-trust/record.js +90 -0
  211. package/src/template-trust/status.js +182 -0
  212. package/src/template-trust.js +24 -687
  213. package/src/text-helpers.d.ts +1 -0
  214. package/src/topogram-types.d.ts +69 -0
  215. package/src/validator/common.js +488 -0
  216. package/src/validator/data-model.js +237 -0
  217. package/src/validator/docs.js +167 -0
  218. package/src/validator/expressions.js +146 -1
  219. package/src/validator/index.d.ts +23 -0
  220. package/src/validator/index.js +32 -3585
  221. package/src/validator/kinds.d.ts +41 -0
  222. package/src/validator/kinds.js +2 -0
  223. package/src/validator/model-helpers.js +46 -0
  224. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  225. package/src/validator/per-kind/bug.js +6 -0
  226. package/src/validator/per-kind/domain.js +15 -2
  227. package/src/validator/per-kind/pitch.js +7 -0
  228. package/src/validator/per-kind/requirement.js +5 -0
  229. package/src/validator/per-kind/task.js +7 -0
  230. package/src/validator/per-kind/widget.js +14 -0
  231. package/src/validator/projections/api-http-async.js +410 -0
  232. package/src/validator/projections/api-http-authz.js +88 -0
  233. package/src/validator/projections/api-http-core.js +205 -0
  234. package/src/validator/projections/api-http-policies.js +339 -0
  235. package/src/validator/projections/api-http-responses.js +233 -0
  236. package/src/validator/projections/api-http.js +44 -0
  237. package/src/validator/projections/db.js +353 -0
  238. package/src/validator/projections/generator-defaults.js +45 -0
  239. package/src/validator/projections/helpers.js +87 -0
  240. package/src/validator/projections/ui-helpers.js +214 -0
  241. package/src/validator/projections/ui-navigation.js +344 -0
  242. package/src/validator/projections/ui-structure.js +364 -0
  243. package/src/validator/projections/ui-widgets.js +493 -0
  244. package/src/validator/projections/ui.js +46 -0
  245. package/src/validator/registry.js +48 -1
  246. package/src/validator/utils.d.ts +20 -0
  247. package/src/validator/utils.js +115 -12
  248. package/src/widget-behavior.d.ts +1 -0
  249. package/src/workflows/import-app/api/collect.js +221 -0
  250. package/src/workflows/import-app/api/openapi.js +257 -0
  251. package/src/workflows/import-app/api/routes.js +327 -0
  252. package/src/workflows/import-app/api/sources.js +22 -0
  253. package/src/workflows/import-app/api.js +2 -797
  254. package/src/workflows/reconcile/adoption-plan/build.js +212 -0
  255. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  256. package/src/workflows/reconcile/adoption-plan/outputs.js +153 -0
  257. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  258. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  259. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  260. package/src/workflows/reconcile/adoption-plan.js +30 -740
  261. package/src/workflows/reconcile/auth/closures.js +115 -0
  262. package/src/workflows/reconcile/auth/formatters.js +142 -0
  263. package/src/workflows/reconcile/auth/inference.js +330 -0
  264. package/src/workflows/reconcile/auth/roles.js +122 -0
  265. package/src/workflows/reconcile/auth.js +35 -690
  266. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  267. package/src/workflows/reconcile/bundle-core.js +12 -598
  268. package/src/workflows/reconcile/candidate-model.js +18 -2
  269. package/src/workflows/reconcile/canonical-surface.js +1 -1
  270. package/src/workflows/reconcile/impacts/adoption-plan.js +196 -0
  271. package/src/workflows/reconcile/impacts/indexes.js +105 -0
  272. package/src/workflows/reconcile/impacts/patches.js +252 -0
  273. package/src/workflows/reconcile/impacts/reports.js +80 -0
  274. package/src/workflows/reconcile/impacts.js +14 -623
  275. package/src/workflows/reconcile/renderers.js +41 -6
  276. package/src/workflows/shared.js +5 -11
  277. package/src/workspace-docs.d.ts +29 -0
  278. package/src/workspace-paths.js +328 -0
@@ -0,0 +1,389 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { relativeTo } from "../../../path-helpers.js";
5
+ import { canonicalCandidateTerm, idHintify } from "./candidates.js";
6
+ import { listFilesRecursive, readTextIfExists } from "./files.js";
7
+ import {
8
+ inferApiCapabilityIdFromOperation,
9
+ inferRouteCapabilityId,
10
+ normalizeEndpointPathForMatch,
11
+ normalizeOpenApiPath
12
+ } from "./api-routes.js";
13
+ import { entityIdForRoute, screenIdForRoute, screenKindForRoute, uiCapabilityHintsForRoute } from "./ui-routes.js";
14
+
15
+ /**
16
+ * @param {string} rootDir
17
+ * @returns {any}
18
+ */
19
+ export function inferNextAppRoutes(rootDir) {
20
+ const appDir = path.join(rootDir, "app");
21
+ if (!fs.existsSync(appDir)) return [];
22
+ const routeFiles = listFilesRecursive(
23
+ appDir,
24
+ /** @param {any} child */ (child) => /\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) || /\/route\.(tsx|ts|jsx|js)$/.test(child)
25
+ );
26
+ const routes = [];
27
+ for (const filePath of routeFiles) {
28
+ const relative = relativeTo(appDir, filePath);
29
+ const isPage = /\/page\.(tsx|ts|jsx|js|mdx)$/.test(`/${relative}`) || /^page\.(tsx|ts|jsx|js|mdx)$/.test(relative);
30
+ const normalizedPath = `/${relative}`
31
+ .replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
32
+ .replace(/\/route\.(tsx|ts|jsx|js)$/, "")
33
+ .replace(/\[(\.\.\.)?([^\]]+)\]/g, /** @param {any} _m @param {any} catchAll @param {any} name */ (_m, catchAll, name) => catchAll ? `:${name}*` : `:${name}`)
34
+ .replace(/\/index$/, "")
35
+ .replace(/^\/$/, "/");
36
+ routes.push({
37
+ path: normalizedPath === "" ? "/" : normalizedPath,
38
+ kind: isPage ? "page" : "route",
39
+ file: filePath
40
+ });
41
+ }
42
+ return routes.sort(/** @param {any} a @param {any} b */ (a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
43
+ }
44
+
45
+ /**
46
+ * @param {string} routePath
47
+ * @returns {any}
48
+ */
49
+ export function nextScreenKindForRoute(routePath) {
50
+ const normalized = String(routePath || "");
51
+ if (/\/(login|register|setup)$/.test(normalized)) return "flow";
52
+ return screenKindForRoute(routePath);
53
+ }
54
+
55
+ /**
56
+ * @param {string} routePath
57
+ * @returns {any}
58
+ */
59
+ export function nextScreenIdForRoute(routePath) {
60
+ const normalized = String(routePath || "");
61
+ if (normalized === "/") return "home";
62
+ if (/\/login$/.test(normalized)) return "login";
63
+ if (/\/register$/.test(normalized)) return "register";
64
+ if (/\/setup$/.test(normalized)) return "setup";
65
+ return screenIdForRoute(routePath);
66
+ }
67
+
68
+ /**
69
+ * @param {string} routePath
70
+ * @returns {any}
71
+ */
72
+ export function entityIdForNextRoute(routePath) {
73
+ const normalized = String(routePath || "");
74
+ if (/^\/posts(\/|$)/.test(normalized)) return "entity_post";
75
+ if (/^\/users(\/|$)/.test(normalized)) return "entity_user";
76
+ return null;
77
+ }
78
+
79
+ /**
80
+ * @param {string} routePath
81
+ * @returns {any}
82
+ */
83
+ export function conceptIdForNextRoute(routePath) {
84
+ const normalized = String(routePath || "");
85
+ if (normalized === "/") return "surface_home";
86
+ if (/\/login$/.test(normalized)) return "flow_login";
87
+ if (/\/register$/.test(normalized)) return "flow_register";
88
+ if (/\/setup$/.test(normalized)) return "flow_setup";
89
+ return entityIdForNextRoute(routePath) || entityIdForRoute(routePath);
90
+ }
91
+
92
+ /**
93
+ * @param {string} routePath
94
+ * @returns {any}
95
+ */
96
+ export function uiCapabilityHintsForNextRoute(routePath) {
97
+ const normalized = String(routePath || "");
98
+ if (normalized === "/") return { load: null, submit: null, primary_action: null };
99
+ if (/\/login$/.test(normalized)) return { load: null, submit: "cap_sign_in_user", primary_action: "cap_sign_in_user" };
100
+ if (/\/register$/.test(normalized)) return { load: null, submit: "cap_register_user", primary_action: "cap_register_user" };
101
+ if (/\/setup$/.test(normalized)) return { load: null, submit: null, primary_action: null };
102
+ if (/^\/posts\/new$/.test(normalized)) return { load: null, submit: "cap_create_post", primary_action: "cap_create_post" };
103
+ if (/^\/posts\/:id$/.test(normalized) || /^\/posts\/:[^/]+$/.test(normalized)) return { load: "cap_get_post", submit: null, primary_action: "cap_update_post" };
104
+ if (/^\/posts$/.test(normalized)) return { load: "cap_list_posts", submit: null, primary_action: "cap_create_post" };
105
+ if (/^\/users\/new$/.test(normalized)) return { load: null, submit: "cap_create_user", primary_action: "cap_create_user" };
106
+ return uiCapabilityHintsForRoute(routePath);
107
+ }
108
+
109
+ /**
110
+ * @param {string} text
111
+ * @returns {any}
112
+ */
113
+ export function inferRouteQueryParams(text) {
114
+ const params = new Set();
115
+ for (const match of String(text || "").matchAll(/\bquery\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) params.add(match[1]);
116
+ for (const match of String(text || "").matchAll(/\bquery\.([A-Za-z_][A-Za-z0-9_]*)\b/g)) params.add(match[1]);
117
+ return [...params].sort();
118
+ }
119
+
120
+ /**
121
+ * @param {string} routeArguments
122
+ * @param {string} handlerContext
123
+ * @returns {any}
124
+ */
125
+ export function inferRouteAuthHint(routeArguments, handlerContext) {
126
+ const combined = `${routeArguments || ""}\n${handlerContext || ""}`.toLowerCase();
127
+ if (/\b(signin|sign_in|login|register|credentialsprovider)\b/.test(combined)) {
128
+ return "public";
129
+ }
130
+ return /\b(auth|session|permission|guard|protected|require_auth|requireauth|ensureauth)\b/.test(combined)
131
+ ? "secured"
132
+ : "unknown";
133
+ }
134
+
135
+ /**
136
+ * @param {string} text
137
+ * @param {string} exportName
138
+ * @returns {any}
139
+ */
140
+ export function extractNamedExportBlock(text, exportName) {
141
+ const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
+ const match = text.match(new RegExp(`export\\s+async\\s+function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,2000}?)\\n\\}`, "m"));
143
+ return match ? match[1] : "";
144
+ }
145
+
146
+ /**
147
+ * @param {string} text
148
+ * @returns {any}
149
+ */
150
+ export function inferNextRequestSearchParams(text) {
151
+ const params = new Set();
152
+ for (const match of String(text || "").matchAll(/searchParams\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
153
+ params.add(match[1]);
154
+ }
155
+ return [...params].sort();
156
+ }
157
+
158
+ /**
159
+ * @param {string} text
160
+ * @returns {any}
161
+ */
162
+ export function inferNextJsonFields(text) {
163
+ const fields = new Set();
164
+ for (const match of String(text || "").matchAll(/NextResponse\.json\(\s*\{([\s\S]{0,400}?)\}\s*\)/g)) {
165
+ for (const fieldMatch of match[1].matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\b\s*[:,]/g)) {
166
+ fields.add(fieldMatch[1]);
167
+ }
168
+ }
169
+ return [...fields].sort();
170
+ }
171
+
172
+ /**
173
+ * @param {string} text
174
+ * @param {string} handlerName
175
+ * @returns {any}
176
+ */
177
+ export function extractHandlerContext(text, handlerName) {
178
+ if (!handlerName) return "";
179
+ const escapedName = handlerName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
180
+ const patterns = [
181
+ new RegExp(`function\\s+${escapedName}\\s*\\([^)]*\\)\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m"),
182
+ new RegExp(`const\\s+${escapedName}\\s*=\\s*(?:async\\s*)?\\([^)]*\\)\\s*=>\\s*\\{([\\s\\S]{0,1200}?)\\n\\}`, "m")
183
+ ];
184
+ for (const pattern of patterns) {
185
+ const match = text.match(pattern);
186
+ if (match) return match[1];
187
+ }
188
+ return "";
189
+ }
190
+
191
+ /**
192
+ * @param {string} appRoot
193
+ * @param {string} filePath
194
+ * @returns {any}
195
+ */
196
+ export function nextAppRoutePathFromFile(appRoot, filePath) {
197
+ const relative = relativeTo(appRoot, filePath);
198
+ return `/${relative}`
199
+ .replace(/\/actions\.(tsx|ts|jsx|js)$/, "")
200
+ .replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
201
+ .replace(/\[(\.\.\.)?([^\]]+)\]/g, /** @param {any} _match @param {any} catchAll @param {any} name */ (_match, catchAll, name) => catchAll ? `:${name}*` : `:${name}`)
202
+ .replace(/\/index$/, "")
203
+ .replace(/^\/$/, "/") || "/";
204
+ }
205
+
206
+ /**
207
+ * @param {string} text
208
+ * @returns {any}
209
+ */
210
+ export function inferFormDataFields(text) {
211
+ const fields = new Set();
212
+ for (const match of String(text || "").matchAll(/formData\.get\(\s*["'`]([^"'`]+)["'`]\s*\)/g)) {
213
+ fields.add(match[1]);
214
+ }
215
+ return [...fields].sort();
216
+ }
217
+
218
+ /**
219
+ * @param {string} text
220
+ * @returns {any}
221
+ */
222
+ export function inferInputNames(text) {
223
+ const fields = new Set();
224
+ for (const match of String(text || "").matchAll(/\bname=["'`]([^"'`]+)["'`]/g)) {
225
+ fields.add(match[1]);
226
+ }
227
+ return [...fields].sort();
228
+ }
229
+
230
+ /**
231
+ * @param {string} workspaceRoot
232
+ * @param {import("./types.d.ts").ImportReadHelpers} helpers
233
+ * @returns {any}
234
+ */
235
+ export function inferNextApiRoutes(workspaceRoot, helpers = { readTextIfExists }) {
236
+ const apiRoot = path.join(workspaceRoot, "app", "api");
237
+ if (!fs.existsSync(apiRoot)) return [];
238
+ const routeFiles = listFilesRecursive(
239
+ apiRoot,
240
+ /** @param {any} child */ (child) => /\/route\.(tsx|ts|jsx|js)$/.test(child) || /^route\.(tsx|ts|jsx|js)$/.test(path.basename(child))
241
+ );
242
+ const routes = [];
243
+ for (const filePath of routeFiles) {
244
+ const text = helpers.readTextIfExists(filePath) || "";
245
+ const relative = relativeTo(apiRoot, filePath);
246
+ const routePath = `/${relative}`
247
+ .replace(/\/route\.(tsx|ts|jsx|js)$/, "")
248
+ .replace(/\[(\.\.\.)?([^\]]+)\]/g, /** @param {any} _match @param {any} catchAll @param {any} name */ (_match, catchAll, name) => catchAll ? `:${name}*` : `:${name}`);
249
+ for (const match of text.matchAll(/export\s+async\s+function\s+(GET|POST|PUT|PATCH|DELETE)\s*\(([^)]*)\)/g)) {
250
+ const method = match[1].toUpperCase();
251
+ const handlerContext = extractNamedExportBlock(text, match[1]) || "";
252
+ routes.push({
253
+ file: filePath,
254
+ method,
255
+ path: routePath === "" ? "/" : routePath,
256
+ handler_hint: match[1].toLowerCase(),
257
+ path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map(/** @param {any} entry */ (entry) => entry[1]),
258
+ query_params: inferNextRequestSearchParams(handlerContext),
259
+ output_fields: inferNextJsonFields(handlerContext),
260
+ auth_hint: inferRouteAuthHint(match[0], handlerContext),
261
+ source_kind: "route_code"
262
+ });
263
+ }
264
+ }
265
+ return routes;
266
+ }
267
+
268
+ /**
269
+ * @param {import("./types.d.ts").ImportRouteRecord} record
270
+ * @returns {any}
271
+ */
272
+ export function inferCapabilityEntityId(record) {
273
+ if (record.entity_id) {
274
+ return record.entity_id;
275
+ }
276
+ const pathSegments = normalizeEndpointPathForMatch(record.endpoint?.path || "")
277
+ .split("/")
278
+ .filter(Boolean)
279
+ .filter(/** @param {any} segment */ (segment) => segment !== "{}");
280
+ const resourceSegment = pathSegments[0] || String(record.id_hint || "").replace(/^cap_(create|update|delete|get|list)_/, "");
281
+ return `entity_${idHintify(canonicalCandidateTerm(resourceSegment))}`;
282
+ }
283
+
284
+ /**
285
+ * @param {string} workspaceRoot
286
+ * @param {import("./types.d.ts").ImportReadHelpers} helpers
287
+ * @returns {any}
288
+ */
289
+ export function inferNextServerActionCapabilities(workspaceRoot, helpers = { readTextIfExists }) {
290
+ const appRoot = path.join(workspaceRoot, "app");
291
+ if (!fs.existsSync(appRoot)) return [];
292
+ const actionFiles = listFilesRecursive(
293
+ appRoot,
294
+ /** @param {any} child */ (child) =>
295
+ /\/actions\.(tsx|ts|jsx|js)$/.test(child) ||
296
+ /\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) ||
297
+ /^page\.(tsx|ts|jsx|js|mdx)$/.test(path.basename(child))
298
+ );
299
+ const capabilities = [];
300
+ for (const filePath of actionFiles) {
301
+ const text = helpers.readTextIfExists(filePath) || "";
302
+ const routePath = nextAppRoutePathFromFile(appRoot, filePath);
303
+ for (const match of text.matchAll(/(?:export\s+)?async\s+function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*\{([\s\S]{0,2400}?)\n\}/g)) {
304
+ const functionName = match[1];
305
+ const body = match[3] || "";
306
+ const trimmedBody = body.trimStart();
307
+ const isServerAction =
308
+ /\/actions\.(tsx|ts|jsx|js)$/.test(filePath) ||
309
+ trimmedBody.startsWith('"use server"') ||
310
+ trimmedBody.startsWith("'use server'");
311
+ if (!isServerAction) continue;
312
+ const routeLike = {
313
+ file: filePath,
314
+ method: "POST",
315
+ path: routePath,
316
+ handler_hint: functionName,
317
+ auth_hint: inferRouteAuthHint(functionName, body)
318
+ };
319
+ const idHint = inferRouteCapabilityId(routeLike);
320
+ capabilities.push({
321
+ file: filePath,
322
+ function_name: functionName,
323
+ method: "POST",
324
+ path: routePath,
325
+ id_hint: idHint,
326
+ input_fields: inferFormDataFields(body),
327
+ output_fields: [],
328
+ path_params: [...normalizeOpenApiPath(routePath).matchAll(/\{([^}]+)\}/g)].map(/** @param {any} entry */ (entry) => entry[1]),
329
+ auth_hint: routeLike.auth_hint,
330
+ entity_id: inferCapabilityEntityId({ endpoint: { path: routePath }, id_hint: idHint }),
331
+ source_kind: "route_code"
332
+ });
333
+ }
334
+ }
335
+ return capabilities.sort(/** @param {any} a @param {any} b */ (a, b) => a.id_hint.localeCompare(b.id_hint) || a.path.localeCompare(b.path));
336
+ }
337
+
338
+ /**
339
+ * @param {import("./types.d.ts").ImportPaths} paths
340
+ * @param {import("./types.d.ts").ImportReadHelpers} helpers
341
+ * @returns {any}
342
+ */
343
+ export function inferNextAuthCapabilities(paths, helpers = { readTextIfExists }) {
344
+ const authConfigPath = path.join(paths.workspaceRoot, "auth.ts");
345
+ const authConfigText = helpers.readTextIfExists(authConfigPath) || "";
346
+ const hasCredentialsProvider = /CredentialsProvider\s*\(/.test(authConfigText);
347
+ const createsUserOnAuthorize = /prisma\.user\.create\s*\(/.test(authConfigText);
348
+ const pages = [
349
+ {
350
+ file: path.join(paths.workspaceRoot, "app", "login", "page.tsx"),
351
+ path: "/login",
352
+ id_hint: "cap_sign_in_user",
353
+ label: "Sign In User",
354
+ target_state: "authenticated"
355
+ },
356
+ {
357
+ file: path.join(paths.workspaceRoot, "app", "register", "page.tsx"),
358
+ path: "/register",
359
+ id_hint: "cap_register_user",
360
+ label: "Register User",
361
+ target_state: createsUserOnAuthorize ? "registered" : "created"
362
+ }
363
+ ];
364
+ const capabilities = [];
365
+ for (const page of pages) {
366
+ const text = helpers.readTextIfExists(page.file) || "";
367
+ if (!text || !/signIn\(\s*["'`]credentials["'`]/.test(text)) continue;
368
+ capabilities.push({
369
+ file: page.file,
370
+ function_name: page.id_hint.replace(/^cap_/, ""),
371
+ method: "POST",
372
+ path: page.path,
373
+ id_hint: page.id_hint,
374
+ label: page.label,
375
+ input_fields: inferInputNames(text),
376
+ output_fields: [],
377
+ path_params: [],
378
+ auth_hint: "public",
379
+ entity_id: "entity_user",
380
+ target_state: page.target_state,
381
+ provenance: [
382
+ relativeTo(paths.repoRoot, page.file),
383
+ ...(hasCredentialsProvider ? [relativeTo(paths.repoRoot, authConfigPath)] : [])
384
+ ],
385
+ source_kind: "route_code"
386
+ });
387
+ }
388
+ return capabilities.sort(/** @param {any} a @param {any} b */ (a, b) => a.id_hint.localeCompare(b.id_hint));
389
+ }
@@ -0,0 +1,51 @@
1
+ export type ImportPaths = {
2
+ repoRoot: string;
3
+ workspaceRoot: string;
4
+ topogramRoot: string;
5
+ [key: string]: any;
6
+ };
7
+
8
+ export type ImportReadHelpers = {
9
+ readTextIfExists(filePath: string): string | null;
10
+ [key: string]: any;
11
+ };
12
+
13
+ export type ImportRouteRecord = {
14
+ endpoint?: {
15
+ path?: string;
16
+ };
17
+ path?: string;
18
+ method?: string;
19
+ handler_hint?: string;
20
+ id_hint?: string;
21
+ entity_id?: string | null;
22
+ auth_hint?: string;
23
+ tags?: string[];
24
+ [key: string]: any;
25
+ };
26
+
27
+ export type ImportCandidateRecord = {
28
+ kind: string;
29
+ id?: string;
30
+ id_hint?: string;
31
+ idHint?: string;
32
+ label: string;
33
+ confidence?: string;
34
+ source_kind?: string;
35
+ source_of_truth?: string;
36
+ [key: string]: any;
37
+ };
38
+
39
+ export type NavigationStructure = {
40
+ hasHeader?: boolean;
41
+ hasSidebar?: boolean;
42
+ hasTopbar?: boolean;
43
+ hasBottomTabs?: boolean;
44
+ hasTabs?: boolean;
45
+ hasBreadcrumbs?: boolean;
46
+ hasCommandPalette?: boolean;
47
+ hasSegmentedControl?: boolean;
48
+ hasRail?: boolean;
49
+ navLinks?: string[];
50
+ [key: string]: any;
51
+ };
@@ -0,0 +1,230 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { relativeTo } from "../../../path-helpers.js";
5
+ import { canonicalCandidateTerm } from "./candidates.js";
6
+ import { listFilesRecursive, readTextIfExists } from "./files.js";
7
+
8
+ /**
9
+ * @param {string} routePath
10
+ * @returns {any}
11
+ */
12
+ export function routeSegments(routePath) {
13
+ return String(routePath || "")
14
+ .split("/")
15
+ .filter(Boolean)
16
+ .map(/** @param {any} segment */ (segment) => segment.replace(/^:/, ""));
17
+ }
18
+
19
+ /**
20
+ * @param {string} routePath
21
+ * @returns {any}
22
+ */
23
+ export function screenKindForRoute(routePath) {
24
+ const normalized = String(routePath || "");
25
+ const segments = routeSegments(normalized);
26
+ if (/\/new$/.test(normalized)) return "form";
27
+ if (/\/:?[A-Za-z0-9_]+\/edit$/.test(normalized)) return "form";
28
+ if (segments.length >= 2 && !/\/new$/.test(normalized) && !/\/edit$/.test(normalized)) return "detail";
29
+ return "list";
30
+ }
31
+
32
+ /**
33
+ * @param {string} routePath
34
+ * @returns {any}
35
+ */
36
+ export function screenIdForRoute(routePath) {
37
+ const segments = routeSegments(routePath);
38
+ const resource = canonicalCandidateTerm(segments[0] || "home");
39
+ const kind = screenKindForRoute(routePath);
40
+ if (kind === "form" && /\/new$/.test(routePath)) return `${resource}_create`;
41
+ if (kind === "form" && /\/edit$/.test(routePath)) return `${resource}_edit`;
42
+ if (kind === "detail") return `${resource}_detail`;
43
+ return `${resource}_list`;
44
+ }
45
+
46
+ /**
47
+ * @param {string} routePath
48
+ * @returns {any}
49
+ */
50
+ export function uiCapabilityHintsForRoute(routePath) {
51
+ const segments = routeSegments(routePath);
52
+ const resource = canonicalCandidateTerm(segments[0] || "item");
53
+ const idSegment = segments[1] || null;
54
+ if (/\/new$/.test(routePath)) {
55
+ return { load: null, submit: `cap_create_${resource}`, primary_action: `cap_create_${resource}` };
56
+ }
57
+ if (/\/edit$/.test(routePath)) {
58
+ return { load: `cap_get_${resource}`, submit: `cap_update_${resource}`, primary_action: `cap_update_${resource}` };
59
+ }
60
+ if (idSegment && !/new|edit/.test(idSegment)) {
61
+ return { load: `cap_get_${resource}`, submit: null, primary_action: `cap_update_${resource}` };
62
+ }
63
+ return { load: `cap_list_${resource}s`, submit: null, primary_action: `cap_create_${resource}` };
64
+ }
65
+
66
+ /**
67
+ * @param {string} routePath
68
+ * @returns {any}
69
+ */
70
+ export function entityIdForRoute(routePath) {
71
+ const segments = routeSegments(routePath);
72
+ return `entity_${canonicalCandidateTerm(segments[0] || "item")}`;
73
+ }
74
+
75
+ /**
76
+ * @param {string} rootDir
77
+ * @returns {any}
78
+ */
79
+ export function inferReactRoutes(rootDir) {
80
+ const appPath = path.join(rootDir, "src", "App.tsx");
81
+ const text = readTextIfExists(appPath);
82
+ if (!text) return [];
83
+ const routes = new Set();
84
+ for (const match of text.matchAll(/path:\s*"([^"]+)"/g)) routes.add(match[1]);
85
+ for (const match of text.matchAll(/path="([^"]+)"/g)) routes.add(match[1]);
86
+ return [...routes].sort();
87
+ }
88
+
89
+ /**
90
+ * @param {string} rootDir
91
+ * @returns {any}
92
+ */
93
+ export function inferSvelteRoutes(rootDir) {
94
+ const routesRoot = path.join(rootDir, "src", "routes");
95
+ if (!fs.existsSync(routesRoot)) return [];
96
+ const files = listFilesRecursive(routesRoot, /** @param {any} child */ (child) => child.endsWith("+page.svelte") || child.endsWith("+page.ts") || child.endsWith("+page.server.ts"));
97
+ const routes = new Set();
98
+ for (const filePath of files) {
99
+ const relative = relativeTo(routesRoot, filePath)
100
+ .replace(/(^|\/)\+page(\.server|)\.(svelte|ts)$/, "")
101
+ .replace(/\[(.+?)\]/g, ":$1")
102
+ .replace(/^$/, "/");
103
+ routes.add(relative.startsWith("/") ? relative : `/${relative}`);
104
+ }
105
+ return [...routes].sort();
106
+ }
107
+
108
+ /**
109
+ * @param {string} rootDir
110
+ * @param {any} options
111
+ * @returns {any}
112
+ */
113
+ export function inferNavigationStructure(rootDir, options = {}) {
114
+ const filePatterns = options.filePatterns || [/(^|\/)App\.(tsx|jsx)$/i, /(^|\/)\+layout\.svelte$/i];
115
+ const files = listFilesRecursive(rootDir, /** @param {any} filePath */ (filePath) => filePatterns.some(/** @param {any} pattern */ (pattern) => pattern.test(filePath)));
116
+ const result = /** @type {any} */ ({
117
+ hasHeader: false,
118
+ hasSidebar: false,
119
+ hasTopbar: false,
120
+ hasBottomTabs: false,
121
+ hasTabs: false,
122
+ hasBreadcrumbs: false,
123
+ hasCommandPalette: false,
124
+ hasSegmentedControl: false,
125
+ navLinks: []
126
+ });
127
+
128
+ for (const filePath of files) {
129
+ const text = readTextIfExists(filePath) || "";
130
+ if (!text) continue;
131
+ if (/<header\b|class(Name)?=["'][^"']*(topbar|app-nav|navbar)/i.test(text)) {
132
+ result.hasHeader = true;
133
+ result.hasTopbar = true;
134
+ }
135
+ if (/<aside\b|class(Name)?=["'][^"']*(sidebar|side-nav|sidenav)/i.test(text)) {
136
+ result.hasSidebar = true;
137
+ }
138
+ if (/\bbottom[-_ ]tabs\b|\bTabBar\b|\bBottomNavigation\b/i.test(text)) {
139
+ result.hasBottomTabs = true;
140
+ }
141
+ if (/breadcrumb/i.test(text)) {
142
+ result.hasBreadcrumbs = true;
143
+ }
144
+ if (/\bcommand palette\b|\bCommandPalette\b/i.test(text)) {
145
+ result.hasCommandPalette = true;
146
+ }
147
+ if (/\bsegmented\b|\bSegmentedControl\b/i.test(text)) {
148
+ result.hasSegmentedControl = true;
149
+ }
150
+ if (/<nav\b|role=["']tablist["']|class(Name)?=["'][^"']*\btabs?\b/i.test(text)) {
151
+ result.hasTabs = result.hasTabs || /role=["']tablist["']|class(Name)?=["'][^"']*\btabs?\b/i.test(text);
152
+ for (const match of text.matchAll(/(?:href|to)=["'`]([^"'`]+)["'`]/g)) {
153
+ result.navLinks.push(match[1]);
154
+ }
155
+ }
156
+ }
157
+
158
+ result.navLinks = [...new Set(result.navLinks)].sort();
159
+ return result;
160
+ }
161
+
162
+ /**
163
+ * @param {import("./types.d.ts").NavigationStructure} navigation
164
+ * @returns {any}
165
+ */
166
+ export function shellKindFromNavigation(navigation) {
167
+ if (!navigation) return null;
168
+ if (navigation.hasBottomTabs) return "bottom_tabs";
169
+ if (navigation.hasSidebar && navigation.hasHeader) return "split_view";
170
+ if (navigation.hasSidebar) return "sidebar";
171
+ if (navigation.hasTopbar || navigation.hasHeader) return "topbar";
172
+ return null;
173
+ }
174
+
175
+ /**
176
+ * @param {import("./types.d.ts").NavigationStructure} navigation
177
+ * @returns {any}
178
+ */
179
+ export function navigationPatternsFromStructure(navigation) {
180
+ const patterns = new Set();
181
+ if (!navigation) return [];
182
+ if (navigation.hasTabs) patterns.add("tabs");
183
+ if (navigation.hasBreadcrumbs) patterns.add("breadcrumbs");
184
+ if (navigation.hasBottomTabs) patterns.add("bottom_tabs");
185
+ if (navigation.hasRail) patterns.add("navigation_rail");
186
+ if (navigation.hasCommandPalette) patterns.add("command_palette");
187
+ if (navigation.hasSegmentedControl) patterns.add("segmented_control");
188
+ if (navigation.hasSidebar && navigation.hasHeader) patterns.add("split_view");
189
+ return [...patterns].sort();
190
+ }
191
+
192
+ /**
193
+ * @param {string} rootDir
194
+ * @returns {any}
195
+ */
196
+ export function detectUiPresentationFeatures(rootDir) {
197
+ const files = listFilesRecursive(rootDir, /** @param {any} filePath */ (filePath) => /\.(tsx|ts|jsx|js|svelte|vue|html)$/i.test(filePath));
198
+ const features = new Set();
199
+
200
+ for (const filePath of files) {
201
+ const text = readTextIfExists(filePath) || "";
202
+ if (!text) continue;
203
+ if (/\bDataGrid\b|\bag-grid\b|\bAGGrid\b|\bTanStackTable\b|\breact-data-grid\b|\bmui[-_ ]?datagrid\b/i.test(text)) {
204
+ features.add("data_grid");
205
+ }
206
+ if (/<table\b|react-table/i.test(text)) features.add("table");
207
+ if (/\b(card|cards|Card)\b/.test(text)) features.add("cards");
208
+ if (/\bkanban|board\b/i.test(text)) features.add("board");
209
+ if (/\bcalendar\b/i.test(text)) features.add("calendar");
210
+ if (/\bgallery\b/i.test(text)) features.add("gallery");
211
+ if (/\bmodal\b|Dialog|AlertDialog/i.test(text)) features.add("modal");
212
+ if (/\bdrawer\b|Sheet/i.test(text)) features.add("drawer");
213
+ if (/\bsheet\b|BottomSheet|ModalBottomSheet/i.test(text)) features.add("sheet");
214
+ if (/\bBottomSheet\b|\bModalBottomSheet\b/i.test(text)) features.add("bottom_sheet");
215
+ if (/\bFloatingActionButton\b|\bExtendedFloatingActionButton\b|\bfloating action button\b/i.test(text)) features.add("fab");
216
+ if (/\bempty state\b|empty-state|No results|No items/i.test(text)) features.add("empty_state");
217
+ if (/\berror state\b|Something went wrong|error/i.test(text)) features.add("error_state");
218
+ if (/\bloading\b|skeleton|spinner/i.test(text)) features.add("loading_state");
219
+ if (/\bbreadcrumb/i.test(text)) features.add("breadcrumbs");
220
+ if (/\bactivity\b|\btimeline\b|\bcomment/i.test(text)) features.add("activity");
221
+ if (/\bsettings\b|\bpreferences\b|\bbilling\b|\bsecurity\b/i.test(text)) features.add("settings");
222
+ if (/\bonboarding\b|\bwizard\b|\bstepper\b/i.test(text)) features.add("wizard");
223
+ if (/\bpull[-_ ]to[-_ ]refresh\b|SwipeRefresh|refreshable\b/i.test(text)) features.add("pull_to_refresh");
224
+ if (/\bcommand palette\b|\bCommandPalette\b/i.test(text)) features.add("command_palette");
225
+ if (/\binspector\b|\bDetailsPane\b|\bproperties pane\b/i.test(text)) features.add("inspector_pane");
226
+ if (/\bWindowGroup\b|\bBrowserWindow\b|\bmulti[-_ ]window\b/i.test(text)) features.add("multi_window");
227
+ }
228
+
229
+ return [...features].sort();
230
+ }