@documentation-robotics/cli 0.3.0 → 0.4.0

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 (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
package/README.md CHANGED
@@ -302,31 +302,27 @@ dr mcp
302
302
  dr mcp --regenerate-key
303
303
  ```
304
304
 
305
- **API key**:
305
+ **Which project**: the server serves the project the client has open. It asks the client for its MCP roots after connecting and moves to the first root inside a DR project (or the first root, if none has a model yet). `DR_MCP_ROOT_PATH` pins it to one model instead, and tool calls can pass `rootPath`.
306
306
 
307
- - On first launch, `dr mcp` generates a random API key and stores it on disk with `0600` permissions. Interactive sessions are prompted for a storage path; non-interactive launches (the normal case — an MCP client spawning `dr mcp`) default to `~/.dr-mcp-key`.
308
- - The chosen path is recorded under `mcp.api_key_path` in `~/.dr-config.yaml` (or the path from `DR_CONFIG_PATH`, if set), so subsequent launches reuse the same key without re-prompting.
309
- - Every launch prints the current key to stderr (never stdout, which carries the MCP protocol) and requires the client to supply it back via the `DR_MCP_API_KEY` environment variable. The connection is rejected if `DR_MCP_API_KEY` is missing or doesn't match.
310
- - Run `dr mcp --regenerate-key` to rotate the key (e.g. after a suspected leak). This overwrites the key at its existing configured path (or the default path, prompting once if none is configured yet), updates `~/.dr-config.yaml`, prints the new key, and exits without starting the server — no manual deletion of config or key files required.
311
-
312
- **Configuring an MCP client**:
313
-
314
- Point your MCP client at the `dr` binary and pass the key via `env`:
307
+ **Configuring an MCP client**: in Claude Code, install the `dr` plugin (see [MCP_SERVER.md](../integrations/claude_code/docs/MCP_SERVER.md)). Other clients point at the `dr` binary:
315
308
 
316
309
  ```json
317
310
  {
318
311
  "mcpServers": {
319
- "documentation-robotics": {
312
+ "dr": {
320
313
  "command": "dr",
321
- "args": ["mcp"],
322
- "env": {
323
- "DR_MCP_API_KEY": "<key printed by dr mcp>"
324
- }
314
+ "args": ["mcp"]
325
315
  }
326
316
  }
327
317
  }
328
318
  ```
329
319
 
320
+ **API key (HTTP transport only)**: over stdio the client starts the server as your own user, so there is no key. `dr mcp --transport http` requires the key as a bearer token:
321
+
322
+ - On first launch it generates a random key and stores it with `0600` permissions. Interactive sessions are prompted for a storage path; non-interactive launches default to `~/.dr-mcp-key`. The path is recorded under `mcp.api_key_path` in `~/.dr-config.yaml` (or the path from `DR_CONFIG_PATH`).
323
+ - Every HTTP launch prints the key to stderr (`DR_MCP_QUIET_KEY=1` suppresses it).
324
+ - `dr mcp --regenerate-key` rotates it: it overwrites the key at its configured path, updates `~/.dr-config.yaml`, prints the new key and exits without starting the server.
325
+
330
326
  ## Configuration
331
327
 
332
328
  ### Model Structure
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { AnalyzerRegistry } from "./registry.js";
3
+ import { MappingLoader } from "./mapping-loader.js";
4
+ import { readIndexMeta, readSession } from "./session-state.js";
5
+ import { CLIError, ErrorCategory } from "../utils/errors.js";
6
+ const NO_ANALYZER_SELECTED = "No analyzer selected. Run `dr analyzer discover` first.", VERIFIABLE_LAYERS = Object.freeze(["api"]);
7
+ async function resolveAnalyzerName(projectRoot, explicit) {
8
+ if (explicit)
9
+ return explicit;
10
+ const session = await readSession(projectRoot);
11
+ if (!session)
12
+ throw new CLIError(NO_ANALYZER_SELECTED, ErrorCategory.USER);
13
+ return session.active_analyzer;
14
+ }
15
+ async function getAnalyzerBackend(name, suggestions) {
16
+ const registry = AnalyzerRegistry.getInstance();
17
+ await registry.initialize();
18
+ const backend = await registry.getAnalyzer(name);
19
+ if (!backend)
20
+ throw new CLIError(`Analyzer not found: ${name}`, ErrorCategory.NOT_FOUND, suggestions);
21
+ return backend;
22
+ }
23
+ async function analyzerHomepage(name) {
24
+ try {
25
+ return (await MappingLoader.load(name)).getAnalyzerMetadata()?.homepage || "https://github.com/search?q=codebase-memory";
26
+ } catch {
27
+ return "https://github.com/search?q=codebase-memory";
28
+ }
29
+ }
30
+ function splitVerifyLayers(requested) {
31
+ const layers = requested && requested.length > 0 ? [...requested] : ["api"];
32
+ return {
33
+ verifiable: layers.filter((layer) => VERIFIABLE_LAYERS.includes(layer)),
34
+ unsupported: layers.filter((layer) => !VERIFIABLE_LAYERS.includes(layer))
35
+ };
36
+ }
37
+ function currentGitHead(dir) {
38
+ try {
39
+ const result = spawnSync("git", ["rev-parse", "HEAD"], { cwd: dir, stdio: "pipe", encoding: "utf-8" });
40
+ return result.status !== 0 ? null : result.stdout.trim() || null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ async function readIndexFreshness(projectRoot, analyzerName, codebaseRoot, gitHead = currentGitHead) {
46
+ const meta = await readIndexMeta(projectRoot, analyzerName);
47
+ if (!meta)
48
+ return { indexed: !1, fresh: !1 };
49
+ const head = gitHead(codebaseRoot);
50
+ return { indexed: !0, fresh: head !== null && head === meta.git_head, meta, currentGitHead: head };
51
+ }
52
+ export {
53
+ NO_ANALYZER_SELECTED,
54
+ VERIFIABLE_LAYERS,
55
+ analyzerHomepage,
56
+ currentGitHead,
57
+ getAnalyzerBackend,
58
+ readIndexFreshness,
59
+ resolveAnalyzerName,
60
+ splitVerifyLayers
61
+ };
62
+ //# sourceMappingURL=active-analyzer.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/analyzers/active-analyzer.ts"],
4
+ "sourcesContent": ["/**\n * Shared steps of the analyzer commands (`dr analyzer index`, `dr analyzer verify`) and the\n * codebase MCP tools (`codebase_index`, `codebase_verify`): which analyzer a project uses, its\n * backend, how to install it, which layers verify covers, and whether the index is fresh.\n */\n\nimport { spawnSync } from \"node:child_process\";\nimport { AnalyzerRegistry } from \"./registry.js\";\nimport { MappingLoader } from \"./mapping-loader.js\";\nimport { readIndexMeta, readSession } from \"./session-state.js\";\nimport type { AnalyzerBackend } from \"./base-analyzer.js\";\nimport type { IndexMeta } from \"./types.js\";\nimport { CLIError, ErrorCategory } from \"../utils/errors.js\";\n\nexport const NO_ANALYZER_SELECTED = \"No analyzer selected. Run `dr analyzer discover` first.\";\n\n/** Layers `verify` can check. Others are reported as having no verification. */\nexport const VERIFIABLE_LAYERS: readonly string[] = Object.freeze([\"api\"]);\n\n/** The analyzer to use: `explicit`, else the one selected for the project (session.json). */\nexport async function resolveAnalyzerName(projectRoot: string, explicit?: string): Promise<string> {\n if (explicit) return explicit;\n const session = await readSession(projectRoot);\n if (!session) {\n throw new CLIError(NO_ANALYZER_SELECTED, ErrorCategory.USER);\n }\n return session.active_analyzer;\n}\n\n/** The registered backend for `name`. Throws a not-found CLIError, with `suggestions`, if none. */\nexport async function getAnalyzerBackend(name: string, suggestions?: string[]): Promise<AnalyzerBackend> {\n const registry = AnalyzerRegistry.getInstance();\n await registry.initialize();\n const backend = await registry.getAnalyzer(name);\n if (!backend) {\n throw new CLIError(`Analyzer not found: ${name}`, ErrorCategory.NOT_FOUND, suggestions);\n }\n return backend;\n}\n\n/** The analyzer's homepage, from its mapping metadata. */\nexport async function analyzerHomepage(name: string): Promise<string> {\n try {\n const mapper = await MappingLoader.load(name);\n return mapper.getAnalyzerMetadata()?.homepage || \"https://github.com/search?q=codebase-memory\";\n } catch {\n return \"https://github.com/search?q=codebase-memory\";\n }\n}\n\n/** Splits requested layers into the ones verify checks and the ones it has no check for. */\nexport function splitVerifyLayers(requested: readonly string[] | undefined): {\n verifiable: string[];\n unsupported: string[];\n} {\n const layers = requested && requested.length > 0 ? [...requested] : [\"api\"];\n return {\n verifiable: layers.filter((layer) => VERIFIABLE_LAYERS.includes(layer)),\n unsupported: layers.filter((layer) => !VERIFIABLE_LAYERS.includes(layer)),\n };\n}\n\n/** The git HEAD of `dir`, or null when it isn't a git repository or git fails. */\nexport function currentGitHead(dir: string): string | null {\n try {\n const result = spawnSync(\"git\", [\"rev-parse\", \"HEAD\"], { cwd: dir, stdio: \"pipe\", encoding: \"utf-8\" });\n if (result.status !== 0) return null;\n return result.stdout.trim() || null;\n } catch {\n return null;\n }\n}\n\nexport type IndexFreshness =\n | { indexed: false; fresh: false }\n | {\n indexed: true;\n /** The index was taken at the code's current git HEAD. */\n fresh: boolean;\n meta: IndexMeta;\n /** The code's current git HEAD; null when it can't be read (the index then counts as stale). */\n currentGitHead: string | null;\n };\n\n/**\n * Whether the project has an index from `analyzerName`, and whether it was taken at the code's\n * current git HEAD. Reads `.dr/analyzers/{analyzer}/index-meta.json` and runs git; it does not\n * start the analyzer. The same test as the analyzer's own `status()`.\n */\nexport async function readIndexFreshness(\n projectRoot: string,\n analyzerName: string,\n codebaseRoot: string,\n gitHead: (dir: string) => string | null = currentGitHead\n): Promise<IndexFreshness> {\n const meta = await readIndexMeta(projectRoot, analyzerName);\n if (!meta) return { indexed: false, fresh: false };\n const head = gitHead(codebaseRoot);\n return { indexed: true, fresh: head !== null && head === meta.git_head, meta, currentGitHead: head };\n}\n"],
5
+ "mappings": "AAMA,SAAS,iBAAiB;AAC1B,SAAS,wBAAwB;AACjC,SAAS,qBAAqB;AAC9B,SAAS,eAAe,mBAAmB;AAG3C,SAAS,UAAU,qBAAqB;AAEjC,MAAM,uBAAuB,2DAGvB,oBAAuC,OAAO,OAAO,CAAC,KAAK,CAAC;AAGzE,eAAsB,oBAAoB,aAAqB,UAAiB;AAC9E,MAAI;AAAU,WAAO;AACrB,QAAM,UAAU,MAAM,YAAY,WAAW;AAC7C,MAAI,CAAC;AACH,UAAM,IAAI,SAAS,sBAAsB,cAAc,IAAI;AAE7D,SAAO,QAAQ;AACjB;AAGA,eAAsB,mBAAmB,MAAc,aAAsB;AAC3E,QAAM,WAAW,iBAAiB,YAAW;AAC7C,QAAM,SAAS,WAAU;AACzB,QAAM,UAAU,MAAM,SAAS,YAAY,IAAI;AAC/C,MAAI,CAAC;AACH,UAAM,IAAI,SAAS,uBAAuB,IAAI,IAAI,cAAc,WAAW,WAAW;AAExF,SAAO;AACT;AAGA,eAAsB,iBAAiB,MAAY;AACjD,MAAI;AAEF,YADe,MAAM,cAAc,KAAK,IAAI,GAC9B,oBAAmB,GAAI,YAAY;EACnD,QAAQ;AACN,WAAO;EACT;AACF;AAGM,SAAU,kBAAkB,WAAwC;AAIxE,QAAM,SAAS,aAAa,UAAU,SAAS,IAAI,CAAC,GAAG,SAAS,IAAI,CAAC,KAAK;AAC1E,SAAO;IACL,YAAY,OAAO,OAAO,CAAC,UAAU,kBAAkB,SAAS,KAAK,CAAC;IACtE,aAAa,OAAO,OAAO,CAAC,UAAU,CAAC,kBAAkB,SAAS,KAAK,CAAC;;AAE5E;AAGM,SAAU,eAAe,KAAW;AACxC,MAAI;AACF,UAAM,SAAS,UAAU,OAAO,CAAC,aAAa,MAAM,GAAG,EAAE,KAAK,KAAK,OAAO,QAAQ,UAAU,QAAO,CAAE;AACrG,WAAI,OAAO,WAAW,IAAU,OACzB,OAAO,OAAO,KAAI,KAAM;EACjC,QAAQ;AACN,WAAO;EACT;AACF;AAkBA,eAAsB,mBACpB,aACA,cACA,cACA,UAA0C,gBAAc;AAExD,QAAM,OAAO,MAAM,cAAc,aAAa,YAAY;AAC1D,MAAI,CAAC;AAAM,WAAO,EAAE,SAAS,IAAO,OAAO,GAAK;AAChD,QAAM,OAAO,QAAQ,YAAY;AACjC,SAAO,EAAE,SAAS,IAAM,OAAO,SAAS,QAAQ,SAAS,KAAK,UAAU,MAAM,gBAAgB,KAAI;AACpG;",
6
+ "names": []
7
+ }
@@ -12,7 +12,26 @@ const CARD_VERSION = 1, CARD_LABELS = Object.freeze(["Class", "Interface", "Func
12
12
  Enum: ["docstring", "is_exported", "start_line", "end_line"],
13
13
  Trait: ["docstring", "is_exported", "start_line", "end_line"],
14
14
  Struct: ["docstring", "is_exported", "start_line", "end_line"]
15
- }), SOURCE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|rs|cs|php|rb|swift|scala|vue|svelte)$/i, VENDOR_EXCLUDE = /(^|\/)(dist|build|node_modules|vendor|\.venv|venv|__pycache__)(\/|$)/, SAMPLE_EXCLUDE = /(^|\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\.venv|venv)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|(^|\/)conftest\.py$/;
15
+ }), SOURCE_EXTENSIONS = Object.freeze([
16
+ "ts",
17
+ "tsx",
18
+ "js",
19
+ "jsx",
20
+ "mjs",
21
+ "cjs",
22
+ "py",
23
+ "go",
24
+ "java",
25
+ "kt",
26
+ "rs",
27
+ "cs",
28
+ "php",
29
+ "rb",
30
+ "swift",
31
+ "scala",
32
+ "vue",
33
+ "svelte"
34
+ ]), SOURCE_EXT = new RegExp(`\\.(${SOURCE_EXTENSIONS.join("|")})$`, "i"), VENDOR_EXCLUDE = /(^|\/)(dist|build|node_modules|vendor|\.venv|venv|__pycache__)(\/|$)/, SAMPLE_EXCLUDE = /(^|\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\.venv|venv)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|(^|\/)conftest\.py$/;
16
35
  function cardId(scope, qualifiedName) {
17
36
  return createHash("sha1").update(`${scope}:${qualifiedName}`).digest("hex").slice(0, 12);
18
37
  }
@@ -164,6 +183,7 @@ export {
164
183
  CardBuilder,
165
184
  SAMPLE_EXCLUDE,
166
185
  SOURCE_EXT,
186
+ SOURCE_EXTENSIONS,
167
187
  VENDOR_EXCLUDE,
168
188
  cardId,
169
189
  isCandidate
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/analyzers/cards/build.ts"],
4
- "sourcesContent": ["/**\n * Build symbol cards from a CBM project.\n *\n * The classify-eval harness (cli/scripts/classify-eval/) builds its labelled card sets with this\n * module, so its accuracy numbers measure the code `dr analyzer classify` runs.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { EdgeIndex } from \"./edge-index.js\";\nimport type { CardGraph, GraphNode, SymbolCard } from \"./types.js\";\n\n/**\n * Version of the card's fields and how they are derived. Bump it when a change would make a card\n * built now differ from one built before for the same graph, so stored cards and their\n * classifications can be told apart.\n */\nexport const CARD_VERSION = 1;\n\n/** CBM labels that become cards in the harness's card sets (classify adds the rules' labels). */\nexport const CARD_LABELS: readonly string[] = Object.freeze([\"Class\", \"Interface\", \"Function\", \"Method\", \"Module\", \"Route\"]);\n\n/** Extra search_graph fields each label's card reads. */\nexport const CARD_FIELDS: Readonly<Record<string, string[]>> = Object.freeze({\n Class: [\"docstring\", \"base_classes\", \"is_exported\", \"is_entry_point\", \"complexity\", \"start_line\", \"end_line\"],\n Interface: [\"docstring\", \"base_classes\", \"is_exported\", \"start_line\", \"end_line\"],\n Function: [\"signature\", \"docstring\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Method: [\"signature\", \"docstring\", \"parent_class\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Module: [\"is_entry_point\", \"is_exported\", \"lines\", \"start_line\", \"end_line\"],\n Route: [\"method\", \"source\"],\n Type: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Enum: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Trait: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Struct: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n});\n\nexport const SOURCE_EXT = /\\.(ts|tsx|js|jsx|mjs|cjs|py|go|java|kt|rs|cs|php|rb|swift|scala|vue|svelte)$/i;\n\n/** Build output and vendored dependencies: never cards. The default for `dr analyzer classify`. */\nexport const VENDOR_EXCLUDE = /(^|\\/)(dist|build|node_modules|vendor|\\.venv|venv|__pycache__)(\\/|$)/;\n\n/**\n * The harness's sampling exclusion: vendor paths plus tests, fixtures, examples and scripts, so\n * labelled card sets are about application code. Tests are not excluded in production, where the\n * Tier-0 test rules file them in the testing layer.\n */\nexport const SAMPLE_EXCLUDE =\n /(^|\\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\\.venv|venv)(\\/|$)|\\.(test|spec)\\.[cm]?[jt]sx?$|(^|\\/)test_[^/]*\\.py$|(^|\\/)conftest\\.py$/;\n\n/** Stable card id for a symbol (full, project-prefixed qualified name) within a scope. */\nexport function cardId(scope: string, qualifiedName: string): string {\n return createHash(\"sha1\").update(`${scope}:${qualifiedName}`).digest(\"hex\").slice(0, 12);\n}\n\n/**\n * Whether a node of this label becomes a card: a route with a path, or a symbol in a source file\n * outside `exclude`, leaving out constructors, dunder methods and anonymous functions.\n */\nexport function isCandidate(node: GraphNode, label: string, exclude: RegExp = VENDOR_EXCLUDE): boolean {\n if (label === \"Route\") {\n if (!/^__route__[A-Z]+__\\//.test(node.name) || /\\.(ts|js|py|json)$/.test(node.name)) return false;\n return !(node.file_path && exclude.test(node.file_path));\n }\n if (!node.file_path) return false;\n if (!SOURCE_EXT.test(node.file_path) || node.file_path.startsWith(\"<\")) return false;\n if (exclude.test(node.file_path)) return false;\n if (label === \"Method\" && /^(constructor|__init__|__str__|__repr__)$/.test(node.name)) return false;\n if (label === \"Function\" && /^(anonymous|arrow|lambda|<anonymous>)$/i.test(node.name)) return false;\n return true;\n}\n\nfunction lineRange(node: GraphNode): { start: number | null; end: number | null } {\n const s = node.start_line != null ? Number(node.start_line) : null;\n const e = node.end_line != null ? Number(node.end_line) : null;\n if (s != null && !Number.isNaN(s)) return { start: s, end: e != null && !Number.isNaN(e) ? e : null };\n const m = /^(\\d+)-(\\d+)$/.exec(String(node.lines ?? \"\"));\n return m ? { start: Number(m[1]), end: Number(m[2]) } : { start: null, end: null };\n}\n\nfunction strOrNull(v: unknown): string | null {\n return typeof v === \"string\" && v.trim() ? v : null;\n}\nfunction boolOrNull(v: unknown): boolean | null {\n return typeof v === \"boolean\" ? v : null;\n}\nfunction numOrNull(v: unknown): number | null {\n return typeof v === \"number\" ? v : null;\n}\nfunction strArray(v: unknown): string[] {\n if (Array.isArray(v)) return v.filter((x): x is string => typeof x === \"string\");\n if (typeof v === \"string\" && v.trim()) return v.split(\",\").map((s) => s.trim()).filter(Boolean);\n return [];\n}\n\nexport interface CardBuilderOptions {\n /** Scope for card ids: a corpus name in the harness, the CBM project in production. */\n scope: string;\n /** Source lines kept per card (default 120). */\n sourceLines?: number;\n /**\n * Absolute path of the indexed codebase. When set, a symbol's source is read from its file by\n * the node's line range, one read per file; CBM's `get_code_snippet` (full source, at most 500\n * lines) is the fallback. Without it every card costs a snippet call, about 15 ms each.\n */\n sourceRoot?: string;\n}\n\n/**\n * Builds cards for one project. Holds the per-file handler cache that route-handler resolution\n * fills, so build every card of a run with the same builder.\n */\nexport class CardBuilder {\n private readonly sourceLines: number;\n private readonly handlerCache = new Map<string, Array<{ qn: string; source: string }>>();\n private readonly fileLines = new Map<string, Promise<string[] | null>>();\n private readonly root: string | null;\n\n constructor(\n private readonly graph: CardGraph,\n private readonly edges: EdgeIndex,\n private readonly options: CardBuilderOptions\n ) {\n this.sourceLines = options.sourceLines ?? 120;\n this.root = options.sourceRoot ? path.resolve(options.sourceRoot) : null;\n }\n\n /** A file's lines, read once; null when it is outside the root or unreadable. */\n private lines(file: string): Promise<string[] | null> {\n let cached = this.fileLines.get(file);\n if (!cached) {\n const abs = path.resolve(this.root!, file);\n cached =\n abs === this.root || !abs.startsWith(this.root! + path.sep)\n ? Promise.resolve(null)\n : readFile(abs, \"utf-8\").then((text) => text.split(\"\\n\"), () => null);\n this.fileLines.set(file, cached);\n }\n return cached;\n }\n\n /**\n * A symbol's source: its line range read from disk when the builder has a root and the node\n * has lines in a readable file, else CBM's snippet.\n */\n private async sourceOf(qn: string, node: { file_path?: unknown; start_line?: unknown; end_line?: unknown; lines?: unknown } | null): Promise<string | null> {\n if (this.root && node && typeof node.file_path === \"string\" && node.file_path) {\n const { start, end } = lineRange(node as GraphNode);\n if (start !== null && end !== null && start >= 1 && end >= start) {\n const lines = await this.lines(node.file_path);\n if (lines && end <= lines.length) return lines.slice(start - 1, end).join(\"\\n\");\n }\n }\n return (await this.graph.snippet(qn))?.source ?? null;\n }\n\n /** Strip the project prefix from a qualified name. */\n shortQn(qn: string): string {\n const p = this.graph.project;\n return qn.startsWith(p + \".\") ? qn.slice(p.length + 1) : qn;\n }\n\n /**\n * A route's handler when no HANDLES edge names one: the function, in a file with an edge into\n * the route, whose source quotes the route's path (or calls the route's method with it).\n */\n private async resolveRouteHandler(routeName: string): Promise<string | null> {\n const pathPart = routeName.replace(/^__route__[A-Z]+__/, \"\").replace(/\\{\\}.*$/, \"\");\n const method = (/^__route__([A-Z]+)__/.exec(routeName)?.[1] ?? \"\").toLowerCase();\n const files = new Set<string>();\n for (const qn of this.edges.routeCallers.get(routeName) ?? []) {\n const f = this.edges.qnFile.get(qn);\n if (f) files.add(f);\n }\n for (const file of files) {\n let fns = this.handlerCache.get(file);\n if (!fns) {\n fns = [];\n for (const n of await this.graph.search(\"Function\", file, [\"start_line\"])) {\n const source = await this.sourceOf(n.qualified_name, n);\n if (source) fns.push({ qn: n.qualified_name, source });\n }\n this.handlerCache.set(file, fns);\n }\n const hit =\n fns.find((f) => f.source.includes(`\"${pathPart}`) || f.source.includes(`'${pathPart}`)) ??\n fns.find((f) => method && f.source.includes(`.${method}(`) && f.source.includes(pathPart));\n if (hit) return hit.qn;\n }\n return null;\n }\n\n private cut(source: string): { source: string; total: number; truncated: boolean } {\n const all = source.split(\"\\n\");\n return all.length > this.sourceLines\n ? { source: all.slice(0, this.sourceLines).join(\"\\n\"), total: all.length, truncated: true }\n : { source, total: all.length, truncated: false };\n }\n\n async build(node: GraphNode): Promise<SymbolCard> {\n const { edges } = this;\n const qn = node.qualified_name;\n const short = (s: string) => this.shortQn(s);\n const { start, end } = lineRange(node);\n let source: string | null = null;\n let totalLines: number | null = null;\n let truncated = false;\n let routeHandlerQn: string | null = null;\n if (node.label !== \"Route\") {\n const text = await this.sourceOf(qn, node);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n } else {\n // A Route borrows its handler's source: by HANDLES edge, else by resolveRouteHandler.\n const direct = (edges.routeHandlers.get(qn) ?? []).filter((h) => edges.qnFile.has(h));\n routeHandlerQn = direct[0] ?? (await this.resolveRouteHandler(qn));\n if (routeHandlerQn) {\n const text = await this.sourceOf(routeHandlerQn, null);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n }\n }\n const callers = (edges.callers.get(qn) ?? []).map(short);\n const callees = (edges.callees.get(qn) ?? []).map(short);\n return {\n // Hashed on the full qualified name, as the labelled card sets were.\n id: cardId(this.options.scope, qn),\n label: node.label,\n name: node.name,\n qualified_name: short(qn),\n file_path: node.file_path || (routeHandlerQn ? edges.qnFile.get(routeHandlerQn) ?? null : null),\n start_line: start,\n end_line: end,\n signature: strOrNull(node.signature),\n docstring: strOrNull(node.docstring),\n is_exported: boolOrNull(node.is_exported),\n is_entry_point: boolOrNull(node.is_entry_point),\n complexity: numOrNull(node.complexity),\n parent_class: node.parent_class ? short(String(node.parent_class)) : null,\n base_classes: strArray(node.base_classes),\n return_type: strOrNull(node.return_type),\n callers: (node.label === \"Route\" && routeHandlerQn ? [short(routeHandlerQn), ...callers] : callers).slice(0, 12),\n callees: callees.slice(0, 12),\n callers_total: callers.length,\n callees_total: callees.length,\n imports: (node.file_path ? edges.importsByFile.get(node.file_path) ?? [] : []).slice(0, 15),\n env_vars: edges.envVars.get(qn) ?? [],\n routes_handled: node.label === \"Route\" ? [node.name.replace(/^__route__[A-Z]+__/, \"\")] : edges.handles.get(qn) ?? [],\n http_calls: (edges.httpCalls.get(qn) ?? []).slice(0, 8),\n inherits: (edges.inherits.get(qn) ?? []).map(short),\n implements: (edges.implements.get(qn) ?? []).map(short),\n route_method: node.label === \"Route\" ? strOrNull(node.method) : null,\n source,\n source_lines_total: totalLines,\n source_truncated: truncated,\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Build symbol cards from a CBM project.\n *\n * The classify-eval harness (cli/scripts/classify-eval/) builds its labelled card sets with this\n * module, so its accuracy numbers measure the code `dr analyzer classify` runs.\n */\nimport { createHash } from \"node:crypto\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { EdgeIndex } from \"./edge-index.js\";\nimport type { CardGraph, GraphNode, SymbolCard } from \"./types.js\";\n\n/**\n * Version of the card's fields and how they are derived. Bump it when a change would make a card\n * built now differ from one built before for the same graph, so stored cards and their\n * classifications can be told apart.\n */\nexport const CARD_VERSION = 1;\n\n/** CBM labels that become cards in the harness's card sets (classify adds the rules' labels). */\nexport const CARD_LABELS: readonly string[] = Object.freeze([\"Class\", \"Interface\", \"Function\", \"Method\", \"Module\", \"Route\"]);\n\n/** Extra search_graph fields each label's card reads. */\nexport const CARD_FIELDS: Readonly<Record<string, string[]>> = Object.freeze({\n Class: [\"docstring\", \"base_classes\", \"is_exported\", \"is_entry_point\", \"complexity\", \"start_line\", \"end_line\"],\n Interface: [\"docstring\", \"base_classes\", \"is_exported\", \"start_line\", \"end_line\"],\n Function: [\"signature\", \"docstring\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Method: [\"signature\", \"docstring\", \"parent_class\", \"is_exported\", \"is_entry_point\", \"complexity\", \"return_type\", \"start_line\", \"end_line\"],\n Module: [\"is_entry_point\", \"is_exported\", \"lines\", \"start_line\", \"end_line\"],\n Route: [\"method\", \"source\"],\n Type: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Enum: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Trait: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n Struct: [\"docstring\", \"is_exported\", \"start_line\", \"end_line\"],\n});\n\n/** Source file extensions: files whose symbols become cards. */\nexport const SOURCE_EXTENSIONS: readonly string[] = Object.freeze([\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"py\", \"go\", \"java\", \"kt\", \"rs\", \"cs\", \"php\", \"rb\", \"swift\", \"scala\", \"vue\", \"svelte\",\n]);\n\nexport const SOURCE_EXT = new RegExp(`\\\\.(${SOURCE_EXTENSIONS.join(\"|\")})$`, \"i\");\n\n/** Build output and vendored dependencies: never cards. The default for `dr analyzer classify`. */\nexport const VENDOR_EXCLUDE = /(^|\\/)(dist|build|node_modules|vendor|\\.venv|venv|__pycache__)(\\/|$)/;\n\n/**\n * The harness's sampling exclusion: vendor paths plus tests, fixtures, examples and scripts, so\n * labelled card sets are about application code. Tests are not excluded in production, where the\n * Tier-0 test rules file them in the testing layer.\n */\nexport const SAMPLE_EXCLUDE =\n /(^|\\/)(tests?|__tests__|fixtures?|__fixtures__|test-[^/]*|examples?|scripts|dist|node_modules|\\.venv|venv)(\\/|$)|\\.(test|spec)\\.[cm]?[jt]sx?$|(^|\\/)test_[^/]*\\.py$|(^|\\/)conftest\\.py$/;\n\n/** Stable card id for a symbol (full, project-prefixed qualified name) within a scope. */\nexport function cardId(scope: string, qualifiedName: string): string {\n return createHash(\"sha1\").update(`${scope}:${qualifiedName}`).digest(\"hex\").slice(0, 12);\n}\n\n/**\n * Whether a node of this label becomes a card: a route with a path, or a symbol in a source file\n * outside `exclude`, leaving out constructors, dunder methods and anonymous functions.\n */\nexport function isCandidate(node: GraphNode, label: string, exclude: RegExp = VENDOR_EXCLUDE): boolean {\n if (label === \"Route\") {\n if (!/^__route__[A-Z]+__\\//.test(node.name) || /\\.(ts|js|py|json)$/.test(node.name)) return false;\n return !(node.file_path && exclude.test(node.file_path));\n }\n if (!node.file_path) return false;\n if (!SOURCE_EXT.test(node.file_path) || node.file_path.startsWith(\"<\")) return false;\n if (exclude.test(node.file_path)) return false;\n if (label === \"Method\" && /^(constructor|__init__|__str__|__repr__)$/.test(node.name)) return false;\n if (label === \"Function\" && /^(anonymous|arrow|lambda|<anonymous>)$/i.test(node.name)) return false;\n return true;\n}\n\nfunction lineRange(node: GraphNode): { start: number | null; end: number | null } {\n const s = node.start_line != null ? Number(node.start_line) : null;\n const e = node.end_line != null ? Number(node.end_line) : null;\n if (s != null && !Number.isNaN(s)) return { start: s, end: e != null && !Number.isNaN(e) ? e : null };\n const m = /^(\\d+)-(\\d+)$/.exec(String(node.lines ?? \"\"));\n return m ? { start: Number(m[1]), end: Number(m[2]) } : { start: null, end: null };\n}\n\nfunction strOrNull(v: unknown): string | null {\n return typeof v === \"string\" && v.trim() ? v : null;\n}\nfunction boolOrNull(v: unknown): boolean | null {\n return typeof v === \"boolean\" ? v : null;\n}\nfunction numOrNull(v: unknown): number | null {\n return typeof v === \"number\" ? v : null;\n}\nfunction strArray(v: unknown): string[] {\n if (Array.isArray(v)) return v.filter((x): x is string => typeof x === \"string\");\n if (typeof v === \"string\" && v.trim()) return v.split(\",\").map((s) => s.trim()).filter(Boolean);\n return [];\n}\n\nexport interface CardBuilderOptions {\n /** Scope for card ids: a corpus name in the harness, the CBM project in production. */\n scope: string;\n /** Source lines kept per card (default 120). */\n sourceLines?: number;\n /**\n * Absolute path of the indexed codebase. When set, a symbol's source is read from its file by\n * the node's line range, one read per file; CBM's `get_code_snippet` (full source, at most 500\n * lines) is the fallback. Without it every card costs a snippet call, about 15 ms each.\n */\n sourceRoot?: string;\n}\n\n/**\n * Builds cards for one project. Holds the per-file handler cache that route-handler resolution\n * fills, so build every card of a run with the same builder.\n */\nexport class CardBuilder {\n private readonly sourceLines: number;\n private readonly handlerCache = new Map<string, Array<{ qn: string; source: string }>>();\n private readonly fileLines = new Map<string, Promise<string[] | null>>();\n private readonly root: string | null;\n\n constructor(\n private readonly graph: CardGraph,\n private readonly edges: EdgeIndex,\n private readonly options: CardBuilderOptions\n ) {\n this.sourceLines = options.sourceLines ?? 120;\n this.root = options.sourceRoot ? path.resolve(options.sourceRoot) : null;\n }\n\n /** A file's lines, read once; null when it is outside the root or unreadable. */\n private lines(file: string): Promise<string[] | null> {\n let cached = this.fileLines.get(file);\n if (!cached) {\n const abs = path.resolve(this.root!, file);\n cached =\n abs === this.root || !abs.startsWith(this.root! + path.sep)\n ? Promise.resolve(null)\n : readFile(abs, \"utf-8\").then((text) => text.split(\"\\n\"), () => null);\n this.fileLines.set(file, cached);\n }\n return cached;\n }\n\n /**\n * A symbol's source: its line range read from disk when the builder has a root and the node\n * has lines in a readable file, else CBM's snippet.\n */\n private async sourceOf(qn: string, node: { file_path?: unknown; start_line?: unknown; end_line?: unknown; lines?: unknown } | null): Promise<string | null> {\n if (this.root && node && typeof node.file_path === \"string\" && node.file_path) {\n const { start, end } = lineRange(node as GraphNode);\n if (start !== null && end !== null && start >= 1 && end >= start) {\n const lines = await this.lines(node.file_path);\n if (lines && end <= lines.length) return lines.slice(start - 1, end).join(\"\\n\");\n }\n }\n return (await this.graph.snippet(qn))?.source ?? null;\n }\n\n /** Strip the project prefix from a qualified name. */\n shortQn(qn: string): string {\n const p = this.graph.project;\n return qn.startsWith(p + \".\") ? qn.slice(p.length + 1) : qn;\n }\n\n /**\n * A route's handler when no HANDLES edge names one: the function, in a file with an edge into\n * the route, whose source quotes the route's path (or calls the route's method with it).\n */\n private async resolveRouteHandler(routeName: string): Promise<string | null> {\n const pathPart = routeName.replace(/^__route__[A-Z]+__/, \"\").replace(/\\{\\}.*$/, \"\");\n const method = (/^__route__([A-Z]+)__/.exec(routeName)?.[1] ?? \"\").toLowerCase();\n const files = new Set<string>();\n for (const qn of this.edges.routeCallers.get(routeName) ?? []) {\n const f = this.edges.qnFile.get(qn);\n if (f) files.add(f);\n }\n for (const file of files) {\n let fns = this.handlerCache.get(file);\n if (!fns) {\n fns = [];\n for (const n of await this.graph.search(\"Function\", file, [\"start_line\"])) {\n const source = await this.sourceOf(n.qualified_name, n);\n if (source) fns.push({ qn: n.qualified_name, source });\n }\n this.handlerCache.set(file, fns);\n }\n const hit =\n fns.find((f) => f.source.includes(`\"${pathPart}`) || f.source.includes(`'${pathPart}`)) ??\n fns.find((f) => method && f.source.includes(`.${method}(`) && f.source.includes(pathPart));\n if (hit) return hit.qn;\n }\n return null;\n }\n\n private cut(source: string): { source: string; total: number; truncated: boolean } {\n const all = source.split(\"\\n\");\n return all.length > this.sourceLines\n ? { source: all.slice(0, this.sourceLines).join(\"\\n\"), total: all.length, truncated: true }\n : { source, total: all.length, truncated: false };\n }\n\n async build(node: GraphNode): Promise<SymbolCard> {\n const { edges } = this;\n const qn = node.qualified_name;\n const short = (s: string) => this.shortQn(s);\n const { start, end } = lineRange(node);\n let source: string | null = null;\n let totalLines: number | null = null;\n let truncated = false;\n let routeHandlerQn: string | null = null;\n if (node.label !== \"Route\") {\n const text = await this.sourceOf(qn, node);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n } else {\n // A Route borrows its handler's source: by HANDLES edge, else by resolveRouteHandler.\n const direct = (edges.routeHandlers.get(qn) ?? []).filter((h) => edges.qnFile.has(h));\n routeHandlerQn = direct[0] ?? (await this.resolveRouteHandler(qn));\n if (routeHandlerQn) {\n const text = await this.sourceOf(routeHandlerQn, null);\n if (text) ({ source, total: totalLines, truncated } = this.cut(text));\n }\n }\n const callers = (edges.callers.get(qn) ?? []).map(short);\n const callees = (edges.callees.get(qn) ?? []).map(short);\n return {\n // Hashed on the full qualified name, as the labelled card sets were.\n id: cardId(this.options.scope, qn),\n label: node.label,\n name: node.name,\n qualified_name: short(qn),\n file_path: node.file_path || (routeHandlerQn ? edges.qnFile.get(routeHandlerQn) ?? null : null),\n start_line: start,\n end_line: end,\n signature: strOrNull(node.signature),\n docstring: strOrNull(node.docstring),\n is_exported: boolOrNull(node.is_exported),\n is_entry_point: boolOrNull(node.is_entry_point),\n complexity: numOrNull(node.complexity),\n parent_class: node.parent_class ? short(String(node.parent_class)) : null,\n base_classes: strArray(node.base_classes),\n return_type: strOrNull(node.return_type),\n callers: (node.label === \"Route\" && routeHandlerQn ? [short(routeHandlerQn), ...callers] : callers).slice(0, 12),\n callees: callees.slice(0, 12),\n callers_total: callers.length,\n callees_total: callees.length,\n imports: (node.file_path ? edges.importsByFile.get(node.file_path) ?? [] : []).slice(0, 15),\n env_vars: edges.envVars.get(qn) ?? [],\n routes_handled: node.label === \"Route\" ? [node.name.replace(/^__route__[A-Z]+__/, \"\")] : edges.handles.get(qn) ?? [],\n http_calls: (edges.httpCalls.get(qn) ?? []).slice(0, 8),\n inherits: (edges.inherits.get(qn) ?? []).map(short),\n implements: (edges.implements.get(qn) ?? []).map(short),\n route_method: node.label === \"Route\" ? strOrNull(node.method) : null,\n source,\n source_lines_total: totalLines,\n source_truncated: truncated,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,138 @@
1
+ import { CARD_FIELDS, CardBuilder, VENDOR_EXCLUDE, isCandidate } from "./build.js";
2
+ import { defaultLabels } from "./classify.js";
3
+ import { buildEdgeIndex } from "./edge-index.js";
4
+ import { buildFactsIndex, factsFor } from "./facts.js";
5
+ import { TEST_PATH, namedRoutePath, routesCalledIn } from "./routes.js";
6
+ import { tier0ForCard } from "./tier0.js";
7
+ const CHECK_FILES_PER_QUERY = 200, FILE_JOINED_EDGES = /* @__PURE__ */ new Set(["IMPORTS", "TESTS_FILE"]);
8
+ function extractionConfigFrom(rules, edgeMappings, nodeMappings) {
9
+ const labelDecisions = {}, labelDefaults = {};
10
+ for (const m of nodeMappings)
11
+ !m.dr_layer || !m.dr_element_type || (labelDefaults[m.cbm_label] = `${m.dr_layer}.${m.dr_element_type}`, m.confidence === "high" && (labelDecisions[m.cbm_label] = { layer: m.dr_layer, type: m.dr_element_type }));
12
+ return {
13
+ rules,
14
+ edgeMappings: edgeMappings.filter((m) => m.usage === "verification_and_mapping"),
15
+ labelDecisions,
16
+ labelDefaults
17
+ };
18
+ }
19
+ class ExtractionTimeoutError extends Error {
20
+ stage;
21
+ constructor(stage) {
22
+ super(`Extraction ran out of time while ${stage}`), this.stage = stage, this.name = "ExtractionTimeoutError";
23
+ }
24
+ }
25
+ function parseRouteName(name) {
26
+ const m = /^__route__([A-Z]+)__(\/.*)$/.exec(name);
27
+ return m ? { method: m[1], path: m[2] } : null;
28
+ }
29
+ function shortQualifiedName(qn, project) {
30
+ return qn.startsWith(project + ".") ? qn.slice(project.length + 1) : qn;
31
+ }
32
+ function symbolSpellings(qualifiedName, name, project) {
33
+ const route = parseRouteName(name) ?? parseRouteName(qualifiedName);
34
+ if (route)
35
+ return [`${route.method} ${route.path}`, `__route__${route.method}__${route.path}`];
36
+ const short = shortQualifiedName(qualifiedName, project), parts = short.split("."), out = /* @__PURE__ */ new Set([name]);
37
+ return parts.length >= 2 && parts[parts.length - 1] === name && out.add(`${parts[parts.length - 2]}.${name}`), out.add(short), [...out].filter(Boolean);
38
+ }
39
+ function cypherString(s) {
40
+ return `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
41
+ }
42
+ function str(v) {
43
+ return typeof v == "string" && v ? v : null;
44
+ }
45
+ async function collectExtraction(graph, config, analyzer, options = {}) {
46
+ const checkDeadline = (stage) => {
47
+ if (options.deadline !== void 0 && Date.now() > options.deadline)
48
+ throw new ExtractionTimeoutError(stage);
49
+ }, labels = options.labels?.length ? options.labels : [.../* @__PURE__ */ new Set([...defaultLabels(config.rules), ...Object.keys(config.labelDecisions)])], routes = /* @__PURE__ */ new Set(), routeNodes = [], nodes = [], seen = /* @__PURE__ */ new Set();
50
+ for (const label of labels)
51
+ for (const node of await graph.search(label, void 0, CARD_FIELDS[label] ?? [])) {
52
+ if (label === "Route") {
53
+ for (const s of symbolSpellings(node.qualified_name, node.name, graph.project))
54
+ routes.add(s);
55
+ routeNodes.push(node);
56
+ }
57
+ !isCandidate(node, label, VENDOR_EXCLUDE) || seen.has(node.qualified_name) || label === "Method" && node.qualified_name.split(".").slice(-2, -1)[0] === node.name || options.files && node.file_path && !options.files.has(node.file_path) || (seen.add(node.qualified_name), nodes.push(node));
58
+ }
59
+ const edgeIndex = await buildEdgeIndex(graph), factsIndex = await buildFactsIndex(graph);
60
+ if (checkDeadline("reading the graph"), options.files) {
61
+ const files = options.files, inFiles = (qns) => (qns ?? []).some((qn) => files.has(edgeIndex.qnFile.get(qn) ?? ""));
62
+ for (let i = nodes.length - 1; i >= 0; i--) {
63
+ const n = nodes[i];
64
+ n.label === "Route" && !n.file_path && !inFiles(edgeIndex.routeHandlers.get(n.qualified_name)) && !inFiles(edgeIndex.routeCallers.get(n.qualified_name)) && nodes.splice(i, 1);
65
+ }
66
+ }
67
+ const builder = new CardBuilder(graph, edgeIndex, { scope: graph.project, sourceRoot: options.sourceRoot }), handled = (n) => !!n.file_path || (edgeIndex.routeHandlers.get(n.qualified_name) ?? []).length > 0, served = routeNodes.filter(handled).map((n) => ({ name: n.qualified_name, ...parseRouteName(n.qualified_name) })).filter((r) => r.method), routeNames = /* @__PURE__ */ new Map();
68
+ if (routeNodes.length)
69
+ for (const [qn, name] of await graph.rows("MATCH (r:Route) RETURN r.qualified_name, r.name"))
70
+ typeof qn == "string" && typeof name == "string" && routeNames.set(qn, name);
71
+ const symbols = [], summary = { cards: 0, decided: 0, rejected: 0, narrowed: 0, undecided: 0, no_source: 0 };
72
+ let done = 0;
73
+ for (const node of nodes) {
74
+ done % 50 === 0 && checkDeadline("building cards");
75
+ const card = await builder.build(node);
76
+ if (options.onProgress?.(++done, nodes.length), options.files && !(card.file_path && options.files.has(card.file_path)))
77
+ continue;
78
+ const facts = factsFor(factsIndex, node.qualified_name, card.file_path), route = card.label === "Route" ? parseRouteName(card.name) : null, unhandled = !!route && !handled(node);
79
+ let tier0 = unhandled ? null : tier0ForCard(card, facts, config.rules);
80
+ const byLabel = config.labelDecisions[card.label];
81
+ !tier0 && byLabel && !unhandled && (tier0 = { kind: "decide", layer: byLabel.layer, type: byLabel.type, rule: `node-mapping:${card.label}`, confidence: "high" }), summary.cards++, card.source === null && card.label !== "Route" && summary.no_source++, tier0 ? tier0.kind === "decide" ? summary.decided++ : tier0.kind === "reject" ? summary.rejected++ : summary.narrowed++ : summary.undecided++, symbols.push({
82
+ qualifiedName: node.qualified_name,
83
+ card,
84
+ facts,
85
+ tier0,
86
+ ...route ? { routePath: namedRoutePath(route.path, routeNames.get(node.qualified_name), card.source) } : {},
87
+ ...unhandled ? { unhandled: !0 } : {}
88
+ });
89
+ }
90
+ const inScope = new Set(symbols.map((s) => s.qualifiedName)), scopeFiles = new Set(symbols.map((s) => s.card.file_path).filter((f) => !!f)), edges = [], edgeSeen = /* @__PURE__ */ new Set();
91
+ for (const type of [...new Set(config.edgeMappings.map((m) => m.cbm_edge))]) {
92
+ checkDeadline("reading edges");
93
+ const rows = await graph.rows(`MATCH (a)-[r:${type}]->(b) RETURN a.qualified_name, a.file_path, a.name, b.qualified_name, b.file_path, b.name`), fileJoined = FILE_JOINED_EDGES.has(type);
94
+ for (const [aq, af, an, bq, bf, bn] of rows) {
95
+ if (typeof aq != "string" || typeof bq != "string" || aq === bq)
96
+ continue;
97
+ const from = { qualifiedName: aq, name: str(an) ?? aq, file: str(af) }, to = { qualifiedName: bq, name: str(bn) ?? bq, file: str(bf) };
98
+ if (!(!options.files || inScope.has(aq) || inScope.has(bq) || fileJoined && (from.file !== null && scopeFiles.has(from.file) || to.file !== null && scopeFiles.has(to.file))))
99
+ continue;
100
+ const key = `${type}|${aq}|${bq}`;
101
+ edgeSeen.has(key) || (edgeSeen.add(key), edges.push({ type, from, to }));
102
+ }
103
+ }
104
+ if (config.edgeMappings.some((m) => m.cbm_edge === "HTTP_CALLS")) {
105
+ for (const s of symbols)
106
+ if (!(!(s.tier0?.kind === "decide" && s.tier0.layer === "testing") && !(s.card.file_path && TEST_PATH.test(s.card.file_path))))
107
+ for (const name of routesCalledIn(s.card.source, served)) {
108
+ const key = `HTTP_CALLS|${s.qualifiedName}|${name}`;
109
+ edgeSeen.has(key) || (edgeSeen.add(key), edges.push({
110
+ type: "HTTP_CALLS",
111
+ from: { qualifiedName: s.qualifiedName, name: s.card.name, file: s.card.file_path },
112
+ to: { qualifiedName: name, name, file: null }
113
+ }));
114
+ }
115
+ }
116
+ const fileSymbols = {}, checkFiles = [...new Set(options.checkFiles ?? [])];
117
+ for (let i = 0; i < checkFiles.length; i += CHECK_FILES_PER_QUERY) {
118
+ const batch = checkFiles.slice(i, i + CHECK_FILES_PER_QUERY), rows = await graph.rows(`MATCH (n) WHERE n.file_path IN [${batch.map(cypherString).join(", ")}] RETURN n.file_path, n.qualified_name, n.name, n.label`);
119
+ for (const [file, qn, name, label] of rows) {
120
+ if (typeof file != "string" || typeof qn != "string" || label === "File" || label === "Folder")
121
+ continue;
122
+ const found = fileSymbols[file] ??= [];
123
+ for (const sp of symbolSpellings(qn, str(name) ?? qn, graph.project))
124
+ found.includes(sp) || found.push(sp);
125
+ }
126
+ }
127
+ return { analyzer, project: graph.project, config, symbols, edges, fileSymbols, routes: [...routes], summary };
128
+ }
129
+ export {
130
+ ExtractionTimeoutError,
131
+ FILE_JOINED_EDGES,
132
+ collectExtraction,
133
+ extractionConfigFrom,
134
+ parseRouteName,
135
+ shortQualifiedName,
136
+ symbolSpellings
137
+ };
138
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/analyzers/cards/extract.ts"],
4
+ "sourcesContent": ["/**\n * What the extraction cascade reads from a CBM project: a card, its facts and its Tier-0 outcome\n * for every candidate symbol in scope, and the CBM edges the edge mapping turns into\n * relationships. No network, no model calls, no writes. `codebase_extract` plans and stages a\n * changeset from it (src/analyzers/extraction-plan.ts).\n *\n * Tier 1 (a decision model) is not wired: a symbol Tier 0 narrows or leaves open is returned\n * undecided, for the calling agent. A Tier-1 pass would run between the Tier-0 outcome and the\n * plan, on the cards this returns (docs/analyzers/CASCADE_DESIGN.md, build step 3).\n */\nimport type { Tier0Outcome, Tier0Rule } from \"../tier0-rules.js\";\nimport type { AnalyzerEdgeMapping, AnalyzerNodeMapping } from \"../types.js\";\nimport { CARD_FIELDS, CardBuilder, VENDOR_EXCLUDE, isCandidate } from \"./build.js\";\nimport { defaultLabels } from \"./classify.js\";\nimport { buildEdgeIndex } from \"./edge-index.js\";\nimport { buildFactsIndex, factsFor } from \"./facts.js\";\nimport { TEST_PATH, namedRoutePath, routesCalledIn } from \"./routes.js\";\nimport { tier0ForCard } from \"./tier0.js\";\nimport type { CardFacts, CardGraph, GraphNode, SymbolCard } from \"./types.js\";\n\n/** Files whose symbols one query lists. */\nconst CHECK_FILES_PER_QUERY = 200;\n\n/** CBM edges whose ends are files: an end is joined to the Module card of its file. */\nexport const FILE_JOINED_EDGES: ReadonlySet<string> = new Set([\"IMPORTS\", \"TESTS_FILE\"]);\n\n/** A layer and type a label maps to outright (the node mapping's high-confidence entries). */\nexport interface LabelDecision {\n layer: string;\n type: string;\n}\n\nexport interface ExtractionConfig {\n rules: Tier0Rule[];\n /** Edge mappings that create relationships (`usage: \"verification_and_mapping\"`). */\n edgeMappings: AnalyzerEdgeMapping[];\n /**\n * Labels whose node mapping decides the type when no Tier-0 rule matches: a CBM Route is an\n * `api.operation` (docs/analyzers/CASCADE_DESIGN.md, \"A route handler is an application\n * function, not an operation\").\n */\n labelDecisions: Record<string, LabelDecision>;\n /** The node mapping's default type per label, offered to the agent as a hint for undecided cards. */\n labelDefaults: Record<string, string>;\n}\n\n/** The extraction settings an analyzer mapping gives: its Tier-0 rules, edge and node mappings. */\nexport function extractionConfigFrom(\n rules: Tier0Rule[],\n edgeMappings: AnalyzerEdgeMapping[],\n nodeMappings: AnalyzerNodeMapping[]\n): ExtractionConfig {\n const labelDecisions: Record<string, LabelDecision> = {};\n const labelDefaults: Record<string, string> = {};\n for (const m of nodeMappings) {\n if (!m.dr_layer || !m.dr_element_type) continue;\n labelDefaults[m.cbm_label] = `${m.dr_layer}.${m.dr_element_type}`;\n if (m.confidence === \"high\") labelDecisions[m.cbm_label] = { layer: m.dr_layer, type: m.dr_element_type };\n }\n return {\n rules,\n edgeMappings: edgeMappings.filter((m) => m.usage === \"verification_and_mapping\"),\n labelDecisions,\n labelDefaults,\n };\n}\n\nexport interface ExtractOptions {\n /** Absolute path of the indexed codebase, so source is read from disk. */\n sourceRoot?: string;\n /** Only symbols in these files (paths relative to the codebase root); every file when absent. */\n files?: ReadonlySet<string>;\n /** CBM labels to extract (default: the card labels plus every label a rule names). */\n labels?: string[];\n /**\n * Files whose symbols the caller wants listed (in {@link ExtractionGraph.fileSymbols}), to check\n * that the symbols existing elements name are still there.\n */\n checkFiles?: string[];\n /** Give up past this time (epoch ms), with an {@link ExtractionTimeoutError}. */\n deadline?: number;\n onProgress?: (done: number, total: number) => void;\n}\n\n/** The extraction ran past its deadline. */\nexport class ExtractionTimeoutError extends Error {\n constructor(readonly stage: string) {\n super(`Extraction ran out of time while ${stage}`);\n this.name = \"ExtractionTimeoutError\";\n }\n}\n\nexport interface ExtractedSymbol {\n /** Full qualified name (with the CBM project prefix; none for routes). */\n qualifiedName: string;\n card: SymbolCard;\n facts: CardFacts;\n /** The Tier-0 outcome, or a node-mapping decision (`rule: \"node-mapping:<label>\"`), or null. */\n tier0: Tier0Outcome | null;\n /** For a route: its path with the parameters named where the code names them (`/owners/{ownerId}`). */\n routePath?: string;\n /**\n * For a route: nothing in the code handles it (no file of its own, no HANDLES edge). It is a URL\n * client or test code calls, not one this code serves, so it is never decided.\n */\n unhandled?: true;\n}\n\nexport interface EdgeEnd {\n qualifiedName: string;\n name: string;\n file: string | null;\n}\n\nexport interface ExtractedEdge {\n type: string;\n from: EdgeEnd;\n to: EdgeEnd;\n}\n\nexport interface ExtractionGraph {\n analyzer: string;\n project: string;\n config: ExtractionConfig;\n symbols: ExtractedSymbol[];\n /** Mapped CBM edges with at least one end in scope. */\n edges: ExtractedEdge[];\n /**\n * For each requested file the graph has nodes in: every spelling of their symbols (name,\n * `Parent.name`, qualified name without the project). A file the graph has no nodes in is absent.\n */\n fileSymbols: Record<string, string[]>;\n /** Every route the graph has, as `METHOD /path` and as `__route__METHOD__/path`. */\n routes: string[];\n summary: { cards: number; decided: number; rejected: number; narrowed: number; undecided: number; no_source: number };\n}\n\n/** `__route__GET__/owners/{}` -> { method: \"GET\", path: \"/owners/{}\" }, or null for anything else. */\nexport function parseRouteName(name: string): { method: string; path: string } | null {\n const m = /^__route__([A-Z]+)__(\\/.*)$/.exec(name);\n return m ? { method: m[1], path: m[2] } : null;\n}\n\n/** A qualified name without the project prefix. */\nexport function shortQualifiedName(qn: string, project: string): string {\n return qn.startsWith(project + \".\") ? qn.slice(project.length + 1) : qn;\n}\n\n/**\n * The ways an element's source reference may name a symbol: its name, `Parent.name` and its\n * qualified name without the project; for a route, `METHOD /path` and CBM's route name.\n */\nexport function symbolSpellings(qualifiedName: string, name: string, project: string): string[] {\n const route = parseRouteName(name) ?? parseRouteName(qualifiedName);\n if (route) return [`${route.method} ${route.path}`, `__route__${route.method}__${route.path}`];\n const short = shortQualifiedName(qualifiedName, project);\n const parts = short.split(\".\");\n const out = new Set<string>([name]);\n if (parts.length >= 2 && parts[parts.length - 1] === name) out.add(`${parts[parts.length - 2]}.${name}`);\n out.add(short);\n return [...out].filter(Boolean);\n}\n\nfunction cypherString(s: string): string {\n return `'${s.replace(/\\\\/g, \"\\\\\\\\\").replace(/'/g, \"\\\\'\")}'`;\n}\n\nfunction str(v: unknown): string | null {\n return typeof v === \"string\" && v ? v : null;\n}\n\n/**\n * Read a project for extraction. Cards and Tier 0 are those of `dr analyzer classify\n * --tier0-only`; a label the node mapping decides outright (Route) is decided when no rule matches.\n */\nexport async function collectExtraction(\n graph: CardGraph,\n config: ExtractionConfig,\n analyzer: string,\n options: ExtractOptions = {}\n): Promise<ExtractionGraph> {\n const checkDeadline = (stage: string) => {\n if (options.deadline !== undefined && Date.now() > options.deadline) throw new ExtractionTimeoutError(stage);\n };\n const labels = options.labels?.length\n ? options.labels\n : [...new Set([...defaultLabels(config.rules), ...Object.keys(config.labelDecisions)])];\n\n // Candidates for cards, and every route.\n const routes = new Set<string>();\n const routeNodes: GraphNode[] = [];\n const nodes: GraphNode[] = [];\n const seen = new Set<string>();\n for (const label of labels) {\n for (const node of await graph.search(label, undefined, CARD_FIELDS[label] ?? [])) {\n if (label === \"Route\") {\n for (const s of symbolSpellings(node.qualified_name, node.name, graph.project)) routes.add(s);\n routeNodes.push(node);\n }\n if (!isCandidate(node, label, VENDOR_EXCLUDE) || seen.has(node.qualified_name)) continue;\n // A Java or C# constructor is named after its class: like `constructor`, not an element.\n if (label === \"Method\" && node.qualified_name.split(\".\").slice(-2, -1)[0] === node.name) continue;\n // A route often has no file of its own (it takes its handler's); keep it until its card says.\n if (options.files && node.file_path && !options.files.has(node.file_path)) continue;\n seen.add(node.qualified_name);\n nodes.push(node);\n }\n }\n\n const edgeIndex = await buildEdgeIndex(graph);\n const factsIndex = await buildFactsIndex(graph);\n checkDeadline(\"reading the graph\");\n // With a file scope, a route with no file of its own is built (its handler resolved, its source\n // read) only when its handler, or something that reaches it, is in a scoped file.\n if (options.files) {\n const files = options.files;\n const inFiles = (qns: string[] | undefined) => (qns ?? []).some((qn) => files.has(edgeIndex.qnFile.get(qn) ?? \"\"));\n for (let i = nodes.length - 1; i >= 0; i--) {\n const n = nodes[i];\n if (n.label === \"Route\" && !n.file_path && !inFiles(edgeIndex.routeHandlers.get(n.qualified_name)) && !inFiles(edgeIndex.routeCallers.get(n.qualified_name))) {\n nodes.splice(i, 1);\n }\n }\n }\n const builder = new CardBuilder(graph, edgeIndex, { scope: graph.project, sourceRoot: options.sourceRoot });\n\n // A route is served here when it has a file of its own or a HANDLES edge.\n const handled = (n: GraphNode) => !!n.file_path || (edgeIndex.routeHandlers.get(n.qualified_name) ?? []).length > 0;\n const served = routeNodes\n .filter(handled)\n .map((n) => ({ name: n.qualified_name, ...parseRouteName(n.qualified_name)! }))\n .filter((r) => r.method);\n // CBM's stored route names keep the code's parameter names (`/owners/{ownerId}`); search_graph\n // returns them as `{}`, a query returns them as stored.\n const routeNames = new Map<string, string>();\n if (routeNodes.length) {\n for (const [qn, name] of await graph.rows(\"MATCH (r:Route) RETURN r.qualified_name, r.name\")) {\n if (typeof qn === \"string\" && typeof name === \"string\") routeNames.set(qn, name);\n }\n }\n\n const symbols: ExtractedSymbol[] = [];\n const summary = { cards: 0, decided: 0, rejected: 0, narrowed: 0, undecided: 0, no_source: 0 };\n let done = 0;\n for (const node of nodes) {\n if (done % 50 === 0) checkDeadline(\"building cards\");\n const card = await builder.build(node);\n options.onProgress?.(++done, nodes.length);\n if (options.files && !(card.file_path && options.files.has(card.file_path))) continue;\n const facts = factsFor(factsIndex, node.qualified_name, card.file_path);\n const route = card.label === \"Route\" ? parseRouteName(card.name) : null;\n // A route with no file of its own and no HANDLES edge: a URL that test or client code calls,\n // whoever the callers are.\n const unhandled = !!route && !handled(node);\n let tier0 = unhandled ? null : tier0ForCard(card, facts, config.rules);\n const byLabel = config.labelDecisions[card.label];\n if (!tier0 && byLabel && !unhandled) {\n tier0 = { kind: \"decide\", layer: byLabel.layer, type: byLabel.type, rule: `node-mapping:${card.label}`, confidence: \"high\" };\n }\n summary.cards++;\n if (card.source === null && card.label !== \"Route\") summary.no_source++;\n if (!tier0) summary.undecided++;\n else if (tier0.kind === \"decide\") summary.decided++;\n else if (tier0.kind === \"reject\") summary.rejected++;\n else summary.narrowed++;\n symbols.push({\n qualifiedName: node.qualified_name,\n card,\n facts,\n tier0,\n ...(route ? { routePath: namedRoutePath(route.path, routeNames.get(node.qualified_name), card.source) } : {}),\n ...(unhandled ? { unhandled: true as const } : {}),\n });\n }\n\n // Mapped edges with an end in scope (all of them when the whole project is in scope).\n const inScope = new Set(symbols.map((s) => s.qualifiedName));\n const scopeFiles = new Set(symbols.map((s) => s.card.file_path).filter((f): f is string => !!f));\n const edges: ExtractedEdge[] = [];\n const edgeSeen = new Set<string>();\n for (const type of [...new Set(config.edgeMappings.map((m) => m.cbm_edge))]) {\n checkDeadline(\"reading edges\");\n const rows = await graph.rows(\n `MATCH (a)-[r:${type}]->(b) RETURN a.qualified_name, a.file_path, a.name, b.qualified_name, b.file_path, b.name`\n );\n const fileJoined = FILE_JOINED_EDGES.has(type);\n for (const [aq, af, an, bq, bf, bn] of rows) {\n if (typeof aq !== \"string\" || typeof bq !== \"string\" || aq === bq) continue;\n const from = { qualifiedName: aq, name: str(an) ?? aq, file: str(af) };\n const to = { qualifiedName: bq, name: str(bn) ?? bq, file: str(bf) };\n const touches =\n !options.files ||\n inScope.has(aq) ||\n inScope.has(bq) ||\n (fileJoined && ((from.file !== null && scopeFiles.has(from.file)) || (to.file !== null && scopeFiles.has(to.file))));\n if (!touches) continue;\n const key = `${type}|${aq}|${bq}`;\n if (edgeSeen.has(key)) continue;\n edgeSeen.add(key);\n edges.push({ type, from, to });\n }\n }\n\n // A test that calls a served route by its URL (MockMvc, RestTemplate, supertest) makes the HTTP\n // call CBM has no edge for: one HTTP_CALLS edge from the test to the route.\n if (config.edgeMappings.some((m) => m.cbm_edge === \"HTTP_CALLS\")) {\n for (const s of symbols) {\n const testing = s.tier0?.kind === \"decide\" && s.tier0.layer === \"testing\";\n if (!testing && !(s.card.file_path && TEST_PATH.test(s.card.file_path))) continue;\n for (const name of routesCalledIn(s.card.source, served)) {\n const key = `HTTP_CALLS|${s.qualifiedName}|${name}`;\n if (edgeSeen.has(key)) continue;\n edgeSeen.add(key);\n edges.push({\n type: \"HTTP_CALLS\",\n from: { qualifiedName: s.qualifiedName, name: s.card.name, file: s.card.file_path },\n to: { qualifiedName: name, name, file: null },\n });\n }\n }\n }\n\n // The symbols in each file the caller asked about, of every label (a React component declared\n // as `const X = () => ...` is a Variable), a batch of files per query.\n const fileSymbols: Record<string, string[]> = {};\n const checkFiles = [...new Set(options.checkFiles ?? [])];\n for (let i = 0; i < checkFiles.length; i += CHECK_FILES_PER_QUERY) {\n const batch = checkFiles.slice(i, i + CHECK_FILES_PER_QUERY);\n const rows = await graph.rows(\n `MATCH (n) WHERE n.file_path IN [${batch.map(cypherString).join(\", \")}] RETURN n.file_path, n.qualified_name, n.name, n.label`\n );\n for (const [file, qn, name, label] of rows) {\n // A file the graph holds only as a File node (a document, a config) has no symbols to check.\n if (typeof file !== \"string\" || typeof qn !== \"string\" || label === \"File\" || label === \"Folder\") continue;\n const found = (fileSymbols[file] ??= []);\n for (const sp of symbolSpellings(qn, str(name) ?? qn, graph.project)) if (!found.includes(sp)) found.push(sp);\n }\n }\n\n return { analyzer, project: graph.project, config, symbols, edges, fileSymbols, routes: [...routes], summary };\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,96 @@
1
+ function normRoutePath(p) {
2
+ return p.replace(/\{[^}]*\}|:[A-Za-z_]\w*/g, "{}").replace(/\/+$/, "") || "/";
3
+ }
4
+ function slots(p) {
5
+ return (p.match(/\{\}/g) ?? []).length;
6
+ }
7
+ function fill(p, names) {
8
+ let i = 0;
9
+ return p.replace(/\{\}/g, () => `{${names[i++]}}`);
10
+ }
11
+ function templateNames(p) {
12
+ return [...p.matchAll(/\{(\w+)[^}]*\}|:([A-Za-z_]\w*)/g)].map((m) => m[1] ?? m[2]);
13
+ }
14
+ const NOT_PATH = /^(req|res|request|response|next|ctx|context|model|modelMap|session|self|cls|body|payload|result|bindingResult|redirectAttributes|principal|page|pageable|locale|headers?|form|dto)$/i, SEGMENT_TYPE = /^(final\s+)?(int|long|short|Integer|Long|Short|String|string|number|str|UUID|uuid\.UUID|bool|boolean|Boolean|Int|i32|i64|u32|u64|&str)\b/, NOT_PATH_TYPE = /\b(Model|ModelMap|BindingResult|HttpServletRequest|HttpServletResponse|RedirectAttributes|Principal|Pageable|Locale|Errors|WebRequest|Request|Response|NextFunction)\b/;
15
+ function parameterList(source) {
16
+ const m = /\b[\w$]+\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)\s*(?::[^{=]+)?(?:throws [\w., ]+)?\s*(?:\{|=>|->|:)/.exec(source);
17
+ return m ? m[1] : null;
18
+ }
19
+ function splitParams(list) {
20
+ const out = [];
21
+ let depth = 0, cur = "";
22
+ for (const ch of list)
23
+ "(<[{".includes(ch) ? depth++ : ")>]}".includes(ch) && depth--, ch === "," && depth === 0 ? (out.push(cur.trim()), cur = "") : cur += ch;
24
+ return cur.trim() && out.push(cur.trim()), out;
25
+ }
26
+ function paramName(p) {
27
+ const annotated = /@PathVariable\s*\(\s*(?:(?:name|value)\s*=\s*)?["'](\w+)["']/.exec(p)?.[1] ?? null, pathMarked = annotated !== null || /@(PathVariable|PathParam|Param)\b|\bPath\s*\(/.test(p), bare = p.replace(/@\w+(\s*\([^)]*\))?/g, "").replace(/=.*$/, "").trim();
28
+ let name, type = "";
29
+ const colon = /^([A-Za-z_$][\w$]*)\??\s*:\s*(.+)$/.exec(bare);
30
+ if (colon)
31
+ name = colon[1], type = colon[2];
32
+ else {
33
+ const words = bare.split(/\s+/).filter(Boolean);
34
+ name = words[words.length - 1]?.replace(/^[*&.]+/, ""), type = words.slice(0, -1).join(" ");
35
+ }
36
+ return !name || !/^[A-Za-z_$][\w$]*$/.test(name) ? null : { name, annotated: pathMarked ? annotated ?? name : null, type };
37
+ }
38
+ function namedRoutePath(path, cbmName, handlerSource) {
39
+ const n = slots(path);
40
+ if (n === 0)
41
+ return path;
42
+ if (typeof cbmName == "string" && cbmName.startsWith("/") && normRoutePath(cbmName) === normRoutePath(path) && templateNames(cbmName).length === n)
43
+ return fill(path, templateNames(cbmName));
44
+ if (!handlerSource)
45
+ return path;
46
+ const norm = normRoutePath(path);
47
+ for (const m of handlerSource.matchAll(/["'`](\/[^"'`\s]*)["'`]/g)) {
48
+ const literal = m[1], names = templateNames(literal);
49
+ if (names.length === n && norm.endsWith(normRoutePath(literal)))
50
+ return fill(path, names);
51
+ }
52
+ const list = parameterList(handlerSource);
53
+ if (list === null)
54
+ return path;
55
+ const params = splitParams(list).map(paramName).filter((p) => !!p), marked = params.filter((p) => p.annotated).map((p) => p.annotated);
56
+ if (marked.length === n)
57
+ return fill(path, marked);
58
+ const plain = params.filter((p) => !NOT_PATH.test(p.name) && !NOT_PATH_TYPE.test(p.type) && (!p.type || SEGMENT_TYPE.test(p.type.trim()))).map((p) => p.name);
59
+ return plain.length === n ? fill(path, plain) : path;
60
+ }
61
+ const TEST_PATH = /(^|\/)(tests?|__tests__)(\/|$)|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)test_[^/]*\.py$|_test\.(py|go)$|Tests?\.(java|kt|cs)$|_spec\.rb$|Spec\.(kt|groovy|scala)$/, METHODS = "get|post|put|patch|delete", PATH_ARG = "\\(\\s*[\"'`](\\/[^\"'`\\s]*)[\"'`]", CLIENT_CALL = new RegExp(`\\b(?:mockMvc|mvc|webTestClient|testClient|client|restTemplate|testRestTemplate|RequestEntity|MockMvcRequestBuilders|http|httpClient|axios|supertest\\([^)]*\\)|request\\([^)]*\\)|agent|api)\\s*\\.\\s*(${METHODS})(?:ForEntity|ForObject|ForLocation)?\\s*${PATH_ARG}`, "gi"), BARE_CALL = new RegExp(`(?<![\\w$.])(${METHODS})\\s*${PATH_ARG}`, "g"), FETCH_CALL = new RegExp(`\\bfetch\\s*${PATH_ARG}`, "g"), USES_HTTP_CLIENT = /\b(mockMvc|perform\s*\(|webTestClient|TestClient|restTemplate|supertest|request\s*\(\s*app|axios|httpClient)\b/i;
62
+ function routesCalledIn(source, served) {
63
+ if (!source)
64
+ return [];
65
+ const calls = [];
66
+ for (const m of source.matchAll(CLIENT_CALL))
67
+ calls.push({ method: m[1].toUpperCase(), url: m[2] });
68
+ for (const m of source.matchAll(FETCH_CALL))
69
+ calls.push({ method: "GET", url: m[1] });
70
+ if (USES_HTTP_CLIENT.test(source))
71
+ for (const m of source.matchAll(BARE_CALL))
72
+ calls.push({ method: m[1].toUpperCase(), url: m[2] });
73
+ const out = /* @__PURE__ */ new Set();
74
+ for (const call of calls) {
75
+ const method = call.method, url = call.url.replace(/[?#].*$/, ""), segs = normRoutePath(url).split("/");
76
+ let best = null;
77
+ for (const r of served) {
78
+ if (r.method !== method)
79
+ continue;
80
+ const rsegs = normRoutePath(r.path).split("/");
81
+ if (rsegs.length !== segs.length || rsegs.length > 1 && rsegs.slice(1).every((s) => s === "{}") || !rsegs.every((s, i) => s === "{}" ? segs[i] !== "" : s === segs[i]))
82
+ continue;
83
+ const params = slots(normRoutePath(r.path));
84
+ (!best || params < best.params) && (best = { name: r.name, params });
85
+ }
86
+ best && out.add(best.name);
87
+ }
88
+ return [...out];
89
+ }
90
+ export {
91
+ TEST_PATH,
92
+ namedRoutePath,
93
+ normRoutePath,
94
+ routesCalledIn
95
+ };
96
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/analyzers/cards/routes.ts"],
4
+ "sourcesContent": ["/**\n * Route helpers for extraction: the parameter names a route's path has in the code, and the\n * routes test code calls by URL.\n *\n * CBM names a route's parameters `{}` (`__route__GET__/owners/{}`). The names come from CBM's own\n * route name when it has them (`/owners/{ownerId}`), else from the handler: a path literal in its\n * source with as many parameters, or its path-annotated parameters, or its parameters.\n */\n\n/** A path with every parameter as `{}` and no trailing slash: `/owners/{ownerId}/` -> `/owners/{}`. */\nexport function normRoutePath(p: string): string {\n return p.replace(/\\{[^}]*\\}|:[A-Za-z_]\\w*/g, \"{}\").replace(/\\/+$/, \"\") || \"/\";\n}\n\nfunction slots(p: string): number {\n return (p.match(/\\{\\}/g) ?? []).length;\n}\n\n/** Fill a `{}` path's parameters with these names, in order. */\nfunction fill(p: string, names: string[]): string {\n let i = 0;\n return p.replace(/\\{\\}/g, () => `{${names[i++]}}`);\n}\n\n/** Parameter names in a path template, `{name}` or `:name`, in order. */\nfunction templateNames(p: string): string[] {\n return [...p.matchAll(/\\{(\\w+)[^}]*\\}|:([A-Za-z_]\\w*)/g)].map((m) => m[1] ?? m[2]);\n}\n\n/** Parameters that never carry a path segment: request objects, models, framework plumbing. */\nconst NOT_PATH = /^(req|res|request|response|next|ctx|context|model|modelMap|session|self|cls|body|payload|result|bindingResult|redirectAttributes|principal|page|pageable|locale|headers?|form|dto)$/i;\nconst SEGMENT_TYPE = /^(final\\s+)?(int|long|short|Integer|Long|Short|String|string|number|str|UUID|uuid\\.UUID|bool|boolean|Boolean|Int|i32|i64|u32|u64|&str)\\b/;\nconst NOT_PATH_TYPE =/\\b(Model|ModelMap|BindingResult|HttpServletRequest|HttpServletResponse|RedirectAttributes|Principal|Pageable|Locale|Errors|WebRequest|Request|Response|NextFunction)\\b/;\n\n/** The parameter list of the first function or method declared in a source text. */\nfunction parameterList(source: string): string | null {\n const m = /\\b[\\w$]+\\s*\\(([^()]*(?:\\([^()]*\\)[^()]*)*)\\)\\s*(?::[^{=]+)?(?:throws [\\w., ]+)?\\s*(?:\\{|=>|->|:)/.exec(source);\n return m ? m[1] : null;\n}\n\nfunction splitParams(list: string): string[] {\n const out: string[] = [];\n let depth = 0;\n let cur = \"\";\n for (const ch of list) {\n if (\"(<[{\".includes(ch)) depth++;\n else if (\")>]}\".includes(ch)) depth--;\n if (ch === \",\" && depth === 0) {\n out.push(cur.trim());\n cur = \"\";\n } else cur += ch;\n }\n if (cur.trim()) out.push(cur.trim());\n return out;\n}\n\n/** A parameter's name: `int ownerId`, `ownerId: int`, `owner_id: int = Path(...)`, `@PathVariable(\"ownerId\") int id`. */\nfunction paramName(p: string): { name: string; annotated: string | null; type: string } | null {\n const annotated = /@PathVariable\\s*\\(\\s*(?:(?:name|value)\\s*=\\s*)?[\"'](\\w+)[\"']/.exec(p)?.[1] ?? null;\n const pathMarked = annotated !== null || /@(PathVariable|PathParam|Param)\\b|\\bPath\\s*\\(/.test(p);\n const bare = p.replace(/@\\w+(\\s*\\([^)]*\\))?/g, \"\").replace(/=.*$/, \"\").trim();\n let name: string | undefined;\n let type = \"\";\n const colon = /^([A-Za-z_$][\\w$]*)\\??\\s*:\\s*(.+)$/.exec(bare);\n if (colon) {\n name = colon[1];\n type = colon[2];\n } else {\n const words = bare.split(/\\s+/).filter(Boolean);\n name = words[words.length - 1]?.replace(/^[*&.]+/, \"\");\n type = words.slice(0, -1).join(\" \");\n }\n if (!name || !/^[A-Za-z_$][\\w$]*$/.test(name)) return null;\n return { name, annotated: pathMarked ? (annotated ?? name) : null, type };\n}\n\n/**\n * The route's path with its parameters named where the code names them. `path` is CBM's\n * (`/owners/{}`); `cbmName` is CBM's own route name; `handlerSource` the handler's source.\n * Unnamed parameters stay `{}`.\n */\nexport function namedRoutePath(path: string, cbmName: unknown, handlerSource: string | null): string {\n const n = slots(path);\n if (n === 0) return path;\n if (typeof cbmName === \"string\" && cbmName.startsWith(\"/\") && normRoutePath(cbmName) === normRoutePath(path) && templateNames(cbmName).length === n) {\n return fill(path, templateNames(cbmName));\n }\n if (!handlerSource) return path;\n // A path literal in the handler (its mapping annotation) whose parameters line up with the route's.\n const norm = normRoutePath(path);\n for (const m of handlerSource.matchAll(/[\"'`](\\/[^\"'`\\s]*)[\"'`]/g)) {\n const literal = m[1];\n const names = templateNames(literal);\n if (names.length === n && norm.endsWith(normRoutePath(literal))) return fill(path, names);\n }\n // The handler's parameters: those marked as path parameters, else the ones that could be.\n const list = parameterList(handlerSource);\n if (list === null) return path;\n const params = splitParams(list).map(paramName).filter((p): p is NonNullable<typeof p> => !!p);\n const marked = params.filter((p) => p.annotated).map((p) => p.annotated!);\n if (marked.length === n) return fill(path, marked);\n // Unmarked, only those of a type a path segment can be (or of no stated type).\n const plain = params\n .filter((p) => !NOT_PATH.test(p.name) && !NOT_PATH_TYPE.test(p.type) && (!p.type || SEGMENT_TYPE.test(p.type.trim())))\n .map((p) => p.name);\n return plain.length === n ? fill(path, plain) : path;\n}\n\n/**\n * A test file, by its path: under a `test`/`tests`/`__tests__` folder, or named as a test\n * (`*.test.ts`, `*.spec.ts`, `test_*.py`, `*_test.go`, `*Tests.java`, `*_spec.rb`, `*Spec.kt`). A\n * `spec/` folder alone is not tests (this repository keeps its specification there).\n */\nexport const TEST_PATH =\n /(^|\\/)(tests?|__tests__)(\\/|$)|\\.(test|spec)\\.[cm]?[jt]sx?$|(^|\\/)test_[^/]*\\.py$|_test\\.(py|go)$|Tests?\\.(java|kt|cs)$|_spec\\.rb$|Spec\\.(kt|groovy|scala)$/;\n\nconst METHODS = \"get|post|put|patch|delete\";\nconst PATH_ARG = `\\\\(\\\\s*[\"'\\`](\\\\/[^\"'\\`\\\\s]*)[\"'\\`]`;\n/** An HTTP or test client's call: `mockMvc.perform(get(...))` aside, `client.get(\"/x\")`, `request(app).post('/x')`. */\nconst CLIENT_CALL = new RegExp(\n `\\\\b(?:mockMvc|mvc|webTestClient|testClient|client|restTemplate|testRestTemplate|RequestEntity|MockMvcRequestBuilders|http|httpClient|axios|supertest\\\\([^)]*\\\\)|request\\\\([^)]*\\\\)|agent|api)\\\\s*\\\\.\\\\s*(${METHODS})(?:ForEntity|ForObject|ForLocation)?\\\\s*${PATH_ARG}`,\n \"gi\"\n);\n/** A bare `get(\"/x\")`: a request builder, but only in a test that uses an HTTP client. */\nconst BARE_CALL = new RegExp(`(?<![\\\\w$.])(${METHODS})\\\\s*${PATH_ARG}`, \"g\");\nconst FETCH_CALL = new RegExp(`\\\\bfetch\\\\s*${PATH_ARG}`, \"g\");\nconst USES_HTTP_CLIENT = /\\b(mockMvc|perform\\s*\\(|webTestClient|TestClient|restTemplate|supertest|request\\s*\\(\\s*app|axios|httpClient)\\b/i;\n\n/**\n * The served routes (`__route__METHOD__/path` names) a test's source calls by URL:\n * `client.get(\"/owners/1\")`, `RequestEntity.get(\"/owners/1\")`, `request(app).post('/users')`,\n * `fetch(\"/users\")`, and in a test that uses an HTTP client, MockMvc's `get(\"/owners/{ownerId}\", 1)`.\n * A URL matches a route whose literal segments it repeats, a parameter matching any segment; the\n * query string is ignored. Of several, the one with fewest parameters wins. A route with no\n * literal segment (`GET /{}`) matches nothing: it would match any URL.\n */\nexport function routesCalledIn(source: string | null, served: ReadonlyArray<{ name: string; method: string; path: string }>): string[] {\n if (!source) return [];\n const calls: Array<{ method: string; url: string }> = [];\n for (const m of source.matchAll(CLIENT_CALL)) calls.push({ method: m[1].toUpperCase(), url: m[2] });\n for (const m of source.matchAll(FETCH_CALL)) calls.push({ method: \"GET\", url: m[1] });\n if (USES_HTTP_CLIENT.test(source)) for (const m of source.matchAll(BARE_CALL)) calls.push({ method: m[1].toUpperCase(), url: m[2] });\n const out = new Set<string>();\n for (const call of calls) {\n const method = call.method;\n const url = call.url.replace(/[?#].*$/, \"\");\n const segs = normRoutePath(url).split(\"/\");\n let best: { name: string; params: number } | null = null;\n for (const r of served) {\n if (r.method !== method) continue;\n const rsegs = normRoutePath(r.path).split(\"/\");\n if (rsegs.length !== segs.length) continue;\n if (rsegs.length > 1 && rsegs.slice(1).every((s) => s === \"{}\")) continue;\n if (!rsegs.every((s, i) => s === \"{}\" ? segs[i] !== \"\" : s === segs[i])) continue;\n const params = slots(normRoutePath(r.path));\n if (!best || params < best.params) best = { name: r.name, params };\n }\n if (best) out.add(best.name);\n }\n return [...out];\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }