@documentation-robotics/cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -14
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/cards/build.js +21 -1
- package/dist/analyzers/cards/build.js.map +2 -2
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +76 -4
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +12 -6
- package/dist/analyzers/cbm-responses.js.map +2 -2
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +6 -2
- package/dist/analyzers/edge-relationships.js.map +2 -2
- 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/cli.js +12 -10
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +120 -18
- package/dist/coding-agents/drbot-mcp.js.map +2 -2
- package/dist/commands/add.js +4 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +35 -35
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +16 -8
- package/dist/commands/changeset-verify.js.map +2 -2
- package/dist/commands/changeset.js +41 -11
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +35 -31
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +2 -12
- 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 +14 -13
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +28 -120
- package/dist/commands/upgrade.js.map +2 -2
- 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 +30 -12
- 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 +2 -1
- package/dist/core/catalog-migration.js.map +2 -2
- package/dist/core/changeset-base-index.js +7 -0
- package/dist/core/changeset-base-index.js.map +2 -2
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-verify.js +356 -26
- package/dist/core/changeset-verify.js.map +3 -3
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/migration-registry.js +13 -1
- package/dist/core/migration-registry.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-staging.js +94 -1
- package/dist/core/relationship-staging.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +83 -3
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staging-area.js +85 -8
- 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/generated/relationship-index.js +124 -15
- 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 +19 -5
- package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +7 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
- package/dist/integrations/claude_code/docs/README.md +20 -1
- 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_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
- package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
- package/dist/mcp/changeset-only.js +14 -3
- package/dist/mcp/changeset-only.js.map +2 -2
- 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 +39 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
- package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
- 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 +133 -57
- package/dist/mcp/tools/changeset-editing.js.map +3 -3
- package/dist/mcp/tools/changeset-lifecycle.js +1 -1
- package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +12 -6
- package/dist/mcp/tools/changeset-verify.js.map +2 -2
- 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 +15 -0
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +189 -73
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-update.js +16 -12
- 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 +89 -20
- package/dist/mcp/tools/relationship-tools.js.map +2 -2
- 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 +10 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- 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 +22 -10
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +1 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +1 -1
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +3 -3
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +145 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +508 -237
- 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/telemetry/index.js +1 -1
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/source-reference.js +58 -27
- 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/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- 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 +1 -1
- package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
- package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/mcp/tools/model-validate.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * model_validate \u2014 run the 5-stage validation pipeline against the model.\n * Mirrors `dr validate`, reusing the shared Validator and ValidationFormatter.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { Model } from \"../../core/model.js\";\nimport { getAllLayerIds, isValidLayer } from \"../../generated/layer-registry.js\";\nimport { CLIError, ErrorCategory, findSimilar, formatValidOptions } from \"../../utils/errors.js\";\nimport { ValidationFormatter } from \"../../validators/validation-formatter.js\";\nimport { Validator } from \"../../validators/validator.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\n\nconst inputSchema = {\n layers: z.array(z.string()).optional().describe(\"Restrict validation to these layers. Omit to validate the whole model.\"),\n strict: z.boolean().optional().describe(\"Also flag elements with no description as warnings.\"),\n orphans: z\n .boolean()\n .optional()\n .describe(\"Return only the orphaned-elements report instead of running full validation.\"),\n rootPath: rootPathSchema,\n};\n\nexport type ModelValidateArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\nexport async function modelValidateHandler(args: ModelValidateArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n\n // `Validator` is typed to accept `Model`, but a projected changeset is a\n // `ProjectedModel` (manifest/layers/relationships only, no `rootPath` or the\n // mutation methods `Model` exposes). The validation pipeline only reads\n // `rootPath` for telemetry (validator.ts), so it's carried over explicitly\n // rather than left undefined. This mirrors the identical cast in\n // `cli/src/commands/validate.ts`; a proper fix would give `Validator` a\n // narrower \"validatable model\" interface shared by both types.\n const activeChangesetId = model.getActiveChangesetId();\n let modelToValidate = (\n activeChangesetId\n ? { ...(await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)), rootPath: model.rootPath }\n : model\n ) as unknown as Model;\n\n // `loadModel()` returns the full cached model (all layers), so a `layers` scope\n // is applied here rather than at load time. Copies the layers Map onto a new\n // object instead of mutating `model.layers` directly, since that Map is the\n // shared cached instance other tool calls read from.\n if (args.layers && args.layers.length > 0) {\n const unknownLayers = args.layers.filter((name) => !isValidLayer(name));\n if (unknownLayers.length > 0) {\n const validLayers = getAllLayerIds();\n const suggestions: string[] = [`Use a valid layer name: ${formatValidOptions(validLayers)}`];\n for (const unknown of unknownLayers) {\n const similar = findSimilar(unknown, validLayers, 3);\n if (similar.length > 0) suggestions.unshift(`Did you mean \"${similar.join(\" or \")}\" instead of \"${unknown}\"?`);\n }\n throw new CLIError(`Unknown layer(s): ${unknownLayers.join(\", \")}`, ErrorCategory.USER, suggestions);\n }\n\n const requestedLayers = new Set(args.layers);\n modelToValidate = {\n ...modelToValidate,\n layers: new Map(\n Array.from(modelToValidate.layers.entries()).filter(([name]) => requestedLayers.has(name))\n ),\n } as unknown as Model;\n }\n\n if (args.orphans) {\n const stats = ValidationFormatter.calculateStats(modelToValidate);\n return jsonResult({\n orphanCount: stats.orphanedElements.length,\n orphanedElements: stats.orphanedElements,\n });\n }\n\n const validator = new Validator();\n const result = await validator.validateModel(modelToValidate);\n\n const stats = ValidationFormatter.calculateStats(modelToValidate);\n for (const orphanId of stats.orphanedElements) {\n result.addWarning({\n message: `Element '${orphanId}' is orphaned (no cross-layer references or intra-layer relationships)`,\n layer: orphanId.split(\".\")[0] ?? \"\",\n elementId: orphanId,\n category: \"orphan\",\n fixSuggestion: \"Add cross-layer references or relationships to connect this element to the rest of the model\",\n });\n }\n\n for (const [layerName, layer] of modelToValidate.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n if (!element.source_reference) {\n result.addWarning({\n message: `Element '${elementId}' has no source reference`,\n layer: layerName,\n elementId,\n fixSuggestion: \"Add a source_reference with provenance to link this element to its implementation\",\n });\n }\n if (args.strict && (!element.description || element.description.trim() === \"\")) {\n result.addWarning({\n message: `Element '${elementId}' has no description`,\n layer: layerName,\n elementId,\n fixSuggestion: \"Add a description to document what this element represents\",\n });\n }\n }\n }\n\n const formatted = ValidationFormatter.toJSON(result, modelToValidate);\n const valid = result.isValid() && !(args.strict && result.warnings.length > 0);\n\n return jsonResult({ ...formatted, valid });\n });\n}\n\nexport const modelValidateTool: McpToolDefinition<typeof inputSchema> = {\n name: \"model_validate\",\n description: \"Run the full schema/naming/reference/semantic/relationship validation pipeline against the model.\",\n inputSchema,\n handler: modelValidateHandler,\n};\n"],
|
|
5
|
-
"mappings": "AAKA,SAAS,SAAS;AAGlB,SAAS,gBAAgB,oBAAoB;AAC7C,SAAS,UAAU,eAAe,aAAa,0BAA0B;AACzE,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;
|
|
4
|
+
"sourcesContent": ["/**\n * model_validate \u2014 run the 5-stage validation pipeline against the model.\n * Mirrors `dr validate`, reusing the shared Validator and ValidationFormatter.\n */\n\nimport { z } from \"zod\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport type { Model } from \"../../core/model.js\";\nimport { getAllLayerIds, isValidLayer } from \"../../generated/layer-registry.js\";\nimport { CLIError, ErrorCategory, findSimilar, formatValidOptions } from \"../../utils/errors.js\";\nimport { ValidationFormatter } from \"../../validators/validation-formatter.js\";\nimport { Validator } from \"../../validators/validator.js\";\nimport type { ValidationIssue } from \"../../validators/types.js\";\nimport { jsonResult, loadModel, rootPathSchema, runTool, type McpToolDefinition } from \"./shared.js\";\n\nconst inputSchema = {\n layers: z.array(z.string()).optional().describe(\"Restrict validation to these layers. Omit to validate the whole model.\"),\n strict: z.boolean().optional().describe(\"Also flag elements with no description as warnings.\"),\n orphans: z\n .boolean()\n .optional()\n .describe(\"Return only the orphaned-elements report instead of running full validation.\"),\n rootPath: rootPathSchema,\n};\n\nexport type ModelValidateArgs = z.infer<z.ZodObject<typeof inputSchema>>;\n\nexport async function modelValidateHandler(args: ModelValidateArgs): Promise<CallToolResult> {\n return runTool(async () => {\n const model = await loadModel(args.rootPath);\n\n // `Validator` is typed to accept `Model`, but a projected changeset is a\n // `ProjectedModel` (manifest/layers/relationships only, no `rootPath` or the\n // mutation methods `Model` exposes). The validation pipeline only reads\n // `rootPath` for telemetry (validator.ts), so it's carried over explicitly\n // rather than left undefined. This mirrors the identical cast in\n // `cli/src/commands/validate.ts`; a proper fix would give `Validator` a\n // narrower \"validatable model\" interface shared by both types.\n const activeChangesetId = model.getActiveChangesetId();\n let modelToValidate = (\n activeChangesetId\n ? { ...(await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)), rootPath: model.rootPath }\n : model\n ) as unknown as Model;\n\n // Ends outside a `layers` scope that exist nowhere in the full model (see danglingOutOfScopeEnds).\n let outOfScopeIssues: Array<Omit<ValidationIssue, \"severity\">> = [];\n\n // `loadModel()` returns the full cached model (all layers), so a `layers` scope\n // is applied here rather than at load time. Copies the layers Map onto a new\n // object instead of mutating `model.layers` directly, since that Map is the\n // shared cached instance other tool calls read from.\n if (args.layers && args.layers.length > 0) {\n const unknownLayers = args.layers.filter((name) => !isValidLayer(name));\n if (unknownLayers.length > 0) {\n const validLayers = getAllLayerIds();\n const suggestions: string[] = [`Use a valid layer name: ${formatValidOptions(validLayers)}`];\n for (const unknown of unknownLayers) {\n const similar = findSimilar(unknown, validLayers, 3);\n if (similar.length > 0) suggestions.unshift(`Did you mean \"${similar.join(\" or \")}\" instead of \"${unknown}\"?`);\n }\n throw new CLIError(`Unknown layer(s): ${unknownLayers.join(\", \")}`, ErrorCategory.USER, suggestions);\n }\n\n const requestedLayers = new Set(args.layers);\n outOfScopeIssues = danglingOutOfScopeEnds(modelToValidate, requestedLayers);\n // Relationships stay whole, as in `dr validate --layers`, which loads only the requested\n // layers. `loadedLayerFilter` is what tells the validators that elements outside it were\n // left out on purpose: a relationship or reference whose other end lies outside the scope\n // is then skipped instead of reported as pointing at a missing element.\n modelToValidate = {\n ...modelToValidate,\n layers: new Map(\n Array.from(modelToValidate.layers.entries()).filter(([name]) => requestedLayers.has(name))\n ),\n loadedLayerFilter: [...requestedLayers],\n } as unknown as Model;\n }\n\n if (args.orphans) {\n const stats = ValidationFormatter.calculateStats(modelToValidate);\n return jsonResult({\n orphanCount: stats.orphanedElements.length,\n orphanedElements: stats.orphanedElements,\n });\n }\n\n const validator = new Validator();\n const result = await validator.validateModel(modelToValidate);\n for (const issue of outOfScopeIssues) result.addError(issue);\n\n const stats = ValidationFormatter.calculateStats(modelToValidate);\n for (const orphanId of stats.orphanedElements) {\n result.addWarning({\n message: `Element '${orphanId}' is orphaned (no cross-layer references or intra-layer relationships)`,\n layer: orphanId.split(\".\")[0] ?? \"\",\n elementId: orphanId,\n category: \"orphan\",\n fixSuggestion: \"Add cross-layer references or relationships to connect this element to the rest of the model\",\n });\n }\n\n for (const [layerName, layer] of modelToValidate.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n if (!element.source_reference) {\n result.addWarning({\n message: `Element '${elementId}' has no source reference`,\n layer: layerName,\n elementId,\n fixSuggestion: \"Add a source_reference with provenance to link this element to its implementation\",\n });\n }\n if (args.strict && (!element.description || element.description.trim() === \"\")) {\n result.addWarning({\n message: `Element '${elementId}' has no description`,\n layer: layerName,\n elementId,\n fixSuggestion: \"Add a description to document what this element represents\",\n });\n }\n }\n }\n\n const formatted = ValidationFormatter.toJSON(result, modelToValidate);\n const valid = result.isValid() && !(args.strict && result.warnings.length > 0);\n\n return jsonResult({ ...formatted, valid });\n });\n}\n\n/**\n * The relationships and references that leave a `layers` scope and point at an element that\n * exists nowhere in the model. The validators skip an end outside the scope (as `dr validate\n * --layers` does, having loaded only those layers); the MCP server holds the whole model, so it\n * can still tell a real dangling end from one that is merely out of scope. A relationship with\n * both ends outside the scope isn't looked at, as in the CLI.\n */\nfunction danglingOutOfScopeEnds(model: Model, scope: Set<string>): Array<Omit<ValidationIssue, \"severity\">> {\n const known = new Set<string>();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n known.add(element.id);\n if (element.path) known.add(element.path);\n }\n }\n const layerOf = (id: string) => id.split(\".\")[0] ?? \"\";\n // The layer the reference validator reads from a target (reference-validator.ts\n // extractLayerFromId): a known layer the id starts with, else its first segment.\n const referenceLayerOf = (id: string) => {\n const separator = id.includes(\".\") ? \".\" : \"-\";\n const layer = [...getAllLayerIds()]\n .sort((a, b) => b.length - a.length)\n .find((name) => id.startsWith(name + separator));\n return layer ?? id.split(separator)[0] ?? \"\";\n };\n const issues: Array<Omit<ValidationIssue, \"severity\">> = [];\n\n for (const rel of model.relationships.getAll()) {\n // An id with no layer segment is looked up, and reported, by the relationship validator\n // whatever the scope (relationship-schema-validator.ts extractLayerFromElementId).\n if (!rel.source.includes(\".\") || !rel.target.includes(\".\")) continue;\n const sourceIn = scope.has(layerOf(rel.source));\n const targetIn = scope.has(layerOf(rel.target));\n if (sourceIn === targetIn) continue; // both in scope: the validators check it; neither: skipped\n const [end, role] = sourceIn ? [rel.target, \"target\"] : [rel.source, \"source\"];\n if (known.has(end)) continue;\n issues.push({\n layer: rel.layer || layerOf(rel.source),\n elementId: end,\n message: `[Relationship Schema]: Relationship ${role} element '${end}' not found (outside the validated layers, and not in the model)`,\n category: \"reference\",\n });\n }\n\n for (const [layerName, layer] of model.layers) {\n if (!scope.has(layerName)) continue;\n for (const element of layer.listElements()) {\n for (const ref of element.references ?? []) {\n const destination = ref.target;\n if (!destination || destination.startsWith(\"@\") || scope.has(referenceLayerOf(destination)) || known.has(destination)) continue;\n issues.push({\n layer: layerName,\n elementId: element.path || element.id,\n message: `Broken reference: target '${destination}' does not exist (outside the validated layers, and not in the model)`,\n fixSuggestion: `Remove reference or create element '${destination}'`,\n category: \"reference\",\n });\n }\n }\n }\n return issues;\n}\n\nexport const modelValidateTool: McpToolDefinition<typeof inputSchema> = {\n name: \"model_validate\",\n description: \"Run the full schema/naming/reference/semantic/relationship validation pipeline against the model.\",\n inputSchema,\n handler: modelValidateHandler,\n};\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stats"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { readdir, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { BaseSnapshotManager } from "../../core/base-snapshot-manager.js";
|
|
4
|
+
import { MigrationRegistry } from "../../core/migration-registry.js";
|
|
5
|
+
import { StagedChangesetStorage } from "../../core/staged-changeset-storage.js";
|
|
6
|
+
import { readIndexMeta, readSession } from "../../analyzers/session-state.js";
|
|
7
|
+
import { getCliBundledSpecVersion, getModelSpecVersion } from "../../utils/spec-version.js";
|
|
8
|
+
import { fileExists, readFile } from "../../utils/file-io.js";
|
|
9
|
+
import { probeViewer, viewerUrl } from "../../server/viewer-registry.js";
|
|
10
|
+
import { readIndexJob } from "../../analyzers/index-job.js";
|
|
11
|
+
import { getErrorMessage } from "../../utils/errors.js";
|
|
12
|
+
import { indexProbe, judgeIndex, readWorktreeSnapshot } from "./codebase-shared.js";
|
|
13
|
+
import { isExtractionChangeset } from "./codebase-extract.js";
|
|
14
|
+
import { runGit } from "./codebase-shared.js";
|
|
15
|
+
function splitZ(stdout) {
|
|
16
|
+
return stdout.split("\0").filter((entry) => entry.length > 0);
|
|
17
|
+
}
|
|
18
|
+
async function real(dir) {
|
|
19
|
+
try {
|
|
20
|
+
return await realpath(dir);
|
|
21
|
+
} catch {
|
|
22
|
+
return path.resolve(dir);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function gitTopLevel(git, dir) {
|
|
26
|
+
if (!await fileExists(dir))
|
|
27
|
+
return { reason: `${dir} does not exist` };
|
|
28
|
+
const result = await git(["rev-parse", "--show-toplevel"], dir);
|
|
29
|
+
return result.ok ? { top: await real(result.stdout.trim()) } : result.missing ? { reason: "git is not installed" } : { reason: `${dir} is not in a git repository` };
|
|
30
|
+
}
|
|
31
|
+
async function collectModel(ctx) {
|
|
32
|
+
if (!ctx.modelDir)
|
|
33
|
+
return {
|
|
34
|
+
status: "absent",
|
|
35
|
+
reason: ctx.projectRoot ? `${path.join(ctx.projectRoot, "documentation-robotics")} has no model/manifest.yaml` : `no documentation-robotics/ folder at or above ${ctx.root}`
|
|
36
|
+
};
|
|
37
|
+
const section = { status: "present", path: ctx.modelDir };
|
|
38
|
+
try {
|
|
39
|
+
const model = await ctx.model();
|
|
40
|
+
section.name = model.manifest.name, section.elements = [...model.layers.values()].reduce((sum, layer) => sum + layer.listElements().length, 0);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
section.loadError = getErrorMessage(error);
|
|
43
|
+
}
|
|
44
|
+
return section;
|
|
45
|
+
}
|
|
46
|
+
async function collectSpec(ctx) {
|
|
47
|
+
const cli = getCliBundledSpecVersion();
|
|
48
|
+
if (!ctx.modelDir)
|
|
49
|
+
return { status: "no-model", model: null, cli, upgradeNeeded: !1 };
|
|
50
|
+
const declared = await getModelSpecVersion(ctx.modelDir), modelVersion = declared ?? ASSUMED_SPEC_VERSION, assumed = declared ? {} : { assumed: !0, note: `manifest.yaml has no spec_version; assumed ${ASSUMED_SPEC_VERSION}, as dr upgrade does` }, order = compareVersions(modelVersion, cli);
|
|
51
|
+
if (order === 0)
|
|
52
|
+
return { status: "current", model: modelVersion, cli, upgradeNeeded: !1, ...assumed };
|
|
53
|
+
if (order > 0)
|
|
54
|
+
return {
|
|
55
|
+
status: "ahead",
|
|
56
|
+
model: modelVersion,
|
|
57
|
+
cli,
|
|
58
|
+
upgradeNeeded: !1,
|
|
59
|
+
reason: `the model is on spec ${modelVersion}, newer than this CLI's ${cli}; update the dr CLI`,
|
|
60
|
+
...assumed
|
|
61
|
+
};
|
|
62
|
+
const steps = new MigrationRegistry().getMigrationPath(modelVersion, cli), reaches = steps.length > 0 && steps[steps.length - 1].toVersion === cli;
|
|
63
|
+
return {
|
|
64
|
+
status: "behind",
|
|
65
|
+
model: modelVersion,
|
|
66
|
+
cli,
|
|
67
|
+
upgradeNeeded: !0,
|
|
68
|
+
migration: {
|
|
69
|
+
available: reaches,
|
|
70
|
+
steps: steps.map((step) => `${step.fromVersion} \u2192 ${step.toVersion}`)
|
|
71
|
+
},
|
|
72
|
+
...reaches ? {} : { reason: `no migration path from ${modelVersion} to ${cli}` },
|
|
73
|
+
...assumed
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const ASSUMED_SPEC_VERSION = "0.6.0";
|
|
77
|
+
function compareVersions(a, b) {
|
|
78
|
+
const pa = a.split(".").map((part) => Number.parseInt(part, 10) || 0), pb = b.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
79
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
80
|
+
const x = pa[i] ?? 0, y = pb[i] ?? 0;
|
|
81
|
+
if (x !== y)
|
|
82
|
+
return x < y ? -1 : 1;
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
async function collectChangesets(ctx) {
|
|
87
|
+
if (!ctx.projectRoot || !ctx.modelDir)
|
|
88
|
+
return { status: "no-model" };
|
|
89
|
+
const changesets = await new StagedChangesetStorage(ctx.projectRoot).list(), activeId = await readActiveChangesetId(ctx.projectRoot), staged = changesets.filter((changeset) => changeset.status === "staged");
|
|
90
|
+
let current = null, driftUnknown;
|
|
91
|
+
if (staged.length > 0)
|
|
92
|
+
try {
|
|
93
|
+
current = await new BaseSnapshotManager().captureSnapshot(await ctx.model());
|
|
94
|
+
} catch (error) {
|
|
95
|
+
driftUnknown = `the model could not be loaded: ${getErrorMessage(error)}`;
|
|
96
|
+
}
|
|
97
|
+
const projectRoot = ctx.projectRoot, entry = async (changeset) => {
|
|
98
|
+
const asBuilt = EXTRACTION_MARKER.test(changeset.description ?? "") || await isExtractionChangeset(projectRoot, changeset.id, changeset.name).catch(() => !1), previousActive = asBuilt ? await extractionPreviousActive(projectRoot, changeset.id) : void 0;
|
|
99
|
+
return {
|
|
100
|
+
id: changeset.id,
|
|
101
|
+
name: changeset.name,
|
|
102
|
+
changes: countChanges(changeset),
|
|
103
|
+
baseDrifted: current && changeset.baseSnapshot ? changeset.baseSnapshot !== current : null,
|
|
104
|
+
...asBuilt ? { asBuilt: !0 } : {},
|
|
105
|
+
...previousActive ? { previousActive } : {}
|
|
106
|
+
};
|
|
107
|
+
}, stagedEntries = await Promise.all(staged.map(entry)), active = activeId ? changesets.find((changeset) => changeset.id === activeId) : void 0;
|
|
108
|
+
return {
|
|
109
|
+
status: "ok",
|
|
110
|
+
active: active && active.status === "staged" ? stagedEntries.find((e) => e.id === active.id) ?? null : null,
|
|
111
|
+
staged: stagedEntries,
|
|
112
|
+
committed: changesets.filter((changeset) => changeset.status === "committed").length,
|
|
113
|
+
discarded: changesets.filter((changeset) => changeset.status === "discarded").length,
|
|
114
|
+
...driftUnknown ? { driftUnknown } : {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const EXTRACTION_MARKER = /Staged by codebase_extract/;
|
|
118
|
+
async function extractionPreviousActive(projectRoot, changesetId) {
|
|
119
|
+
const record = path.join(projectRoot, ".dr", "analyzers", "extract", `${changesetId.replace(/[^\w.-]/g, "_")}.json`);
|
|
120
|
+
try {
|
|
121
|
+
const previous = JSON.parse(await readFile(record)).previousActive;
|
|
122
|
+
if (previous && typeof previous.id == "string")
|
|
123
|
+
return { id: previous.id, name: typeof previous.name == "string" ? previous.name : previous.id };
|
|
124
|
+
} catch {
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function readActiveChangesetId(projectRoot) {
|
|
128
|
+
const activePath = path.join(projectRoot, "documentation-robotics", "changesets", ".active");
|
|
129
|
+
return await fileExists(activePath) && (await readFile(activePath)).trim() || null;
|
|
130
|
+
}
|
|
131
|
+
function countChanges(changeset) {
|
|
132
|
+
const counts = { total: changeset.changes.length };
|
|
133
|
+
for (const change of changeset.changes)
|
|
134
|
+
counts[change.type] = (counts[change.type] ?? 0) + 1;
|
|
135
|
+
return counts;
|
|
136
|
+
}
|
|
137
|
+
async function collectCodeIndex(ctx) {
|
|
138
|
+
const base = ctx.projectRoot ?? ctx.root;
|
|
139
|
+
let analyzer = (await readSession(base))?.active_analyzer ?? null;
|
|
140
|
+
const analyzerSource = analyzer ? "session" : "first-indexed";
|
|
141
|
+
if (analyzer ??= await firstIndexedAnalyzer(base), !analyzer)
|
|
142
|
+
return { status: "absent", analyzer: null, reason: "no code index in .dr/analyzers/" };
|
|
143
|
+
const meta = await readIndexMeta(base, analyzer);
|
|
144
|
+
if (!meta)
|
|
145
|
+
return {
|
|
146
|
+
status: "absent",
|
|
147
|
+
analyzer,
|
|
148
|
+
analyzerSource,
|
|
149
|
+
reason: `${analyzer}, the active analyzer, has not indexed this codebase`
|
|
150
|
+
};
|
|
151
|
+
const section = {
|
|
152
|
+
analyzer,
|
|
153
|
+
analyzerSource,
|
|
154
|
+
indexedAt: meta.timestamp,
|
|
155
|
+
indexedCommit: meta.git_head,
|
|
156
|
+
nodes: meta.node_count,
|
|
157
|
+
edges: meta.edge_count,
|
|
158
|
+
...analyzerSource === "first-indexed" ? { note: `no analyzer is active in .dr/analyzers/session.json; this is ${analyzer}'s index` } : {}
|
|
159
|
+
}, codebaseRoot = await codebaseRootOf(ctx);
|
|
160
|
+
if (!await fileExists(codebaseRoot))
|
|
161
|
+
return { status: "unknown", ...section, reason: `the codebase root ${codebaseRoot} does not exist` };
|
|
162
|
+
const head = await ctx.git(["rev-parse", "HEAD"], codebaseRoot);
|
|
163
|
+
if (!head.ok)
|
|
164
|
+
return {
|
|
165
|
+
status: "unknown",
|
|
166
|
+
...section,
|
|
167
|
+
reason: head.missing ? "git is not installed, and freshness is judged by commit" : `${codebaseRoot} has no git HEAD, and freshness is judged by commit`
|
|
168
|
+
};
|
|
169
|
+
const headCommit = head.stdout.trim(), judgement = await judgeIndex({ indexedCommit: meta.git_head, indexedAt: meta.timestamp }, headCommit, indexProbe(await real(codebaseRoot), { git: ctx.git, snapshot: await readWorktreeSnapshot(base, analyzer, meta.timestamp) }));
|
|
170
|
+
return {
|
|
171
|
+
status: judgement.state,
|
|
172
|
+
...section,
|
|
173
|
+
headCommit,
|
|
174
|
+
...judgement.reason ? { reason: judgement.reason } : {},
|
|
175
|
+
...judgement.committedSince.length > 0 ? { changedSinceIndex: judgement.committedSince } : {},
|
|
176
|
+
...judgement.editedSince.length > 0 ? { uncommittedEdits: judgement.editedSince } : {}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
async function firstIndexedAnalyzer(base) {
|
|
180
|
+
let names;
|
|
181
|
+
try {
|
|
182
|
+
names = (await readdir(path.join(base, ".dr", "analyzers"), { withFileTypes: !0 })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
for (const name of names)
|
|
187
|
+
if (await fileExists(path.join(base, ".dr", "analyzers", name, "index-meta.json")))
|
|
188
|
+
return name;
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
async function codebaseRootOf(ctx) {
|
|
192
|
+
const fallback = ctx.projectRoot ?? ctx.root;
|
|
193
|
+
if (!ctx.modelDir)
|
|
194
|
+
return fallback;
|
|
195
|
+
try {
|
|
196
|
+
return (await ctx.model()).codebaseRoot ?? fallback;
|
|
197
|
+
} catch {
|
|
198
|
+
return fallback;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const MAX_FILES = 50, MAX_ELEMENTS_PER_FILE = 5;
|
|
202
|
+
async function collectCodeDrift(ctx) {
|
|
203
|
+
if (!ctx.projectRoot || !ctx.modelDir)
|
|
204
|
+
return { status: "no-model" };
|
|
205
|
+
const projectRoot = await real(ctx.projectRoot), modelDir = await real(ctx.modelDir), repo = await gitTopLevel(ctx.git, projectRoot);
|
|
206
|
+
if ("reason" in repo)
|
|
207
|
+
return { status: "no-git", reason: repo.reason };
|
|
208
|
+
const modelPathspec = path.relative(repo.top, modelDir) || ".", last = await ctx.git(["log", "-1", "--format=%H%x00%cI%x00%s", "--", modelPathspec], repo.top);
|
|
209
|
+
if (!last.ok)
|
|
210
|
+
return { status: "unknown", reason: `git log failed: ${last.error}` };
|
|
211
|
+
const lastModelCommit = parseCommitLine(last.stdout.trim());
|
|
212
|
+
if (!lastModelCommit)
|
|
213
|
+
return { status: "unknown", reason: "documentation-robotics/model/ has never been committed" };
|
|
214
|
+
let found;
|
|
215
|
+
try {
|
|
216
|
+
found = await lastNonMigrationCommit(ctx.git, repo.top, modelPathspec);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
found = { error: getErrorMessage(error) };
|
|
219
|
+
}
|
|
220
|
+
const baseline = "error" in found ? lastModelCommit : found.commit ?? lastModelCommit, basis = "error" in found || !found.commit ? "last-model-commit" : "last-non-migration-commit", { sha, date, subject } = baseline, baselineNote = "error" in found ? `Could not tell spec-migration commits apart (${found.error}), so drift is counted from the last model commit, which may be a migration.` : found.commit ? lastModelCommit.sha !== sha ? `Later model commits (the last: ${lastModelCommit.sha.slice(0, 7)} "${lastModelCommit.subject}") migrated the model to a new spec version, which doesn't check it against the code; drift is counted from the last commit before them. Any other model edits made in those commits don't reset drift either.` : void 0 : `The last ${found.skipped.length} model commits are all spec migrations, so drift is counted from the last model commit.`;
|
|
221
|
+
let model;
|
|
222
|
+
try {
|
|
223
|
+
model = await ctx.model();
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return { status: "unknown", reason: `the model could not be loaded: ${getErrorMessage(error)}` };
|
|
226
|
+
}
|
|
227
|
+
const codebaseRoot = await real(model.codebaseRoot ?? projectRoot), codeRepo = await gitTopLevel(ctx.git, codebaseRoot);
|
|
228
|
+
if ("reason" in codeRepo || codeRepo.top !== repo.top)
|
|
229
|
+
return {
|
|
230
|
+
status: "unknown",
|
|
231
|
+
reason: `the code (${codebaseRoot}) is not in the model's git repository, so commits since the model changed can't be compared`
|
|
232
|
+
};
|
|
233
|
+
const [countResult, diffResult, uncommittedResult, modelStatus] = await Promise.all([
|
|
234
|
+
ctx.git(["rev-list", "--count", `${sha}..HEAD`], repo.top),
|
|
235
|
+
// --no-renames: a rename lists both paths, so a reference to the old path still counts.
|
|
236
|
+
ctx.git(["diff", "--name-only", "--no-renames", "-z", sha, "HEAD"], repo.top),
|
|
237
|
+
ctx.git(["diff", "--name-only", "--no-renames", "-z", "HEAD"], repo.top),
|
|
238
|
+
ctx.git(["status", "--porcelain", "--", modelPathspec], repo.top)
|
|
239
|
+
]);
|
|
240
|
+
if (!countResult.ok)
|
|
241
|
+
return { status: "unknown", reason: `git rev-list failed: ${countResult.error}` };
|
|
242
|
+
if (!diffResult.ok)
|
|
243
|
+
return { status: "unknown", reason: `git diff failed: ${diffResult.error}` };
|
|
244
|
+
if (!uncommittedResult.ok)
|
|
245
|
+
return { status: "unknown", reason: `git diff failed: ${uncommittedResult.error}` };
|
|
246
|
+
const toPosix = (p) => p.split(path.sep).join("/"), prefix = toPosix(path.relative(repo.top, codebaseRoot)), referenced = referencedSourceFiles(model, prefix), drDir = toPosix(path.relative(repo.top, path.join(projectRoot, "documentation-robotics"))) + "/", outsideDr = (file) => !file.startsWith(drDir), changed = splitZ(diffResult.stdout).filter(outsideDr), uncommitted = splitZ(uncommittedResult.stdout).filter(outsideDr), matched = matchReferenced(changed, referenced), matchedUncommitted = matchReferenced(uncommitted, referenced), truncated = matched.length > MAX_FILES || matchedUncommitted.length > MAX_FILES;
|
|
247
|
+
return {
|
|
248
|
+
status: referenced.size === 0 ? "unlinked" : matched.length + matchedUncommitted.length > 0 ? "drift" : "in-step",
|
|
249
|
+
modelCommit: { sha, date, subject },
|
|
250
|
+
basis,
|
|
251
|
+
...lastModelCommit.sha !== sha ? { lastModelCommit } : {},
|
|
252
|
+
...baselineNote ? { note: baselineNote } : {},
|
|
253
|
+
commitsSince: Number.parseInt(countResult.stdout.trim(), 10) || 0,
|
|
254
|
+
filesChangedSince: changed.length,
|
|
255
|
+
changedSourceFiles: matched.slice(0, MAX_FILES),
|
|
256
|
+
uncommittedSourceFiles: matchedUncommitted.slice(0, MAX_FILES),
|
|
257
|
+
modelUncommitted: modelStatus.ok && modelStatus.stdout.trim().length > 0,
|
|
258
|
+
referencedFiles: referenced.size,
|
|
259
|
+
...truncated ? { truncated: !0 } : {}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function parseCommitLine(line) {
|
|
263
|
+
const [sha, date, subject] = line.split("\0");
|
|
264
|
+
return sha ? { sha, date: date ?? "", subject: subject ?? "" } : null;
|
|
265
|
+
}
|
|
266
|
+
const MAX_BASELINE_CANDIDATES = 50, SPEC_VERSION_LINE = /^([+-])spec_version:\s*["']?([^"'\s#]*)["']?/;
|
|
267
|
+
function changesSpecVersion(manifestDiff) {
|
|
268
|
+
let removed, added;
|
|
269
|
+
for (const line of manifestDiff.split(`
|
|
270
|
+
`)) {
|
|
271
|
+
if (line.startsWith("+++ ") || line.startsWith("--- "))
|
|
272
|
+
continue;
|
|
273
|
+
const match = SPEC_VERSION_LINE.exec(line);
|
|
274
|
+
match && (match[1] === "-" ? removed = match[2] : added = match[2]);
|
|
275
|
+
}
|
|
276
|
+
return removed !== void 0 && added !== void 0 && removed !== added;
|
|
277
|
+
}
|
|
278
|
+
async function lastNonMigrationCommit(git, repoTop, modelPathspec) {
|
|
279
|
+
const manifest = path.join(modelPathspec, "manifest.yaml"), [commits, manifestLog] = await Promise.all([
|
|
280
|
+
git(["log", `-n${MAX_BASELINE_CANDIDATES}`, "--format=%H%x00%cI%x00%s", "--", modelPathspec], repoTop),
|
|
281
|
+
git(
|
|
282
|
+
// A merge that brings in a migration shows its manifest change only diffed against its first
|
|
283
|
+
// parent; by default git log prints no diff for a merge, and it would become the baseline.
|
|
284
|
+
["log", `-n${MAX_BASELINE_CANDIDATES}`, "-p", "-U0", "--diff-merges=first-parent", "--no-color", "--no-ext-diff", "--format=%x01%H", "--", manifest],
|
|
285
|
+
repoTop
|
|
286
|
+
)
|
|
287
|
+
]);
|
|
288
|
+
if (!commits.ok)
|
|
289
|
+
return { error: `git log failed: ${commits.error}` };
|
|
290
|
+
if (!manifestLog.ok)
|
|
291
|
+
return { error: `git log failed: ${manifestLog.error}` };
|
|
292
|
+
const migrations = /* @__PURE__ */ new Set();
|
|
293
|
+
for (const chunk of manifestLog.stdout.split("").slice(1)) {
|
|
294
|
+
const newline = chunk.indexOf(`
|
|
295
|
+
`), sha = (newline === -1 ? chunk : chunk.slice(0, newline)).trim();
|
|
296
|
+
sha && changesSpecVersion(newline === -1 ? "" : chunk.slice(newline + 1)) && migrations.add(sha);
|
|
297
|
+
}
|
|
298
|
+
const skipped = [];
|
|
299
|
+
for (const line of commits.stdout.split(`
|
|
300
|
+
`)) {
|
|
301
|
+
const commit = parseCommitLine(line.trim());
|
|
302
|
+
if (commit) {
|
|
303
|
+
if (!migrations.has(commit.sha))
|
|
304
|
+
return { commit, skipped };
|
|
305
|
+
skipped.push(commit);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return { commit: null, skipped };
|
|
309
|
+
}
|
|
310
|
+
function referencedSourceFiles(model, prefix) {
|
|
311
|
+
const files = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const layer of model.layers.values())
|
|
313
|
+
for (const element of layer.listElements())
|
|
314
|
+
for (const location of element.getSourceReference()?.locations ?? []) {
|
|
315
|
+
if (typeof location?.file != "string")
|
|
316
|
+
continue;
|
|
317
|
+
const relative = location.file.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
318
|
+
if (!relative)
|
|
319
|
+
continue;
|
|
320
|
+
const file = prefix ? path.posix.join(prefix, relative) : relative, ids = files.get(file) ?? [];
|
|
321
|
+
ids.includes(element.path) || ids.push(element.path), files.set(file, ids);
|
|
322
|
+
}
|
|
323
|
+
return files;
|
|
324
|
+
}
|
|
325
|
+
function matchReferenced(changed, referenced) {
|
|
326
|
+
const out = [];
|
|
327
|
+
for (const file of changed) {
|
|
328
|
+
const ids = referenced.get(file);
|
|
329
|
+
ids && out.push({ file, elements: ids.slice(0, MAX_ELEMENTS_PER_FILE), elementCount: ids.length });
|
|
330
|
+
}
|
|
331
|
+
return out.sort((a, b) => a.file.localeCompare(b.file));
|
|
332
|
+
}
|
|
333
|
+
async function collectViewer(ctx) {
|
|
334
|
+
if (!ctx.projectRoot)
|
|
335
|
+
return { status: "none" };
|
|
336
|
+
const probe = await probeViewer(ctx.projectRoot, { probeTimeoutMs: 500, retryTimeoutMs: 1e3 });
|
|
337
|
+
if (probe.state === "none")
|
|
338
|
+
return { status: "none" };
|
|
339
|
+
const { record } = probe;
|
|
340
|
+
return probe.state === "unreachable" ? {
|
|
341
|
+
status: "unreachable",
|
|
342
|
+
port: record.port,
|
|
343
|
+
pid: record.pid,
|
|
344
|
+
note: "The viewer's process is alive but it did not answer; it may still be loading."
|
|
345
|
+
} : {
|
|
346
|
+
status: "running",
|
|
347
|
+
url: viewerUrl(record),
|
|
348
|
+
port: record.port,
|
|
349
|
+
pid: record.pid,
|
|
350
|
+
startedAt: record.startedAt,
|
|
351
|
+
withChangesets: record.withChangesets
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
async function collectIndexing(ctx) {
|
|
355
|
+
if (!ctx.projectRoot)
|
|
356
|
+
return { status: "none" };
|
|
357
|
+
const job = await readIndexJob(ctx.projectRoot);
|
|
358
|
+
return job ? {
|
|
359
|
+
status: job.state,
|
|
360
|
+
jobId: job.jobId,
|
|
361
|
+
phase: job.progress?.phase ?? null,
|
|
362
|
+
startedAt: job.startedAt,
|
|
363
|
+
finishedAt: job.finishedAt,
|
|
364
|
+
error: job.error,
|
|
365
|
+
logPath: job.logPath
|
|
366
|
+
} : { status: "none" };
|
|
367
|
+
}
|
|
368
|
+
export {
|
|
369
|
+
changesSpecVersion,
|
|
370
|
+
collectChangesets,
|
|
371
|
+
collectCodeDrift,
|
|
372
|
+
collectCodeIndex,
|
|
373
|
+
collectIndexing,
|
|
374
|
+
collectModel,
|
|
375
|
+
collectSpec,
|
|
376
|
+
collectViewer,
|
|
377
|
+
compareVersions,
|
|
378
|
+
lastNonMigrationCommit,
|
|
379
|
+
runGit
|
|
380
|
+
};
|
|
381
|
+
//# sourceMappingURL=project-status-collectors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/mcp/tools/project-status-collectors.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * The collectors behind project_status. Each one reports one section from local files and git,\n * independently of the others: a collector that fails reports its own error and the rest still\n * answer. A new section (the running viewer, indexing progress) is one more collector.\n *\n * Nothing here touches the network or starts the code analyzer.\n */\n\nimport { readdir, realpath } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Model } from \"../../core/model.js\";\nimport type { Changeset } from \"../../core/changeset.js\";\nimport { BaseSnapshotManager } from \"../../core/base-snapshot-manager.js\";\nimport { MigrationRegistry } from \"../../core/migration-registry.js\";\nimport { StagedChangesetStorage } from \"../../core/staged-changeset-storage.js\";\nimport { readIndexMeta, readSession } from \"../../analyzers/session-state.js\";\nimport { getCliBundledSpecVersion, getModelSpecVersion } from \"../../utils/spec-version.js\";\nimport { fileExists, readFile } from \"../../utils/file-io.js\";\nimport { probeViewer, viewerUrl } from \"../../server/viewer-registry.js\";\nimport { readIndexJob } from \"../../analyzers/index-job.js\";\nimport { getErrorMessage } from \"../../utils/errors.js\";\nimport { indexProbe, judgeIndex, readWorktreeSnapshot, type GitRunner } from \"./codebase-shared.js\";\nimport { isExtractionChangeset } from \"./codebase-extract.js\";\n\n/** How the project root was chosen. */\nexport type RootResolution = \"rootPath\" | \"DR_MCP_ROOT_PATH\" | \"mcp-roots\" | \"cwd\";\n\n/** What every collector gets: where the project is, and shared, lazily loaded resources. */\nexport interface StatusContext {\n /** The directory the call resolved to (the `rootPath` argument, or the served root). */\n root: string;\n resolvedBy: RootResolution;\n /** The directory holding documentation-robotics/, at or above `root`; null when there is none. */\n projectRoot: string | null;\n /** documentation-robotics/model/ when its manifest.yaml exists; null when there is no model. */\n modelDir: string | null;\n /** The loaded model, shared by every collector (and with the other tools through the cache). */\n model(): Promise<Model>;\n git: GitRunner;\n}\n\n/** A section a collector could not produce. */\nexport interface ErrorSection {\n status: \"error\";\n error: string;\n}\n\n// ---------------------------------------------------------------------------------------------\n// git\n\n// The read-only git runner lives with the index freshness rule (codebase-shared.ts), which uses it too.\nexport { runGit, type GitResult, type GitRunner } from \"./codebase-shared.js\";\n\n/** NUL-separated git output (`-z`) as a list. */\nfunction splitZ(stdout: string): string[] {\n return stdout.split(\"\\0\").filter((entry) => entry.length > 0);\n}\n\n/** `dir` with symlinks resolved, as git reports paths (macOS's /var is /private/var). */\nasync function real(dir: string): Promise<string> {\n try {\n return await realpath(dir);\n } catch {\n return path.resolve(dir);\n }\n}\n\n/** The top of the git work tree holding `dir`, or why there is none. */\nasync function gitTopLevel(git: GitRunner, dir: string): Promise<{ top: string } | { reason: string }> {\n if (!(await fileExists(dir))) return { reason: `${dir} does not exist` };\n const result = await git([\"rev-parse\", \"--show-toplevel\"], dir);\n if (result.ok) return { top: await real(result.stdout.trim()) };\n if (result.missing) return { reason: \"git is not installed\" };\n return { reason: `${dir} is not in a git repository` };\n}\n\n// ---------------------------------------------------------------------------------------------\n// model\n\nexport type ModelSection =\n | { status: \"absent\"; reason: string }\n | { status: \"present\"; path: string; name?: string; elements?: number; loadError?: string };\n\nexport async function collectModel(ctx: StatusContext): Promise<ModelSection> {\n if (!ctx.modelDir) {\n return {\n status: \"absent\",\n reason: ctx.projectRoot\n ? `${path.join(ctx.projectRoot, \"documentation-robotics\")} has no model/manifest.yaml`\n : `no documentation-robotics/ folder at or above ${ctx.root}`,\n };\n }\n const section: ModelSection = { status: \"present\", path: ctx.modelDir };\n try {\n const model = await ctx.model();\n section.name = model.manifest.name;\n section.elements = [...model.layers.values()].reduce((sum, layer) => sum + layer.listElements().length, 0);\n } catch (error) {\n section.loadError = getErrorMessage(error);\n }\n return section;\n}\n\n// ---------------------------------------------------------------------------------------------\n// spec version\n\nexport type SpecSection = {\n /** current: same as the CLI's; behind: project_upgrade moves it; ahead: the CLI is older than the model. */\n status: \"current\" | \"behind\" | \"ahead\" | \"no-model\";\n model: string | null;\n cli: string;\n upgradeNeeded: boolean;\n /** When behind: whether the CLI has a migration path to its spec, and its steps. */\n migration?: { available: boolean; steps: string[] };\n reason?: string;\n /** Set when the manifest names no spec version and `model` is the assumed one. */\n assumed?: true;\n note?: string;\n};\n\nexport async function collectSpec(ctx: StatusContext): Promise<SpecSection> {\n const cli = getCliBundledSpecVersion();\n if (!ctx.modelDir) return { status: \"no-model\", model: null, cli, upgradeNeeded: false };\n\n const declared = await getModelSpecVersion(ctx.modelDir);\n // A manifest without spec_version is read as 0.6.0 by Model.load and dr upgrade; say the same.\n const modelVersion = declared ?? ASSUMED_SPEC_VERSION;\n const assumed = declared\n ? {}\n : { assumed: true as const, note: `manifest.yaml has no spec_version; assumed ${ASSUMED_SPEC_VERSION}, as dr upgrade does` };\n\n const order = compareVersions(modelVersion, cli);\n if (order === 0) return { status: \"current\", model: modelVersion, cli, upgradeNeeded: false, ...assumed };\n if (order > 0) {\n return {\n status: \"ahead\",\n model: modelVersion,\n cli,\n upgradeNeeded: false,\n reason: `the model is on spec ${modelVersion}, newer than this CLI's ${cli}; update the dr CLI`,\n ...assumed,\n };\n }\n\n const steps = new MigrationRegistry().getMigrationPath(modelVersion, cli);\n const reaches = steps.length > 0 && steps[steps.length - 1].toVersion === cli;\n return {\n status: \"behind\",\n model: modelVersion,\n cli,\n upgradeNeeded: true,\n migration: {\n available: reaches,\n steps: steps.map((step) => `${step.fromVersion} \u2192 ${step.toVersion}`),\n },\n ...(reaches ? {} : { reason: `no migration path from ${modelVersion} to ${cli}` }),\n ...assumed,\n };\n}\n\n/** The spec version Model.load and dr upgrade read when the manifest names none. */\nconst ASSUMED_SPEC_VERSION = \"0.6.0\";\n\n/** -1, 0 or 1, comparing dotted numeric versions part by part. */\nexport function compareVersions(a: string, b: string): number {\n const pa = a.split(\".\").map((part) => Number.parseInt(part, 10) || 0);\n const pb = b.split(\".\").map((part) => Number.parseInt(part, 10) || 0);\n for (let i = 0; i < Math.max(pa.length, pb.length); i++) {\n const x = pa[i] ?? 0;\n const y = pb[i] ?? 0;\n if (x !== y) return x < y ? -1 : 1;\n }\n return 0;\n}\n\n// ---------------------------------------------------------------------------------------------\n// changesets\n\nexport interface ChangesetEntry {\n id: string;\n name: string;\n /** Staged changes by operation; `total` counts them all. */\n changes: Record<string, number> & { total: number };\n /** Whether the base model changed since the changeset was opened; null when that can't be told. */\n baseDrifted: boolean | null;\n /**\n * Set when codebase_extract staged it: the as-built side of a verify (or a keep-as-built\n * catch-up), not a plan.\n */\n asBuilt?: true;\n /** For an as-built changeset, the changeset that was active when extraction started it. */\n previousActive?: { id: string; name: string };\n}\n\nexport type ChangesetsSection =\n | { status: \"no-model\" }\n | {\n status: \"ok\";\n active: ChangesetEntry | null;\n /** Every changeset still staged, the active one included. */\n staged: ChangesetEntry[];\n committed: number;\n discarded: number;\n /** Set when base drift could not be checked. */\n driftUnknown?: string;\n };\n\nexport async function collectChangesets(ctx: StatusContext): Promise<ChangesetsSection> {\n if (!ctx.projectRoot || !ctx.modelDir) return { status: \"no-model\" };\n\n const changesets = await new StagedChangesetStorage(ctx.projectRoot).list();\n const activeId = await readActiveChangesetId(ctx.projectRoot);\n const staged = changesets.filter((changeset) => changeset.status === \"staged\");\n\n // One snapshot of the base, compared with each changeset's: the hash drift check\n // StagingAreaManager.detectDrift makes, without re-hashing the model per changeset.\n let current: string | null = null;\n let driftUnknown: string | undefined;\n if (staged.length > 0) {\n try {\n current = await new BaseSnapshotManager().captureSnapshot(await ctx.model());\n } catch (error) {\n driftUnknown = `the model could not be loaded: ${getErrorMessage(error)}`;\n }\n }\n\n const projectRoot = ctx.projectRoot;\n const entry = async (changeset: Changeset): Promise<ChangesetEntry> => {\n const asBuilt =\n EXTRACTION_MARKER.test(changeset.description ?? \"\") ||\n (await isExtractionChangeset(projectRoot, changeset.id, changeset.name).catch(() => false));\n const previousActive = asBuilt ? await extractionPreviousActive(projectRoot, changeset.id) : undefined;\n return {\n id: changeset.id,\n name: changeset.name,\n changes: countChanges(changeset),\n baseDrifted: current && changeset.baseSnapshot ? changeset.baseSnapshot !== current : null,\n ...(asBuilt ? { asBuilt: true as const } : {}),\n ...(previousActive ? { previousActive } : {}),\n };\n };\n\n const stagedEntries = await Promise.all(staged.map(entry));\n const active = activeId ? changesets.find((changeset) => changeset.id === activeId) : undefined;\n return {\n status: \"ok\",\n active: active && active.status === \"staged\" ? stagedEntries.find((e) => e.id === active.id) ?? null : null,\n staged: stagedEntries,\n committed: changesets.filter((changeset) => changeset.status === \"committed\").length,\n discarded: changesets.filter((changeset) => changeset.status === \"discarded\").length,\n ...(driftUnknown ? { driftUnknown } : {}),\n };\n}\n\n/** The description codebase_extract gives the changesets it creates. */\nconst EXTRACTION_MARKER = /Staged by codebase_extract/;\n\n/**\n * The changeset that was active when codebase_extract started this one, from the extraction\n * record (.dr/analyzers/extract/<id>.json, `previousActive`); undefined when it has none.\n */\nasync function extractionPreviousActive(\n projectRoot: string,\n changesetId: string\n): Promise<{ id: string; name: string } | undefined> {\n const record = path.join(projectRoot, \".dr\", \"analyzers\", \"extract\", `${changesetId.replace(/[^\\w.-]/g, \"_\")}.json`);\n try {\n const stored = JSON.parse(await readFile(record)) as { previousActive?: { id?: unknown; name?: unknown } | null };\n const previous = stored.previousActive;\n if (previous && typeof previous.id === \"string\") {\n return { id: previous.id, name: typeof previous.name === \"string\" ? previous.name : previous.id };\n }\n } catch {\n // No record, or one that can't be read: nothing to go on.\n }\n return undefined;\n}\n\nasync function readActiveChangesetId(projectRoot: string): Promise<string | null> {\n const activePath = path.join(projectRoot, \"documentation-robotics\", \"changesets\", \".active\");\n if (!(await fileExists(activePath))) return null;\n return (await readFile(activePath)).trim() || null;\n}\n\nfunction countChanges(changeset: Changeset): ChangesetEntry[\"changes\"] {\n const counts: ChangesetEntry[\"changes\"] = { total: changeset.changes.length };\n for (const change of changeset.changes) counts[change.type] = (counts[change.type] ?? 0) + 1;\n return counts;\n}\n\n// ---------------------------------------------------------------------------------------------\n// code index\n\n/**\n * Which analyzer the section describes. `session`: the active one in .dr/analyzers/session.json,\n * the one codebase_index and codebase_verify use. `first-indexed`: none is active, so the first\n * analyzer with an index.\n */\nexport type AnalyzerSource = \"session\" | \"first-indexed\";\n\nexport type CodeIndexSection =\n | { status: \"absent\"; analyzer: string | null; analyzerSource?: AnalyzerSource; reason: string }\n | {\n /** By the rule the codebase tools use (codebase-shared.ts judgeIndex); unknown when git can't say. */\n status: \"fresh\" | \"fresh-with-uncommitted-edits\" | \"stale\" | \"unknown\";\n analyzer: string;\n analyzerSource: AnalyzerSource;\n indexedAt: string;\n indexedCommit: string;\n headCommit?: string;\n nodes: number;\n edges: number;\n reason?: string;\n /** Source files changed by commits since the indexed commit. */\n changedSinceIndex?: string[];\n /** Source files with uncommitted edits written after the index was taken. */\n uncommittedEdits?: string[];\n note?: string;\n };\n\n/**\n * Reads what `dr analyzer status` reads (.dr/analyzers/), and judges freshness by the rule the\n * codebase tools use: stale when commits since the index changed source files (commits that touch\n * only the model or DR's folders don't count), and fresh-with-uncommitted-edits when uncommitted\n * source edits are newer than the index.\n */\nexport async function collectCodeIndex(ctx: StatusContext): Promise<CodeIndexSection> {\n const base = ctx.projectRoot ?? ctx.root;\n const session = await readSession(base);\n let analyzer: string | null = session?.active_analyzer ?? null;\n const analyzerSource: AnalyzerSource = analyzer ? \"session\" : \"first-indexed\";\n analyzer ??= await firstIndexedAnalyzer(base);\n if (!analyzer) {\n return { status: \"absent\", analyzer: null, reason: \"no code index in .dr/analyzers/\" };\n }\n const meta = await readIndexMeta(base, analyzer);\n if (!meta) {\n return {\n status: \"absent\",\n analyzer,\n analyzerSource,\n reason: `${analyzer}, the active analyzer, has not indexed this codebase`,\n };\n }\n\n const section = {\n analyzer,\n analyzerSource,\n indexedAt: meta.timestamp,\n indexedCommit: meta.git_head,\n nodes: meta.node_count,\n edges: meta.edge_count,\n ...(analyzerSource === \"first-indexed\"\n ? { note: `no analyzer is active in .dr/analyzers/session.json; this is ${analyzer}'s index` }\n : {}),\n };\n const codebaseRoot = await codebaseRootOf(ctx);\n if (!(await fileExists(codebaseRoot))) {\n return { status: \"unknown\", ...section, reason: `the codebase root ${codebaseRoot} does not exist` };\n }\n const head = await ctx.git([\"rev-parse\", \"HEAD\"], codebaseRoot);\n if (!head.ok) {\n return {\n status: \"unknown\",\n ...section,\n reason: head.missing\n ? \"git is not installed, and freshness is judged by commit\"\n : `${codebaseRoot} has no git HEAD, and freshness is judged by commit`,\n };\n }\n const headCommit = head.stdout.trim();\n const judgement = await judgeIndex(\n { indexedCommit: meta.git_head, indexedAt: meta.timestamp },\n headCommit,\n indexProbe(await real(codebaseRoot), { git: ctx.git, snapshot: await readWorktreeSnapshot(base, analyzer, meta.timestamp) })\n );\n return {\n status: judgement.state,\n ...section,\n headCommit,\n ...(judgement.reason ? { reason: judgement.reason } : {}),\n ...(judgement.committedSince.length > 0 ? { changedSinceIndex: judgement.committedSince } : {}),\n ...(judgement.editedSince.length > 0 ? { uncommittedEdits: judgement.editedSince } : {}),\n };\n}\n\n/** With no analyzer selected, the first one that has an index. */\nasync function firstIndexedAnalyzer(base: string): Promise<string | null> {\n let names: string[];\n try {\n const entries = await readdir(path.join(base, \".dr\", \"analyzers\"), { withFileTypes: true });\n names = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();\n } catch {\n return null;\n }\n for (const name of names) {\n if (await fileExists(path.join(base, \".dr\", \"analyzers\", name, \"index-meta.json\"))) return name;\n }\n return null;\n}\n\n/** Where the code lives: the model's codebase root, else the project root. */\nasync function codebaseRootOf(ctx: StatusContext): Promise<string> {\n const fallback = ctx.projectRoot ?? ctx.root;\n if (!ctx.modelDir) return fallback;\n try {\n return (await ctx.model()).codebaseRoot ?? fallback;\n } catch {\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------------------------\n// code drift (git only)\n\n/** How many changed source files, and elements per file, the section lists. */\nconst MAX_FILES = 50;\nconst MAX_ELEMENTS_PER_FILE = 5;\n\nexport interface ChangedSourceFile {\n file: string;\n /** Elements whose source reference names the file (the first few). */\n elements: string[];\n elementCount: number;\n}\n\nexport type CodeDriftSection =\n | { status: \"no-model\" }\n | { status: \"no-git\" | \"unknown\"; reason: string }\n | {\n /**\n * drift: files the model references changed since `modelCommit`. unlinked: no element\n * has a source reference, so changed code can't be tied to the model.\n */\n status: \"drift\" | \"in-step\" | \"unlinked\";\n /**\n * The commit drift is counted from: the last commit to documentation-robotics/model/ that\n * isn't a spec migration (one whose manifest.yaml diff changes `spec_version`). A migration\n * rewrites the model without checking it against the code, so it doesn't reset drift.\n */\n modelCommit: CommitInfo;\n /**\n * Which rule chose `modelCommit`: last-non-migration-commit, or last-model-commit when every\n * recent model commit is a migration or the migrations couldn't be told apart (`note` says).\n */\n basis: \"last-non-migration-commit\" | \"last-model-commit\";\n /** The last commit that touched documentation-robotics/model/, when newer than `modelCommit`. */\n lastModelCommit?: CommitInfo;\n note?: string;\n commitsSince: number;\n /** Files changed in those commits, outside documentation-robotics/. */\n filesChangedSince: number;\n /** Of those, the ones the model's elements reference as source files. */\n changedSourceFiles: ChangedSourceFile[];\n /** Referenced source files with uncommitted changes. */\n uncommittedSourceFiles: ChangedSourceFile[];\n /** Whether documentation-robotics/model/ itself has uncommitted changes. */\n modelUncommitted: boolean;\n /** How many distinct source files the model references. */\n referencedFiles: number;\n /** Set when a list was cut to its first entries. */\n truncated?: boolean;\n };\n\nexport async function collectCodeDrift(ctx: StatusContext): Promise<CodeDriftSection> {\n if (!ctx.projectRoot || !ctx.modelDir) return { status: \"no-model\" };\n const projectRoot = await real(ctx.projectRoot);\n const modelDir = await real(ctx.modelDir);\n\n const repo = await gitTopLevel(ctx.git, projectRoot);\n if (\"reason\" in repo) return { status: \"no-git\", reason: repo.reason };\n\n const modelPathspec = path.relative(repo.top, modelDir) || \".\";\n const last = await ctx.git([\"log\", \"-1\", \"--format=%H%x00%cI%x00%s\", \"--\", modelPathspec], repo.top);\n if (!last.ok) return { status: \"unknown\", reason: `git log failed: ${last.error}` };\n const lastModelCommit = parseCommitLine(last.stdout.trim());\n if (!lastModelCommit) {\n return { status: \"unknown\", reason: \"documentation-robotics/model/ has never been committed\" };\n }\n // The baseline: the last model commit that isn't a spec migration. A failure here falls back\n // to the last model commit and says so, rather than blanking the section.\n let found: Awaited<ReturnType<typeof lastNonMigrationCommit>>;\n try {\n found = await lastNonMigrationCommit(ctx.git, repo.top, modelPathspec);\n } catch (error) {\n found = { error: getErrorMessage(error) };\n }\n const baseline = \"error\" in found ? lastModelCommit : (found.commit ?? lastModelCommit);\n const basis: \"last-non-migration-commit\" | \"last-model-commit\" =\n \"error\" in found || !found.commit ? \"last-model-commit\" : \"last-non-migration-commit\";\n const { sha, date, subject } = baseline;\n const baselineNote =\n \"error\" in found\n ? `Could not tell spec-migration commits apart (${found.error}), so drift is counted from the last model commit, which may be a migration.`\n : !found.commit\n ? `The last ${found.skipped.length} model commits are all spec migrations, so drift is counted from the last model commit.`\n : lastModelCommit.sha !== sha\n ? `Later model commits (the last: ${lastModelCommit.sha.slice(0, 7)} \"${lastModelCommit.subject}\") migrated the ` +\n \"model to a new spec version, which doesn't check it against the code; drift is counted from the last commit before them. \" +\n \"Any other model edits made in those commits don't reset drift either.\"\n : undefined;\n\n let model: Model;\n try {\n model = await ctx.model();\n } catch (error) {\n return { status: \"unknown\", reason: `the model could not be loaded: ${getErrorMessage(error)}` };\n }\n\n const codebaseRoot = await real(model.codebaseRoot ?? projectRoot);\n const codeRepo = await gitTopLevel(ctx.git, codebaseRoot);\n if (\"reason\" in codeRepo || codeRepo.top !== repo.top) {\n return {\n status: \"unknown\",\n reason: `the code (${codebaseRoot}) is not in the model's git repository, so commits since the model changed can't be compared`,\n };\n }\n\n const [countResult, diffResult, uncommittedResult, modelStatus] = await Promise.all([\n ctx.git([\"rev-list\", \"--count\", `${sha}..HEAD`], repo.top),\n // --no-renames: a rename lists both paths, so a reference to the old path still counts.\n ctx.git([\"diff\", \"--name-only\", \"--no-renames\", \"-z\", sha, \"HEAD\"], repo.top),\n ctx.git([\"diff\", \"--name-only\", \"--no-renames\", \"-z\", \"HEAD\"], repo.top),\n ctx.git([\"status\", \"--porcelain\", \"--\", modelPathspec], repo.top),\n ]);\n if (!countResult.ok) return { status: \"unknown\", reason: `git rev-list failed: ${countResult.error}` };\n if (!diffResult.ok) return { status: \"unknown\", reason: `git diff failed: ${diffResult.error}` };\n if (!uncommittedResult.ok) return { status: \"unknown\", reason: `git diff failed: ${uncommittedResult.error}` };\n\n // Source references are relative to the codebase root; git names files from the top of the repo.\n const toPosix = (p: string) => p.split(path.sep).join(\"/\");\n const prefix = toPosix(path.relative(repo.top, codebaseRoot));\n const referenced = referencedSourceFiles(model, prefix);\n const drDir = toPosix(path.relative(repo.top, path.join(projectRoot, \"documentation-robotics\"))) + \"/\";\n const outsideDr = (file: string) => !file.startsWith(drDir);\n const changed = splitZ(diffResult.stdout).filter(outsideDr);\n const uncommitted = splitZ(uncommittedResult.stdout).filter(outsideDr);\n\n const matched = matchReferenced(changed, referenced);\n const matchedUncommitted = matchReferenced(uncommitted, referenced);\n const truncated = matched.length > MAX_FILES || matchedUncommitted.length > MAX_FILES;\n\n return {\n status:\n referenced.size === 0 ? \"unlinked\" : matched.length + matchedUncommitted.length > 0 ? \"drift\" : \"in-step\",\n modelCommit: { sha, date, subject },\n basis,\n ...(lastModelCommit.sha !== sha ? { lastModelCommit } : {}),\n ...(baselineNote ? { note: baselineNote } : {}),\n commitsSince: Number.parseInt(countResult.stdout.trim(), 10) || 0,\n filesChangedSince: changed.length,\n changedSourceFiles: matched.slice(0, MAX_FILES),\n uncommittedSourceFiles: matchedUncommitted.slice(0, MAX_FILES),\n modelUncommitted: modelStatus.ok && modelStatus.stdout.trim().length > 0,\n referencedFiles: referenced.size,\n ...(truncated ? { truncated: true } : {}),\n };\n}\n\nexport interface CommitInfo {\n sha: string;\n date: string;\n subject: string;\n}\n\n/** `%H%x00%cI%x00%s` \u2192 the commit, or null for empty output. */\nfunction parseCommitLine(line: string): CommitInfo | null {\n const [sha, date, subject] = line.split(\"\\0\");\n return sha ? { sha, date: date ?? \"\", subject: subject ?? \"\" } : null;\n}\n\n/** How many model commits, newest first, the walk past spec migrations looks at. */\nconst MAX_BASELINE_CANDIDATES = 50;\n\n/** A `spec_version:` line in a diff of manifest.yaml: its sign and its value. */\nconst SPEC_VERSION_LINE = /^([+-])spec_version:\\s*[\"']?([^\"'\\s#]*)[\"']?/;\n\n/**\n * Whether a diff of manifest.yaml changes the model's spec version: it removes a `spec_version`\n * and adds a different one. The commit that creates the manifest only adds one, so it isn't a\n * migration.\n */\nexport function changesSpecVersion(manifestDiff: string): boolean {\n let removed: string | undefined;\n let added: string | undefined;\n for (const line of manifestDiff.split(\"\\n\")) {\n if (line.startsWith(\"+++ \") || line.startsWith(\"--- \")) continue;\n const match = SPEC_VERSION_LINE.exec(line);\n if (!match) continue;\n if (match[1] === \"-\") removed = match[2];\n else added = match[2];\n }\n return removed !== undefined && added !== undefined && removed !== added;\n}\n\n/**\n * The commit drift is counted from: the last commit to the model that isn't a spec migration.\n * Any other model commit (a landed changeset, a catch-up after codebase_verify, a hand edit) is\n * someone looking at the model, so drift counts from it; a migration (`project_upgrade`, `dr\n * upgrade`) rewrites the model to a new spec without looking at the code, so it is walked past.\n * A commit is a migration when its diff of manifest.yaml changes `spec_version` (a merge's diff\n * against its first parent, so merging in an upgrade counts too).\n *\n * Limits: other model edits made in a migration commit don't reset drift. An upgrade committed in\n * two parts, the model files first and the manifest after, leaves the first part looking like an\n * ordinary model commit, so drift can be under-reported from it.\n *\n * Looks at the last MAX_BASELINE_CANDIDATES model commits. `commit` is null when every one of\n * them is a migration; the caller then uses the last model commit.\n */\nexport async function lastNonMigrationCommit(\n git: GitRunner,\n repoTop: string,\n modelPathspec: string\n): Promise<{ commit: CommitInfo | null; skipped: CommitInfo[] } | { error: string }> {\n const manifest = path.join(modelPathspec, \"manifest.yaml\");\n const [commits, manifestLog] = await Promise.all([\n git([\"log\", `-n${MAX_BASELINE_CANDIDATES}`, \"--format=%H%x00%cI%x00%s\", \"--\", modelPathspec], repoTop),\n git(\n // A merge that brings in a migration shows its manifest change only diffed against its first\n // parent; by default git log prints no diff for a merge, and it would become the baseline.\n [\"log\", `-n${MAX_BASELINE_CANDIDATES}`, \"-p\", \"-U0\", \"--diff-merges=first-parent\", \"--no-color\", \"--no-ext-diff\", \"--format=%x01%H\", \"--\", manifest],\n repoTop\n ),\n ]);\n if (!commits.ok) return { error: `git log failed: ${commits.error}` };\n if (!manifestLog.ok) return { error: `git log failed: ${manifestLog.error}` };\n\n const migrations = new Set<string>();\n for (const chunk of manifestLog.stdout.split(\"\\u0001\").slice(1)) {\n const newline = chunk.indexOf(\"\\n\");\n const sha = (newline === -1 ? chunk : chunk.slice(0, newline)).trim();\n if (sha && changesSpecVersion(newline === -1 ? \"\" : chunk.slice(newline + 1))) migrations.add(sha);\n }\n\n const skipped: CommitInfo[] = [];\n for (const line of commits.stdout.split(\"\\n\")) {\n const commit = parseCommitLine(line.trim());\n if (!commit) continue;\n if (!migrations.has(commit.sha)) return { commit, skipped };\n skipped.push(commit);\n }\n return { commit: null, skipped };\n}\n\n/** Repo-relative source file \u2192 the ids of the elements that reference it. */\nfunction referencedSourceFiles(model: Model, prefix: string): Map<string, string[]> {\n const files = new Map<string, string[]>();\n for (const layer of model.layers.values()) {\n for (const element of layer.listElements()) {\n for (const location of element.getSourceReference()?.locations ?? []) {\n if (typeof location?.file !== \"string\") continue;\n const relative = location.file.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n if (!relative) continue;\n const file = prefix ? path.posix.join(prefix, relative) : relative;\n const ids = files.get(file) ?? [];\n if (!ids.includes(element.path)) ids.push(element.path);\n files.set(file, ids);\n }\n }\n }\n return files;\n}\n\nfunction matchReferenced(changed: string[], referenced: Map<string, string[]>): ChangedSourceFile[] {\n const out: ChangedSourceFile[] = [];\n for (const file of changed) {\n const ids = referenced.get(file);\n if (ids) out.push({ file, elements: ids.slice(0, MAX_ELEMENTS_PER_FILE), elementCount: ids.length });\n }\n return out.sort((a, b) => a.file.localeCompare(b.file));\n}\n\n// ---------------------------------------------------------------------------------------------\n// viewer\n\nexport type ViewerSection =\n | { status: \"none\" }\n | { status: \"running\"; url: string; port: number; pid: number; startedAt: string; withChangesets: boolean }\n | { status: \"unreachable\"; port: number; pid: number; note: string };\n\n/**\n * The viewer `dr visualize` (or review_open) runs for this project, from its record in the user's\n * state directory. The health probe is kept short so the call stays fast.\n */\nexport async function collectViewer(ctx: StatusContext): Promise<ViewerSection> {\n if (!ctx.projectRoot) return { status: \"none\" };\n const probe = await probeViewer(ctx.projectRoot, { probeTimeoutMs: 500, retryTimeoutMs: 1000 });\n if (probe.state === \"none\") return { status: \"none\" };\n const { record } = probe;\n if (probe.state === \"unreachable\") {\n return {\n status: \"unreachable\",\n port: record.port,\n pid: record.pid,\n note: \"The viewer's process is alive but it did not answer; it may still be loading.\",\n };\n }\n return {\n status: \"running\",\n url: viewerUrl(record),\n port: record.port,\n pid: record.pid,\n startedAt: record.startedAt,\n withChangesets: record.withChangesets,\n };\n}\n\n// ---------------------------------------------------------------------------------------------\n// indexing\n\nexport type IndexingSection =\n | { status: \"none\" }\n | {\n status: \"running\" | \"done\" | \"failed\";\n jobId: string;\n phase: string | null;\n startedAt: string;\n finishedAt: string | null;\n error: string | null;\n logPath: string;\n };\n\n/** The last codebase_index run, from `.dr/analyzers/index-job.json`. */\nexport async function collectIndexing(ctx: StatusContext): Promise<IndexingSection> {\n if (!ctx.projectRoot) return { status: \"none\" };\n const job = await readIndexJob(ctx.projectRoot);\n if (!job) return { status: \"none\" };\n return {\n status: job.state,\n jobId: job.jobId,\n phase: job.progress?.phase ?? null,\n startedAt: job.startedAt,\n finishedAt: job.finishedAt,\n error: job.error,\n logPath: job.logPath,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|