@documentation-robotics/cli 0.2.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.
- package/README.md +46 -16
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +191 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +270 -174
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +342 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +49 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/extraction-plan.js +541 -0
- package/dist/analyzers/extraction-plan.js.map +7 -0
- package/dist/analyzers/index-job.js +229 -0
- package/dist/analyzers/index-job.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +269 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +7 -4
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +143 -40
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +63 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +80 -29
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +40 -21
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +208 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +3 -3
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +52 -32
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -103
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +19 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +34 -15
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/annotation-store.js +15 -5
- package/dist/core/annotation-store.js.map +2 -2
- package/dist/core/catalog-migration.js +34 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +160 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +848 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +88 -47
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/project-init.js +53 -0
- package/dist/core/project-init.js.map +7 -0
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +161 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +145 -18
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +166 -24
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/upgrade-plan.js +117 -0
- package/dist/core/upgrade-plan.js.map +7 -0
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2468 -7913
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +20 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +66 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +47 -26
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +68 -2
- package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
- package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +95 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +3 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +225 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +45 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/project-root.js +85 -0
- package/dist/mcp/project-root.js.map +7 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/server-instructions.js.map +7 -0
- package/dist/mcp/strict-schema.js +52 -0
- package/dist/mcp/strict-schema.js.map +7 -0
- package/dist/mcp/tool-registry.js +55 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-commit.js +224 -0
- package/dist/mcp/tools/changeset-commit.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +260 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +49 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/codebase-extract.js +836 -0
- package/dist/mcp/tools/codebase-extract.js.map +7 -0
- package/dist/mcp/tools/codebase-index.js +125 -0
- package/dist/mcp/tools/codebase-index.js.map +7 -0
- package/dist/mcp/tools/codebase-shared.js +265 -0
- package/dist/mcp/tools/codebase-shared.js.map +7 -0
- package/dist/mcp/tools/codebase-verify.js +48 -0
- package/dist/mcp/tools/codebase-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +34 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +191 -74
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +46 -26
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/model-validate.js +46 -4
- package/dist/mcp/tools/model-validate.js.map +2 -2
- package/dist/mcp/tools/project-status-collectors.js +381 -0
- package/dist/mcp/tools/project-status-collectors.js.map +7 -0
- package/dist/mcp/tools/project-status.js +237 -0
- package/dist/mcp/tools/project-status.js.map +7 -0
- package/dist/mcp/tools/project-tools.js +505 -0
- package/dist/mcp/tools/project-tools.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +167 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/review-open.js +210 -0
- package/dist/mcp/tools/review-open.js.map +7 -0
- package/dist/mcp/tools/shared.js +35 -4
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-report-orchestrator.js +31 -2
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +865 -1646
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +686 -231
- package/dist/server/server.js.map +3 -3
- package/dist/server/viewer-registry.js +264 -0
- package/dist/server/viewer-registry.js.map +7 -0
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +64 -31
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
- package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/analyzers/extraction-plan.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Plan an as-built changeset from what the extraction read (cards/extract.ts): what to add, what\n * to update, which relationships to stage, what is already modelled, what looks deleted, and which\n * symbols need the agent's decision. Pure: it reads the projected model and writes nothing.\n * `codebase_extract` stages the plan.\n *\n * Existing elements are matched to symbols by their source reference (file and symbol); a route\n * also matches an `api.operation` with the same method and path, and a decided symbol matches an\n * element of its type and id that has no source location yet. A match whose code-derived fields\n * are unchanged is left out, a match that changed is an update, and a decided symbol with no match\n * is an add. So an empty model gets the bootstrap and an unchanged repository gets nothing.\n *\n * Deletes are never planned, only listed: an element whose source file is gone, or whose symbol\n * the file no longer has, is a candidate delete, with the evidence.\n */\nimport path from \"node:path\";\nimport type { ChangesetGraph, GraphElement } from \"../core/changeset-graph.js\";\nimport type { SourceReference } from \"../types/source-reference.js\";\nimport { toKebabCase } from \"../utils/id-generator.js\";\nimport { changedInRange } from \"./code-changes.js\";\nimport { dedupeRelationships, relationshipFor, type EdgeRelationship } from \"./edge-relationships.js\";\nimport {\n FILE_JOINED_EDGES,\n parseRouteName,\n shortQualifiedName,\n symbolSpellings,\n type EdgeEnd,\n type ExtractedSymbol,\n type ExtractionGraph,\n} from \"./cards/extract.js\";\n\n/** Edges that say a file refers to a type: it names it, imports it, or calls it (a constructor). */\nconst REFERENCE_EDGES: ReadonlySet<string> = new Set([\"USAGE\", \"IMPORTS\", \"CALLS\"]);\n\n/**\n * Whether `refs` (what a file names, imports or calls) refers to the class `cls` itself: the class, or\n * its constructor (`Owner.Owner` in Java and C#, `constructor` in TypeScript, `__init__` in Python),\n * which is how the index records `new Owner()`. A member such as `Owner.name` doesn't count: the\n * index matches members by name.\n */\nfunction refersToClass(refs: Set<string> | undefined, cls: string): boolean {\n if (!refs) return false;\n const simple = cls.slice(cls.lastIndexOf(\".\") + 1);\n return refs.has(cls) || refs.has(`${cls}.${simple}`) || refs.has(`${cls}.constructor`) || refs.has(`${cls}.__init__`);\n}\n\n/** Files a `since` range changed, relative to the codebase root. */\nexport interface CodeChanges {\n /** Files added or modified (a renamed file under its new path). */\n changed: Set<string>;\n /** Files deleted, or renamed away: old path -> new path (null when deleted). */\n deleted: Map<string, string | null>;\n /**\n * The lines each changed file changed, on its new side: line ranges, or \"all\" for a file that\n * is new in the range. A changed file missing here changed in a way git can't place (binary).\n */\n hunks?: Map<string, LineRanges>;\n /**\n * A file's text at the range's base commit; null when it didn't exist there, undefined when it\n * can't be read.\n */\n fileAtBase?: (file: string) => string | null | undefined;\n /** Why the changed lines couldn't be read, when they couldn't (so no card says changedInRange). */\n hunksError?: string;\n}\n\n/**\n * A changed line range, [start, end] (1-based, inclusive). A pure deletion has no new lines: it is\n * the line before the deleted block, [N, N, true], so it says where code went, not which symbol\n * changed.\n */\nexport type LineRange = [start: number, end: number] | [start: number, end: number, deletion: true];\n\n/** Changed lines of a file: line ranges, or the whole file. */\nexport type LineRanges = LineRange[] | \"all\";\n\nexport interface PlannedElement {\n id: string;\n layer: string;\n type: string;\n name: string;\n description?: string;\n attributes: Record<string, unknown>;\n /** Where the code is; absent for a route CBM found no file for (it is matched by method and path). */\n sourceReference?: SourceReference;\n /** The symbol it came from. */\n symbol: ExtractedSymbol;\n rule: string;\n}\n\nexport interface PlannedUpdate {\n id: string;\n layer: string;\n /** The changed fields and their new values (`description`, `attributes.http_path`, `source_reference`). */\n fields: Record<string, unknown>;\n symbol: ExtractedSymbol;\n}\n\nexport interface PlannedRelationship {\n source: string;\n predicate: string;\n target: string;\n sourceLayer: string;\n targetLayer: string;\n /** The CBM edge type it came from. */\n edge: string;\n}\n\nexport interface CandidateDelete {\n id: string;\n layer: string;\n type: string;\n file: string;\n symbol?: string;\n /**\n * `symbol-missing`: the file no longer has the symbol (with `since`, it had it at the base).\n * `reference-never-matched`: with `since`, the file didn't have it at the base either, so the\n * range didn't remove it; the model's reference was wrong before the range.\n */\n evidence: \"file-deleted\" | \"file-renamed\" | \"file-missing\" | \"symbol-missing\" | \"reference-never-matched\";\n detail: string;\n /** The element this run stages for the same symbol at the file's new path, after a rename. */\n replacedBy?: string;\n}\n\nexport interface Retyped {\n id: string;\n currentType: string;\n decidedType: string;\n rule: string;\n file: string | null;\n symbol: string;\n}\n\n/** `unhandled-route`: a route nothing in the code handles (a URL tests or clients call); last in the list. */\nexport type UndecidedKind = \"needs-attributes\" | \"ambiguous\" | \"narrowed\" | \"no-rule\" | \"unhandled-route\";\n\nexport interface UndecidedCard {\n cardId: string;\n kind: UndecidedKind;\n label: string;\n /** The value for model_add's sourceSymbol. */\n symbol: string;\n /** With `since`: the symbol's lines are new or changed in the range. Such cards come first. */\n changedInRange?: true;\n /** A name for model_add whose id keeps the symbol's word breaks (`PetTypeRepository findPetTypes`). */\n name?: string;\n file: string | null;\n lines: string | null;\n qualifiedName: string;\n signature?: string;\n docstring?: string;\n decorators?: string[];\n /** The types Tier 0 allows (`layer.type`, or `layer.*` for any type in the layer); empty when open. */\n candidates: string[];\n /** The analyzer's default type for the label, when Tier 0 left it open. */\n hint?: string;\n rule?: string;\n reason: string;\n /**\n * For needs-attributes: the element Tier 0 decided, with the attributes it could derive. model_add\n * with `name` gives the element `id`.\n */\n proposed?: { layer: string; type: string; name: string; id: string; attributes: Record<string, unknown> };\n}\n\nexport interface ElementPlan {\n adds: PlannedElement[];\n updates: PlannedUpdate[];\n /** Decided symbols whose element is already as the code says. */\n unchanged: number;\n /** Symbols Tier 0 did not decide (or rejected) that already have an element. */\n modelled: number;\n /** Rejected by Tier 0 (not model elements), with no element. */\n rejected: number;\n /** Symbols left out because a recorded decision says they are not elements ({@link PlanOptions.notElements}). */\n skipped: number;\n /** Decided into a layer outside `layers`. */\n outsideLayers: number;\n retyped: Retyped[];\n undecided: UndecidedCard[];\n candidateDeletes: CandidateDelete[];\n /** Symbol (full qualified name) -> the element it is: an add or a match. */\n elementOf: Map<string, string>;\n /** Descriptions this run writes from docstrings, by element id (adds and updates). */\n descriptions: Record<string, string>;\n}\n\nexport interface PlanOptions {\n /** Only these layers (default every layer). */\n layers?: string[];\n /** The files the extraction covered, with what a `since` range deleted; null for the whole codebase. */\n changes?: CodeChanges | null;\n /** Whether a file exists, relative to the codebase root (used without `since`). */\n fileExists: (file: string) => boolean;\n /**\n * A file's text, or null when it can't be read. A symbol the analyzer doesn't list (a prop, a\n * local) still counts as present while its name appears in the file.\n */\n fileText?: (file: string) => string | null;\n /**\n * The descriptions earlier runs wrote from docstrings, by element id. A description is only\n * replaced when it is empty or still the one extraction wrote, so someone's own text stays.\n */\n extractedDescriptions?: Record<string, string>;\n /**\n * Symbols someone decided are not elements, by {@link decisionKey}. An unmatched symbol with a\n * decision is neither staged nor carded; one that has an element is matched as usual.\n */\n notElements?: ReadonlySet<string>;\n}\n\n/**\n * The key a \"not an element\" decision is recorded under: `file#symbol`, a route's parameters\n * normalised (`file#GET /owners/{}`). A route keeps its file, so a rejected client URL with no file\n * can't hide a route served in a file.\n */\nexport function decisionKey(file: string | null, symbol: string): string {\n return `${file ?? \"\"}#${routeKey(symbol) ?? symbol}`;\n}\n\n// ---------------------------------------------------------------------------\n// Naming and attributes\n// ---------------------------------------------------------------------------\n\n/** A route symbol's path, with its parameters named where the code names them. */\nfunction routePathOf(s: ExtractedSymbol, route: { path: string }): string {\n return s.routePath ?? route.path;\n}\n\n/** The source symbol an extracted element records: `METHOD /path`, `Parent.method`, the name, or a module's qualified name. */\nexport function sourceSymbolOf(s: ExtractedSymbol, project: string): string {\n const { card } = s;\n const route = parseRouteName(card.name);\n if (route) return `${route.method} ${routePathOf(s, route)}`;\n if (card.label === \"Module\") return shortQualifiedName(s.qualifiedName, project);\n if (card.label === \"Method\" && card.parent_class) {\n const parent = card.parent_class.split(\".\").pop();\n if (parent) return `${parent}.${card.name}`;\n }\n return card.name;\n}\n\n/**\n * The element name for a symbol: `GET /owners/{ownerId}` (`{param}` where the code names no\n * parameter), `OwnerController.showOwner`, `Owner`, a module's file path without its extension\n * (`src/utils/graphDegree`).\n */\nfunction baseNameOf(s: ExtractedSymbol, project: string): string {\n const route = parseRouteName(s.card.name);\n if (route) return `${route.method} ${routePathOf(s, route).replace(/\\{\\}/g, \"{param}\")}`;\n if (s.card.label === \"Module\") return (s.card.file_path || s.card.name).replace(/\\.[^./]+$/, \"\");\n return sourceSymbolOf(s, project);\n}\n\n/**\n * A name for model_add that gives the id extraction would ({@link extractedElementId}). model_add\n * drops a dot or a slash rather than breaking the word there, so `PetTypeRepository.findPetTypes`\n * becomes `PetTypeRepository findPetTypes`, a module's `src/utils/graphDegree` becomes\n * `src utils graphDegree`, and `GET /owners/{ownerId}` becomes `GET owners ownerId` (`GET /` is\n * `GET root`). Undefined when the name needs no change.\n */\nexport function modelAddName(name: string): string | undefined {\n const root = /^\\s*([A-Z]+)\\s+\\/\\s*$/.exec(name);\n const out = root\n ? `${root[1]} root`\n : /[/{}]/.test(name)\n ? name.replace(/[./{}]+/g, \" \").replace(/\\s+/g, \" \").trim()\n : name.replace(/([\\w$)])\\.(?=[A-Za-z_$])/g, \"$1 \");\n return out !== name ? out : undefined;\n}\n\n/**\n * An element id for a name: dots, slashes and braces become word breaks before kebab-casing. A\n * route on the root path is `<method> root` (`api.operation.get-root`).\n */\nexport function extractedElementId(layer: string, type: string, name: string): string {\n const root = /^\\s*([A-Z]+)\\s+\\/\\s*$/.exec(name);\n return `${layer}.${type}.${toKebabCase(root ? `${root[1]} root` : name.replace(/[./{}]+/g, \" \"))}`;\n}\n\n/** First paragraph of a docstring, without comment markers, at most 280 characters. */\nexport function describeDocstring(doc: string | null): string | undefined {\n if (!doc) return undefined;\n const text = doc\n .replace(/^\\s*\\/\\*\\*?/, \"\")\n .replace(/\\*\\/\\s*$/, \"\")\n .split(\"\\n\")\n .map((l) => l.replace(/^\\s*(\\*|\\/\\/\\/?|#)\\s?/, \"\").trim())\n .join(\"\\n\")\n .replace(/^[\"']{3}|[\"']{3}$/g, \"\")\n .trim()\n .split(/\\n\\s*\\n/)[0]\n .replace(/\\s+/g, \" \")\n .trim();\n if (!text || text.startsWith(\"@\")) return undefined;\n return text.length > 280 ? `${text.slice(0, 277)}...` : text;\n}\n\n/**\n * Attributes the code gives for a decided type: `facts` are read from the code and compared on a\n * re-run; `defaults` fill a type's required attributes on an add only, so a value someone set by\n * hand is never overwritten.\n */\nexport function deriveAttributes(\n layer: string,\n type: string,\n s: ExtractedSymbol\n): { facts: Record<string, unknown>; defaults: Record<string, unknown> } {\n const key = `${layer}.${type}`;\n const route = parseRouteName(s.card.name);\n switch (key) {\n case \"api.operation\": {\n if (!route) return { facts: {}, defaults: {} };\n const p = routePathOf(s, route);\n const segment = p.split(\"/\").find((x) => x && !x.startsWith(\"{\") && !x.startsWith(\":\")) ?? \"default\";\n const words = p.replace(/[/{}:]+/g, \" \").trim();\n return {\n facts: { http_method: route.method, http_path: p },\n defaults: {\n operationId: toKebabCase(`${route.method} ${words || \"root\"}`),\n summary: `${route.method} ${p}`,\n tags: segment,\n },\n };\n }\n case \"testing.testcasesketch\":\n // It exists as a test in the code.\n return { facts: {}, defaults: { status: \"automated\" } };\n case \"application.applicationinterface\":\n return { facts: {}, defaults: { protocol: s.facts.decorator_names.includes(\"RestController\") ? \"REST\" : \"HTTP\" } };\n case \"application.applicationcomponent\":\n return { facts: {}, defaults: { type: \"internal\" } };\n case \"application.applicationservice\":\n return { facts: {}, defaults: { serviceType: \"synchronous\" } };\n case \"data-model.objectschema\":\n return { facts: {}, defaults: { type: \"object\" } };\n case \"data-store.accesspattern\": {\n // A repository interface names its entity: `extends JpaRepository<Owner, Integer>`.\n const entity = /Repository\\s*<\\s*([\\w.]+)/.exec(s.card.source ?? s.card.signature ?? \"\")?.[1];\n return { facts: {}, defaults: { patternType: \"CUSTOM\", ...(entity ? { targetCollection: entity.split(\".\").pop() } : {}) } };\n }\n default:\n return { facts: {}, defaults: {} };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Matching\n// ---------------------------------------------------------------------------\n\nfunction codeLocations(e: GraphElement): Array<{ file: string; symbol?: string }> {\n return (e.sourceReference?.locations ?? [])\n .filter((l) => typeof l.file === \"string\" && l.file)\n .map((l) => ({ file: l.file.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\"), ...(l.symbol ? { symbol: l.symbol } : {}) }));\n}\n\nfunction normPath(p: string): string {\n return p.replace(/\\{[^}]*\\}|:[A-Za-z_]\\w*/g, \"{}\").replace(/\\/+$/, \"\") || \"/\";\n}\n\ninterface ExistingIndex {\n /** `file#symbol` -> elements naming that symbol. */\n bySymbol: Map<string, GraphElement[]>;\n /** `file#METHOD /path` -> elements whose symbol is that route, parameters normalised. */\n byRouteSymbol: Map<string, GraphElement[]>;\n /** `METHOD /path` -> elements whose symbol is that route, in any file. */\n byRouteSymbolAnyFile: Map<string, GraphElement[]>;\n /** `METHOD /path` -> operations by their attributes, only those naming no code. */\n byRoute: Map<string, GraphElement[]>;\n byId: Map<string, GraphElement>;\n live: GraphElement[];\n}\n\nfunction indexExisting(graph: ChangesetGraph): ExistingIndex {\n const bySymbol = new Map<string, GraphElement[]>();\n const byRouteSymbol = new Map<string, GraphElement[]>();\n const byRouteSymbolAnyFile = new Map<string, GraphElement[]>();\n const byRoute = new Map<string, GraphElement[]>();\n const byId = new Map<string, GraphElement>();\n const live: GraphElement[] = [];\n const push = (m: Map<string, GraphElement[]>, k: string, e: GraphElement) => (m.get(k) ?? m.set(k, []).get(k)!).push(e);\n for (const e of graph.elements.values()) {\n byId.set(e.id, e);\n if (e.status === \"deleted\") continue;\n live.push(e);\n const locations = codeLocations(e);\n for (const loc of locations) {\n if (!loc.symbol) continue;\n push(bySymbol, `${loc.file}#${loc.symbol}`, e);\n const route = routeKey(loc.symbol);\n if (route) {\n push(byRouteSymbol, `${loc.file}#${route}`, e);\n push(byRouteSymbolAnyFile, route, e);\n }\n }\n // By method and path only an operation that names no code, so code elsewhere (another\n // service's GET /health) can't claim one that does.\n if (e.layer === \"api\" && e.type === \"operation\" && !locations.length) {\n const method = e.attributes.http_method;\n const p = e.attributes.http_path;\n if (typeof method === \"string\" && typeof p === \"string\") push(byRoute, `${method.toUpperCase()} ${normPath(p)}`, e);\n }\n }\n return { bySymbol, byRouteSymbol, byRouteSymbolAnyFile, byRoute, byId, live };\n}\n\nfunction findBySymbol(index: ExistingIndex, file: string | null, spellings: string[]): GraphElement | undefined {\n for (const s of spellings) {\n if (file) {\n const hit = index.bySymbol.get(`${file}#${s}`);\n if (hit?.length) return hit[0];\n }\n }\n return undefined;\n}\n\nfunction hasSymbol(spellings: Set<string> | undefined, symbol: string): boolean {\n return !!spellings && spellings.has(symbol);\n}\n\n// ---------------------------------------------------------------------------\n// Elements\n// ---------------------------------------------------------------------------\n\nconst LABEL_ORDER = [\"Route\", \"Class\", \"Interface\", \"Type\", \"Enum\", \"Trait\", \"Struct\", \"Function\", \"Method\", \"Module\"];\nconst KIND_ORDER: UndecidedKind[] = [\"needs-attributes\", \"ambiguous\", \"narrowed\", \"no-rule\", \"unhandled-route\"];\n\nfunction inLayers(layer: string, layers: Set<string> | null): boolean {\n return !layers || layers.has(layer);\n}\n\n/** Cards changed in the range first, then by kind, label, file and name. */\nfunction sortCards(cards: UndecidedCard[]): UndecidedCard[] {\n const rank = (l: string) => {\n const i = LABEL_ORDER.indexOf(l);\n return i < 0 ? LABEL_ORDER.length : i;\n };\n return cards.sort(\n (a, b) =>\n (a.changedInRange ? 0 : 1) - (b.changedInRange ? 0 : 1) ||\n KIND_ORDER.indexOf(a.kind) - KIND_ORDER.indexOf(b.kind) ||\n rank(a.label) - rank(b.label) ||\n (a.file ?? \"\").localeCompare(b.file ?? \"\") ||\n a.qualifiedName.localeCompare(b.qualifiedName)\n );\n}\n\nfunction cardOf(\n s: ExtractedSymbol,\n project: string,\n kind: UndecidedKind,\n reason: string,\n extra: Partial<UndecidedCard> = {},\n hunks?: Map<string, LineRanges>,\n renamedTo?: ReadonlySet<string>\n): UndecidedCard {\n const { card } = s;\n const doc = describeDocstring(card.docstring);\n const name = modelAddName(baseNameOf(s, project));\n // A module spans its whole file, so any edit in the file would touch it: it counts as changed\n // only when the range added the file. Its identity is its path, so the new path of a rename\n // is added too, whatever its hunks say.\n const ranges = hunks && card.file_path ? hunks.get(card.file_path) : undefined;\n const changed =\n card.label === \"Module\"\n ? !!hunks && !!card.file_path && (ranges === \"all\" || !!renamedTo?.has(card.file_path))\n : changedInRange(ranges, card.start_line, card.end_line);\n return {\n cardId: card.id,\n kind,\n label: card.label,\n symbol: sourceSymbolOf(s, project),\n ...(changed ? { changedInRange: true as const } : {}),\n ...(name ? { name } : {}),\n file: card.file_path,\n lines: card.start_line !== null ? `${card.start_line}-${card.end_line ?? card.start_line}` : null,\n qualifiedName: card.qualified_name,\n ...(card.signature ? { signature: card.signature.slice(0, 200) } : {}),\n ...(doc ? { docstring: doc } : {}),\n ...(s.facts.decorator_names.length ? { decorators: s.facts.decorator_names } : {}),\n candidates: [],\n reason,\n ...extra,\n };\n}\n\n/** Plan the element changes. Relationships are planned after the adds are validated ({@link planRelationships}). */\nexport function planElements(extraction: ExtractionGraph, existing: ChangesetGraph, options: PlanOptions): ElementPlan {\n const { project, config } = extraction;\n const layers = options.layers?.length ? new Set(options.layers) : null;\n const index = indexExisting(existing);\n const taken = new Set(index.byId.keys());\n const plan: ElementPlan = {\n adds: [],\n updates: [],\n unchanged: 0,\n modelled: 0,\n rejected: 0,\n skipped: 0,\n outsideLayers: 0,\n retyped: [],\n undecided: [],\n candidateDeletes: [],\n elementOf: new Map(),\n descriptions: {},\n };\n const matched = new Set<string>();\n const hunks = options.changes?.hunks;\n const renamedTo = renameTargets(options.changes);\n const cardFor = (s: ExtractedSymbol, kind: UndecidedKind, reason: string, extra: Partial<UndecidedCard> = {}) =>\n cardOf(s, project, kind, reason, extra, hunks, renamedTo);\n const notElements = options.notElements ?? new Set<string>();\n\n // Stable order, so names that collide are qualified the same way each run.\n const symbols = [...extraction.symbols].sort(\n (a, b) => (a.card.file_path ?? \"\").localeCompare(b.card.file_path ?? \"\") || a.qualifiedName.localeCompare(b.qualifiedName)\n );\n\n // Match symbols to elements in two passes, each element to one symbol: first by the symbol's\n // own spellings (`Parent.method`, the qualified name, a route), then by the looser ones (a route\n // by method and path, the bare name, the id). A symbol whose only match another symbol already\n // took is ambiguous: it goes to the agent rather than updating that element too.\n const claimed = new Set<string>();\n const matchOf = new Map<ExtractedSymbol, GraphElement>();\n const ambiguousWith = new Map<ExtractedSymbol, string>();\n const pick = (candidates: GraphElement[] | undefined, prefer?: (e: GraphElement) => boolean): GraphElement | \"claimed\" | undefined => {\n if (!candidates?.length) return undefined;\n const free = candidates.filter((e) => !claimed.has(e.id));\n if (!free.length) return \"claimed\";\n return (prefer && free.find(prefer)) ?? free[0];\n };\n const tryLookups = (s: ExtractedSymbol, lookups: Array<() => GraphElement | \"claimed\" | undefined>): void => {\n for (const lookup of lookups) {\n const hit = lookup();\n if (hit === \"claimed\") {\n const owner = lookupOwner(s);\n if (owner) ambiguousWith.set(s, owner);\n continue;\n }\n if (hit) {\n claimed.add(hit.id);\n matchOf.set(s, hit);\n ambiguousWith.delete(s);\n return;\n }\n }\n };\n // The element a looser lookup would have hit, for the ambiguity note.\n const lookupOwner = (s: ExtractedSymbol): string | undefined => {\n const file = s.card.file_path;\n const route = parseRouteName(s.card.name);\n const keys = route\n ? [file ? index.byRouteSymbol.get(`${file}#${routeKey(s.card.name)}`) : index.byRouteSymbolAnyFile.get(routeKey(s.card.name)!), index.byRoute.get(`${route.method} ${normPath(route.path)}`)]\n : [file ? index.bySymbol.get(`${file}#${s.card.name}`) : undefined];\n return keys.find((k) => k?.length)?.[0]?.id;\n };\n const strongSpellings = (s: ExtractedSymbol) => {\n const symbol = sourceSymbolOf(s, project);\n return [symbol, ...symbolSpellings(s.qualifiedName, s.card.name, project)].filter(\n (sp, i, all) => all.indexOf(sp) === i && (sp === symbol || sp !== s.card.name)\n );\n };\n for (const s of symbols) {\n const file = s.card.file_path;\n if (!file) continue;\n tryLookups(\n s,\n strongSpellings(s).map((sp) => () => pick(index.bySymbol.get(`${file}#${sp}`)))\n );\n }\n for (const s of symbols) {\n if (matchOf.has(s)) continue;\n const { card, tier0 } = s;\n const file = card.file_path;\n const route = parseRouteName(card.name);\n const lookups: Array<() => GraphElement | \"claimed\" | undefined> = [];\n if (route) {\n const key = routeKey(card.name)!;\n const exact = (e: GraphElement) => e.attributes.http_path === route.path;\n lookups.push(() => pick(file ? index.byRouteSymbol.get(`${file}#${key}`) : index.byRouteSymbolAnyFile.get(key)));\n lookups.push(() => pick(index.byRoute.get(`${route.method} ${normPath(route.path)}`), exact));\n } else if (file && sourceSymbolOf(s, project) !== card.name) {\n lookups.push(() => pick(index.bySymbol.get(`${file}#${card.name}`)));\n }\n if (tier0?.kind === \"decide\") {\n // An element of this type and id that names no code yet (modelled by hand, or planned).\n const decided = tier0;\n lookups.push(() => {\n const byId = index.byId.get(extractedElementId(decided.layer, decided.type, baseNameOf(s, project)));\n if (!byId || byId.status === \"deleted\" || byId.layer !== decided.layer || byId.type !== decided.type || codeLocations(byId).length) {\n return undefined;\n }\n return pick([byId]);\n });\n }\n tryLookups(s, lookups);\n }\n\n for (const s of symbols) {\n const { card, tier0 } = s;\n const file = card.file_path;\n const symbol = sourceSymbolOf(s, project);\n const decided = tier0?.kind === \"decide\" ? tier0 : null;\n const spellings = [symbol, ...symbolSpellings(s.qualifiedName, card.name, project)];\n const match = matchOf.get(s);\n\n // Someone decided it is not an element: neither staged nor carded, unless it has one now.\n if (!match && notElements.has(decisionKey(file, symbol))) {\n plan.skipped++;\n continue;\n }\n\n // A rejected symbol is not an element, ambiguous or not.\n const ambiguous = !match && tier0?.kind !== \"reject\" ? ambiguousWith.get(s) : undefined;\n if (ambiguous) {\n if (decided && !inLayers(decided.layer, layers)) {\n plan.outsideLayers++;\n continue;\n }\n plan.undecided.push(\n cardFor(\n s,\n \"ambiguous\",\n `Its name also matches ${ambiguous}, which another symbol already matched. If it is a separate element, stage it under its own symbol; if it is that one, add this location to it.`,\n {\n candidates: decided\n ? [`${decided.layer}.${decided.type}`]\n : tier0?.kind === \"narrow\"\n ? tier0.candidates.map((c) => `${c.layer}.${c.type ?? \"*\"}`)\n : [],\n ...(tier0 && tier0.kind !== \"reject\" ? { rule: tier0.rule } : {}),\n }\n )\n );\n continue;\n }\n\n if (match) {\n matched.add(match.id);\n plan.elementOf.set(s.qualifiedName, match.id);\n if (!decided) {\n plan.modelled++;\n continue;\n }\n if (match.layer !== decided.layer || match.type !== decided.type) {\n plan.retyped.push({ id: match.id, currentType: `${match.layer}.${match.type}`, decidedType: `${decided.layer}.${decided.type}`, rule: decided.rule, file, symbol });\n continue;\n }\n if (!inLayers(decided.layer, layers)) {\n plan.outsideLayers++;\n continue;\n }\n const fields = changedFields(match, s, symbol, spellings, decided.layer, decided.type, options.extractedDescriptions ?? {});\n if (typeof fields.description === \"string\") plan.descriptions[match.id] = fields.description;\n if (Object.keys(fields).length) plan.updates.push({ id: match.id, layer: match.layer, fields, symbol: s });\n else plan.unchanged++;\n continue;\n }\n\n if (s.unhandled) {\n if (layers && !layers.has(\"api\")) {\n plan.outsideLayers++;\n continue;\n }\n plan.undecided.push(\n cardFor(\n s,\n \"unhandled-route\",\n \"Nothing in this code handles this route (no handler, no file of its own): it is most likely a URL that tests or client code call. \" +\n \"Model it as an api.operation only if this codebase serves it; otherwise reject it.\",\n { candidates: [\"api.operation\"] }\n )\n );\n continue;\n }\n\n if (!tier0 || tier0.kind === \"narrow\") {\n const candidates = tier0 ? tier0.candidates.map((c) => `${c.layer}.${c.type ?? \"*\"}`) : [];\n const hint = config.labelDefaults[card.label];\n const relevant = layers\n ? tier0\n ? tier0.candidates.some((c) => layers.has(c.layer))\n : !!hint && layers.has(hint.split(\".\")[0])\n : true;\n if (!relevant) {\n plan.outsideLayers++;\n continue;\n }\n plan.undecided.push(\n tier0\n ? cardFor(s, \"narrowed\", `Tier 0 rule ${tier0.rule} narrows it to these types; choosing needs judgment (Tier 1 is not wired).`, {\n candidates: candidates.filter((c) => !layers || layers.has(c.split(\".\")[0])),\n rule: tier0.rule,\n })\n : cardFor(s, \"no-rule\", `No Tier-0 rule covers this ${card.label}: decide whether it is a model element, and which.`, {\n ...(hint ? { hint } : {}),\n })\n );\n continue;\n }\n if (tier0.kind === \"reject\") {\n plan.rejected++;\n continue;\n }\n\n // Decided, and not modelled yet: an add.\n if (!inLayers(tier0.layer, layers)) {\n plan.outsideLayers++;\n continue;\n }\n const { facts, defaults } = deriveAttributes(tier0.layer, tier0.type, s);\n const base = baseNameOf(s, project);\n const name = uniqueName(base, s, tier0.layer, tier0.type, taken);\n const id = extractedElementId(tier0.layer, tier0.type, name);\n taken.add(id);\n const description = describeDocstring(card.docstring);\n if (description) plan.descriptions[id] = description;\n plan.adds.push({\n id,\n layer: tier0.layer,\n type: tier0.type,\n name,\n ...(description ? { description } : {}),\n attributes: { ...defaults, ...facts },\n ...(file ? { sourceReference: { provenance: \"extracted\" as const, locations: [{ file, symbol }] } } : {}),\n symbol: s,\n rule: tier0.rule,\n });\n plan.elementOf.set(s.qualifiedName, id);\n }\n\n plan.candidateDeletes = candidateDeletes(extraction, index, matched, plan.adds, options, layers);\n plan.undecided = sortCards(plan.undecided);\n return plan;\n}\n\n/**\n * A decided add whose attributes don't validate (a required one the code can't give), handed to\n * the agent with what Tier 0 decided instead of being staged invalid.\n */\n/** The new paths of the files a range renamed. */\nfunction renameTargets(changes: CodeChanges | null | undefined): Set<string> {\n return new Set([...(changes?.deleted.values() ?? [])].filter((to): to is string => !!to));\n}\n\nexport function needsAttributesCard(add: PlannedElement, project: string, errors: string[], hunks?: Map<string, LineRanges>): UndecidedCard {\n return cardOf(\n add.symbol,\n project,\n \"needs-attributes\",\n `Tier 0 decided ${add.layer}.${add.type} (${add.rule}), but its attributes don't validate: ${errors\n .map((e) => e.replace(/^Element '[^']*':\\s*/, \"\"))\n .join(\"; \")}. Stage it with valid attributes (model_add with proposed.name gives it proposed.id), or reject it if it is not an element.`,\n {\n candidates: [`${add.layer}.${add.type}`],\n rule: add.rule,\n proposed: { layer: add.layer, type: add.type, name: modelAddName(add.name) ?? add.name, id: add.id, attributes: add.attributes },\n },\n hunks\n );\n}\n\nfunction lineSpan(card: UndecidedCard): [number, number] | null {\n const m = /^(\\d+)-(\\d+)$/.exec(card.lines ?? \"\");\n return m ? [Number(m[1]), Number(m[2])] : null;\n}\n\n/**\n * Credit a change to the innermost card it touched: a card whose changed lines all lie inside\n * members that are changed cards themselves (a class whose only edits are in a changed method)\n * loses changedInRange, so only the member says it changed.\n */\nexport function attributeChangedLines(cards: UndecidedCard[], hunks?: Map<string, LineRanges>): UndecidedCard[] {\n if (!hunks) return cards;\n const flagged = cards.filter((c) => c.changedInRange && c.file);\n return cards.map((card) => {\n if (!card.changedInRange || !card.file || card.label === \"Module\") return card;\n const ranges = hunks.get(card.file);\n const own = lineSpan(card);\n if (!ranges || ranges === \"all\" || !own) return card;\n const members = flagged\n .filter((m) => m !== card && m.file === card.file && m.qualifiedName.startsWith(card.qualifiedName + \".\"))\n .map(lineSpan)\n .filter((span): span is [number, number] => span !== null);\n if (!members.length) return card;\n const inMember = (line: number) => members.some(([a, b]) => line >= a && line <= b);\n const ownChange = ranges.some(([a, b, deletion]) => {\n // A deletion inside the card stays with it: the line it follows may be a member's last,\n // but what was removed (a member, say) was the container's.\n if (deletion) return a >= own[0] && a < own[1] && !members.some(([m0, m1]) => a >= m0 && a < m1);\n for (let line = Math.max(a, own[0]); line <= Math.min(b, own[1]); line++) if (!inMember(line)) return true;\n return false;\n });\n if (ownChange) return card;\n const { changedInRange: _changed, ...rest } = card;\n return rest;\n });\n}\n\n/** Re-sort undecided cards after adding to them. */\nexport function sortUndecided(cards: UndecidedCard[]): UndecidedCard[] {\n return sortCards(cards);\n}\n\n/** A name whose id is free: the symbol's, else qualified by its file, else by its qualified name. */\nfunction uniqueName(base: string, s: ExtractedSymbol, layer: string, type: string, taken: Set<string>): string {\n const file = s.card.file_path ?? \"\";\n const stem = path.posix.basename(file).replace(/\\.[^.]+$/, \"\");\n const dir = path.posix.dirname(file);\n const options = [base, `${stem} ${base}`, `${dir}/${stem} ${base}`, s.card.qualified_name];\n for (const name of options) {\n if (!taken.has(extractedElementId(layer, type, name))) return name;\n }\n for (let i = 2; ; i++) {\n const name = `${s.card.qualified_name} ${i}`;\n if (!taken.has(extractedElementId(layer, type, name))) return name;\n }\n}\n\n/**\n * The code-derived fields of a matched element that differ from the code: its source location\n * (added when the element names none for this symbol), the type's code facts (an operation's\n * method and path) and the description from the docstring, when the code has one and the element's\n * description is not someone's own (`manual` provenance).\n */\nfunction changedFields(\n match: GraphElement,\n s: ExtractedSymbol,\n symbol: string,\n spellings: string[],\n layer: string,\n type: string,\n extractedDescriptions: Record<string, string>\n): Record<string, unknown> {\n const fields: Record<string, unknown> = {};\n const file = s.card.file_path;\n const locations = codeLocations(match);\n // A route location counts with its parameters named or not (`GET /owners/{ownerId}`).\n const route = routeKey(s.card.name);\n const names = (l: { symbol?: string }) => !!l.symbol && (spellings.includes(l.symbol) || (!!route && routeKey(l.symbol) === route));\n if (file && !locations.some((l) => l.file === file && names(l))) {\n const ref = match.sourceReference;\n fields.source_reference = {\n provenance: ref?.provenance ?? \"extracted\",\n locations: [...(ref?.locations ?? []), { file, symbol }],\n ...(ref?.repository ? { repository: ref.repository } : {}),\n } satisfies SourceReference;\n }\n const { facts } = deriveAttributes(layer, type, s);\n for (const [k, v] of Object.entries(facts)) {\n if (!sameFact(k, match.attributes[k], v)) fields[`attributes.${k}`] = v;\n }\n // The docstring replaces a description only when there is none, or it is still the one an\n // earlier run wrote: a description someone wrote is theirs.\n const description = describeDocstring(s.card.docstring);\n const current = match.description ?? \"\";\n if (description && current !== description && (!current || current === extractedDescriptions[match.id])) {\n fields.description = description;\n }\n return fields;\n}\n\n/**\n * Whether an element's attribute already says what the code says. CBM drops path parameter names\n * (`/owners/{}`), so paths compare with parameters and a trailing slash normalised, and methods\n * ignore case: `/owners/{ownerId}` and `get` are left as they are.\n */\nexport function sameFact(key: string, current: unknown, fromCode: unknown): boolean {\n if (typeof current === \"string\" && typeof fromCode === \"string\") {\n if (key === \"http_path\") return normPath(current) === normPath(fromCode);\n if (key === \"http_method\") return current.toUpperCase() === fromCode.toUpperCase();\n }\n return current === fromCode;\n}\n\nfunction candidateDeletes(\n extraction: ExtractionGraph,\n index: ExistingIndex,\n matched: Set<string>,\n adds: PlannedElement[],\n options: PlanOptions,\n layers: Set<string> | null\n): CandidateDelete[] {\n const routes = new Set(extraction.routes.map(routeKey).filter((k): k is string => !!k));\n const fileSymbols = new Map(Object.entries(extraction.fileSymbols).map(([f, s]) => [f, new Set(s)]));\n const addAt = new Map<string, string>();\n for (const a of adds) for (const l of a.sourceReference?.locations ?? []) addAt.set(`${l.file}#${l.symbol ?? \"\"}`, a.id);\n const out: CandidateDelete[] = [];\n for (const e of index.live) {\n if (matched.has(e.id) || !inLayers(e.layer, layers)) continue;\n const locations = codeLocations(e);\n if (!locations.length) continue;\n let gone: Omit<CandidateDelete, \"id\" | \"layer\" | \"type\"> | null = null;\n let present = false;\n for (const loc of locations) {\n const verdict = judgeLocation(loc, options, fileSymbols, routes, addAt);\n if (verdict) gone ??= verdict;\n else present = true;\n }\n if (gone && !present) out.push({ id: e.id, layer: e.layer, type: e.type, ...gone });\n }\n return out.sort((a, b) => a.id.localeCompare(b.id));\n}\n\n/** Why a source location is gone, or null when it is there (or outside the run's scope). */\nfunction judgeLocation(\n loc: { file: string; symbol?: string },\n options: PlanOptions,\n fileSymbols: Map<string, Set<string>>,\n routes: Set<string>,\n addAt: Map<string, string>\n): Omit<CandidateDelete, \"id\" | \"layer\" | \"type\"> | null {\n const base = { file: loc.file, ...(loc.symbol ? { symbol: loc.symbol } : {}) };\n const changes = options.changes;\n if (changes) {\n if (changes.deleted.has(loc.file)) {\n const to = changes.deleted.get(loc.file);\n if (to) {\n const replacedBy = loc.symbol ? addAt.get(`${to}#${loc.symbol}`) : undefined;\n return { ...base, evidence: \"file-renamed\", detail: `${loc.file} was renamed to ${to} in the range`, ...(replacedBy ? { replacedBy } : {}) };\n }\n return { ...base, evidence: \"file-deleted\", detail: `${loc.file} was deleted in the range` };\n }\n if (!changes.changed.has(loc.file)) return null;\n } else if (!options.fileExists(loc.file)) {\n return { ...base, evidence: \"file-missing\", detail: `${loc.file} does not exist in the codebase` };\n }\n if (!loc.symbol) return null;\n const route = routeKey(loc.symbol);\n if (route) {\n return routes.has(route) ? null : { ...base, evidence: \"symbol-missing\", detail: `the code has no route ${loc.symbol}` };\n }\n // Only a name the code could declare can be checked; prose (\"the layout loop\") can't.\n if (!/^[A-Za-z_$][\\w$]*(\\.[A-Za-z_$][\\w$]*)*$/.test(loc.symbol)) return null;\n const symbols = fileSymbols.get(loc.file);\n if (hasSymbol(symbols, loc.symbol)) return null;\n const last = loc.symbol.split(\".\").pop()!.replace(/\\$/g, \"\\\\$\");\n const names = (text: string) => new RegExp(`(^|[^\\\\w$])${last}($|[^\\\\w$])`).test(text);\n const text = options.fileText?.(loc.file) ?? null;\n if (text === null) {\n if (!symbols) return null; // not a file the analyzer reads symbols from, and unreadable\n } else if (names(text)) return null;\n // With a range: gone in the range only if the file had it at the base.\n if (changes?.fileAtBase) {\n // A file renamed in the range had its old path at the base.\n const oldPath = [...changes.deleted].find(([, to]) => to === loc.file)?.[0];\n const before = changes.fileAtBase(oldPath ?? loc.file);\n if (before === undefined) {\n return { ...base, evidence: \"symbol-missing\", detail: `${loc.file} has no ${loc.symbol} (not among its symbols, and the name is not in the file); its text at the start of the range couldn't be read` };\n }\n if (before === null || !names(before)) {\n return {\n ...base,\n evidence: \"reference-never-matched\",\n detail: `The model's reference to ${loc.symbol} matches nothing in ${loc.file}, and ${\n before === null ? \"the file didn't exist\" : \"the file didn't have it either\"\n } at the start of the range: the range didn't remove it, the reference was wrong before it`,\n };\n }\n return { ...base, evidence: \"symbol-missing\", detail: `${loc.file} had ${loc.symbol} at the start of the range and doesn't now (not among its symbols, and the name is not in the file)` };\n }\n return { ...base, evidence: \"symbol-missing\", detail: `${loc.file} has no ${loc.symbol} (not among its symbols, and the name is not in the file)` };\n}\n\n/** `GET /owners/{}`, `__route__GET__/owners/{}` or `GET /owners/{id}` -> `GET /owners/{}`; null for a non-route. */\nfunction routeKey(symbol: string): string | null {\n const parsed = parseRouteName(symbol);\n if (parsed) return `${parsed.method} ${normPath(parsed.path)}`;\n const m = /^([A-Z]+) (\\/\\S*)$/.exec(symbol);\n return m ? `${m[1]} ${normPath(m[2])}` : null;\n}\n\n// ---------------------------------------------------------------------------\n// Relationships\n// ---------------------------------------------------------------------------\n\nexport interface RelationshipPlan {\n relationships: PlannedRelationship[];\n /** Mapped edges whose relationship the model (or the changeset) already has. */\n alreadyPresent: number;\n}\n\n/**\n * The relationships the edge mapping gives the mapped CBM edges between two elements, at least one\n * of them a symbol in scope; ends are the run's adds and matches (elements staged by hand from\n * cards included: they are matched by source file and symbol), or existing elements found by\n * their source reference. An end with no element of its own (a method of a class modelled as a\n * component) counts as the class or module it is in, when that is an element. `elements` are the\n * elements the plan keeps (adds after validation and every live existing element), by id, with\n * their `layer.type`.\n */\nexport function planRelationships(\n extraction: ExtractionGraph,\n existing: ChangesetGraph,\n elementOf: Map<string, string>,\n elements: Map<string, { layer: string; type: string }>,\n hasTriple: (id: string) => boolean,\n layers?: string[]\n): RelationshipPlan {\n const { project } = extraction;\n const index = indexExisting(existing);\n const inScope = new Set(extraction.symbols.map((s) => s.qualifiedName));\n const moduleOf = new Map<string, string>();\n for (const s of extraction.symbols) {\n const id = elementOf.get(s.qualifiedName);\n if (s.card.label === \"Module\" && s.card.file_path && id) moduleOf.set(s.card.file_path, id);\n }\n const moduleIds = new Set(moduleOf.values());\n const layerSet = layers?.length ? new Set(layers) : null;\n const mappings = new Map(extraction.config.edgeMappings.map((m) => [m.cbm_edge, m]));\n // What each file refers to (USAGE, IMPORTS or CALLS edges from any of its symbols). The index\n // resolves a call by its member's name, so MockMvc's `view().name(..)` can land on an entity's\n // `name`: a test's call is a call to a class only when the test's file refers to that class.\n const referencedIn = new Map<string, Set<string>>();\n for (const e of extraction.edges) {\n if (!REFERENCE_EDGES.has(e.type) || !e.from.file) continue;\n let refs = referencedIn.get(e.from.file);\n if (!refs) referencedIn.set(e.from.file, (refs = new Set()));\n refs.add(e.to.qualifiedName);\n }\n\n // The element a symbol is: this run's add or match, else (out of scope) an element naming it.\n const direct = (qualifiedName: string, name: string, file: string | null): string | undefined => {\n const own = elementOf.get(qualifiedName);\n if (own) return own;\n if (inScope.has(qualifiedName)) return undefined; // a symbol in scope with no element\n const route = parseRouteName(name) ?? parseRouteName(qualifiedName);\n if (route) {\n const key = `${route.method} ${normPath(route.path)}`;\n return (index.byRouteSymbolAnyFile.get(key)?.[0] ?? index.byRoute.get(key)?.[0])?.id;\n }\n return findBySymbol(index, file, symbolSpellings(qualifiedName, name, project))?.id;\n };\n // An edge end: its own element; for a file-joined edge, its file's module; else, for a member\n // with no element of its own (a method, a module's function), the class or module it is in.\n const resolve = (end: EdgeEnd, fileJoined: boolean): { id: string; rolledUp?: string } | undefined => {\n const own = direct(end.qualifiedName, end.name, end.file);\n if (own) return { id: own };\n if (fileJoined) return end.file && moduleOf.has(end.file) ? { id: moduleOf.get(end.file)! } : undefined;\n if (parseRouteName(end.name)) return undefined;\n const cut = end.qualifiedName.lastIndexOf(\".\");\n const parent = cut > 0 ? end.qualifiedName.slice(0, cut) : \"\";\n if (!parent || parent === project) return undefined;\n const id = direct(parent, parent.slice(parent.lastIndexOf(\".\") + 1), end.file);\n return id ? { id, rolledUp: parent } : undefined;\n };\n\n const found: Array<{ sourceId: string; destinationId: string; relationship: EdgeRelationship; edge: string }> = [];\n for (const edge of extraction.edges) {\n const m = mappings.get(edge.type);\n if (!m) continue;\n const fileJoined = FILE_JOINED_EDGES.has(edge.type);\n const ra = resolve(edge.from, fileJoined);\n const rb = resolve(edge.to, fileJoined);\n if (!ra || !rb) continue;\n // A member rolled up to its class says nothing about that class's own members.\n if ((ra.rolledUp && edge.to.qualifiedName.startsWith(ra.rolledUp + \".\")) || (rb.rolledUp && edge.from.qualifiedName.startsWith(rb.rolledUp + \".\"))) continue;\n const a = ra.id;\n const b = rb.id;\n if (a === b) continue;\n const ea = elements.get(a);\n const eb = elements.get(b);\n if (!ea || !eb) continue;\n // A test's call to a member of a class it never refers to is the index matching a name, not\n // the test exercising that class. A module's function is left alone: tests import modules.\n if (ea.layer === \"testing\" && rb.rolledUp && !moduleIds.has(b) && !(edge.from.file && refersToClass(referencedIn.get(edge.from.file), rb.rolledUp))) {\n continue;\n }\n const rel = relationshipFor(m, `${ea.layer}.${ea.type}`, `${eb.layer}.${eb.type}`, hasTriple);\n if (!rel) continue;\n const [sourceId, destinationId] = rel.inverted ? [b, a] : [a, b];\n found.push({ sourceId, destinationId, relationship: rel, edge: edge.type });\n }\n\n const relationships: PlannedRelationship[] = [];\n let alreadyPresent = 0;\n for (const r of dedupeRelationships(found)) {\n const source = elements.get(r.sourceId)!;\n const target = elements.get(r.destinationId)!;\n if (layerSet && !layerSet.has(source.layer) && !layerSet.has(target.layer)) continue;\n // In the model already, or deleted by this changeset on purpose: leave it.\n if (existing.getRelationship(r.sourceId, r.relationship.predicate, r.destinationId)) {\n alreadyPresent++;\n continue;\n }\n relationships.push({\n source: r.sourceId,\n predicate: r.relationship.predicate,\n target: r.destinationId,\n sourceLayer: source.layer,\n targetLayer: target.layer,\n edge: r.edge,\n });\n }\n relationships.sort((x, y) => x.source.localeCompare(y.source) || x.predicate.localeCompare(y.predicate) || x.target.localeCompare(y.target));\n return { relationships, alreadyPresent };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["text"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { closeSync, openSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { spawn as nodeSpawn, spawnSync } from "node:child_process";
|
|
6
|
+
import { readIndexMeta } from "./session-state.js";
|
|
7
|
+
import { resolveDrCommand } from "../coding-agents/drbot-mcp.js";
|
|
8
|
+
const INDEX_JOB_FILE = "index-job.json", INDEX_JOB_LOG_FILE = "index-job.log";
|
|
9
|
+
function analyzersDir(projectRoot) {
|
|
10
|
+
return path.join(projectRoot, ".dr", "analyzers");
|
|
11
|
+
}
|
|
12
|
+
function indexJobPath(projectRoot) {
|
|
13
|
+
return path.join(analyzersDir(projectRoot), INDEX_JOB_FILE);
|
|
14
|
+
}
|
|
15
|
+
function indexJobLogPath(projectRoot) {
|
|
16
|
+
return path.join(analyzersDir(projectRoot), INDEX_JOB_LOG_FILE);
|
|
17
|
+
}
|
|
18
|
+
function isProcessAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
return process.kill(pid, 0), !0;
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return error.code === "EPERM";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const INDEX_JOB_TIMEOUT_MS = 6 * 60 * 60 * 1e3, INDEX_JOB_MAX_AGE_MS = 12 * 60 * 60 * 1e3;
|
|
26
|
+
function isIndexJobProcess(pid, jobId) {
|
|
27
|
+
if (process.platform === "win32")
|
|
28
|
+
return null;
|
|
29
|
+
try {
|
|
30
|
+
const ps = spawnSync("ps", ["-ww", "-o", "args=", "-p", String(pid)], { encoding: "utf-8", stdio: "pipe" });
|
|
31
|
+
return ps.error ? null : ps.status !== 0 ? !1 : ps.stdout.includes(`--job ${jobId}`);
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function jobStillRunning(job, options) {
|
|
37
|
+
if (job.pid === null)
|
|
38
|
+
return !1;
|
|
39
|
+
if (options.isAlive)
|
|
40
|
+
return options.isAlive(job.pid);
|
|
41
|
+
if (!isProcessAlive(job.pid))
|
|
42
|
+
return !1;
|
|
43
|
+
const ownsPid = isIndexJobProcess(job.pid, job.jobId);
|
|
44
|
+
return ownsPid !== null ? ownsPid : Date.now() - Date.parse(job.startedAt) < INDEX_JOB_MAX_AGE_MS;
|
|
45
|
+
}
|
|
46
|
+
async function readIndexJob(projectRoot, options = {}) {
|
|
47
|
+
const job = await readRawIndexJob(projectRoot);
|
|
48
|
+
return !job || job.state !== "running" || jobStillRunning(job, options) ? job : {
|
|
49
|
+
...job,
|
|
50
|
+
state: "failed",
|
|
51
|
+
error: job.pid === null ? "The index process did not start." : `The index process (pid ${job.pid}) exited without finishing. See ${job.logPath}.`,
|
|
52
|
+
progress: job.progress ? { ...job.progress, phase: "failed" } : null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function readRawIndexJob(projectRoot) {
|
|
56
|
+
let content;
|
|
57
|
+
try {
|
|
58
|
+
content = await fs.readFile(indexJobPath(projectRoot), "utf-8");
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error.code === "ENOENT")
|
|
61
|
+
return null;
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(content);
|
|
66
|
+
return !parsed || typeof parsed.jobId != "string" || !["running", "done", "failed"].includes(parsed.state) || typeof parsed.startedAt != "string" ? null : {
|
|
67
|
+
version: 1,
|
|
68
|
+
jobId: parsed.jobId,
|
|
69
|
+
pid: typeof parsed.pid == "number" ? parsed.pid : null,
|
|
70
|
+
state: parsed.state,
|
|
71
|
+
analyzer: typeof parsed.analyzer == "string" ? parsed.analyzer : "",
|
|
72
|
+
force: parsed.force === !0,
|
|
73
|
+
startedAt: parsed.startedAt,
|
|
74
|
+
finishedAt: typeof parsed.finishedAt == "string" ? parsed.finishedAt : null,
|
|
75
|
+
error: typeof parsed.error == "string" ? parsed.error : null,
|
|
76
|
+
progress: parsed.progress ?? null,
|
|
77
|
+
logPath: typeof parsed.logPath == "string" ? parsed.logPath : indexJobLogPath(projectRoot),
|
|
78
|
+
result: parsed.result ?? null
|
|
79
|
+
};
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function writeIndexJobSync(projectRoot, job) {
|
|
85
|
+
mkdirSync(analyzersDir(projectRoot), { recursive: !0 });
|
|
86
|
+
const target = indexJobPath(projectRoot), temp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
87
|
+
writeFileSync(temp, JSON.stringify(job, null, 2), "utf-8"), renameSync(temp, target);
|
|
88
|
+
}
|
|
89
|
+
async function updateIndexJob(projectRoot, jobId, patch) {
|
|
90
|
+
const job = await readRawIndexJob(projectRoot);
|
|
91
|
+
return !job || job.jobId !== jobId ? !1 : (writeIndexJobSync(projectRoot, { ...job, ...patch(job) }), !0);
|
|
92
|
+
}
|
|
93
|
+
async function markIndexJobPhase(projectRoot, jobId, phase) {
|
|
94
|
+
return updateIndexJob(projectRoot, jobId, () => ({
|
|
95
|
+
progress: { phase, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
async function finishIndexJob(projectRoot, jobId, outcome) {
|
|
99
|
+
const now = (/* @__PURE__ */ new Date()).toISOString(), job = await readRawIndexJob(projectRoot);
|
|
100
|
+
if (!job || job.jobId !== jobId || job.state !== "running")
|
|
101
|
+
return !1;
|
|
102
|
+
const done = "result" in outcome;
|
|
103
|
+
return writeIndexJobSync(projectRoot, {
|
|
104
|
+
...job,
|
|
105
|
+
state: done ? "done" : "failed",
|
|
106
|
+
finishedAt: now,
|
|
107
|
+
error: done ? null : outcome.error,
|
|
108
|
+
result: done ? outcome.result : null,
|
|
109
|
+
progress: { phase: done ? "done" : "failed", updatedAt: now }
|
|
110
|
+
}), !0;
|
|
111
|
+
}
|
|
112
|
+
function stopIndexJobProcess(pid, kill = process.kill) {
|
|
113
|
+
try {
|
|
114
|
+
kill(-pid, "SIGTERM");
|
|
115
|
+
} catch {
|
|
116
|
+
try {
|
|
117
|
+
kill(pid, "SIGTERM");
|
|
118
|
+
} catch {
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const startLocks = /* @__PURE__ */ new Map();
|
|
123
|
+
async function startIndexJob(options) {
|
|
124
|
+
const key = path.resolve(options.projectRoot), run = (startLocks.get(key) ?? Promise.resolve()).catch(() => {
|
|
125
|
+
}).then(() => startIndexJobUnlocked(options));
|
|
126
|
+
startLocks.set(key, run);
|
|
127
|
+
try {
|
|
128
|
+
return await run;
|
|
129
|
+
} finally {
|
|
130
|
+
startLocks.get(key) === run && startLocks.delete(key);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function startIndexJobUnlocked(options) {
|
|
134
|
+
const { projectRoot, analyzer } = options, current = await readIndexJob(projectRoot, { isAlive: options.isAlive });
|
|
135
|
+
let replaced;
|
|
136
|
+
if (current?.state === "running") {
|
|
137
|
+
if (!options.replaceRunning)
|
|
138
|
+
return { started: !1, job: current };
|
|
139
|
+
current.pid !== null && stopIndexJobProcess(current.pid, options.kill);
|
|
140
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
141
|
+
replaced = {
|
|
142
|
+
...current,
|
|
143
|
+
state: "failed",
|
|
144
|
+
finishedAt: now,
|
|
145
|
+
error: "Stopped: replaced by a forced run.",
|
|
146
|
+
progress: { phase: "failed", updatedAt: now }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
const spawn = options.spawn ?? nodeSpawn, drCommand = options.drCommand ?? resolveDrCommand(), jobId = randomUUID(), logPath = indexJobLogPath(projectRoot), startedAt = (/* @__PURE__ */ new Date()).toISOString(), args = [
|
|
150
|
+
...drCommand.args,
|
|
151
|
+
...options.codebaseRoot ? ["--codebase-path", options.codebaseRoot] : [],
|
|
152
|
+
"analyzer",
|
|
153
|
+
"index",
|
|
154
|
+
"--name",
|
|
155
|
+
analyzer,
|
|
156
|
+
"--job",
|
|
157
|
+
jobId
|
|
158
|
+
];
|
|
159
|
+
options.force && args.push("--force"), mkdirSync(analyzersDir(projectRoot), { recursive: !0 });
|
|
160
|
+
const logFd = openSync(logPath, "w");
|
|
161
|
+
let child;
|
|
162
|
+
try {
|
|
163
|
+
child = spawn(drCommand.command, args, {
|
|
164
|
+
cwd: projectRoot,
|
|
165
|
+
detached: !0,
|
|
166
|
+
stdio: ["ignore", logFd, logFd],
|
|
167
|
+
env: { ...process.env, NO_COLOR: "1" }
|
|
168
|
+
});
|
|
169
|
+
} finally {
|
|
170
|
+
closeSync(logFd);
|
|
171
|
+
}
|
|
172
|
+
const job = {
|
|
173
|
+
version: 1,
|
|
174
|
+
jobId,
|
|
175
|
+
pid: child.pid ?? null,
|
|
176
|
+
state: child.pid ? "running" : "failed",
|
|
177
|
+
analyzer,
|
|
178
|
+
force: options.force === !0,
|
|
179
|
+
startedAt,
|
|
180
|
+
finishedAt: child.pid ? null : startedAt,
|
|
181
|
+
error: child.pid ? null : `Could not start ${drCommand.command}.`,
|
|
182
|
+
progress: { phase: child.pid ? "starting" : "failed", updatedAt: startedAt },
|
|
183
|
+
logPath,
|
|
184
|
+
result: null
|
|
185
|
+
};
|
|
186
|
+
return writeIndexJobSync(projectRoot, job), child.on("error", (error) => {
|
|
187
|
+
const message = `Could not start the index process: ${error.message}`;
|
|
188
|
+
updateIndexJob(projectRoot, jobId, (recorded) => recorded.state === "done" ? {} : {
|
|
189
|
+
state: "failed",
|
|
190
|
+
error: message,
|
|
191
|
+
finishedAt: recorded.finishedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
192
|
+
progress: { phase: "failed", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
193
|
+
}).catch(() => {
|
|
194
|
+
});
|
|
195
|
+
}), child.on("exit", (code, signal) => {
|
|
196
|
+
settleExitedJob(projectRoot, jobId, analyzer, code, signal, logPath).catch(() => {
|
|
197
|
+
});
|
|
198
|
+
}), child.unref(), { started: job.state === "running", job, ...replaced ? { replaced } : {} };
|
|
199
|
+
}
|
|
200
|
+
async function settleExitedJob(projectRoot, jobId, analyzer, code, signal, logPath) {
|
|
201
|
+
if (code === 0) {
|
|
202
|
+
let meta = null;
|
|
203
|
+
try {
|
|
204
|
+
meta = await readIndexMeta(projectRoot, analyzer);
|
|
205
|
+
} catch {
|
|
206
|
+
meta = null;
|
|
207
|
+
}
|
|
208
|
+
await finishIndexJob(projectRoot, jobId, { result: meta });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const how = signal ? `was killed (${signal})` : `exited with code ${code}`;
|
|
212
|
+
await finishIndexJob(projectRoot, jobId, { error: `The index process ${how}. See ${logPath}.` });
|
|
213
|
+
}
|
|
214
|
+
export {
|
|
215
|
+
INDEX_JOB_FILE,
|
|
216
|
+
INDEX_JOB_LOG_FILE,
|
|
217
|
+
INDEX_JOB_MAX_AGE_MS,
|
|
218
|
+
INDEX_JOB_TIMEOUT_MS,
|
|
219
|
+
finishIndexJob,
|
|
220
|
+
indexJobLogPath,
|
|
221
|
+
indexJobPath,
|
|
222
|
+
isIndexJobProcess,
|
|
223
|
+
isProcessAlive,
|
|
224
|
+
markIndexJobPhase,
|
|
225
|
+
readIndexJob,
|
|
226
|
+
startIndexJob,
|
|
227
|
+
stopIndexJobProcess
|
|
228
|
+
};
|
|
229
|
+
//# sourceMappingURL=index-job.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/analyzers/index-job.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Background codebase index jobs.\n *\n * `codebase_index` (MCP) starts `dr analyzer index` in a detached child process and returns at\n * once; indexing a large codebase can outlast an MCP call. The run is recorded in\n * `.dr/analyzers/index-job.json`, and its output goes to `.dr/analyzers/index-job.log`. The child\n * marks the record done or failed when it finishes (see the `--job` option of `dr analyzer\n * index`), so the record outlives the MCP call and the MCP server.\n *\n * Public API (stable; `project_status` reads it):\n * - `readIndexJob(projectRoot)`: the last run, or null. A record still marked `running` whose\n * process has died is returned as `failed`.\n * - `IndexJob`, `IndexJobState`, `IndexJobProgress`: the record's shape.\n * - `indexJobPath(projectRoot)`, `indexJobLogPath(projectRoot)`: where the files live.\n *\n * Internal to the CLI: `startIndexJob` and `stopIndexJobProcess` (the MCP tool),\n * `markIndexJobPhase`, `finishIndexJob` and `INDEX_JOB_TIMEOUT_MS` (the child).\n */\n\nimport { closeSync, openSync, writeFileSync, renameSync, mkdirSync } from \"node:fs\";\nimport { promises as fs } from \"node:fs\";\nimport * as path from \"node:path\";\nimport { randomUUID } from \"node:crypto\";\nimport { spawn as nodeSpawn, spawnSync } from \"node:child_process\";\nimport type { IndexResult } from \"./types.js\";\nimport { readIndexMeta } from \"./session-state.js\";\nimport { resolveDrCommand, type DrCommand } from \"../coding-agents/drbot-mcp.js\";\n\n/** File name of the job record, under `.dr/analyzers/`. */\nexport const INDEX_JOB_FILE = \"index-job.json\";\n/** File name of the child's combined stdout and stderr, under `.dr/analyzers/`. */\nexport const INDEX_JOB_LOG_FILE = \"index-job.log\";\n\nexport type IndexJobState = \"running\" | \"done\" | \"failed\";\n\n/**\n * How far a run has got. The analyzer (codebase-memory-mcp) reports nothing while it indexes, so\n * this is the phase only: `starting` (the child is booting), `indexing` (the analyzer is\n * indexing), then `done` or `failed`.\n */\nexport interface IndexJobProgress {\n phase: \"starting\" | \"indexing\" | \"done\" | \"failed\";\n updatedAt: string;\n}\n\n/** One index run, as recorded in `.dr/analyzers/index-job.json`. */\nexport interface IndexJob {\n /** Record format version. */\n version: 1;\n /** Identifies the run; the child only updates a record with its own id. */\n jobId: string;\n /** The child's process id; null if it could not be started. */\n pid: number | null;\n state: IndexJobState;\n /** Analyzer the run uses (its name in the analyzer registry, e.g. \"cbm\"). */\n analyzer: string;\n /** Whether the run re-indexes a fresh index. */\n force: boolean;\n /** ISO 8601. */\n startedAt: string;\n /** ISO 8601; null while running, and when the process died without saying. */\n finishedAt: string | null;\n /** Why the run failed; null otherwise. */\n error: string | null;\n progress: IndexJobProgress | null;\n /** The child's combined stdout and stderr. */\n logPath: string;\n /** Node and edge counts and the indexed git HEAD, when done. */\n result: IndexResult | null;\n}\n\nfunction analyzersDir(projectRoot: string): string {\n return path.join(projectRoot, \".dr\", \"analyzers\");\n}\n\n/** Path of the job record for a project. */\nexport function indexJobPath(projectRoot: string): string {\n return path.join(analyzersDir(projectRoot), INDEX_JOB_FILE);\n}\n\n/** Path of the job log for a project. */\nexport function indexJobLogPath(projectRoot: string): string {\n return path.join(analyzersDir(projectRoot), INDEX_JOB_LOG_FILE);\n}\n\n/** Whether a process is running. EPERM means it exists but belongs to someone else. */\nexport function isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * How long a background run waits for the analyzer's index call. The analyzer client's default\n * (30 seconds) suits queries, not indexing a large codebase, which is why the run is in the\n * background at all.\n */\nexport const INDEX_JOB_TIMEOUT_MS = 6 * 60 * 60 * 1000;\n\n/**\n * How long a `running` record is believed when `ps` can't say whether its pid is still the job's\n * process (Windows, or no `ps`): a pid can be reused once the job has died.\n */\nexport const INDEX_JOB_MAX_AGE_MS = 12 * 60 * 60 * 1000;\n\n/**\n * Whether `pid` is still this job's `dr analyzer index --job <jobId>` process, from its command\n * line. Null when that can't be checked (no `ps`).\n */\nexport function isIndexJobProcess(pid: number, jobId: string): boolean | null {\n if (process.platform === \"win32\") return null;\n try {\n const ps = spawnSync(\"ps\", [\"-ww\", \"-o\", \"args=\", \"-p\", String(pid)], { encoding: \"utf-8\", stdio: \"pipe\" });\n if (ps.error) return null;\n // ps exits non-zero when there is no such process.\n if (ps.status !== 0) return false;\n return ps.stdout.includes(`--job ${jobId}`);\n } catch {\n return null;\n }\n}\n\nexport interface ReadIndexJobOptions {\n /**\n * Whether a running job's pid is still its process (tests). By default the pid must be alive\n * and its command line must be the job's `dr analyzer index --job <id>`; where the command line\n * can't be read, a record older than INDEX_JOB_MAX_AGE_MS counts as dead.\n */\n isAlive?: (pid: number) => boolean;\n}\n\n/** Whether a record marked running still has its process. */\nfunction jobStillRunning(job: IndexJob, options: ReadIndexJobOptions): boolean {\n if (job.pid === null) return false;\n if (options.isAlive) return options.isAlive(job.pid);\n if (!isProcessAlive(job.pid)) return false;\n const ownsPid = isIndexJobProcess(job.pid, job.jobId);\n if (ownsPid !== null) return ownsPid;\n return Date.now() - Date.parse(job.startedAt) < INDEX_JOB_MAX_AGE_MS;\n}\n\n/**\n * The last index run for a project, or null if there has been none (or the record is unreadable).\n *\n * A record marked `running` whose process is gone (or whose pid now runs something else) is\n * returned as `failed`, with an error that points at the log. This read never writes the record.\n */\nexport async function readIndexJob(projectRoot: string, options: ReadIndexJobOptions = {}): Promise<IndexJob | null> {\n const job = await readRawIndexJob(projectRoot);\n if (!job || job.state !== \"running\") return job;\n if (jobStillRunning(job, options)) return job;\n return {\n ...job,\n state: \"failed\",\n error:\n job.pid === null\n ? \"The index process did not start.\"\n : `The index process (pid ${job.pid}) exited without finishing. See ${job.logPath}.`,\n progress: job.progress ? { ...job.progress, phase: \"failed\" } : null,\n };\n}\n\n/** The record as written, without the liveness check. */\nasync function readRawIndexJob(projectRoot: string): Promise<IndexJob | null> {\n let content: string;\n try {\n content = await fs.readFile(indexJobPath(projectRoot), \"utf-8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n try {\n const parsed = JSON.parse(content) as Partial<IndexJob>;\n if (\n !parsed ||\n typeof parsed.jobId !== \"string\" ||\n ![\"running\", \"done\", \"failed\"].includes(parsed.state as string) ||\n typeof parsed.startedAt !== \"string\"\n ) {\n return null;\n }\n return {\n version: 1,\n jobId: parsed.jobId,\n pid: typeof parsed.pid === \"number\" ? parsed.pid : null,\n state: parsed.state as IndexJobState,\n analyzer: typeof parsed.analyzer === \"string\" ? parsed.analyzer : \"\",\n force: parsed.force === true,\n startedAt: parsed.startedAt,\n finishedAt: typeof parsed.finishedAt === \"string\" ? parsed.finishedAt : null,\n error: typeof parsed.error === \"string\" ? parsed.error : null,\n progress: parsed.progress ?? null,\n logPath: typeof parsed.logPath === \"string\" ? parsed.logPath : indexJobLogPath(projectRoot),\n result: parsed.result ?? null,\n };\n } catch {\n // A half-written or hand-edited record is treated as no record.\n return null;\n }\n}\n\n/** Writes the record atomically (temp file, then rename), so a reader never sees half of it. */\nfunction writeIndexJobSync(projectRoot: string, job: IndexJob): void {\n mkdirSync(analyzersDir(projectRoot), { recursive: true });\n const target = indexJobPath(projectRoot);\n const temp = `${target}.${process.pid}.${Date.now()}.tmp`;\n writeFileSync(temp, JSON.stringify(job, null, 2), \"utf-8\");\n renameSync(temp, target);\n}\n\n/** Applies `patch` to the record if it is still the run `jobId`. Returns whether it did. */\nasync function updateIndexJob(\n projectRoot: string,\n jobId: string,\n patch: (job: IndexJob) => Partial<IndexJob>\n): Promise<boolean> {\n const job = await readRawIndexJob(projectRoot);\n if (!job || job.jobId !== jobId) return false;\n writeIndexJobSync(projectRoot, { ...job, ...patch(job) });\n return true;\n}\n\n/** Child side: records the phase the run has reached. */\nexport async function markIndexJobPhase(\n projectRoot: string,\n jobId: string,\n phase: IndexJobProgress[\"phase\"]\n): Promise<boolean> {\n return updateIndexJob(projectRoot, jobId, () => ({\n progress: { phase, updatedAt: new Date().toISOString() },\n }));\n}\n\n/** Child side: records how the run ended. Only a running record is changed. */\nexport async function finishIndexJob(\n projectRoot: string,\n jobId: string,\n outcome: { result: IndexResult | null } | { error: string }\n): Promise<boolean> {\n const now = new Date().toISOString();\n const job = await readRawIndexJob(projectRoot);\n if (!job || job.jobId !== jobId || job.state !== \"running\") return false;\n const done = \"result\" in outcome;\n writeIndexJobSync(projectRoot, {\n ...job,\n state: done ? \"done\" : \"failed\",\n finishedAt: now,\n error: done ? null : outcome.error,\n result: done ? outcome.result : null,\n progress: { phase: done ? \"done\" : \"failed\", updatedAt: now },\n });\n return true;\n}\n\n/** Stops a job's process and its children (the analyzer): its process group, else the pid. */\nexport function stopIndexJobProcess(pid: number, kill: (pid: number, signal: NodeJS.Signals) => void = process.kill): void {\n try {\n // The child was started detached, so it leads its own process group.\n kill(-pid, \"SIGTERM\");\n } catch {\n try {\n kill(pid, \"SIGTERM\");\n } catch {\n // Already gone.\n }\n }\n}\n\n/** What `startIndexJob` needs from a spawned child. */\nexport interface SpawnedChild {\n pid?: number;\n unref(): void;\n on(event: \"error\", listener: (error: Error) => void): unknown;\n on(event: \"exit\", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;\n}\n\nexport type SpawnFn = (\n command: string,\n args: string[],\n options: { cwd: string; detached: boolean; stdio: [\"ignore\", number, number]; env: NodeJS.ProcessEnv }\n) => SpawnedChild;\n\nexport interface StartIndexJobOptions {\n /** Project root; pass the canonical (real) path, the one the child will find from its cwd. */\n projectRoot: string;\n analyzer: string;\n /**\n * The code to index, passed to the child as `--codebase-path`, so the child indexes the same\n * directory, spelled the same way, as the caller checks freshness and verifies against.\n */\n codebaseRoot?: string;\n force?: boolean;\n /** Stop a run in flight and start a new one, instead of returning the run in flight. */\n replaceRunning?: boolean;\n /** Spawns the child (tests). Defaults to node's spawn. */\n spawn?: SpawnFn;\n /** Sends signals (tests). Defaults to `process.kill`. */\n kill?: (pid: number, signal: NodeJS.Signals) => void;\n /** How to run this dr installation. Defaults to `resolveDrCommand()`. */\n drCommand?: DrCommand;\n isAlive?: (pid: number) => boolean;\n}\n\nexport interface StartIndexJobResult {\n /** False when a run was already in flight; `job` is then that run. */\n started: boolean;\n job: IndexJob;\n /** The run in flight that this start stopped (`replaceRunning`). */\n replaced?: IndexJob;\n}\n\n/** One start at a time per project in this process, so two calls can't both start a run. */\nconst startLocks = new Map<string, Promise<unknown>>();\n\n/**\n * Starts `dr analyzer index` for the project in a detached child process and returns at once.\n * If a run is already in flight, returns it instead of starting another, unless\n * `replaceRunning` is set: then it stops that run's process group and starts a new one.\n *\n * The child runs in its own process group with its output in the job log, so it survives the\n * caller returning and the caller's process exiting.\n */\nexport async function startIndexJob(options: StartIndexJobOptions): Promise<StartIndexJobResult> {\n const key = path.resolve(options.projectRoot);\n const previous = startLocks.get(key) ?? Promise.resolve();\n const run = previous.catch(() => undefined).then(() => startIndexJobUnlocked(options));\n startLocks.set(key, run);\n try {\n return await run;\n } finally {\n if (startLocks.get(key) === run) startLocks.delete(key);\n }\n}\n\nasync function startIndexJobUnlocked(options: StartIndexJobOptions): Promise<StartIndexJobResult> {\n const { projectRoot, analyzer } = options;\n const current = await readIndexJob(projectRoot, { isAlive: options.isAlive });\n let replaced: IndexJob | undefined;\n if (current?.state === \"running\") {\n if (!options.replaceRunning) return { started: false, job: current };\n // readIndexJob has just confirmed the pid is still this job's process.\n if (current.pid !== null) stopIndexJobProcess(current.pid, options.kill);\n const now = new Date().toISOString();\n replaced = {\n ...current,\n state: \"failed\",\n finishedAt: now,\n error: \"Stopped: replaced by a forced run.\",\n progress: { phase: \"failed\", updatedAt: now },\n };\n }\n\n const spawn = options.spawn ?? (nodeSpawn as unknown as SpawnFn);\n const drCommand = options.drCommand ?? resolveDrCommand();\n const jobId = randomUUID();\n const logPath = indexJobLogPath(projectRoot);\n const startedAt = new Date().toISOString();\n // --codebase-path is a global option, so it goes before the subcommand.\n const args = [\n ...drCommand.args,\n ...(options.codebaseRoot ? [\"--codebase-path\", options.codebaseRoot] : []),\n \"analyzer\",\n \"index\",\n \"--name\",\n analyzer,\n \"--job\",\n jobId,\n ];\n if (options.force) args.push(\"--force\");\n\n mkdirSync(analyzersDir(projectRoot), { recursive: true });\n const logFd = openSync(logPath, \"w\");\n let child: SpawnedChild;\n try {\n child = spawn(drCommand.command, args, {\n cwd: projectRoot,\n detached: true,\n stdio: [\"ignore\", logFd, logFd],\n env: { ...process.env, NO_COLOR: \"1\" },\n });\n } finally {\n closeSync(logFd);\n }\n\n const job: IndexJob = {\n version: 1,\n jobId,\n pid: child.pid ?? null,\n state: child.pid ? \"running\" : \"failed\",\n analyzer,\n force: options.force === true,\n startedAt,\n finishedAt: child.pid ? null : startedAt,\n error: child.pid ? null : `Could not start ${drCommand.command}.`,\n progress: { phase: child.pid ? \"starting\" : \"failed\", updatedAt: startedAt },\n logPath,\n result: null,\n };\n // Written synchronously, right after the spawn: the child takes far longer to boot than this.\n writeIndexJobSync(projectRoot, job);\n\n child.on(\"error\", (error) => {\n const message = `Could not start the index process: ${error.message}`;\n void updateIndexJob(projectRoot, jobId, (recorded) =>\n recorded.state === \"done\"\n ? {}\n : {\n state: \"failed\",\n error: message,\n finishedAt: recorded.finishedAt ?? new Date().toISOString(),\n progress: { phase: \"failed\", updatedAt: new Date().toISOString() },\n }\n ).catch(() => undefined);\n });\n // The child records done or failed itself; this covers a child that exits without doing so.\n child.on(\"exit\", (code, signal) => {\n void settleExitedJob(projectRoot, jobId, analyzer, code, signal, logPath).catch(() => undefined);\n });\n child.unref();\n\n return { started: job.state === \"running\", job, ...(replaced ? { replaced } : {}) };\n}\n\n/**\n * After the child exits: a record it left running is done if it exited cleanly (with the index\n * metadata as the result, when there is any), and failed otherwise.\n */\nasync function settleExitedJob(\n projectRoot: string,\n jobId: string,\n analyzer: string,\n code: number | null,\n signal: NodeJS.Signals | null,\n logPath: string\n): Promise<void> {\n if (code === 0) {\n let meta: IndexResult | null = null;\n try {\n meta = await readIndexMeta(projectRoot, analyzer);\n } catch {\n meta = null;\n }\n await finishIndexJob(projectRoot, jobId, { result: meta });\n return;\n }\n const how = signal ? `was killed (${signal})` : `exited with code ${code}`;\n await finishIndexJob(projectRoot, jobId, { error: `The index process ${how}. See ${logPath}.` });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -82,6 +82,15 @@ class MappingLoader {
|
|
|
82
82
|
if (Array.isArray(heuristics))
|
|
83
83
|
return heuristics.find((h) => h.name === name);
|
|
84
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Get every heuristic, in file order (Tier-0 rules among them; see tier0-rules.ts)
|
|
87
|
+
*
|
|
88
|
+
* @returns Array of heuristics
|
|
89
|
+
*/
|
|
90
|
+
getHeuristics() {
|
|
91
|
+
const heuristics = this.artifact.heuristics.heuristics;
|
|
92
|
+
return Array.isArray(heuristics) ? heuristics : [];
|
|
93
|
+
}
|
|
85
94
|
/**
|
|
86
95
|
* Get all filtering rules
|
|
87
96
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/analyzers/mapping-loader.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * MappingLoader - reads and validates packed analyzer artifact\n *\n * Loads the compiled CBM mapping from cli/src/schemas/bundled/analyzers/<name>.json\n * and provides typed accessors for node mappings, edge mappings, heuristics, and rules.\n *\n * The loader validates the top-level structure and throws CLIError if the artifact\n * is malformed or missing required keys, with a suggestion to run npm run build:spec.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { CLIError, ErrorCategory } from \"../utils/errors.js\";\nimport type {\n AnalyzerEdgeMapping,\n AnalyzerHeuristic,\n AnalyzerNodeMapping,\n ConfidenceRubric,\n FilteringRule,\n} from \"./types.js\";\n\n/**\n * Validate analyzer name to prevent path traversal attacks\n * Rejects names containing `/`, `\\`, or `..`\n *\n * @param analyzerName Analyzer name to validate\n * @throws CLIError if name contains path traversal characters\n */\nfunction validateAnalyzerName(analyzerName: string): void {\n if (!analyzerName) {\n throw new CLIError(\n \"Analyzer name cannot be empty\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"/\") || analyzerName.includes(\"\\\\\")) {\n throw new CLIError(\n \"Analyzer name cannot contain path separators (/ or \\\\)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"..\")) {\n throw new CLIError(\n \"Analyzer name cannot contain parent directory references (..)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n}\n\ninterface AnalyzerManifestMetadata {\n name: string;\n display_name: string;\n description: string;\n homepage: string;\n binary_names?: string[];\n mcp_server_name?: string;\n [key: string]: unknown;\n}\n\ninterface PackedArtifact {\n name: string;\n version: string;\n analyzer?: AnalyzerManifestMetadata;\n nodes_by_label: Record<string, AnalyzerNodeMapping>;\n edges_by_type: Record<string, AnalyzerEdgeMapping>;\n heuristics: {\n confidence_rubric?: ConfidenceRubric;\n heuristics?: AnalyzerHeuristic[];\n filtering_rules?: FilteringRule[];\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\nexport class MappingLoader {\n private artifact: PackedArtifact;\n private analyzerName: string;\n\n private constructor(artifact: PackedArtifact, analyzerName: string) {\n this.artifact = artifact;\n this.analyzerName = analyzerName;\n }\n\n /**\n * Load a mapping artifact from the bundled schemas directory\n *\n * @param analyzerName Name of the analyzer (e.g., \"cbm\")\n * @returns Loaded and validated MappingLoader instance\n * @throws CLIError if artifact is missing, malformed, or analyzerName contains path traversal characters\n */\n static async load(analyzerName: string): Promise<MappingLoader> {\n validateAnalyzerName(analyzerName);\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const artifactPath = path.join(\n currentDir,\n \"../schemas/bundled/analyzers\",\n `${analyzerName}.json`\n );\n\n let artifact: unknown;\n try {\n const content = await fs.readFile(artifactPath, \"utf-8\");\n artifact = JSON.parse(content);\n } catch (error) {\n const message =\n error instanceof Error && \"code\" in error && error.code === \"ENOENT\"\n ? `Analyzer mapping not found: ${analyzerName}`\n : `Failed to read analyzer mapping: ${error instanceof Error ? error.message : String(error)}`;\n\n throw new CLIError(\n message,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `npm run build:spec` to compile the analyzer mapping artifacts\",\n `Check that spec/analyzers/${analyzerName}/ sources exist`,\n ]\n );\n }\n\n // Validate structure\n if (!artifact || typeof artifact !== \"object\") {\n throw new CLIError(\n `Invalid analyzer mapping structure: expected object, got ${typeof artifact}`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n const obj = artifact as Record<string, unknown>;\n const requiredKeys = [\"nodes_by_label\", \"edges_by_type\", \"heuristics\"];\n const missingKeys = requiredKeys.filter((key) => !(key in obj));\n\n if (missingKeys.length > 0) {\n throw new CLIError(\n `Invalid analyzer mapping: missing required keys: ${missingKeys.join(\", \")}`,\n ErrorCategory.VALIDATION,\n [\n \"Run `npm run build:spec` to recompile the analyzer mapping artifacts\",\n `Ensure spec/analyzers/${analyzerName}/ contains valid layer/node/edge definitions`,\n ]\n );\n }\n\n // Validate types of required keys\n if (typeof obj.nodes_by_label !== \"object\" || obj.nodes_by_label === null) {\n throw new CLIError(\n `Invalid analyzer mapping: nodes_by_label must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.edges_by_type !== \"object\" || obj.edges_by_type === null) {\n throw new CLIError(\n `Invalid analyzer mapping: edges_by_type must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.heuristics !== \"object\" || obj.heuristics === null) {\n throw new CLIError(\n `Invalid analyzer mapping: heuristics must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n return new MappingLoader(artifact as PackedArtifact, analyzerName);\n }\n\n /**\n * Get the mapping for a node label\n *\n * @param label CBM node label (e.g., \"Route\", \"Function\")\n * @returns The mapping definition or undefined if not found\n */\n getNodeMapping(label: string): AnalyzerNodeMapping | undefined {\n return this.artifact.nodes_by_label[label];\n }\n\n /**\n * Get the mapping for an edge type\n *\n * @param type CBM edge type (e.g., \"HTTP_CALLS\", \"HANDLES\")\n * @returns The mapping definition or undefined if not found\n */\n getEdgeMapping(type: string): AnalyzerEdgeMapping | undefined {\n return this.artifact.edges_by_type[type];\n }\n\n /**\n * Get a heuristic by name\n *\n * @param name Heuristic name (e.g., \"min_fan_in\")\n * @returns The heuristic definition or undefined if not found\n */\n getHeuristic(name: string): AnalyzerHeuristic | undefined {\n const heuristics = this.artifact.heuristics.heuristics;\n if (!Array.isArray(heuristics)) {\n return undefined;\n }\n return heuristics.find((h) => h.name === name);\n }\n\n /**\n * Get all filtering rules\n *\n * @returns Array of filtering rules\n */\n getFilteringRules(): FilteringRule[] {\n const rules = this.artifact.heuristics.filtering_rules;\n return Array.isArray(rules) ? rules : [];\n }\n\n /**\n * Get the confidence rubric\n *\n * @returns Confidence levels and their score ranges\n */\n getConfidenceRubric(): ConfidenceRubric | undefined {\n return this.artifact.heuristics.confidence_rubric;\n }\n\n /**\n * Get all node labels in the mapping\n *\n * @returns Array of CBM label names\n */\n getNodeLabels(): string[] {\n return Object.keys(this.artifact.nodes_by_label);\n }\n\n /**\n * Get all edge types in the mapping\n *\n * @returns Array of CBM edge type names\n */\n getEdgeTypes(): string[] {\n return Object.keys(this.artifact.edges_by_type);\n }\n\n /**\n * Get analyzer name\n *\n * @returns The name of the loaded analyzer\n */\n getAnalyzerName(): string {\n return this.analyzerName;\n }\n\n /**\n * Get analyzer version\n *\n * @returns The version of the artifact\n */\n getVersion(): string {\n return this.artifact.version;\n }\n\n /**\n * Get analyzer metadata (display name, description, homepage, etc.)\n *\n * @returns Analyzer metadata object or undefined if not available\n */\n getAnalyzerMetadata(): AnalyzerManifestMetadata | undefined {\n return this.artifact.analyzer;\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * MappingLoader - reads and validates packed analyzer artifact\n *\n * Loads the compiled CBM mapping from cli/src/schemas/bundled/analyzers/<name>.json\n * and provides typed accessors for node mappings, edge mappings, heuristics, and rules.\n *\n * The loader validates the top-level structure and throws CLIError if the artifact\n * is malformed or missing required keys, with a suggestion to run npm run build:spec.\n */\n\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { CLIError, ErrorCategory } from \"../utils/errors.js\";\nimport type {\n AnalyzerEdgeMapping,\n AnalyzerHeuristic,\n AnalyzerNodeMapping,\n ConfidenceRubric,\n FilteringRule,\n} from \"./types.js\";\n\n/**\n * Validate analyzer name to prevent path traversal attacks\n * Rejects names containing `/`, `\\`, or `..`\n *\n * @param analyzerName Analyzer name to validate\n * @throws CLIError if name contains path traversal characters\n */\nfunction validateAnalyzerName(analyzerName: string): void {\n if (!analyzerName) {\n throw new CLIError(\n \"Analyzer name cannot be empty\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"/\") || analyzerName.includes(\"\\\\\")) {\n throw new CLIError(\n \"Analyzer name cannot contain path separators (/ or \\\\)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n\n if (analyzerName.includes(\"..\")) {\n throw new CLIError(\n \"Analyzer name cannot contain parent directory references (..)\",\n ErrorCategory.VALIDATION,\n []\n );\n }\n}\n\ninterface AnalyzerManifestMetadata {\n name: string;\n display_name: string;\n description: string;\n homepage: string;\n binary_names?: string[];\n mcp_server_name?: string;\n [key: string]: unknown;\n}\n\ninterface PackedArtifact {\n name: string;\n version: string;\n analyzer?: AnalyzerManifestMetadata;\n nodes_by_label: Record<string, AnalyzerNodeMapping>;\n edges_by_type: Record<string, AnalyzerEdgeMapping>;\n heuristics: {\n confidence_rubric?: ConfidenceRubric;\n heuristics?: AnalyzerHeuristic[];\n filtering_rules?: FilteringRule[];\n [key: string]: unknown;\n };\n [key: string]: unknown;\n}\n\nexport class MappingLoader {\n private artifact: PackedArtifact;\n private analyzerName: string;\n\n private constructor(artifact: PackedArtifact, analyzerName: string) {\n this.artifact = artifact;\n this.analyzerName = analyzerName;\n }\n\n /**\n * Load a mapping artifact from the bundled schemas directory\n *\n * @param analyzerName Name of the analyzer (e.g., \"cbm\")\n * @returns Loaded and validated MappingLoader instance\n * @throws CLIError if artifact is missing, malformed, or analyzerName contains path traversal characters\n */\n static async load(analyzerName: string): Promise<MappingLoader> {\n validateAnalyzerName(analyzerName);\n\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const artifactPath = path.join(\n currentDir,\n \"../schemas/bundled/analyzers\",\n `${analyzerName}.json`\n );\n\n let artifact: unknown;\n try {\n const content = await fs.readFile(artifactPath, \"utf-8\");\n artifact = JSON.parse(content);\n } catch (error) {\n const message =\n error instanceof Error && \"code\" in error && error.code === \"ENOENT\"\n ? `Analyzer mapping not found: ${analyzerName}`\n : `Failed to read analyzer mapping: ${error instanceof Error ? error.message : String(error)}`;\n\n throw new CLIError(\n message,\n ErrorCategory.NOT_FOUND,\n [\n \"Run `npm run build:spec` to compile the analyzer mapping artifacts\",\n `Check that spec/analyzers/${analyzerName}/ sources exist`,\n ]\n );\n }\n\n // Validate structure\n if (!artifact || typeof artifact !== \"object\") {\n throw new CLIError(\n `Invalid analyzer mapping structure: expected object, got ${typeof artifact}`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n const obj = artifact as Record<string, unknown>;\n const requiredKeys = [\"nodes_by_label\", \"edges_by_type\", \"heuristics\"];\n const missingKeys = requiredKeys.filter((key) => !(key in obj));\n\n if (missingKeys.length > 0) {\n throw new CLIError(\n `Invalid analyzer mapping: missing required keys: ${missingKeys.join(\", \")}`,\n ErrorCategory.VALIDATION,\n [\n \"Run `npm run build:spec` to recompile the analyzer mapping artifacts\",\n `Ensure spec/analyzers/${analyzerName}/ contains valid layer/node/edge definitions`,\n ]\n );\n }\n\n // Validate types of required keys\n if (typeof obj.nodes_by_label !== \"object\" || obj.nodes_by_label === null) {\n throw new CLIError(\n `Invalid analyzer mapping: nodes_by_label must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.edges_by_type !== \"object\" || obj.edges_by_type === null) {\n throw new CLIError(\n `Invalid analyzer mapping: edges_by_type must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n if (typeof obj.heuristics !== \"object\" || obj.heuristics === null) {\n throw new CLIError(\n `Invalid analyzer mapping: heuristics must be an object`,\n ErrorCategory.VALIDATION,\n [\"Run `npm run build:spec` to recompile the analyzer mapping artifacts\"]\n );\n }\n\n return new MappingLoader(artifact as PackedArtifact, analyzerName);\n }\n\n /**\n * Get the mapping for a node label\n *\n * @param label CBM node label (e.g., \"Route\", \"Function\")\n * @returns The mapping definition or undefined if not found\n */\n getNodeMapping(label: string): AnalyzerNodeMapping | undefined {\n return this.artifact.nodes_by_label[label];\n }\n\n /**\n * Get the mapping for an edge type\n *\n * @param type CBM edge type (e.g., \"HTTP_CALLS\", \"HANDLES\")\n * @returns The mapping definition or undefined if not found\n */\n getEdgeMapping(type: string): AnalyzerEdgeMapping | undefined {\n return this.artifact.edges_by_type[type];\n }\n\n /**\n * Get a heuristic by name\n *\n * @param name Heuristic name (e.g., \"min_fan_in\")\n * @returns The heuristic definition or undefined if not found\n */\n getHeuristic(name: string): AnalyzerHeuristic | undefined {\n const heuristics = this.artifact.heuristics.heuristics;\n if (!Array.isArray(heuristics)) {\n return undefined;\n }\n return heuristics.find((h) => h.name === name);\n }\n\n /**\n * Get every heuristic, in file order (Tier-0 rules among them; see tier0-rules.ts)\n *\n * @returns Array of heuristics\n */\n getHeuristics(): AnalyzerHeuristic[] {\n const heuristics = this.artifact.heuristics.heuristics;\n return Array.isArray(heuristics) ? heuristics : [];\n }\n\n /**\n * Get all filtering rules\n *\n * @returns Array of filtering rules\n */\n getFilteringRules(): FilteringRule[] {\n const rules = this.artifact.heuristics.filtering_rules;\n return Array.isArray(rules) ? rules : [];\n }\n\n /**\n * Get the confidence rubric\n *\n * @returns Confidence levels and their score ranges\n */\n getConfidenceRubric(): ConfidenceRubric | undefined {\n return this.artifact.heuristics.confidence_rubric;\n }\n\n /**\n * Get all node labels in the mapping\n *\n * @returns Array of CBM label names\n */\n getNodeLabels(): string[] {\n return Object.keys(this.artifact.nodes_by_label);\n }\n\n /**\n * Get all edge types in the mapping\n *\n * @returns Array of CBM edge type names\n */\n getEdgeTypes(): string[] {\n return Object.keys(this.artifact.edges_by_type);\n }\n\n /**\n * Get analyzer name\n *\n * @returns The name of the loaded analyzer\n */\n getAnalyzerName(): string {\n return this.analyzerName;\n }\n\n /**\n * Get analyzer version\n *\n * @returns The version of the artifact\n */\n getVersion(): string {\n return this.artifact.version;\n }\n\n /**\n * Get analyzer metadata (display name, description, homepage, etc.)\n *\n * @returns Analyzer metadata object or undefined if not available\n */\n getAnalyzerMetadata(): AnalyzerManifestMetadata | undefined {\n return this.artifact.analyzer;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|