@documentation-robotics/cli 0.1.13 → 0.2.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 +38 -0
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/cli.js +101 -27
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +3 -1
- package/dist/coding-agents/agents/claude-code.js.map +2 -2
- package/dist/coding-agents/claude-code-client.js +2 -1
- package/dist/coding-agents/claude-code-client.js.map +2 -2
- package/dist/coding-agents/copilot-client.js +17 -12
- package/dist/coding-agents/copilot-client.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +122 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/commands/add.js +10 -6
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +18 -10
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/audit.js +6 -6
- package/dist/commands/audit.js.map +2 -2
- package/dist/commands/changeset.js +238 -145
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +45 -18
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/element.js +3 -2
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/farm.js +657 -0
- package/dist/commands/farm.js.map +7 -0
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +107 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +53 -30
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +19 -6
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/validate.js +41 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +2 -2
- package/dist/commands/version.js.map +2 -2
- package/dist/config.js +83 -0
- package/dist/config.js.map +7 -0
- package/dist/core/annotation-store.js +138 -0
- package/dist/core/annotation-store.js.map +7 -0
- package/dist/core/farm-manifest.js +134 -0
- package/dist/core/farm-manifest.js.map +7 -0
- package/dist/core/farm-sync-engine.js +346 -0
- package/dist/core/farm-sync-engine.js.map +7 -0
- package/dist/core/farm-sync-state.js +159 -0
- package/dist/core/farm-sync-state.js.map +7 -0
- package/dist/core/manifest.js +6 -3
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +55 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +177 -16
- package/dist/core/model.js.map +3 -3
- package/dist/core/virtual-projection.js +1 -0
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/generated/node-types.js +1 -1
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +45 -9
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
- package/dist/integrations/claude_code/docs/README.md +4 -0
- package/dist/mcp/api-key-manager.js +98 -0
- package/dist/mcp/api-key-manager.js.map +7 -0
- package/dist/mcp/http-transport.js +176 -0
- package/dist/mcp/http-transport.js.map +7 -0
- package/dist/mcp/resource-registry.js +97 -0
- package/dist/mcp/resource-registry.js.map +7 -0
- package/dist/mcp/tool-registry.js +44 -0
- package/dist/mcp/tool-registry.js.map +7 -0
- package/dist/mcp/tools/annotation-create.js +39 -0
- package/dist/mcp/tools/annotation-create.js.map +7 -0
- package/dist/mcp/tools/annotation-delete.js +29 -0
- package/dist/mcp/tools/annotation-delete.js.map +7 -0
- package/dist/mcp/tools/annotation-get.js +29 -0
- package/dist/mcp/tools/annotation-get.js.map +7 -0
- package/dist/mcp/tools/annotation-list.js +24 -0
- package/dist/mcp/tools/annotation-list.js.map +7 -0
- package/dist/mcp/tools/annotation-reply.js +34 -0
- package/dist/mcp/tools/annotation-reply.js.map +7 -0
- package/dist/mcp/tools/annotation-shared.js +10 -0
- package/dist/mcp/tools/annotation-shared.js.map +7 -0
- package/dist/mcp/tools/annotation-update.js +39 -0
- package/dist/mcp/tools/annotation-update.js.map +7 -0
- package/dist/mcp/tools/changeset-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +39 -0
- package/dist/mcp/tools/changeset-show.js.map +7 -0
- package/dist/mcp/tools/chat-launch.js +50 -0
- package/dist/mcp/tools/chat-launch.js.map +7 -0
- package/dist/mcp/tools/chat-status.js +26 -0
- package/dist/mcp/tools/chat-status.js.map +7 -0
- package/dist/mcp/tools/index.js +47 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +106 -0
- package/dist/mcp/tools/model-add.js.map +7 -0
- package/dist/mcp/tools/model-delete.js +99 -0
- package/dist/mcp/tools/model-delete.js.map +7 -0
- package/dist/mcp/tools/model-export.js +72 -0
- package/dist/mcp/tools/model-export.js.map +7 -0
- package/dist/mcp/tools/model-info.js +51 -0
- package/dist/mcp/tools/model-info.js.map +7 -0
- package/dist/mcp/tools/model-list.js +53 -0
- package/dist/mcp/tools/model-list.js.map +7 -0
- package/dist/mcp/tools/model-reload.js +34 -0
- package/dist/mcp/tools/model-reload.js.map +7 -0
- package/dist/mcp/tools/model-search.js +88 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +41 -0
- package/dist/mcp/tools/model-show.js.map +7 -0
- package/dist/mcp/tools/model-stats.js +22 -0
- package/dist/mcp/tools/model-stats.js.map +7 -0
- package/dist/mcp/tools/model-trace.js +55 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +105 -0
- package/dist/mcp/tools/model-update.js.map +7 -0
- package/dist/mcp/tools/model-validate.js +78 -0
- package/dist/mcp/tools/model-validate.js.map +7 -0
- package/dist/mcp/tools/shared.js +103 -0
- package/dist/mcp/tools/shared.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +5 -3
- package/dist/reports/model-layer-report-generator.js.map +2 -2
- package/dist/reports/model-readme-generator.js +116 -0
- package/dist/reports/model-readme-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +37 -1
- package/dist/reports/model-report-data.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +29 -6
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/reports/model-report-utils.js +9 -0
- package/dist/reports/model-report-utils.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +2 -2
- 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 +12 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +38 -2
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +4 -4
- 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 +1 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +22 -3
- package/dist/server/server.js.map +2 -2
- package/dist/telemetry/config.js +2 -40
- package/dist/telemetry/config.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/types/index.js.map +2 -2
- package/dist/utils/ansi-suppressor.js +25 -0
- package/dist/utils/ansi-suppressor.js.map +7 -0
- package/dist/utils/color-utils.js +34 -0
- package/dist/utils/color-utils.js.map +7 -0
- package/dist/utils/element-reference-shape.js +15 -1
- package/dist/utils/element-reference-shape.js.map +3 -3
- package/dist/utils/errors.js +69 -10
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/globals.js +17 -1
- package/dist/utils/globals.js.map +2 -2
- package/dist/utils/project-paths.js +24 -2
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/reference-path-parser.js +49 -0
- package/dist/utils/reference-path-parser.js.map +7 -0
- package/dist/utils/source-reference.js +5 -0
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +10 -2
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/validators/composed-reference-validator.js +262 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +59 -8
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +30 -2
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/package.json +6 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/commands/validate.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Validate the architecture model\n */\n\nimport ansis from \"ansis\";\nimport { promises as fs } from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { Model } from \"../core/model.js\";\nimport { Validator } from \"../validators/validator.js\";\nimport { ValidationFormatter } from \"../validators/validation-formatter.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { RELATIONSHIPS_BY_SOURCE, RELATIONSHIPS_BY_DESTINATION } from \"../generated/relationship-index.js\";\nimport { getActiveSpan } from \"../telemetry/index.js\";\n\n\nexport interface ValidateOptions {\n layers?: string[];\n strict?: boolean;\n verbose?: boolean;\n quiet?: boolean;\n output?: string;\n debug?: boolean;\n model?: string;\n orphans?: boolean;\n // Python CLI compatibility options\n all?: boolean;\n markdown?: boolean;\n schemas?: boolean;\n schema?: boolean; // Alias for schemas\n relationships?: boolean;\n structure?: boolean;\n naming?: boolean;\n references?: boolean;\n}\n\n/**\n * Validate schema synchronization between spec/ and cli/src/schemas/bundled/\n * @throws {Error} if schema synchronization validation fails\n */\n/**\n * Recursively find all JSON schema files in a directory\n * Excludes 'layers' subdirectory since layer instances are not schemas\n */\nasync function findJsonFiles(dir: string, baseDir: string = dir): Promise<string[]> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n // Skip 'layers' directory - layer instances are synced but are not schemas\n if (entry.isDirectory() && entry.name !== \"layers\") {\n const subFiles = await findJsonFiles(fullPath, baseDir);\n files.push(...subFiles);\n } else if (entry.name.endsWith(\".json\") && entry.isFile()) {\n // Store relative path from base directory\n files.push(path.relative(baseDir, fullPath));\n }\n }\n\n return files;\n}\n\nasync function validateSchemaSynchronization(): Promise<void> {\n console.log(\"\");\n console.log(ansis.bold(\"Validating schema synchronization...\"));\n console.log(\"\");\n\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n // Resolve paths relative to project root\n const projectRoot = path.resolve(__dirname, \"../../..\");\n // Compare spec/dist/ (15 compiled flat files) vs dist/schemas/bundled/ (same 15 files)\n // spec/schemas/ contains individual source files; spec/dist/ is the compiled distribution\n const specSchemaDir = path.join(projectRoot, \"spec\", \"dist\");\n const bundledSchemaDir = path.join(__dirname, \"../schemas/bundled\");\n\n // Find all JSON files recursively\n const specSchemaFiles = await findJsonFiles(specSchemaDir);\n const bundledSchemaFiles = await findJsonFiles(bundledSchemaDir);\n\n if (specSchemaFiles.length === 0) {\n throw new Error(\"No schema files found in spec/schemas/\");\n }\n\n let mismatches: string[] = [];\n\n for (const schemaFile of specSchemaFiles) {\n const specPath = path.join(specSchemaDir, schemaFile);\n const bundledPath = path.join(bundledSchemaDir, schemaFile);\n\n try {\n const specContent = await fs.readFile(specPath, \"utf-8\");\n\n try {\n const bundledContent = await fs.readFile(bundledPath, \"utf-8\");\n\n // Normalize JSON to account for formatting differences\n const specParsed = JSON.parse(specContent);\n const bundledParsed = JSON.parse(bundledContent);\n const specNormalized = JSON.stringify(specParsed, null, 2);\n const bundledNormalized = JSON.stringify(bundledParsed, null, 2);\n\n if (specNormalized !== bundledNormalized) {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${schemaFile} - Content differs between spec/ and cli/src/schemas/bundled/`\n );\n } else {\n console.log(` ${ansis.green(\"\u2713\")} ${schemaFile}`);\n }\n } catch (error) {\n if ((error as any).code === \"ENOENT\") {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${schemaFile} - Missing in cli/src/schemas/bundled/`\n );\n } else {\n throw error;\n }\n }\n } catch (error) {\n const message = getErrorMessage(error);\n mismatches.push(` ${ansis.red(\"\u2717\")} ${schemaFile} - Error reading spec schema: ${message}`);\n }\n }\n\n // Check for extra files in bundled that aren't in spec\n for (const bundledFile of bundledSchemaFiles) {\n if (!specSchemaFiles.includes(bundledFile)) {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${bundledFile} - Extra file in cli/src/schemas/bundled/ (not in spec/)`\n );\n }\n }\n\n if (mismatches.length > 0) {\n console.log(\"\");\n console.log(\n ansis.bold(ansis.red(`\u2717 Schema synchronization failed with ${mismatches.length} issue(s):`))\n );\n console.log(\"\");\n for (const mismatch of mismatches) {\n console.log(mismatch);\n }\n console.log(\"\");\n console.log(\n ansis.dim(\"To fix: Run 'npm run build:spec' at repo root, then 'npm run build' in cli/ to sync spec/dist/ \u2192 cli/src/schemas/bundled/\")\n );\n console.log(\"\");\n throw new Error(`Schema synchronization failed with ${mismatches.length} issue(s)`);\n } else {\n console.log(\"\");\n console.log(ansis.green(\"\u2713 All schemas synchronized\"));\n console.log(\"\");\n }\n}\n\n/**\n * Display structured orphan report grouped by layer, with dead-end detection.\n * For each orphan, checks whether valid relationship targets exist in the model.\n */\nasync function validateOrphansOnly(model: Model, outputPath?: string): Promise<void> {\n const stats = ValidationFormatter.calculateStats(model);\n\n if (stats.orphanedElements.length === 0) {\n const msg = \"No orphaned elements found. All elements are connected.\";\n if (outputPath) {\n await fs.writeFile(outputPath, JSON.stringify({ total: 0, byLayer: {} }, null, 2), \"utf-8\");\n console.log(`\\nOrphan report exported to ${outputPath}`);\n } else {\n console.log(ansis.green(`\\n\u2713 ${msg}`));\n }\n return;\n }\n\n // Build set of element types currently present in the model\n const typesInModel = new Set<string>();\n for (const [, layer] of model.layers) {\n for (const element of layer.listElements()) {\n const id = element.path || element.id;\n const parts = id.split(\".\");\n if (parts.length >= 2) typesInModel.add(`${parts[0]}.${parts[1]}`);\n }\n }\n\n // Group orphans by layer, annotated with dead-end status\n const byLayer = new Map<string, Array<{\n id: string;\n type: string;\n status: \"dead-end\" | \"connectable\" | \"no-schema\";\n reachablePredicates: string[];\n reachableTargetTypes: string[];\n needsTypes: string[];\n }>>();\n\n for (const elementId of stats.orphanedElements.sort()) {\n const parts = elementId.split(\".\");\n const layer = parts[0] ?? \"unknown\";\n const specNodeType = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : elementId;\n\n // Outgoing: this element can point to others\n const outgoingRels = RELATIONSHIPS_BY_SOURCE.get(specNodeType) ?? [];\n const allDestTypes = [...new Set(outgoingRels.map(r => r.destinationSpecNodeId))];\n const reachableDestTypes = allDestTypes.filter(t => typesInModel.has(t));\n const reachablePredicates = [...new Set(\n outgoingRels.filter(r => reachableDestTypes.includes(r.destinationSpecNodeId)).map(r => r.predicate)\n )];\n\n // Incoming: other element types in the model can point to this element\n const incomingRels = RELATIONSHIPS_BY_DESTINATION.get(specNodeType) ?? [];\n const reachableIncomingSourceTypes = [...new Set(incomingRels.map(r => r.sourceSpecNodeId))]\n .filter(t => typesInModel.has(t));\n\n const hasOutgoingTargets = reachableDestTypes.length > 0;\n const hasIncomingSources = reachableIncomingSourceTypes.length > 0;\n const hasAnySchemas = outgoingRels.length > 0 || incomingRels.length > 0;\n\n let status: \"dead-end\" | \"connectable\" | \"no-schema\";\n if (!hasAnySchemas) {\n status = \"no-schema\";\n } else if (!hasOutgoingTargets && !hasIncomingSources) {\n status = \"dead-end\";\n } else {\n status = \"connectable\";\n }\n\n if (!byLayer.has(layer)) byLayer.set(layer, []);\n byLayer.get(layer)!.push({\n id: elementId,\n type: specNodeType,\n status,\n reachablePredicates,\n reachableTargetTypes: reachableDestTypes,\n needsTypes: allDestTypes,\n });\n }\n\n if (outputPath) {\n const jsonOut: Record<string, unknown> = {\n total: stats.orphanedElements.length,\n byLayer: Object.fromEntries(\n [...byLayer.entries()].map(([layer, items]) => [layer, items])\n ),\n };\n await fs.writeFile(outputPath, JSON.stringify(jsonOut, null, 2), \"utf-8\");\n console.log(`\\nOrphan report exported to ${outputPath}`);\n return;\n }\n\n console.log(ansis.bold(`\\nOrphaned Elements (${stats.orphanedElements.length})`));\n console.log(ansis.dim(\"Elements with no intra-layer relationships and no cross-layer references\\n\"));\n\n let deadEndCount = 0;\n let connectableCount = 0;\n\n for (const [layerName, items] of byLayer) {\n console.log(ansis.bold(ansis.cyan(` ${layerName} (${items.length})`)));\n for (const item of items) {\n if (item.status === \"no-schema\") {\n console.log(` ${ansis.dim(\"\u25CB\")} ${item.id}`);\n console.log(` ${ansis.dim(\"No outgoing relationship schemas defined for type \" + item.type)}`);\n } else if (item.status === \"dead-end\") {\n deadEndCount++;\n const needsPreview = item.needsTypes.slice(0, 4).join(\", \") +\n (item.needsTypes.length > 4 ? ` +${item.needsTypes.length - 4} more` : \"\");\n console.log(` ${ansis.red(\"\u2717\")} ${item.id} ${ansis.dim(\"(dead-end)\")}`);\n console.log(` ${ansis.dim(\"Needs: \" + needsPreview)}`);\n } else {\n connectableCount++;\n const predsPreview = item.reachablePredicates.slice(0, 3).join(\", \");\n const targetsPreview = item.reachableTargetTypes.slice(0, 3).join(\", \");\n console.log(` ${ansis.yellow(\"\u25CB\")} ${item.id}`);\n console.log(` ${ansis.dim(`\u2192 try: ${predsPreview} \u2192 ${targetsPreview}`)}`);\n }\n }\n console.log();\n }\n\n // Summary line\n const noSchemaCount = stats.orphanedElements.length - connectableCount - deadEndCount;\n const parts: string[] = [];\n if (connectableCount > 0) parts.push(`${connectableCount} connectable`);\n if (deadEndCount > 0) parts.push(`${deadEndCount} dead-end (valid target types missing from model)`);\n if (noSchemaCount > 0) parts.push(`${noSchemaCount} no-schema`);\n console.log(ansis.dim(`Summary: ${parts.length > 0 ? parts.join(\", \") : \"0 actionable\"}`));\n if (connectableCount > 0) {\n console.log(ansis.dim(\"Run /dr-relate --orphans to wire connectable elements\"));\n }\n}\n\nexport async function validateCommand(options: ValidateOptions): Promise<void> {\n // Annotate the root cli.execute span with validate-specific attributes\n const activeSpan = getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.layers\", options.layers?.join(\",\") || \"all\");\n activeSpan.setAttribute(\"validate.strict\", options.strict || false);\n activeSpan.setAttribute(\"validate.verbose\", options.verbose || false);\n activeSpan.setAttribute(\"validate.orphans\", options.orphans || false);\n activeSpan.setAttribute(\"validate.schemas\", options.schemas || options.schema || false);\n activeSpan.setAttribute(\"validate.output\", options.output || \"console\");\n }\n\n try {\n // Handle schema validation flag\n if (options.schemas || options.schema) {\n await validateSchemaSynchronization();\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.mode\", \"schema-sync\");\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Load model (forward --layers filter so only requested layers are read from disk)\n const model = await Model.load(options.model, options.layers ? { layers: options.layers } : {});\n\n // --orphans mode: focused orphan report, skip full validation\n if (options.orphans) {\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.mode\", \"orphans-only\");\n }\n\n const activeChangesetId = model.getActiveChangesetId();\n const modelForOrphans = activeChangesetId\n ? (await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)) as unknown as typeof model\n : model;\n await validateOrphansOnly(modelForOrphans, options.output);\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Validate\n const validator = new Validator();\n\n // If an active changeset exists, project staged elements into the model so validators see them\n const activeChangesetId = model.getActiveChangesetId();\n const modelToValidate = activeChangesetId\n ? (await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)) as unknown as typeof model\n : model;\n\n const result = await validator.validateModel(modelToValidate);\n\n // Detect orphaned elements and add them as warnings so they appear in\n // the warning count and are surfaced consistently across all output formats.\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 // Check source traceability in normal mode: elements without source_reference lack the\n // provenance link that dr-sync uses for drift detection. Surface this in every validate\n // run so users discover gaps without having to remember --strict.\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:\n \"Add a source_reference with provenance to link this element to its implementation\",\n });\n }\n }\n }\n\n // Strict-mode additional checks: flag elements missing a description.\n if (options.strict) {\n for (const [layerName, layer] of modelToValidate.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n\n if (!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\n // Record validation results in span\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.valid\", result.isValid());\n activeSpan.setAttribute(\"validate.error_count\", result.errors.length);\n activeSpan.setAttribute(\"validate.warning_count\", result.warnings.length);\n activeSpan.setAttribute(\"validate.orphan_count\", stats.orphanedElements.length);\n }\n\n // Format and display output\n if (options.output) {\n // Export to file\n let content: string;\n\n if (options.output.endsWith(\".md\") || options.output.endsWith(\".markdown\")) {\n content = ValidationFormatter.toMarkdown(result, modelToValidate);\n } else if (options.output.endsWith(\".json\")) {\n content = JSON.stringify(ValidationFormatter.toJSON(result, modelToValidate), null, 2);\n } else {\n // Default to JSON\n content = JSON.stringify(ValidationFormatter.toJSON(result, modelToValidate), null, 2);\n }\n\n await fs.writeFile(options.output, content, \"utf-8\");\n console.log(`\\nValidation report exported to ${options.output}`);\n\n if (!result.isValid()) {\n throw new Error(\"Validation failed\");\n }\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Display formatted output\n const formatted = ValidationFormatter.format(result, modelToValidate, {\n verbose: options.verbose,\n quiet: options.quiet,\n });\n\n console.log(formatted);\n\n // Exit with appropriate code\n if (result.isValid()) {\n if (options.strict && result.warnings.length > 0) {\n console.log(ansis.red(\"Strict mode enabled: treating warnings as errors\"));\n throw new Error(\"Validation failed (strict mode)\");\n }\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n } else {\n throw new Error(\"Validation failed\");\n }\n } catch (error) {\n activeSpan?.setAttribute(\"validate.result\", \"error\");\n\n const message = getErrorMessage(error);\n console.error(ansis.red(`Error: ${message}`));\n\n // Always preserve full error details in stderr for debugging\n if (error instanceof Error && error.stack) {\n console.error(ansis.dim(\"\\nFull error details:\"));\n console.error(ansis.dim(error.stack));\n }\n\n // Additional context for debugging\n if (process.env.DEBUG) {\n console.error(ansis.dim(\"\\nDebug mode enabled. Additional context:\"));\n if (error instanceof Error) {\n console.error(ansis.dim(`Error name: ${error.name}`));\n console.error(ansis.dim(`Error cause: ${(error as any).cause || \"none\"}`));\n }\n }\n\n // Re-throw to let root span handle exit\n throw error;\n }\n}\n"],
|
|
5
|
-
"mappings": "AAIA,OAAO,WAAW;AAClB,SAAS,YAAY,UAAU;AAC/B,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAC9B,SAAS,aAAa;AACtB,SAAS,iBAAiB;AAC1B,SAAS,2BAA2B;AACpC,SAAS,uBAAuB;AAChC,SAAS,yBAAyB,oCAAoC;AACtE,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/**\n * Validate the architecture model\n */\n\nimport ansis from \"ansis\";\nimport { promises as fs } from \"fs\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport { Model } from \"../core/model.js\";\nimport { Validator } from \"../validators/validator.js\";\nimport { ValidationFormatter } from \"../validators/validation-formatter.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { RELATIONSHIPS_BY_SOURCE, RELATIONSHIPS_BY_DESTINATION } from \"../generated/relationship-index.js\";\nimport { getActiveSpan } from \"../telemetry/index.js\";\nimport { StagedChangesetStorage } from \"../core/staged-changeset-storage.js\";\n\n\nexport interface ValidateOptions {\n layers?: string[];\n strict?: boolean;\n verbose?: boolean;\n quiet?: boolean;\n output?: string;\n debug?: boolean;\n model?: string;\n orphans?: boolean;\n // Python CLI compatibility options\n all?: boolean;\n markdown?: boolean;\n schemas?: boolean;\n schema?: boolean; // Alias for schemas\n relationships?: boolean;\n structure?: boolean;\n naming?: boolean;\n references?: boolean;\n // Composed scope options\n scope?: string;\n modelPath?: string[]; // Raw array from CLI; will be parsed into modelPaths\n modelPaths?: Record<string, string>; // Parsed model-path overrides\n}\n\n/**\n * Validate schema synchronization between spec/ and cli/src/schemas/bundled/\n * @throws {Error} if schema synchronization validation fails\n */\n/**\n * Recursively find all JSON schema files in a directory\n * Excludes 'layers' subdirectory since layer instances are not schemas\n */\nasync function findJsonFiles(dir: string, baseDir: string = dir): Promise<string[]> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n const files: string[] = [];\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n // Skip 'layers' directory - layer instances are synced but are not schemas\n if (entry.isDirectory() && entry.name !== \"layers\") {\n const subFiles = await findJsonFiles(fullPath, baseDir);\n files.push(...subFiles);\n } else if (entry.name.endsWith(\".json\") && entry.isFile()) {\n // Store relative path from base directory\n files.push(path.relative(baseDir, fullPath));\n }\n }\n\n return files;\n}\n\nasync function validateSchemaSynchronization(): Promise<void> {\n console.log(\"\");\n console.log(ansis.bold(\"Validating schema synchronization...\"));\n console.log(\"\");\n\n const __filename = fileURLToPath(import.meta.url);\n const __dirname = path.dirname(__filename);\n\n // Resolve paths relative to project root\n const projectRoot = path.resolve(__dirname, \"../../..\");\n // Compare spec/dist/ (15 compiled flat files) vs dist/schemas/bundled/ (same 15 files)\n // spec/schemas/ contains individual source files; spec/dist/ is the compiled distribution\n const specSchemaDir = path.join(projectRoot, \"spec\", \"dist\");\n const bundledSchemaDir = path.join(__dirname, \"../schemas/bundled\");\n\n // Find all JSON files recursively\n const specSchemaFiles = await findJsonFiles(specSchemaDir);\n const bundledSchemaFiles = await findJsonFiles(bundledSchemaDir);\n\n if (specSchemaFiles.length === 0) {\n throw new Error(\"No schema files found in spec/schemas/\");\n }\n\n let mismatches: string[] = [];\n\n for (const schemaFile of specSchemaFiles) {\n const specPath = path.join(specSchemaDir, schemaFile);\n const bundledPath = path.join(bundledSchemaDir, schemaFile);\n\n try {\n const specContent = await fs.readFile(specPath, \"utf-8\");\n\n try {\n const bundledContent = await fs.readFile(bundledPath, \"utf-8\");\n\n // Normalize JSON to account for formatting differences\n const specParsed = JSON.parse(specContent);\n const bundledParsed = JSON.parse(bundledContent);\n const specNormalized = JSON.stringify(specParsed, null, 2);\n const bundledNormalized = JSON.stringify(bundledParsed, null, 2);\n\n if (specNormalized !== bundledNormalized) {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${schemaFile} - Content differs between spec/ and cli/src/schemas/bundled/`\n );\n } else {\n console.log(` ${ansis.green(\"\u2713\")} ${schemaFile}`);\n }\n } catch (error) {\n if ((error as any).code === \"ENOENT\") {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${schemaFile} - Missing in cli/src/schemas/bundled/`\n );\n } else {\n throw error;\n }\n }\n } catch (error) {\n const message = getErrorMessage(error);\n mismatches.push(` ${ansis.red(\"\u2717\")} ${schemaFile} - Error reading spec schema: ${message}`);\n }\n }\n\n // Check for extra files in bundled that aren't in spec\n for (const bundledFile of bundledSchemaFiles) {\n if (!specSchemaFiles.includes(bundledFile)) {\n mismatches.push(\n ` ${ansis.red(\"\u2717\")} ${bundledFile} - Extra file in cli/src/schemas/bundled/ (not in spec/)`\n );\n }\n }\n\n if (mismatches.length > 0) {\n console.log(\"\");\n console.log(\n ansis.bold(ansis.red(`\u2717 Schema synchronization failed with ${mismatches.length} issue(s):`))\n );\n console.log(\"\");\n for (const mismatch of mismatches) {\n console.log(mismatch);\n }\n console.log(\"\");\n console.log(\n ansis.dim(\"To fix: Run 'npm run build:spec' at repo root, then 'npm run build' in cli/ to sync spec/dist/ \u2192 cli/src/schemas/bundled/\")\n );\n console.log(\"\");\n throw new Error(`Schema synchronization failed with ${mismatches.length} issue(s)`);\n } else {\n console.log(\"\");\n console.log(ansis.green(\"\u2713 All schemas synchronized\"));\n console.log(\"\");\n }\n}\n\n/**\n * Display structured orphan report grouped by layer, with dead-end detection.\n * For each orphan, checks whether valid relationship targets exist in the model.\n */\nasync function validateOrphansOnly(model: Model, outputPath?: string): Promise<void> {\n const stats = ValidationFormatter.calculateStats(model);\n\n if (stats.orphanedElements.length === 0) {\n const msg = \"No orphaned elements found. All elements are connected.\";\n if (outputPath) {\n await fs.writeFile(outputPath, JSON.stringify({ total: 0, byLayer: {} }, null, 2), \"utf-8\");\n console.log(`\\nOrphan report exported to ${outputPath}`);\n } else {\n console.log(ansis.green(`\\n\u2713 ${msg}`));\n }\n return;\n }\n\n // Build set of element types currently present in the model\n const typesInModel = new Set<string>();\n for (const [, layer] of model.layers) {\n for (const element of layer.listElements()) {\n const id = element.path || element.id;\n const parts = id.split(\".\");\n if (parts.length >= 2) typesInModel.add(`${parts[0]}.${parts[1]}`);\n }\n }\n\n // Group orphans by layer, annotated with dead-end status\n const byLayer = new Map<string, Array<{\n id: string;\n type: string;\n status: \"dead-end\" | \"connectable\" | \"no-schema\";\n reachablePredicates: string[];\n reachableTargetTypes: string[];\n needsTypes: string[];\n }>>();\n\n for (const elementId of stats.orphanedElements.sort()) {\n const parts = elementId.split(\".\");\n const layer = parts[0] ?? \"unknown\";\n const specNodeType = parts.length >= 2 ? `${parts[0]}.${parts[1]}` : elementId;\n\n // Outgoing: this element can point to others\n const outgoingRels = RELATIONSHIPS_BY_SOURCE.get(specNodeType) ?? [];\n const allDestTypes = [...new Set(outgoingRels.map(r => r.destinationSpecNodeId))];\n const reachableDestTypes = allDestTypes.filter(t => typesInModel.has(t));\n const reachablePredicates = [...new Set(\n outgoingRels.filter(r => reachableDestTypes.includes(r.destinationSpecNodeId)).map(r => r.predicate)\n )];\n\n // Incoming: other element types in the model can point to this element\n const incomingRels = RELATIONSHIPS_BY_DESTINATION.get(specNodeType) ?? [];\n const reachableIncomingSourceTypes = [...new Set(incomingRels.map(r => r.sourceSpecNodeId))]\n .filter(t => typesInModel.has(t));\n\n const hasOutgoingTargets = reachableDestTypes.length > 0;\n const hasIncomingSources = reachableIncomingSourceTypes.length > 0;\n const hasAnySchemas = outgoingRels.length > 0 || incomingRels.length > 0;\n\n let status: \"dead-end\" | \"connectable\" | \"no-schema\";\n if (!hasAnySchemas) {\n status = \"no-schema\";\n } else if (!hasOutgoingTargets && !hasIncomingSources) {\n status = \"dead-end\";\n } else {\n status = \"connectable\";\n }\n\n if (!byLayer.has(layer)) byLayer.set(layer, []);\n byLayer.get(layer)!.push({\n id: elementId,\n type: specNodeType,\n status,\n reachablePredicates,\n reachableTargetTypes: reachableDestTypes,\n needsTypes: allDestTypes,\n });\n }\n\n if (outputPath) {\n const jsonOut: Record<string, unknown> = {\n total: stats.orphanedElements.length,\n byLayer: Object.fromEntries(\n [...byLayer.entries()].map(([layer, items]) => [layer, items])\n ),\n };\n await fs.writeFile(outputPath, JSON.stringify(jsonOut, null, 2), \"utf-8\");\n console.log(`\\nOrphan report exported to ${outputPath}`);\n return;\n }\n\n console.log(ansis.bold(`\\nOrphaned Elements (${stats.orphanedElements.length})`));\n console.log(ansis.dim(\"Elements with no intra-layer relationships and no cross-layer references\\n\"));\n\n let deadEndCount = 0;\n let connectableCount = 0;\n\n for (const [layerName, items] of byLayer) {\n console.log(ansis.bold(ansis.cyan(` ${layerName} (${items.length})`)));\n for (const item of items) {\n if (item.status === \"no-schema\") {\n console.log(` ${ansis.dim(\"\u25CB\")} ${item.id}`);\n console.log(` ${ansis.dim(\"No outgoing relationship schemas defined for type \" + item.type)}`);\n } else if (item.status === \"dead-end\") {\n deadEndCount++;\n const needsPreview = item.needsTypes.slice(0, 4).join(\", \") +\n (item.needsTypes.length > 4 ? ` +${item.needsTypes.length - 4} more` : \"\");\n console.log(` ${ansis.red(\"\u2717\")} ${item.id} ${ansis.dim(\"(dead-end)\")}`);\n console.log(` ${ansis.dim(\"Needs: \" + needsPreview)}`);\n } else {\n connectableCount++;\n const predsPreview = item.reachablePredicates.slice(0, 3).join(\", \");\n const targetsPreview = item.reachableTargetTypes.slice(0, 3).join(\", \");\n console.log(` ${ansis.yellow(\"\u25CB\")} ${item.id}`);\n console.log(` ${ansis.dim(`\u2192 try: ${predsPreview} \u2192 ${targetsPreview}`)}`);\n }\n }\n console.log();\n }\n\n // Summary line\n const noSchemaCount = stats.orphanedElements.length - connectableCount - deadEndCount;\n const parts: string[] = [];\n if (connectableCount > 0) parts.push(`${connectableCount} connectable`);\n if (deadEndCount > 0) parts.push(`${deadEndCount} dead-end (valid target types missing from model)`);\n if (noSchemaCount > 0) parts.push(`${noSchemaCount} no-schema`);\n console.log(ansis.dim(`Summary: ${parts.length > 0 ? parts.join(\", \") : \"0 actionable\"}`));\n if (connectableCount > 0) {\n console.log(ansis.dim(\"Run /dr-relate --orphans to wire connectable elements\"));\n }\n}\n\n/**\n * Print a notice that staged changes from the active changeset were projected into the\n * model before validation, so users don't mistake correctly-projected staged state for\n * a validator bug. Scoped to the layers actually included in this validation run.\n */\nasync function reportStagedChangesIncluded(\n rootPath: string,\n changesetId: string,\n validatedLayerNames: Iterable<string>\n): Promise<void> {\n try {\n const changeset = await new StagedChangesetStorage(rootPath).load(changesetId);\n if (!changeset || changeset.changes.length === 0) {\n return;\n }\n\n const validatedLayers = new Set(validatedLayerNames);\n const relevantChangeCount = changeset.changes.filter((change) =>\n validatedLayers.has(change.layerName)\n ).length;\n\n if (relevantChangeCount === 0) {\n return;\n }\n\n console.log(\n ansis.cyan(\n `\u2139 Including ${relevantChangeCount} staged change${relevantChangeCount === 1 ? \"\" : \"s\"} from changeset '${changesetId}'`\n )\n );\n console.log();\n } catch (error) {\n // Changeset is corrupted or unreadable, but this is purely informational.\n // Log a warning and continue validation rather than crashing.\n const message = getErrorMessage(error);\n console.warn(ansis.yellow(`\u26A0 Could not load changeset '${changesetId}': ${message}`));\n console.log();\n }\n}\n\n/**\n * Parse model-path option strings into a dictionary\n * Format: \"model-name=/path/to/model\" (can be repeated)\n */\nfunction parseModelPaths(modelPathArray?: string[]): Record<string, string> {\n const result: Record<string, string> = {};\n\n if (!modelPathArray || modelPathArray.length === 0) {\n return result;\n }\n\n for (const item of modelPathArray) {\n const [modelName, ...rest] = item.split(\"=\");\n const modelPath = rest.join(\"=\"); // Handle paths with = in them\n\n if (!modelName || !modelPath) {\n throw new Error(\n `Invalid --model-path format: '${item}'. Expected format: 'model-name=/path/to/model'`\n );\n }\n\n result[modelName.trim()] = modelPath.trim();\n }\n\n return result;\n}\n\nexport async function validateCommand(options: ValidateOptions): Promise<void> {\n // Parse model-path options only if provided\n if (options.modelPath) {\n options.modelPaths = parseModelPaths(options.modelPath);\n }\n\n // Annotate the root cli.execute span with validate-specific attributes\n const activeSpan = getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.layers\", options.layers?.join(\",\") || \"all\");\n activeSpan.setAttribute(\"validate.strict\", options.strict || false);\n activeSpan.setAttribute(\"validate.verbose\", options.verbose || false);\n activeSpan.setAttribute(\"validate.orphans\", options.orphans || false);\n activeSpan.setAttribute(\"validate.schemas\", options.schemas || options.schema || false);\n activeSpan.setAttribute(\"validate.output\", options.output || \"console\");\n activeSpan.setAttribute(\"validate.scope\", options.scope || \"local\");\n }\n\n try {\n // Handle schema validation flag\n if (options.schemas || options.schema) {\n await validateSchemaSynchronization();\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.mode\", \"schema-sync\");\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Load model (forward --layers filter so only requested layers are read from disk)\n const model = await Model.load(options.model, options.layers ? { layers: options.layers } : {});\n\n // --orphans mode: focused orphan report, skip full validation\n if (options.orphans) {\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.mode\", \"orphans-only\");\n }\n\n const activeChangesetId = model.getActiveChangesetId();\n const modelForOrphans = activeChangesetId\n ? (await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)) as unknown as typeof model\n : model;\n\n if (activeChangesetId) {\n await reportStagedChangesIncluded(\n model.rootPath,\n activeChangesetId,\n options.layers && options.layers.length > 0 ? options.layers : modelForOrphans.layers.keys()\n );\n }\n\n await validateOrphansOnly(modelForOrphans, options.output);\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Determine validation scope and create appropriate validator\n const scope = options.scope || \"local\";\n\n // If an active changeset exists, project staged elements into the model so validators see them\n const activeChangesetId = model.getActiveChangesetId();\n const modelToValidate = activeChangesetId\n ? (await model.getVirtualProjectionEngine().projectModel(model, activeChangesetId)) as unknown as typeof model\n : model;\n\n if (activeChangesetId) {\n await reportStagedChangesIncluded(\n model.rootPath,\n activeChangesetId,\n options.layers && options.layers.length > 0 ? options.layers : modelToValidate.layers.keys()\n );\n }\n\n let result;\n\n if (scope === \"composed\") {\n // Use composed validator for cross-model reference validation\n const { ComposedReferenceValidator } = await import(\"../validators/composed-reference-validator.js\");\n const composedValidator = new ComposedReferenceValidator(options.modelPaths || {});\n result = await composedValidator.validateModel(modelToValidate);\n } else if (scope === \"local\") {\n // Use standard validator for local validation\n const validator = new Validator();\n result = await validator.validateModel(modelToValidate);\n } else {\n throw new Error(`Invalid validation scope: '${scope}'. Expected 'local' or 'composed'.`);\n }\n\n // Detect orphaned elements and add them as warnings so they appear in\n // the warning count and are surfaced consistently across all output formats.\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 // Check source traceability in normal mode: elements without source_reference lack the\n // provenance link that dr-sync uses for drift detection. Surface this in every validate\n // run so users discover gaps without having to remember --strict.\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:\n \"Add a source_reference with provenance to link this element to its implementation\",\n });\n }\n }\n }\n\n // Strict-mode additional checks: flag elements missing a description.\n if (options.strict) {\n for (const [layerName, layer] of modelToValidate.layers) {\n for (const element of layer.listElements()) {\n const elementId = element.path || element.id;\n\n if (!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\n // Record validation results in span\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.valid\", result.isValid());\n activeSpan.setAttribute(\"validate.error_count\", result.errors.length);\n activeSpan.setAttribute(\"validate.warning_count\", result.warnings.length);\n activeSpan.setAttribute(\"validate.orphan_count\", stats.orphanedElements.length);\n }\n\n // Format and display output\n if (options.output) {\n // Export to file\n let content: string;\n\n if (options.output.endsWith(\".md\") || options.output.endsWith(\".markdown\")) {\n content = ValidationFormatter.toMarkdown(result, modelToValidate);\n } else if (options.output.endsWith(\".json\")) {\n content = JSON.stringify(ValidationFormatter.toJSON(result, modelToValidate), null, 2);\n } else {\n // Default to JSON\n content = JSON.stringify(ValidationFormatter.toJSON(result, modelToValidate), null, 2);\n }\n\n await fs.writeFile(options.output, content, \"utf-8\");\n console.log(`\\nValidation report exported to ${options.output}`);\n\n if (!result.isValid()) {\n throw new Error(\"Validation failed\");\n }\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n }\n\n // Display formatted output\n const formatted = ValidationFormatter.format(result, modelToValidate, {\n verbose: options.verbose,\n quiet: options.quiet,\n });\n\n console.log(formatted);\n\n // Exit with appropriate code\n if (result.isValid()) {\n if (options.strict && result.warnings.length > 0) {\n console.log(ansis.red(\"Strict mode enabled: treating warnings as errors\"));\n throw new Error(\"Validation failed (strict mode)\");\n }\n\n if (activeSpan) {\n activeSpan.setAttribute(\"validate.result\", \"success\");\n }\n return;\n } else {\n throw new Error(\"Validation failed\");\n }\n } catch (error) {\n activeSpan?.setAttribute(\"validate.result\", \"error\");\n\n const message = getErrorMessage(error);\n console.error(ansis.red(`Error: ${message}`));\n\n // Always preserve full error details in stderr for debugging\n if (error instanceof Error && error.stack) {\n console.error(ansis.dim(\"\\nFull error details:\"));\n console.error(ansis.dim(error.stack));\n }\n\n // Additional context for debugging\n if (process.env.DEBUG) {\n console.error(ansis.dim(\"\\nDebug mode enabled. Additional context:\"));\n if (error instanceof Error) {\n console.error(ansis.dim(`Error name: ${error.name}`));\n console.error(ansis.dim(`Error cause: ${(error as any).cause || \"none\"}`));\n }\n }\n\n // Re-throw to let root span handle exit\n throw error;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["parts", "activeChangesetId"]
|
|
7
7
|
}
|
package/dist/commands/version.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import ansis from "ansis";
|
|
2
|
-
import { getCliBundledSpecVersion } from "../utils/spec-version.js";
|
|
2
|
+
import { getCliBundledSpecVersion, getCliVersion } from "../utils/spec-version.js";
|
|
3
3
|
import { isTelemetryBuiltIn, isTelemetryConfigured } from "../telemetry/detect.js";
|
|
4
4
|
import { getSpecReferencePath, getModelPath } from "../utils/project-paths.js";
|
|
5
|
-
const gitHash = "
|
|
5
|
+
const gitHash = "749a455", cliVersion = getCliVersion();
|
|
6
6
|
async function versionCommand() {
|
|
7
7
|
const specVersion = getCliBundledSpecVersion(), telemetryBuiltIn = isTelemetryBuiltIn(), telemetryConfigured = await isTelemetryConfigured();
|
|
8
8
|
let telemetryStatus;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/commands/version.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Version Command - Display CLI and bundled spec versions\n */\n\nimport ansis from \"ansis\";\nimport { getCliBundledSpecVersion } from \"../utils/spec-version.js\";\nimport { isTelemetryBuiltIn, isTelemetryConfigured } from \"../telemetry/detect.js\";\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\";\n\n// Declare build-time
|
|
5
|
-
"mappings": "AAIA,OAAO,WAAW;AAClB,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * Version Command - Display CLI and bundled spec versions\n */\n\nimport ansis from \"ansis\";\nimport { getCliBundledSpecVersion, getCliVersion } from \"../utils/spec-version.js\";\nimport { isTelemetryBuiltIn, isTelemetryConfigured } from \"../telemetry/detect.js\";\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\";\n\n// Declare build-time constant (substituted by esbuild)\ndeclare const GIT_HASH: string;\n\nconst gitHash = typeof GIT_HASH !== \"undefined\" ? GIT_HASH : \"unknown\";\nconst cliVersion = getCliVersion();\n\nexport async function versionCommand(): Promise<void> {\n const specVersion = getCliBundledSpecVersion();\n const telemetryBuiltIn = isTelemetryBuiltIn();\n const telemetryConfigured = await isTelemetryConfigured();\n\n // Determine telemetry status message\n let telemetryStatus: string;\n if (!telemetryBuiltIn) {\n telemetryStatus = ansis.dim(\"not available (build)\");\n } else if (telemetryConfigured) {\n telemetryStatus = ansis.green(\"enabled, configured\");\n } else {\n telemetryStatus = ansis.yellow(\"enabled, not configured\");\n }\n\n console.log(ansis.bold(\"Documentation Robotics CLI\"));\n console.log(`CLI Version: ${ansis.cyan(cliVersion)}`);\n console.log(`Spec Version: ${ansis.cyan(specVersion)}`);\n console.log(`Git Hash: ${ansis.cyan(gitHash)}`);\n console.log(`Telemetry: ${telemetryStatus}`);\n console.log(`Spec source: ${ansis.cyan(\"bundled with CLI\")}`);\n const drFolder = await getSpecReferencePath();\n console.log(`DR folder: ${ansis.cyan(drFolder || \"Not found\")}`);\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`);\n}\n"],
|
|
5
|
+
"mappings": "AAIA,OAAO,WAAW;AAClB,SAAS,0BAA0B,qBAAqB;AACxD,SAAS,oBAAoB,6BAA6B;AAC1D,SAAS,sBAAsB,oBAAoB;AAKnD,MAAM,UAA4C,WAC5C,aAAa,cAAa;AAEhC,eAAsB,iBAAc;AAClC,QAAM,cAAc,yBAAwB,GACtC,mBAAmB,mBAAkB,GACrC,sBAAsB,MAAM,sBAAqB;AAGvD,MAAI;AACJ,EAAK,mBAEM,sBACT,kBAAkB,MAAM,MAAM,qBAAqB,IAEnD,kBAAkB,MAAM,OAAO,yBAAyB,IAJxD,kBAAkB,MAAM,IAAI,uBAAuB,GAOrD,QAAQ,IAAI,MAAM,KAAK,4BAA4B,CAAC,GACpD,QAAQ,IAAI,iBAAiB,MAAM,KAAK,UAAU,CAAC,EAAE,GACrD,QAAQ,IAAI,iBAAiB,MAAM,KAAK,WAAW,CAAC,EAAE,GACtD,QAAQ,IAAI,iBAAiB,MAAM,KAAK,OAAO,CAAC,EAAE,GAClD,QAAQ,IAAI,iBAAiB,eAAe,EAAE,GAC9C,QAAQ,IAAI,iBAAiB,MAAM,KAAK,kBAAkB,CAAC,EAAE;AAC7D,QAAM,WAAW,MAAM,qBAAoB;AAC3C,UAAQ,IAAI,iBAAiB,MAAM,KAAK,YAAY,WAAW,CAAC,EAAE,GAClE,QAAQ,IAAI,iBAAiB,MAAM,KAAM,MAAM,aAAY,KAAO,WAAW,CAAC,EAAE;AAClF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { parse, stringify } from "yaml";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
const DRConfigSchema = z.object({
|
|
8
|
+
telemetry: z.object({
|
|
9
|
+
otlp: z.object({
|
|
10
|
+
endpoint: z.string().optional(),
|
|
11
|
+
logs_endpoint: z.string().optional(),
|
|
12
|
+
service_name: z.string().optional()
|
|
13
|
+
}).passthrough().optional()
|
|
14
|
+
}).passthrough().optional(),
|
|
15
|
+
mcp: z.object({
|
|
16
|
+
api_key_path: z.string().optional()
|
|
17
|
+
}).passthrough().optional()
|
|
18
|
+
}).passthrough(), CONFIG_FILENAME = ".dr-config.yaml";
|
|
19
|
+
class DRConfigParseError extends Error {
|
|
20
|
+
configPath;
|
|
21
|
+
constructor(configPath, options) {
|
|
22
|
+
super(`Failed to parse ${configPath}`, options), this.configPath = configPath, this.name = "DRConfigParseError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getDRConfigPath() {
|
|
26
|
+
return process.env.DR_CONFIG_PATH ?? join(homedir(), CONFIG_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
async function loadDRConfig(options) {
|
|
29
|
+
const configPath = getDRConfigPath();
|
|
30
|
+
if (!existsSync(configPath))
|
|
31
|
+
return {};
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFile(configPath, "utf-8"), parsed = parse(content);
|
|
34
|
+
if (parsed == null)
|
|
35
|
+
return {};
|
|
36
|
+
const result = DRConfigSchema.safeParse(parsed);
|
|
37
|
+
if (!result.success)
|
|
38
|
+
throw new Error(`Invalid config shape: ${result.error.message}`);
|
|
39
|
+
return result.data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (reportConfigLoadError(configPath, error), options?.strict)
|
|
42
|
+
throw new DRConfigParseError(configPath, { cause: error });
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function saveDRConfig(config) {
|
|
47
|
+
const configPath = getDRConfigPath();
|
|
48
|
+
await mkdir(dirname(configPath), { recursive: !0 }), await writeFile(configPath, stringify(config), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
function reportConfigLoadError(configPath, error) {
|
|
51
|
+
if (!(error instanceof Error)) {
|
|
52
|
+
process.stderr.write(`Warning: Failed to parse ${configPath}, using defaults
|
|
53
|
+
`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const errorMsg = error.message;
|
|
57
|
+
errorMsg.includes("EACCES") || errorMsg.includes("permission denied") ? (process.stderr.write(`Error: Cannot read config file ${configPath} - permission denied
|
|
58
|
+
`), process.stderr.write(`Suggestions:
|
|
59
|
+
`), process.stderr.write(` \u2022 Check file permissions with: ls -l ${configPath}
|
|
60
|
+
`), process.stderr.write(` \u2022 Ensure you have read access to the file
|
|
61
|
+
`), process.stderr.write(` \u2022 Try: chmod 644 ${configPath}
|
|
62
|
+
`)) : errorMsg.includes("YAMLException") || error.name === "YAMLException" || errorMsg.includes("bad indentation") || errorMsg.includes("unexpected") ? (process.stderr.write(`Error: Invalid YAML syntax in ${configPath}
|
|
63
|
+
`), process.stderr.write(`Details: ${errorMsg}
|
|
64
|
+
`), process.stderr.write(`Suggestions:
|
|
65
|
+
`), process.stderr.write(` \u2022 Validate your YAML syntax at https://www.yamllint.com/
|
|
66
|
+
`), process.stderr.write(` \u2022 Check for proper indentation (use spaces, not tabs)
|
|
67
|
+
`), process.stderr.write(` \u2022 Verify colons have spaces after them
|
|
68
|
+
`)) : (process.stderr.write(`Error: Failed to load config file ${configPath}
|
|
69
|
+
`), process.stderr.write(`Details: ${errorMsg}
|
|
70
|
+
`), process.stderr.write(`Suggestions:
|
|
71
|
+
`), process.stderr.write(` \u2022 Verify the file is valid UTF-8 encoded text
|
|
72
|
+
`), process.stderr.write(` \u2022 Check if the file system is accessible
|
|
73
|
+
`), process.stderr.write(` \u2022 Try recreating the file if it may be corrupted
|
|
74
|
+
`)), process.stderr.write(`Using default configuration due to config file error
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
DRConfigParseError,
|
|
79
|
+
getDRConfigPath,
|
|
80
|
+
loadDRConfig,
|
|
81
|
+
saveDRConfig
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/config.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared DR CLI configuration loader\n *\n * Reads and writes `~/.dr-config.yaml` (overridable via DR_CONFIG_PATH, primarily for\n * tests). This is the single source of truth for the config file's shape and I/O \u2014\n * feature-specific loaders (telemetry, MCP, ...) should read their section from the\n * object returned by `loadDRConfig()` rather than parsing the file themselves.\n *\n * Configuration file format (~/.dr-config.yaml):\n * ```yaml\n * telemetry:\n * otlp:\n * endpoint: 'http://localhost:4318/v1/traces'\n * mcp:\n * api_key_path: '/home/user/.dr-mcp-key'\n * ```\n */\n\nimport { homedir } from \"node:os\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { parse, stringify } from \"yaml\";\nimport { z } from \"zod\";\n\n/**\n * Runtime schema for ~/.dr-config.yaml, used to validate parsed YAML before it\n * is trusted as a `DRConfig`. Keep in sync with the shape documented above.\n */\nconst DRConfigSchema = z\n .object({\n telemetry: z\n .object({\n otlp: z\n .object({\n endpoint: z.string().optional(),\n logs_endpoint: z.string().optional(),\n service_name: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough()\n .optional(),\n mcp: z\n .object({\n api_key_path: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\n\n/**\n * Configuration structure parsed from ~/.dr-config.yaml\n */\nexport type DRConfig = z.infer<typeof DRConfigSchema>;\n\nconst CONFIG_FILENAME = \".dr-config.yaml\";\n\n/**\n * Thrown by `loadDRConfig({ strict: true })` when the config file exists but\n * cannot be parsed. Callers that would otherwise silently treat a corrupted\n * config as \"nothing configured\" \u2014 and take a destructive action like\n * regenerating a secret \u2014 should opt into strict mode and handle this.\n */\nexport class DRConfigParseError extends Error {\n constructor(\n public readonly configPath: string,\n options?: { cause?: unknown }\n ) {\n super(`Failed to parse ${configPath}`, options);\n this.name = \"DRConfigParseError\";\n }\n}\n\n/**\n * Resolve the path to the DR config file.\n * Supports DR_CONFIG_PATH override for testing; defaults to ~/.dr-config.yaml.\n */\nexport function getDRConfigPath(): string {\n return process.env.DR_CONFIG_PATH ?? join(homedir(), CONFIG_FILENAME);\n}\n\n/**\n * Load the DR config file, returning an empty object if it does not exist or\n * cannot be parsed. Errors are reported to stderr but never thrown by default,\n * since a malformed config file should degrade to defaults rather than crash\n * the CLI for most callers (e.g. telemetry).\n *\n * Pass `{ strict: true }` to instead throw a `DRConfigParseError` when the\n * file exists but fails to parse. Use this for callers where treating a\n * corrupted config the same as \"nothing configured\" would cause data loss\n * (e.g. regenerating and overwriting a secret whose path is recorded here).\n */\nexport async function loadDRConfig(options?: { strict?: boolean }): Promise<DRConfig> {\n const configPath = getDRConfigPath();\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content);\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n const result = DRConfigSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid config shape: ${result.error.message}`);\n }\n return result.data;\n } catch (error) {\n reportConfigLoadError(configPath, error);\n if (options?.strict) {\n throw new DRConfigParseError(configPath, { cause: error });\n }\n return {};\n }\n}\n\n/**\n * Persist the given config object to the DR config file, creating the parent\n * directory if necessary. Callers are responsible for merging with any existing\n * config (typically via `loadDRConfig()`) before calling this.\n */\nexport async function saveDRConfig(config: DRConfig): Promise<void> {\n const configPath = getDRConfigPath();\n await mkdir(dirname(configPath), { recursive: true });\n await writeFile(configPath, stringify(config), \"utf-8\");\n}\n\nfunction reportConfigLoadError(configPath: string, error: unknown): void {\n if (!(error instanceof Error)) {\n process.stderr.write(`Warning: Failed to parse ${configPath}, using defaults\\n`);\n return;\n }\n\n const errorMsg = error.message;\n\n if (errorMsg.includes(\"EACCES\") || errorMsg.includes(\"permission denied\")) {\n process.stderr.write(`Error: Cannot read config file ${configPath} - permission denied\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(` \u2022 Check file permissions with: ls -l ${configPath}\\n`);\n process.stderr.write(\" \u2022 Ensure you have read access to the file\\n\");\n process.stderr.write(` \u2022 Try: chmod 644 ${configPath}\\n`);\n } else if (\n errorMsg.includes(\"YAMLException\") ||\n error.name === \"YAMLException\" ||\n errorMsg.includes(\"bad indentation\") ||\n errorMsg.includes(\"unexpected\")\n ) {\n process.stderr.write(`Error: Invalid YAML syntax in ${configPath}\\n`);\n process.stderr.write(`Details: ${errorMsg}\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(\" \u2022 Validate your YAML syntax at https://www.yamllint.com/\\n\");\n process.stderr.write(\" \u2022 Check for proper indentation (use spaces, not tabs)\\n\");\n process.stderr.write(\" \u2022 Verify colons have spaces after them\\n\");\n } else {\n process.stderr.write(`Error: Failed to load config file ${configPath}\\n`);\n process.stderr.write(`Details: ${errorMsg}\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(\" \u2022 Verify the file is valid UTF-8 encoded text\\n\");\n process.stderr.write(\" \u2022 Check if the file system is accessible\\n\");\n process.stderr.write(\" \u2022 Try recreating the file if it may be corrupted\\n\");\n }\n process.stderr.write(`Using default configuration due to config file error\\n`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFile, readdir, rm } from "fs/promises";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { atomicWrite, ensureDir, fileExists } from "../utils/file-io.js";
|
|
7
|
+
import { FileLock } from "../utils/file-lock.js";
|
|
8
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
9
|
+
const AnnotationReplySchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
author: z.string(),
|
|
12
|
+
content: z.string(),
|
|
13
|
+
createdAt: z.string()
|
|
14
|
+
}), AnnotationSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
elementId: z.string(),
|
|
17
|
+
author: z.string(),
|
|
18
|
+
content: z.string(),
|
|
19
|
+
createdAt: z.string(),
|
|
20
|
+
updatedAt: z.string().optional(),
|
|
21
|
+
tags: z.array(z.string()),
|
|
22
|
+
resolved: z.boolean(),
|
|
23
|
+
replies: z.array(AnnotationReplySchema)
|
|
24
|
+
});
|
|
25
|
+
class AnnotationStore {
|
|
26
|
+
annotationsDir;
|
|
27
|
+
constructor(rootPath) {
|
|
28
|
+
this.annotationsDir = path.join(rootPath, "documentation-robotics", "annotations");
|
|
29
|
+
}
|
|
30
|
+
/** Lists all annotations, optionally filtered to a single element, oldest first. Skips (and warns on) individual files that fail to load rather than failing the whole list. */
|
|
31
|
+
async list(elementId) {
|
|
32
|
+
if (!await fileExists(this.annotationsDir))
|
|
33
|
+
return [];
|
|
34
|
+
const entries = await readdir(this.annotationsDir, { withFileTypes: !0 }), annotations = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isFile() || !entry.name.endsWith(".yaml"))
|
|
37
|
+
continue;
|
|
38
|
+
const id = entry.name.slice(0, -5);
|
|
39
|
+
let annotation;
|
|
40
|
+
try {
|
|
41
|
+
annotation = await this.get(id);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
process.stderr.write(`Warning: Skipping annotation '${id}': ${getErrorMessage(error)}
|
|
44
|
+
`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
annotation && (!elementId || annotation.elementId === elementId) && annotations.push(annotation);
|
|
48
|
+
}
|
|
49
|
+
return annotations.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
50
|
+
}
|
|
51
|
+
async get(id) {
|
|
52
|
+
const filePath = this.getPath(id);
|
|
53
|
+
if (!await fileExists(filePath))
|
|
54
|
+
return null;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
const content = await readFile(filePath, "utf-8");
|
|
58
|
+
parsed = yaml.parse(content);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Failed to load annotation '${id}': ${getErrorMessage(error)}`);
|
|
61
|
+
}
|
|
62
|
+
const result = AnnotationSchema.safeParse(parsed);
|
|
63
|
+
if (!result.success)
|
|
64
|
+
throw new Error(`Failed to load annotation '${id}': invalid annotation data - ${result.error.message}`);
|
|
65
|
+
return result.data;
|
|
66
|
+
}
|
|
67
|
+
async create(input) {
|
|
68
|
+
const annotation = {
|
|
69
|
+
id: this.generateAnnotationId(),
|
|
70
|
+
elementId: input.elementId,
|
|
71
|
+
author: input.author?.trim() || "Anonymous",
|
|
72
|
+
content: input.content,
|
|
73
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
tags: input.tags ?? [],
|
|
75
|
+
resolved: !1,
|
|
76
|
+
replies: []
|
|
77
|
+
};
|
|
78
|
+
return await ensureDir(this.annotationsDir), await atomicWrite(this.getPath(annotation.id), yaml.stringify(annotation)), annotation;
|
|
79
|
+
}
|
|
80
|
+
/** Partially updates an annotation's content, tags, and/or resolved state. Returns null if not found. */
|
|
81
|
+
async update(id, updates) {
|
|
82
|
+
const filePath = this.getPath(id);
|
|
83
|
+
return await ensureDir(this.annotationsDir), new FileLock(filePath).withLock(async () => {
|
|
84
|
+
const annotation = await this.get(id);
|
|
85
|
+
if (!annotation)
|
|
86
|
+
return null;
|
|
87
|
+
const updated = {
|
|
88
|
+
...annotation,
|
|
89
|
+
content: updates.content ?? annotation.content,
|
|
90
|
+
tags: updates.tags ?? annotation.tags,
|
|
91
|
+
resolved: updates.resolved ?? annotation.resolved,
|
|
92
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
93
|
+
};
|
|
94
|
+
return await atomicWrite(filePath, yaml.stringify(updated)), updated;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Deletes an annotation and its replies. Returns false if it didn't exist. */
|
|
98
|
+
async delete(id) {
|
|
99
|
+
const filePath = this.getPath(id);
|
|
100
|
+
return await fileExists(filePath) ? (await rm(filePath, { force: !0 }), !0) : !1;
|
|
101
|
+
}
|
|
102
|
+
/** Appends a reply to an annotation. Returns null if the annotation doesn't exist. */
|
|
103
|
+
async addReply(id, input) {
|
|
104
|
+
const filePath = this.getPath(id);
|
|
105
|
+
return await ensureDir(this.annotationsDir), new FileLock(filePath).withLock(async () => {
|
|
106
|
+
const annotation = await this.get(id);
|
|
107
|
+
if (!annotation)
|
|
108
|
+
return null;
|
|
109
|
+
const reply = {
|
|
110
|
+
id: this.generateReplyId(),
|
|
111
|
+
author: input.author,
|
|
112
|
+
content: input.content,
|
|
113
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114
|
+
};
|
|
115
|
+
return annotation.replies.push(reply), await atomicWrite(filePath, yaml.stringify(annotation)), reply;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
getPath(id) {
|
|
119
|
+
return path.join(this.annotationsDir, `${this.sanitizeId(id)}.yaml`);
|
|
120
|
+
}
|
|
121
|
+
/** Whitelists safe characters so an annotation ID can't escape annotationsDir via path traversal. */
|
|
122
|
+
sanitizeId(id) {
|
|
123
|
+
const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
124
|
+
if (!sanitized)
|
|
125
|
+
throw new Error(`Invalid annotation ID '${id}'`);
|
|
126
|
+
return sanitized;
|
|
127
|
+
}
|
|
128
|
+
generateAnnotationId() {
|
|
129
|
+
return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
130
|
+
}
|
|
131
|
+
generateReplyId() {
|
|
132
|
+
return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
AnnotationStore
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=annotation-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/annotation-store.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * AnnotationStore - filesystem-backed storage for element annotations.\n *\n * Persists annotations under documentation-robotics/annotations/{id}.yaml, one\n * file per annotation with its replies embedded. This is what gives the MCP\n * annotation_* tools (a separate stdio process per session, with no shared\n * memory with a running `dr server`) the same annotation capabilities as the\n * REST server's in-memory /api/annotations endpoints, backed by durable\n * storage that survives process restarts.\n */\n\nimport { readFile, readdir, rm } from \"fs/promises\";\nimport { randomBytes } from \"crypto\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport { z } from \"zod\";\nimport { atomicWrite, ensureDir, fileExists } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\nconst AnnotationReplySchema = z.object({\n id: z.string(),\n author: z.string(),\n content: z.string(),\n createdAt: z.string(),\n});\n\n/**\n * Runtime schema for an annotation file on disk, used to validate parsed YAML\n * before it is trusted as an `Annotation` \u2014 hand-edited files can otherwise\n * omit required fields (e.g. `tags`) and crash downstream consumers.\n */\nconst AnnotationSchema = z.object({\n id: z.string(),\n elementId: z.string(),\n author: z.string(),\n content: z.string(),\n createdAt: z.string(),\n updatedAt: z.string().optional(),\n tags: z.array(z.string()),\n resolved: z.boolean(),\n replies: z.array(AnnotationReplySchema),\n});\n\nexport type AnnotationReply = z.infer<typeof AnnotationReplySchema>;\n\nexport type Annotation = z.infer<typeof AnnotationSchema>;\n\nexport interface AnnotationCreateInput {\n elementId: string;\n author?: string;\n content: string;\n tags?: string[];\n}\n\nexport interface AnnotationUpdateInput {\n content?: string;\n tags?: string[];\n resolved?: boolean;\n}\n\nexport interface AnnotationReplyCreateInput {\n author: string;\n content: string;\n}\n\nexport class AnnotationStore {\n private annotationsDir: string;\n\n constructor(rootPath: string) {\n this.annotationsDir = path.join(rootPath, \"documentation-robotics\", \"annotations\");\n }\n\n /** Lists all annotations, optionally filtered to a single element, oldest first. Skips (and warns on) individual files that fail to load rather than failing the whole list. */\n async list(elementId?: string): Promise<Annotation[]> {\n if (!(await fileExists(this.annotationsDir))) {\n return [];\n }\n\n const entries = await readdir(this.annotationsDir, { withFileTypes: true });\n const annotations: Annotation[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".yaml\")) continue;\n const id = entry.name.slice(0, -\".yaml\".length);\n\n let annotation: Annotation | null;\n try {\n annotation = await this.get(id);\n } catch (error) {\n process.stderr.write(`Warning: Skipping annotation '${id}': ${getErrorMessage(error)}\\n`);\n continue;\n }\n\n if (annotation && (!elementId || annotation.elementId === elementId)) {\n annotations.push(annotation);\n }\n }\n\n return annotations.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n async get(id: string): Promise<Annotation | null> {\n const filePath = this.getPath(id);\n if (!(await fileExists(filePath))) {\n return null;\n }\n\n let parsed: unknown;\n try {\n const content = await readFile(filePath, \"utf-8\");\n parsed = yaml.parse(content);\n } catch (error) {\n throw new Error(`Failed to load annotation '${id}': ${getErrorMessage(error)}`);\n }\n\n const result = AnnotationSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(\n `Failed to load annotation '${id}': invalid annotation data - ${result.error.message}`\n );\n }\n return result.data;\n }\n\n async create(input: AnnotationCreateInput): Promise<Annotation> {\n const annotation: Annotation = {\n id: this.generateAnnotationId(),\n elementId: input.elementId,\n author: input.author?.trim() || \"Anonymous\",\n content: input.content,\n createdAt: new Date().toISOString(),\n tags: input.tags ?? [],\n resolved: false,\n replies: [],\n };\n\n await ensureDir(this.annotationsDir);\n await atomicWrite(this.getPath(annotation.id), yaml.stringify(annotation));\n return annotation;\n }\n\n /** Partially updates an annotation's content, tags, and/or resolved state. Returns null if not found. */\n async update(id: string, updates: AnnotationUpdateInput): Promise<Annotation | null> {\n const filePath = this.getPath(id);\n await ensureDir(this.annotationsDir);\n const lock = new FileLock(filePath);\n\n return lock.withLock(async () => {\n const annotation = await this.get(id);\n if (!annotation) return null;\n\n const updated: Annotation = {\n ...annotation,\n content: updates.content ?? annotation.content,\n tags: updates.tags ?? annotation.tags,\n resolved: updates.resolved ?? annotation.resolved,\n updatedAt: new Date().toISOString(),\n };\n\n await atomicWrite(filePath, yaml.stringify(updated));\n return updated;\n });\n }\n\n /** Deletes an annotation and its replies. Returns false if it didn't exist. */\n async delete(id: string): Promise<boolean> {\n const filePath = this.getPath(id);\n if (!(await fileExists(filePath))) {\n return false;\n }\n await rm(filePath, { force: true });\n return true;\n }\n\n /** Appends a reply to an annotation. Returns null if the annotation doesn't exist. */\n async addReply(id: string, input: AnnotationReplyCreateInput): Promise<AnnotationReply | null> {\n const filePath = this.getPath(id);\n await ensureDir(this.annotationsDir);\n const lock = new FileLock(filePath);\n\n return lock.withLock(async () => {\n const annotation = await this.get(id);\n if (!annotation) return null;\n\n const reply: AnnotationReply = {\n id: this.generateReplyId(),\n author: input.author,\n content: input.content,\n createdAt: new Date().toISOString(),\n };\n\n annotation.replies.push(reply);\n await atomicWrite(filePath, yaml.stringify(annotation));\n return reply;\n });\n }\n\n private getPath(id: string): string {\n return path.join(this.annotationsDir, `${this.sanitizeId(id)}.yaml`);\n }\n\n /** Whitelists safe characters so an annotation ID can't escape annotationsDir via path traversal. */\n private sanitizeId(id: string): string {\n const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized) {\n throw new Error(`Invalid annotation ID '${id}'`);\n }\n return sanitized;\n }\n\n private generateAnnotationId(): string {\n return `ann-${Date.now()}-${randomBytes(4).toString(\"hex\")}`;\n }\n\n private generateReplyId(): string {\n return `reply-${Date.now()}-${randomBytes(4).toString(\"hex\")}`;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,UAAU,SAAS,UAAU;AACtC,SAAS,mBAAmB;AAC5B,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,SAAS;AAClB,SAAS,aAAa,WAAW,kBAAkB;AACnD,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAEhC,MAAM,wBAAwB,EAAE,OAAO;EACrC,IAAI,EAAE,OAAM;EACZ,QAAQ,EAAE,OAAM;EAChB,SAAS,EAAE,OAAM;EACjB,WAAW,EAAE,OAAM;CACpB,GAOK,mBAAmB,EAAE,OAAO;EAChC,IAAI,EAAE,OAAM;EACZ,WAAW,EAAE,OAAM;EACnB,QAAQ,EAAE,OAAM;EAChB,SAAS,EAAE,OAAM;EACjB,WAAW,EAAE,OAAM;EACnB,WAAW,EAAE,OAAM,EAAG,SAAQ;EAC9B,MAAM,EAAE,MAAM,EAAE,OAAM,CAAE;EACxB,UAAU,EAAE,QAAO;EACnB,SAAS,EAAE,MAAM,qBAAqB;CACvC;AAwBK,MAAO,gBAAe;EAClB;EAER,YAAY,UAAgB;AAC1B,SAAK,iBAAiB,KAAK,KAAK,UAAU,0BAA0B,aAAa;EACnF;;EAGA,MAAM,KAAK,WAAkB;AAC3B,QAAI,CAAE,MAAM,WAAW,KAAK,cAAc;AACxC,aAAO,CAAA;AAGT,UAAM,UAAU,MAAM,QAAQ,KAAK,gBAAgB,EAAE,eAAe,GAAI,CAAE,GACpE,cAA4B,CAAA;AAElC,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,OAAM,KAAM,CAAC,MAAM,KAAK,SAAS,OAAO;AAAG;AACtD,YAAM,KAAK,MAAM,KAAK,MAAM,GAAG,EAAe;AAE9C,UAAI;AACJ,UAAI;AACF,qBAAa,MAAM,KAAK,IAAI,EAAE;MAChC,SAAS,OAAO;AACd,gBAAQ,OAAO,MAAM,iCAAiC,EAAE,MAAM,gBAAgB,KAAK,CAAC;CAAI;AACxF;MACF;AAEA,MAAI,eAAe,CAAC,aAAa,WAAW,cAAc,cACxD,YAAY,KAAK,UAAU;IAE/B;AAEA,WAAO,YAAY,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;EAC1E;EAEA,MAAM,IAAI,IAAU;AAClB,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,QAAI,CAAE,MAAM,WAAW,QAAQ;AAC7B,aAAO;AAGT,QAAI;AACJ,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,eAAS,KAAK,MAAM,OAAO;IAC7B,SAAS,OAAO;AACd,YAAM,IAAI,MAAM,8BAA8B,EAAE,MAAM,gBAAgB,KAAK,CAAC,EAAE;IAChF;AAEA,UAAM,SAAS,iBAAiB,UAAU,MAAM;AAChD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MACR,8BAA8B,EAAE,gCAAgC,OAAO,MAAM,OAAO,EAAE;AAG1F,WAAO,OAAO;EAChB;EAEA,MAAM,OAAO,OAA4B;AACvC,UAAM,aAAyB;MAC7B,IAAI,KAAK,qBAAoB;MAC7B,WAAW,MAAM;MACjB,QAAQ,MAAM,QAAQ,KAAI,KAAM;MAChC,SAAS,MAAM;MACf,YAAW,oBAAI,KAAI,GAAG,YAAW;MACjC,MAAM,MAAM,QAAQ,CAAA;MACpB,UAAU;MACV,SAAS,CAAA;;AAGX,iBAAM,UAAU,KAAK,cAAc,GACnC,MAAM,YAAY,KAAK,QAAQ,WAAW,EAAE,GAAG,KAAK,UAAU,UAAU,CAAC,GAClE;EACT;;EAGA,MAAM,OAAO,IAAY,SAA8B;AACrD,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,iBAAM,UAAU,KAAK,cAAc,GACtB,IAAI,SAAS,QAAQ,EAEtB,SAAS,YAAW;AAC9B,YAAM,aAAa,MAAM,KAAK,IAAI,EAAE;AACpC,UAAI,CAAC;AAAY,eAAO;AAExB,YAAM,UAAsB;QAC1B,GAAG;QACH,SAAS,QAAQ,WAAW,WAAW;QACvC,MAAM,QAAQ,QAAQ,WAAW;QACjC,UAAU,QAAQ,YAAY,WAAW;QACzC,YAAW,oBAAI,KAAI,GAAG,YAAW;;AAGnC,mBAAM,YAAY,UAAU,KAAK,UAAU,OAAO,CAAC,GAC5C;IACT,CAAC;EACH;;EAGA,MAAM,OAAO,IAAU;AACrB,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,WAAM,MAAM,WAAW,QAAQ,KAG/B,MAAM,GAAG,UAAU,EAAE,OAAO,GAAI,CAAE,GAC3B,MAHE;EAIX;;EAGA,MAAM,SAAS,IAAY,OAAiC;AAC1D,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,iBAAM,UAAU,KAAK,cAAc,GACtB,IAAI,SAAS,QAAQ,EAEtB,SAAS,YAAW;AAC9B,YAAM,aAAa,MAAM,KAAK,IAAI,EAAE;AACpC,UAAI,CAAC;AAAY,eAAO;AAExB,YAAM,QAAyB;QAC7B,IAAI,KAAK,gBAAe;QACxB,QAAQ,MAAM;QACd,SAAS,MAAM;QACf,YAAW,oBAAI,KAAI,GAAG,YAAW;;AAGnC,wBAAW,QAAQ,KAAK,KAAK,GAC7B,MAAM,YAAY,UAAU,KAAK,UAAU,UAAU,CAAC,GAC/C;IACT,CAAC;EACH;EAEQ,QAAQ,IAAU;AACxB,WAAO,KAAK,KAAK,KAAK,gBAAgB,GAAG,KAAK,WAAW,EAAE,CAAC,OAAO;EACrE;;EAGQ,WAAW,IAAU;AAC3B,UAAM,YAAY,GAAG,QAAQ,mBAAmB,EAAE;AAClD,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,0BAA0B,EAAE,GAAG;AAEjD,WAAO;EACT;EAEQ,uBAAoB;AAC1B,WAAO,OAAO,KAAK,IAAG,CAAE,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;EAC5D;EAEQ,kBAAe;AACrB,WAAO,SAAS,KAAK,IAAG,CAAE,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;EAC9D;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import yaml from "yaml";
|
|
2
|
+
import { readFile, writeFile } from "../utils/file-io.js";
|
|
3
|
+
function validateFarmProject(project, projectKey) {
|
|
4
|
+
if (!project || typeof project != "object")
|
|
5
|
+
throw new Error(`Invalid project entry "${projectKey}": expected object, got ${typeof project}`);
|
|
6
|
+
if (typeof project.name != "string" || project.name === "")
|
|
7
|
+
throw new Error(`Project "${projectKey}" is missing required 'name' field. Each project must have name, source, and model fields.`);
|
|
8
|
+
if (typeof project.source != "string" || project.source === "")
|
|
9
|
+
throw new Error(`Project "${projectKey}" is missing required 'source' field. Each project must have name, source, and model fields.`);
|
|
10
|
+
if (typeof project.model != "string" || project.model === "")
|
|
11
|
+
throw new Error(`Project "${projectKey}" is missing required 'model' field. Each project must have name, source, and model fields.`);
|
|
12
|
+
if (project.name !== projectKey)
|
|
13
|
+
throw new Error(`Project name mismatch: key is "${projectKey}" but project.name is "${project.name}". They must match for consistent sync state tracking.`);
|
|
14
|
+
return project;
|
|
15
|
+
}
|
|
16
|
+
class FarmManifest {
|
|
17
|
+
schema;
|
|
18
|
+
name;
|
|
19
|
+
created;
|
|
20
|
+
modified;
|
|
21
|
+
projects;
|
|
22
|
+
platform_view;
|
|
23
|
+
sync;
|
|
24
|
+
filePath;
|
|
25
|
+
constructor(data) {
|
|
26
|
+
this.schema = data.schema, this.name = data.name, this.created = data.created ?? (/* @__PURE__ */ new Date()).toISOString(), this.modified = data.modified ?? (/* @__PURE__ */ new Date()).toISOString(), this.projects = new Map(Object.entries(data.projects || {})), this.platform_view = data.platform_view, this.sync = data.sync;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Load farm manifest from file
|
|
30
|
+
* @param filePath - Path to farm.yaml
|
|
31
|
+
* @returns FarmManifest instance
|
|
32
|
+
* @throws Error if manifest is malformed or projects lack required fields
|
|
33
|
+
*/
|
|
34
|
+
static async load(filePath) {
|
|
35
|
+
const content = await readFile(filePath), data = yaml.parse(content);
|
|
36
|
+
if (!data)
|
|
37
|
+
throw new Error(`Invalid farm manifest: file is empty or contains invalid YAML at ${filePath}`);
|
|
38
|
+
if (!data.schema)
|
|
39
|
+
throw new Error("Farm manifest must have a 'schema' field");
|
|
40
|
+
if (!data.name)
|
|
41
|
+
throw new Error("Farm manifest must have a 'name' field");
|
|
42
|
+
if (!data.projects)
|
|
43
|
+
data.projects = {};
|
|
44
|
+
else
|
|
45
|
+
for (const [key, project] of Object.entries(data.projects))
|
|
46
|
+
data.projects[key] = validateFarmProject(project, key);
|
|
47
|
+
const manifest = new FarmManifest(data);
|
|
48
|
+
return manifest.filePath = filePath, manifest;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a new farm manifest
|
|
52
|
+
* @param name - Farm name
|
|
53
|
+
* @param options - Optional additional properties
|
|
54
|
+
* @returns FarmManifest instance
|
|
55
|
+
*/
|
|
56
|
+
static create(name, options = {}) {
|
|
57
|
+
return new FarmManifest({
|
|
58
|
+
schema: "dr-farm-v1",
|
|
59
|
+
name,
|
|
60
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
|
+
modified: (/* @__PURE__ */ new Date()).toISOString(),
|
|
62
|
+
projects: {},
|
|
63
|
+
platform_view: options.platform_view,
|
|
64
|
+
sync: options.sync
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Save manifest to file
|
|
69
|
+
* @param filePath - Path where to save farm.yaml
|
|
70
|
+
*/
|
|
71
|
+
async save(filePath) {
|
|
72
|
+
const targetPath = filePath || this.filePath;
|
|
73
|
+
if (!targetPath)
|
|
74
|
+
throw new Error("No file path specified for saving farm manifest");
|
|
75
|
+
this.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
76
|
+
const data = this.toJSON(), yamlContent = yaml.stringify(data, { lineWidth: 0 });
|
|
77
|
+
await writeFile(targetPath, yamlContent), this.filePath = targetPath;
|
|
78
|
+
}
|
|
79
|
+
// Add a project to the farm; ensures name parameter matches project.name
|
|
80
|
+
addProject(name, project) {
|
|
81
|
+
if (project.name !== name)
|
|
82
|
+
throw new Error(`Project name mismatch: key is "${name}" but project.name is "${project.name}". They must match for consistent sync state tracking. Downstream code reads project.name for changeset names and sync state paths.`);
|
|
83
|
+
this.projects.set(name, project), this.modified = (/* @__PURE__ */ new Date()).toISOString();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Remove a project from the farm
|
|
87
|
+
* @param name - Project name to remove
|
|
88
|
+
* @returns true if project was removed, false if not found
|
|
89
|
+
*/
|
|
90
|
+
removeProject(name) {
|
|
91
|
+
const existed = this.projects.has(name);
|
|
92
|
+
return this.projects.delete(name), existed && (this.modified = (/* @__PURE__ */ new Date()).toISOString()), existed;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get a project by name
|
|
96
|
+
* @param name - Project name
|
|
97
|
+
* @returns Project configuration or undefined
|
|
98
|
+
*/
|
|
99
|
+
getProject(name) {
|
|
100
|
+
return this.projects.get(name);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Get all projects
|
|
104
|
+
* @returns Array of all projects
|
|
105
|
+
*/
|
|
106
|
+
getAllProjects() {
|
|
107
|
+
return Array.from(this.projects.values());
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if platform-view is enabled on this farm
|
|
111
|
+
* @returns true if platform-view is enabled
|
|
112
|
+
*/
|
|
113
|
+
isPlatformViewEnabled() {
|
|
114
|
+
return this.platform_view === !0;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Serialize to JSON representation for saving
|
|
118
|
+
*/
|
|
119
|
+
toJSON() {
|
|
120
|
+
return {
|
|
121
|
+
schema: this.schema,
|
|
122
|
+
name: this.name,
|
|
123
|
+
created: this.created,
|
|
124
|
+
modified: this.modified,
|
|
125
|
+
projects: Object.fromEntries(this.projects),
|
|
126
|
+
...this.platform_view !== void 0 && { platform_view: this.platform_view },
|
|
127
|
+
...this.sync && { sync: this.sync }
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export {
|
|
132
|
+
FarmManifest
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=farm-manifest.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/farm-manifest.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Farm Manifest - Manages farm.yaml configuration\n *\n * Represents a farm context containing multiple projects with detached model folders.\n * The farm manifest tracks project registrations and their model folder locations.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile } from \"../utils/file-io.js\";\n\n/**\n * Represents a project registered in a farm\n */\nexport interface FarmProject {\n name: string;\n source: string; // Path to the codebase folder (relative to farm root)\n model: string; // Path to the model folder (relative to farm root)\n remote?: string; // Optional git remote URL\n branch?: string; // Optional git branch\n}\n\n// Validate project object has required fields and key/name consistency\nfunction validateFarmProject(project: any, projectKey: string): FarmProject {\n if (!project || typeof project !== \"object\") {\n throw new Error(\n `Invalid project entry \"${projectKey}\": expected object, got ${typeof project}`\n );\n }\n\n if (typeof project.name !== \"string\" || project.name === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'name' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof project.source !== \"string\" || project.source === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'source' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (typeof project.model !== \"string\" || project.model === \"\") {\n throw new Error(\n `Project \"${projectKey}\" is missing required 'model' field. ` +\n `Each project must have name, source, and model fields.`\n );\n }\n\n if (project.name !== projectKey) {\n throw new Error(\n `Project name mismatch: key is \"${projectKey}\" but project.name is \"${project.name}\". ` +\n `They must match for consistent sync state tracking.`\n );\n }\n\n return project as FarmProject;\n}\n\n/**\n * Farm manifest data structure\n */\nexport interface FarmManifestData {\n schema: string; // Schema version (e.g., \"dr-farm-v1\")\n name: string; // Human-readable farm name\n created?: string; // ISO 8601 timestamp\n modified?: string; // ISO 8601 timestamp\n projects: Record<string, FarmProject>; // Projects indexed by name\n platform_view?: boolean; // Optional: Enable platform view mode\n sync?: {\n track_commits?: boolean; // Optional: Track commits during sync\n };\n}\n\n/**\n * FarmManifest class - Load, validate, and save farm.yaml\n */\nexport class FarmManifest {\n schema: string;\n name: string;\n created: string;\n modified: string;\n projects: Map<string, FarmProject>;\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n filePath?: string;\n\n constructor(data: FarmManifestData) {\n this.schema = data.schema;\n this.name = data.name;\n this.created = data.created ?? new Date().toISOString();\n this.modified = data.modified ?? new Date().toISOString();\n this.projects = new Map(Object.entries(data.projects || {}));\n this.platform_view = data.platform_view;\n this.sync = data.sync;\n }\n\n /**\n * Load farm manifest from file\n * @param filePath - Path to farm.yaml\n * @returns FarmManifest instance\n * @throws Error if manifest is malformed or projects lack required fields\n */\n static async load(filePath: string): Promise<FarmManifest> {\n const content = await readFile(filePath);\n const data = yaml.parse(content) as FarmManifestData;\n\n if (!data) {\n throw new Error(\n `Invalid farm manifest: file is empty or contains invalid YAML at ${filePath}`\n );\n }\n\n if (!data.schema) {\n throw new Error(\"Farm manifest must have a 'schema' field\");\n }\n\n if (!data.name) {\n throw new Error(\"Farm manifest must have a 'name' field\");\n }\n\n if (!data.projects) {\n data.projects = {};\n } else {\n // Validate each project has required fields before proceeding\n for (const [key, project] of Object.entries(data.projects)) {\n data.projects[key] = validateFarmProject(project, key);\n }\n }\n\n const manifest = new FarmManifest(data);\n manifest.filePath = filePath;\n return manifest;\n }\n\n /**\n * Create a new farm manifest\n * @param name - Farm name\n * @param options - Optional additional properties\n * @returns FarmManifest instance\n */\n static create(\n name: string,\n options: {\n platform_view?: boolean;\n sync?: {\n track_commits?: boolean;\n };\n } = {}\n ): FarmManifest {\n return new FarmManifest({\n schema: \"dr-farm-v1\",\n name,\n created: new Date().toISOString(),\n modified: new Date().toISOString(),\n projects: {},\n platform_view: options.platform_view,\n sync: options.sync,\n });\n }\n\n /**\n * Save manifest to file\n * @param filePath - Path where to save farm.yaml\n */\n async save(filePath?: string): Promise<void> {\n const targetPath = filePath || this.filePath;\n if (!targetPath) {\n throw new Error(\"No file path specified for saving farm manifest\");\n }\n\n this.modified = new Date().toISOString();\n const data = this.toJSON();\n const yamlContent = yaml.stringify(data, { lineWidth: 0 });\n\n await writeFile(targetPath, yamlContent);\n this.filePath = targetPath;\n }\n\n // Add a project to the farm; ensures name parameter matches project.name\n addProject(name: string, project: FarmProject): void {\n if (project.name !== name) {\n throw new Error(\n `Project name mismatch: key is \"${name}\" but project.name is \"${project.name}\". ` +\n `They must match for consistent sync state tracking. ` +\n `Downstream code reads project.name for changeset names and sync state paths.`\n );\n }\n\n this.projects.set(name, project);\n this.modified = new Date().toISOString();\n }\n\n /**\n * Remove a project from the farm\n * @param name - Project name to remove\n * @returns true if project was removed, false if not found\n */\n removeProject(name: string): boolean {\n const existed = this.projects.has(name);\n this.projects.delete(name);\n if (existed) {\n this.modified = new Date().toISOString();\n }\n return existed;\n }\n\n /**\n * Get a project by name\n * @param name - Project name\n * @returns Project configuration or undefined\n */\n getProject(name: string): FarmProject | undefined {\n return this.projects.get(name);\n }\n\n /**\n * Get all projects\n * @returns Array of all projects\n */\n getAllProjects(): FarmProject[] {\n return Array.from(this.projects.values());\n }\n\n /**\n * Check if platform-view is enabled on this farm\n * @returns true if platform-view is enabled\n */\n isPlatformViewEnabled(): boolean {\n return this.platform_view === true;\n }\n\n /**\n * Serialize to JSON representation for saving\n */\n toJSON(): FarmManifestData {\n return {\n schema: this.schema,\n name: this.name,\n created: this.created,\n modified: this.modified,\n projects: Object.fromEntries(this.projects),\n ...(this.platform_view !== undefined && { platform_view: this.platform_view }),\n ...(this.sync && { sync: this.sync }),\n };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|