@documentation-robotics/cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/README.md +10 -14
  2. package/dist/analyzers/active-analyzer.js +62 -0
  3. package/dist/analyzers/active-analyzer.js.map +7 -0
  4. package/dist/analyzers/cards/build.js +21 -1
  5. package/dist/analyzers/cards/build.js.map +2 -2
  6. package/dist/analyzers/cards/extract.js +138 -0
  7. package/dist/analyzers/cards/extract.js.map +7 -0
  8. package/dist/analyzers/cards/routes.js +96 -0
  9. package/dist/analyzers/cards/routes.js.map +7 -0
  10. package/dist/analyzers/cbm-analyzer.js +76 -4
  11. package/dist/analyzers/cbm-analyzer.js.map +2 -2
  12. package/dist/analyzers/cbm-responses.js +12 -6
  13. package/dist/analyzers/cbm-responses.js.map +2 -2
  14. package/dist/analyzers/code-changes.js +159 -0
  15. package/dist/analyzers/code-changes.js.map +7 -0
  16. package/dist/analyzers/edge-relationships.js +6 -2
  17. package/dist/analyzers/edge-relationships.js.map +2 -2
  18. package/dist/analyzers/extraction-plan.js +541 -0
  19. package/dist/analyzers/extraction-plan.js.map +7 -0
  20. package/dist/analyzers/index-job.js +229 -0
  21. package/dist/analyzers/index-job.js.map +7 -0
  22. package/dist/cli.js +12 -10
  23. package/dist/cli.js.map +2 -2
  24. package/dist/coding-agents/drbot-mcp.js +120 -18
  25. package/dist/coding-agents/drbot-mcp.js.map +2 -2
  26. package/dist/commands/add.js +4 -2
  27. package/dist/commands/add.js.map +2 -2
  28. package/dist/commands/analyzer.js +35 -35
  29. package/dist/commands/analyzer.js.map +2 -2
  30. package/dist/commands/changeset-verify.js +16 -8
  31. package/dist/commands/changeset-verify.js.map +2 -2
  32. package/dist/commands/changeset.js +41 -11
  33. package/dist/commands/changeset.js.map +2 -2
  34. package/dist/commands/init.js +9 -26
  35. package/dist/commands/init.js.map +2 -2
  36. package/dist/commands/mcp.js +35 -31
  37. package/dist/commands/mcp.js.map +3 -3
  38. package/dist/commands/relationship.js +2 -12
  39. package/dist/commands/relationship.js.map +2 -2
  40. package/dist/commands/reports.js +3 -3
  41. package/dist/commands/reports.js.map +2 -2
  42. package/dist/commands/update.js +14 -13
  43. package/dist/commands/update.js.map +2 -2
  44. package/dist/commands/upgrade.js +28 -120
  45. package/dist/commands/upgrade.js.map +2 -2
  46. package/dist/commands/validate.js +19 -3
  47. package/dist/commands/validate.js.map +2 -2
  48. package/dist/commands/version.js +1 -1
  49. package/dist/commands/visualize.js +30 -12
  50. package/dist/commands/visualize.js.map +2 -2
  51. package/dist/core/annotation-store.js +15 -5
  52. package/dist/core/annotation-store.js.map +2 -2
  53. package/dist/core/catalog-migration.js +2 -1
  54. package/dist/core/catalog-migration.js.map +2 -2
  55. package/dist/core/changeset-base-index.js +7 -0
  56. package/dist/core/changeset-base-index.js.map +2 -2
  57. package/dist/core/changeset-brief-compact.js +256 -0
  58. package/dist/core/changeset-brief-compact.js.map +7 -0
  59. package/dist/core/changeset-verify.js +356 -26
  60. package/dist/core/changeset-verify.js.map +3 -3
  61. package/dist/core/extraction-observability.js +161 -0
  62. package/dist/core/extraction-observability.js.map +7 -0
  63. package/dist/core/migration-registry.js +13 -1
  64. package/dist/core/migration-registry.js.map +2 -2
  65. package/dist/core/project-init.js +53 -0
  66. package/dist/core/project-init.js.map +7 -0
  67. package/dist/core/relationship-staging.js +94 -1
  68. package/dist/core/relationship-staging.js.map +2 -2
  69. package/dist/core/staged-changeset-storage.js +83 -3
  70. package/dist/core/staged-changeset-storage.js.map +2 -2
  71. package/dist/core/staging-area.js +85 -8
  72. package/dist/core/staging-area.js.map +2 -2
  73. package/dist/core/upgrade-plan.js +117 -0
  74. package/dist/core/upgrade-plan.js.map +7 -0
  75. package/dist/generated/relationship-index.js +124 -15
  76. package/dist/generated/relationship-index.js.map +2 -2
  77. package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
  78. package/dist/integrations/claude_code/agents/dr-architect.md +19 -5
  79. package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
  80. package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
  81. package/dist/integrations/claude_code/commands/dr-map.md +7 -1
  82. package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
  83. package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
  84. package/dist/integrations/claude_code/docs/README.md +20 -1
  85. package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
  86. package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
  87. package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +3 -0
  88. package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
  89. package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
  90. package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
  91. package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
  92. package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
  93. package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
  94. package/dist/mcp/changeset-only.js +14 -3
  95. package/dist/mcp/changeset-only.js.map +2 -2
  96. package/dist/mcp/project-root.js +85 -0
  97. package/dist/mcp/project-root.js.map +7 -0
  98. package/dist/mcp/server-instructions.js +27 -0
  99. package/dist/mcp/server-instructions.js.map +7 -0
  100. package/dist/mcp/strict-schema.js +52 -0
  101. package/dist/mcp/strict-schema.js.map +7 -0
  102. package/dist/mcp/tool-registry.js +39 -5
  103. package/dist/mcp/tool-registry.js.map +2 -2
  104. package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
  105. package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
  106. package/dist/mcp/tools/changeset-commit.js +224 -0
  107. package/dist/mcp/tools/changeset-commit.js.map +7 -0
  108. package/dist/mcp/tools/changeset-editing.js +133 -57
  109. package/dist/mcp/tools/changeset-editing.js.map +3 -3
  110. package/dist/mcp/tools/changeset-lifecycle.js +1 -1
  111. package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
  112. package/dist/mcp/tools/changeset-verify.js +12 -6
  113. package/dist/mcp/tools/changeset-verify.js.map +2 -2
  114. package/dist/mcp/tools/codebase-extract.js +836 -0
  115. package/dist/mcp/tools/codebase-extract.js.map +7 -0
  116. package/dist/mcp/tools/codebase-index.js +125 -0
  117. package/dist/mcp/tools/codebase-index.js.map +7 -0
  118. package/dist/mcp/tools/codebase-shared.js +265 -0
  119. package/dist/mcp/tools/codebase-shared.js.map +7 -0
  120. package/dist/mcp/tools/codebase-verify.js +48 -0
  121. package/dist/mcp/tools/codebase-verify.js.map +7 -0
  122. package/dist/mcp/tools/index.js +15 -0
  123. package/dist/mcp/tools/index.js.map +2 -2
  124. package/dist/mcp/tools/model-add.js +189 -73
  125. package/dist/mcp/tools/model-add.js.map +2 -2
  126. package/dist/mcp/tools/model-update.js +16 -12
  127. package/dist/mcp/tools/model-update.js.map +2 -2
  128. package/dist/mcp/tools/model-validate.js +46 -4
  129. package/dist/mcp/tools/model-validate.js.map +2 -2
  130. package/dist/mcp/tools/project-status-collectors.js +381 -0
  131. package/dist/mcp/tools/project-status-collectors.js.map +7 -0
  132. package/dist/mcp/tools/project-status.js +237 -0
  133. package/dist/mcp/tools/project-status.js.map +7 -0
  134. package/dist/mcp/tools/project-tools.js +505 -0
  135. package/dist/mcp/tools/project-tools.js.map +7 -0
  136. package/dist/mcp/tools/relationship-tools.js +89 -20
  137. package/dist/mcp/tools/relationship-tools.js.map +2 -2
  138. package/dist/mcp/tools/review-open.js +210 -0
  139. package/dist/mcp/tools/review-open.js.map +7 -0
  140. package/dist/mcp/tools/shared.js +10 -2
  141. package/dist/mcp/tools/shared.js.map +2 -2
  142. package/dist/reports/model-report-orchestrator.js +31 -2
  143. package/dist/reports/model-report-orchestrator.js.map +2 -2
  144. package/dist/schemas/bundled/analyzers/cbm.json +22 -10
  145. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  146. package/dist/schemas/bundled/api.json +1 -1
  147. package/dist/schemas/bundled/apm.json +1 -1
  148. package/dist/schemas/bundled/application.json +1 -1
  149. package/dist/schemas/bundled/base.json +1 -1
  150. package/dist/schemas/bundled/business.json +1 -1
  151. package/dist/schemas/bundled/data-model.json +1 -1
  152. package/dist/schemas/bundled/data-store.json +1 -1
  153. package/dist/schemas/bundled/manifest.json +3 -3
  154. package/dist/schemas/bundled/motivation.json +1 -1
  155. package/dist/schemas/bundled/navigation.json +1 -1
  156. package/dist/schemas/bundled/product.json +1 -1
  157. package/dist/schemas/bundled/security.json +1 -1
  158. package/dist/schemas/bundled/technology.json +1 -1
  159. package/dist/schemas/bundled/testing.json +145 -1
  160. package/dist/schemas/bundled/ux.json +1 -1
  161. package/dist/server/server.js +508 -237
  162. package/dist/server/server.js.map +3 -3
  163. package/dist/server/viewer-registry.js +264 -0
  164. package/dist/server/viewer-registry.js.map +7 -0
  165. package/dist/telemetry/index.js +1 -1
  166. package/dist/utils/id-generator.js +5 -2
  167. package/dist/utils/id-generator.js.map +2 -2
  168. package/dist/utils/source-reference.js +58 -27
  169. package/dist/utils/source-reference.js.map +2 -2
  170. package/dist/utils/spec-version.js +2 -2
  171. package/dist/utils/spec-version.js.map +1 -1
  172. package/dist/validators/model-warnings.js +28 -0
  173. package/dist/validators/model-warnings.js.map +7 -0
  174. package/dist/viewer/README.md +6 -6
  175. package/dist/viewer/assets/main-CUOQGGRA.css +1 -0
  176. package/dist/viewer/assets/main-DvH3gYrI.js +84 -0
  177. package/dist/viewer/index.html +2 -2
  178. package/dist/viewer/manifest.json +10 -10
  179. package/dist/viewer/version.json +1 -1
  180. package/package.json +1 -1
  181. package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
  182. package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
@@ -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\";\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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",
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\";\nimport { CANONICAL_LAYER_NAMES } from \"../core/layers.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\n/**\n * The --layers values as layer names: commas separate them as well as spaces, so\n * `--layers api,application` means two layers. A name that isn't a layer is an error, since\n * validating no layer would otherwise report success.\n */\nexport function parseLayersOption(layers: string[] | undefined): string[] | undefined {\n if (!layers) return undefined;\n const names = layers.flatMap((value) => value.split(\",\")).map((name) => name.trim()).filter(Boolean);\n const known = new Set<string>(CANONICAL_LAYER_NAMES);\n const unknown = names.filter((name) => !known.has(name));\n if (unknown.length > 0) {\n throw new Error(\n `Unknown layer${unknown.length > 1 ? \"s\" : \"\"}: ${unknown.map((n) => `'${n}'`).join(\", \")}. ` +\n `Layers are: ${CANONICAL_LAYER_NAMES.join(\", \")}.`\n );\n }\n return names;\n}\n\n/**\n * Keeps only the requested layers. Projecting the active changeset adds the layers its staged\n * changes touch, so `--layers api` would otherwise validate those too.\n */\nexport function keepRequestedLayers(model: { layers: Map<string, unknown> }, layers: string[] | undefined): void {\n if (!layers || layers.length === 0) return;\n for (const name of [...model.layers.keys()]) {\n if (!layers.includes(name)) model.layers.delete(name);\n }\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 options.layers = parseLayersOption(options.layers);\n\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 keepRequestedLayers(modelForOrphans, options.layers);\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 keepRequestedLayers(modelToValidate, options.layers);\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
  }
@@ -2,7 +2,7 @@ import ansis from "ansis";
2
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 = "47d9e70", cliVersion = getCliVersion();
5
+ const gitHash = "ca80e41", cliVersion = getCliVersion();
6
6
  async function versionCommand() {
7
7
  const specVersion = getCliBundledSpecVersion(), telemetryBuiltIn = isTelemetryBuiltIn(), telemetryConfigured = await isTelemetryConfigured();
8
8
  let telemetryStatus;
@@ -7,16 +7,21 @@ import { logVerbose, logDebug } from "../utils/globals.js";
7
7
  import { CLIError } from "../utils/errors.js";
8
8
  import { getSpecReferencePath, getModelPath } from "../utils/project-paths.js";
9
9
  import { getErrorMessage } from "../utils/errors.js";
10
- const isTelemetryEnabled = !1;
11
- async function findAvailablePort(startPort) {
10
+ import { removeViewerRecord, removeViewerRecordSync, writeViewerRecord } from "../server/viewer-registry.js";
11
+ const isTelemetryEnabled = !1, PROBE_HOSTS = ["127.0.0.1", "::1", "0.0.0.0", "::"], NO_ADDRESS_CODES = /* @__PURE__ */ new Set(["EADDRNOTAVAIL", "EAFNOSUPPORT"]);
12
+ async function isPortFree(port) {
12
13
  const { createServer } = await import("net");
13
- for (let port = startPort; port <= 65535; port++)
14
+ for (const host of PROBE_HOSTS)
14
15
  if (await new Promise((resolve) => {
15
16
  const server = createServer();
16
- server.once("error", () => resolve(!1)), server.once("listening", () => {
17
- server.close(), resolve(!0);
18
- }), server.listen(port, "127.0.0.1");
19
- }))
17
+ server.once("error", (error) => resolve(NO_ADDRESS_CODES.has(error.code ?? "") ? "no-address" : "taken")), server.once("listening", () => server.close(() => resolve("free"))), server.listen(port, host);
18
+ }) === "taken")
19
+ return !1;
20
+ return !0;
21
+ }
22
+ async function findAvailablePort(startPort) {
23
+ for (let port = startPort; port <= 65535; port++)
24
+ if (await isPortFree(port))
20
25
  return port;
21
26
  throw new Error("No available ports found");
22
27
  }
@@ -63,7 +68,17 @@ async function visualizeCommand(options) {
63
68
  env
64
69
  });
65
70
  let authTokenFromServer = "", serverOutput = "", serverStartupShown = !1;
66
- serverProcess.stdout && serverProcess.stdout.on("data", async (data) => {
71
+ const projectRoot = model.rootPath;
72
+ let serverListening = !1, recordWrite = null;
73
+ const recordViewer = () => recordWrite || (!serverListening || authEnabled && !authTokenFromServer ? Promise.resolve() : (recordWrite = writeViewerRecord({
74
+ pid: process.pid,
75
+ port,
76
+ token: authEnabled ? authTokenFromServer : null,
77
+ projectRoot,
78
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
79
+ withChangesets
80
+ }).then((path) => logDebug(`Viewer record written: ${path}`)).catch((error) => logVerbose(`Could not write the viewer record: ${getErrorMessage(error)}`)), recordWrite)), forgetViewer = () => removeViewerRecordSync(projectRoot, { pid: process.pid });
81
+ process.on("exit", forgetViewer), serverProcess.stdout && serverProcess.stdout.on("data", async (data) => {
67
82
  const chunk = data.toString();
68
83
  serverOutput += chunk;
69
84
  const lines = chunk.split(`
@@ -71,7 +86,7 @@ async function visualizeCommand(options) {
71
86
  for (const rawLine of lines) {
72
87
  const line = rawLine.trim();
73
88
  if (line) {
74
- if (line.startsWith("TOKEN:") && (authTokenFromServer = line.substring(6), logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`), serverStartupShown && authEnabled && authTokenFromServer && (console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)), console.log(ansis.dim(` Auth token: ${authTokenFromServer}`)), !options.noBrowser)))
89
+ if (line.startsWith("TOKEN:") && (authTokenFromServer = line.substring(6), logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`), await recordViewer(), serverStartupShown && authEnabled && authTokenFromServer && (console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)), console.log(ansis.dim(` Auth token: ${authTokenFromServer}`)), !options.noBrowser)))
75
90
  try {
76
91
  const command = getOpenCommand(), url = `http://localhost:${port}?token=${authTokenFromServer}`;
77
92
  logDebug(`Opening browser with command: ${command} ${url}`), spawn(command, [url], {
@@ -82,7 +97,7 @@ async function visualizeCommand(options) {
82
97
  logVerbose("Could not auto-open browser (not critical)");
83
98
  }
84
99
  if (line.includes("running at http://localhost")) {
85
- if (serverStartupShown = !0, isTelemetryEnabled && serverStartupSpan) {
100
+ if (serverListening = !0, await recordViewer(), serverStartupShown = !0, isTelemetryEnabled && serverStartupSpan) {
86
101
  logDebug("[Telemetry] Ending visualize.server.startup span (server started successfully)");
87
102
  const { endSpan } = await import("../telemetry/index.js");
88
103
  endSpan(serverStartupSpan), serverStartupSpan = null, logDebug("[Telemetry] Span ended");
@@ -118,7 +133,8 @@ async function visualizeCommand(options) {
118
133
  });
119
134
  let rejectKeepAlive = null;
120
135
  serverProcess.on("close", (code) => {
121
- if (code !== 0) {
136
+ if (recordWrite && recordWrite.then(() => removeViewerRecord(projectRoot, { pid: process.pid })).catch(() => {
137
+ }), code !== 0) {
122
138
  const message = serverOutput ? `Server exited with code ${code}
123
139
  Server output:
124
140
  ${serverOutput}` : `Server exited with code ${code}`;
@@ -161,7 +177,7 @@ ${serverOutput}` : `Server exited with code ${code}`;
161
177
  serverProcess.on("exit", () => {
162
178
  clearTimeout(timeout), resolve();
163
179
  });
164
- })), logDebug("Visualization server stopped"), process.exit(0);
180
+ })), logDebug("Visualization server stopped"), recordWrite && await recordWrite, forgetViewer(), process.exit(0);
165
181
  };
166
182
  process.on("SIGINT", () => handleShutdown("SIGINT")), process.on("SIGTERM", () => handleShutdown("SIGTERM")), await new Promise((_, reject) => {
167
183
  rejectKeepAlive = reject;
@@ -178,6 +194,8 @@ function getOpenCommand() {
178
194
  return platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
179
195
  }
180
196
  export {
197
+ findAvailablePort,
198
+ isPortFree,
181
199
  visualizeCommand
182
200
  };
183
201
  //# sourceMappingURL=visualize.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/commands/visualize.ts"],
4
- "sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n withChangesets?: boolean\n viewerPath?: string\n}\n\nasync function findAvailablePort(startPort: number): Promise<number> {\n const { createServer } = await import(\"net\")\n for (let port = startPort; port <= 65535; port++) {\n const available = await new Promise<boolean>((resolve) => {\n const server = createServer()\n server.once(\"error\", () => resolve(false))\n server.once(\"listening\", () => { server.close(); resolve(true) })\n server.listen(port, \"127.0.0.1\")\n })\n if (available) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n // Changeset-only writes; danger mode already allows everything, so it wins\n const withChangesets = (options.withChangesets || false) && !withDanger\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Build environment variables for the server subprocess\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n DR_WITH_CHANGESETS: String(withChangesets),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n // Reuse whichever runtime is currently executing the CLI (node or bun) to launch\n // the server subprocess. server-entry.ts has no Bun-specific dependencies (see #800),\n // so this works identically under Node.js or Bun \u2014 and unlike spawning a literal\n // \"bun\" binary, process.execPath is guaranteed to exist since it's already running.\n const serverProcess = spawn(process.execPath, [serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: stdio pipe buffering can batch multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event,\n // regardless of runtime.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n if (options.withChangesets) {\n console.log(ansis.dim(` (--with-changesets ignored: danger mode already allows all writes)`))\n }\n } else if (withChangesets) {\n console.log(\n ansis.yellow(` \u26A0 Changeset mode enabled - chat can stage model changes in a changeset (no commit/apply/delete)`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n // The server subprocess is launched via process.execPath (the same node/bun\n // binary already running this CLI), so it's always present - ENOENT here would\n // indicate something unusual about the runtime environment rather than a missing\n // dependency (see #800: this used to hard-depend on a separately installed `bun`).\n if (errorCode === \"EACCES\") {\n message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`\n suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\nimport { removeViewerRecord, removeViewerRecordSync, writeViewerRecord } from \"../server/viewer-registry.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n withChangesets?: boolean\n viewerPath?: string\n}\n\n/**\n * Addresses a port must be free on before the viewer takes it. The server binds the loopback\n * addresses; the wildcards are checked too, because on macOS a loopback bind succeeds while\n * another process holds `*:<port>`, and the two would then share localhost traffic.\n */\nconst PROBE_HOSTS = [\"127.0.0.1\", \"::1\", \"0.0.0.0\", \"::\"] as const\n\n/** Errors that mean \"this machine has no such address\" (no IPv6), not \"the port is taken\". */\nconst NO_ADDRESS_CODES = new Set([\"EADDRNOTAVAIL\", \"EAFNOSUPPORT\"])\n\n/** Whether nothing listens on the port on any loopback or wildcard address. */\nexport async function isPortFree(port: number): Promise<boolean> {\n const { createServer } = await import(\"net\")\n for (const host of PROBE_HOSTS) {\n const result = await new Promise<\"free\" | \"taken\" | \"no-address\">((resolve) => {\n const server = createServer()\n server.once(\"error\", (error: NodeJS.ErrnoException) =>\n resolve(NO_ADDRESS_CODES.has(error.code ?? \"\") ? \"no-address\" : \"taken\")\n )\n server.once(\"listening\", () => server.close(() => resolve(\"free\")))\n server.listen(port, host)\n })\n if (result === \"taken\") return false\n }\n return true\n}\n\n/** The first port from startPort that is free on every address isPortFree checks. */\nexport async function findAvailablePort(startPort: number): Promise<number> {\n for (let port = startPort; port <= 65535; port++) {\n if (await isPortFree(port)) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n // Changeset-only writes; danger mode already allows everything, so it wins\n const withChangesets = (options.withChangesets || false) && !withDanger\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Build environment variables for the server subprocess\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n DR_WITH_CHANGESETS: String(withChangesets),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n // Reuse whichever runtime is currently executing the CLI (node or bun) to launch\n // the server subprocess. server-entry.ts has no Bun-specific dependencies (see #800),\n // so this works identically under Node.js or Bun \u2014 and unlike spawning a literal\n // \"bun\" binary, process.execPath is guaranteed to exist since it's already running.\n const serverProcess = spawn(process.execPath, [serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Record this viewer (port, token) in the user's state directory once it is listening, so\n // tools like review_open can reuse it; removed again on exit.\n const projectRoot = model.rootPath\n // Written (atomically) once the server is listening and the token is known, and awaited before\n // the startup lines are printed; shutdown waits for a write in flight, so no half-written,\n // token-bearing temp file is left behind.\n let serverListening = false\n let recordWrite: Promise<void> | null = null\n const recordViewer = (): Promise<void> => {\n if (recordWrite) return recordWrite\n if (!serverListening || (authEnabled && !authTokenFromServer)) return Promise.resolve()\n recordWrite = writeViewerRecord({\n pid: process.pid,\n port,\n token: authEnabled ? authTokenFromServer : null,\n projectRoot,\n startedAt: new Date().toISOString(),\n withChangesets,\n })\n .then((path) => logDebug(`Viewer record written: ${path}`))\n .catch((error) => logVerbose(`Could not write the viewer record: ${getErrorMessage(error)}`))\n return recordWrite\n }\n // Removes this process's record and any temp file it left; another viewer's record is kept.\n const forgetViewer = () => removeViewerRecordSync(projectRoot, { pid: process.pid })\n process.on(\"exit\", forgetViewer)\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: stdio pipe buffering can batch multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event,\n // regardless of runtime.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n await recordViewer()\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverListening = true\n await recordViewer()\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n if (options.withChangesets) {\n console.log(ansis.dim(` (--with-changesets ignored: danger mode already allows all writes)`))\n }\n } else if (withChangesets) {\n console.log(\n ansis.yellow(` \u26A0 Changeset mode enabled - chat can stage model changes in a changeset (no commit/apply/delete)`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n // The server is gone, whatever the code: this viewer no longer answers.\n if (recordWrite) {\n recordWrite.then(() => removeViewerRecord(projectRoot, { pid: process.pid })).catch(() => {})\n }\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n // The server subprocess is launched via process.execPath (the same node/bun\n // binary already running this CLI), so it's always present - ENOENT here would\n // indicate something unusual about the runtime environment rather than a missing\n // dependency (see #800: this used to hard-depend on a separately installed `bun`).\n if (errorCode === \"EACCES\") {\n message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`\n suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n // Let a record write in flight finish, so its temp file is renamed and then removed.\n if (recordWrite) await recordWrite\n forgetViewer()\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -22,6 +22,13 @@ const AnnotationReplySchema = z.object({
22
22
  resolved: z.boolean(),
23
23
  replies: z.array(AnnotationReplySchema)
24
24
  });
25
+ let lastCreatedAt = 0;
26
+ function nextCreatedAt() {
27
+ return lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1), new Date(lastCreatedAt).toISOString();
28
+ }
29
+ function notBefore(iso) {
30
+ return new Date(Math.max(Date.now(), Date.parse(iso) || 0)).toISOString();
31
+ }
25
32
  class AnnotationStore {
26
33
  annotationsDir;
27
34
  constructor(rootPath) {
@@ -70,7 +77,7 @@ class AnnotationStore {
70
77
  elementId: input.elementId,
71
78
  author: input.author?.trim() || "Anonymous",
72
79
  content: input.content,
73
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
80
+ createdAt: nextCreatedAt(),
74
81
  tags: input.tags ?? [],
75
82
  resolved: !1,
76
83
  replies: []
@@ -89,15 +96,18 @@ class AnnotationStore {
89
96
  content: updates.content ?? annotation.content,
90
97
  tags: updates.tags ?? annotation.tags,
91
98
  resolved: updates.resolved ?? annotation.resolved,
92
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
99
+ updatedAt: notBefore(annotation.createdAt)
93
100
  };
94
101
  return await atomicWrite(filePath, yaml.stringify(updated)), updated;
95
102
  });
96
103
  }
97
- /** Deletes an annotation and its replies. Returns false if it didn't exist. */
104
+ /**
105
+ * Deletes an annotation and its replies. Returns false if it didn't exist. Holds the same lock
106
+ * as update and addReply, so a write in flight can't bring the annotation back.
107
+ */
98
108
  async delete(id) {
99
109
  const filePath = this.getPath(id);
100
- return await fileExists(filePath) ? (await rm(filePath, { force: !0 }), !0) : !1;
110
+ return await fileExists(filePath) ? new FileLock(filePath).withLock(async () => await fileExists(filePath) ? (await rm(filePath, { force: !0 }), !0) : !1) : !1;
101
111
  }
102
112
  /** Appends a reply to an annotation. Returns null if the annotation doesn't exist. */
103
113
  async addReply(id, input) {
@@ -110,7 +120,7 @@ class AnnotationStore {
110
120
  id: this.generateReplyId(),
111
121
  author: input.author,
112
122
  content: input.content,
113
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
123
+ createdAt: notBefore(annotation.createdAt)
114
124
  };
115
125
  return annotation.replies.push(reply), await atomicWrite(filePath, yaml.stringify(annotation)), reply;
116
126
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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": "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",
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. It is the one annotation store:\n * the viewer server's /api/annotations routes and the MCP annotation_* tools (a\n * separate process per session) both read and write it, so each sees the other's\n * annotations and replies, and they survive restarts. The viewer server watches\n * the directory and broadcasts changes other processes make.\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\nlet lastCreatedAt = 0;\n\n/**\n * A creation time that never repeats within this process. Annotations list oldest first by\n * createdAt, so two made in the same millisecond (a viewer comment and its follow-up, say) would\n * otherwise come back in directory order.\n */\nfunction nextCreatedAt(): string {\n lastCreatedAt = Math.max(Date.now(), lastCreatedAt + 1);\n return new Date(lastCreatedAt).toISOString();\n}\n\n/** Now, or the given time if the clock is behind it (nextCreatedAt can run ahead in a burst). */\nfunction notBefore(iso: string): string {\n return new Date(Math.max(Date.now(), Date.parse(iso) || 0)).toISOString();\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: nextCreatedAt(),\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: notBefore(annotation.createdAt),\n };\n\n await atomicWrite(filePath, yaml.stringify(updated));\n return updated;\n });\n }\n\n /**\n * Deletes an annotation and its replies. Returns false if it didn't exist. Holds the same lock\n * as update and addReply, so a write in flight can't bring the annotation back.\n */\n async delete(id: string): Promise<boolean> {\n const filePath = this.getPath(id);\n if (!(await fileExists(filePath))) {\n return false;\n }\n const lock = new FileLock(filePath);\n return lock.withLock(async () => {\n if (!(await fileExists(filePath))) return false;\n await rm(filePath, { force: true });\n return true;\n });\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: notBefore(annotation.createdAt),\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": "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",
6
6
  "names": []
7
7
  }
@@ -13,7 +13,8 @@ async function applyCatalogMigrations(model, options = {}) {
13
13
  details: {
14
14
  elementTypeChanges: types.changes,
15
15
  relationshipChanges: relationships.changes,
16
- stagedRelationshipChanges: staged.changes
16
+ stagedRelationshipChanges: staged.changes,
17
+ ...!types.errors.length && !relationships.error ? { committedRelationshipsAfter: relationships.committed.after } : {}
17
18
  }
18
19
  };
19
20
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/catalog-migration.ts"],
4
- "sourcesContent": ["/**\n * Catalog migration: brings a model's elements and relationships in line with the current spec\n * catalog, using the generated rewrite table (relationship-migration-table.ts).\n *\n * 1. element type renames (element-type-migration.ts), so the rules below see the new types\n * 2. relationship rewrites in relationships.yaml (relationship-migration.ts)\n * 3. the same rewrites in staged changesets\n *\n * It runs inside the 0.9.0 -> 0.10.0 and 0.10.0 -> 0.11.0 migrations, and on its own when `dr upgrade` finds a model\n * already at the current spec version that still names retired types or relationships: the\n * catalog can change between spec version bumps, and a model stamped with the version before\n * a change would otherwise never receive it. Every step only rewrites what the table matches,\n * so running it again on a migrated model changes nothing.\n */\nimport type { Model } from \"./model.js\";\nimport { migrateElementTypes, summarizeElementTypeChanges, type ElementTypeRenameChange } from \"./element-type-migration.js\";\nimport {\n migrateRelationshipsFile,\n migrateStagedChangesets,\n summarizeRelationshipChanges,\n type RelationshipRewriteChange,\n type StagedRelationshipRewriteChange,\n} from \"./relationship-migration.js\";\n\nexport interface CatalogMigrationResult {\n filesModified: number;\n /** One phrase per kind of change, for the migration description */\n summaries: string[];\n errors: string[];\n details: {\n elementTypeChanges: ElementTypeRenameChange[];\n relationshipChanges: RelationshipRewriteChange[];\n stagedRelationshipChanges: StagedRelationshipRewriteChange[];\n };\n}\n\nexport async function applyCatalogMigrations(\n model: Model,\n options: { dryRun?: boolean } = {}\n): Promise<CatalogMigrationResult> {\n const dryRun = options.dryRun ?? false;\n // Renamed element types first (product.capability -> product.softwarecapability), so the\n // relationship rules, which name the new types, resolve the renamed elements.\n const types = await migrateElementTypes(model, undefined, { dryRun });\n\n const relationships = types.errors.length\n ? { filesModified: 0, changes: [], committed: { before: [], after: [] }, error: undefined }\n : await migrateRelationshipsFile(model, undefined, { dryRun });\n const staged = relationships.error\n ? { changesetsModified: 0, changes: [], errors: [] }\n : await migrateStagedChangesets(model, relationships.committed, undefined, { dryRun });\n\n const summaries = [\n ...(types.changes.length > 0 ? [`element types renamed: ${summarizeElementTypeChanges(types.changes)}`] : []),\n relationships.changes.length > 0 ? summarizeRelationshipChanges(relationships.changes) : \"no relationships to rewrite\",\n ...(staged.changes.length > 0 ? [summarizeRelationshipChanges(staged.changes, \"staged relationship changes\")] : []),\n ];\n\n return {\n filesModified: types.filesModified + types.changesetsModified + relationships.filesModified + staged.changesetsModified,\n summaries,\n errors: [...types.errors, ...(relationships.error ? [relationships.error] : []), ...staged.errors],\n details: {\n elementTypeChanges: types.changes,\n relationshipChanges: relationships.changes,\n stagedRelationshipChanges: staged.changes,\n },\n };\n}\n\n/** How many catalog changes the model still needs; all zero when it is in line with the catalog */\nexport async function pendingCatalogMigrations(\n model: Model\n): Promise<{ elementTypes: number; relationships: number; stagedRelationships: number; errors: string[] }> {\n const r = await applyCatalogMigrations(model, { dryRun: true });\n return {\n elementTypes: r.details.elementTypeChanges.length,\n relationships: r.details.relationshipChanges.length,\n stagedRelationships: r.details.stagedRelationshipChanges.length,\n errors: r.errors,\n };\n}\n"],
5
- "mappings": "AAeA,SAAS,qBAAqB,mCAAiE;AAC/F,SACE,0BACA,yBACA,oCAGK;AAcP,eAAsB,uBACpB,OACA,UAAgC,CAAA,GAAE;AAElC,QAAM,SAAS,QAAQ,UAAU,IAG3B,QAAQ,MAAM,oBAAoB,OAAO,QAAW,EAAE,OAAM,CAAE,GAE9D,gBAAgB,MAAM,OAAO,SAC/B,EAAE,eAAe,GAAG,SAAS,CAAA,GAAI,WAAW,EAAE,QAAQ,CAAA,GAAI,OAAO,CAAA,EAAE,GAAI,OAAO,OAAS,IACvF,MAAM,yBAAyB,OAAO,QAAW,EAAE,OAAM,CAAE,GACzD,SAAS,cAAc,QACzB,EAAE,oBAAoB,GAAG,SAAS,CAAA,GAAI,QAAQ,CAAA,EAAE,IAChD,MAAM,wBAAwB,OAAO,cAAc,WAAW,QAAW,EAAE,OAAM,CAAE,GAEjF,YAAY;IAChB,GAAI,MAAM,QAAQ,SAAS,IAAI,CAAC,0BAA0B,4BAA4B,MAAM,OAAO,CAAC,EAAE,IAAI,CAAA;IAC1G,cAAc,QAAQ,SAAS,IAAI,6BAA6B,cAAc,OAAO,IAAI;IACzF,GAAI,OAAO,QAAQ,SAAS,IAAI,CAAC,6BAA6B,OAAO,SAAS,6BAA6B,CAAC,IAAI,CAAA;;AAGlH,SAAO;IACL,eAAe,MAAM,gBAAgB,MAAM,qBAAqB,cAAc,gBAAgB,OAAO;IACrG;IACA,QAAQ,CAAC,GAAG,MAAM,QAAQ,GAAI,cAAc,QAAQ,CAAC,cAAc,KAAK,IAAI,CAAA,GAAK,GAAG,OAAO,MAAM;IACjG,SAAS;MACP,oBAAoB,MAAM;MAC1B,qBAAqB,cAAc;MACnC,2BAA2B,OAAO;;;AAGxC;AAGA,eAAsB,yBACpB,OAAY;AAEZ,QAAM,IAAI,MAAM,uBAAuB,OAAO,EAAE,QAAQ,GAAI,CAAE;AAC9D,SAAO;IACL,cAAc,EAAE,QAAQ,mBAAmB;IAC3C,eAAe,EAAE,QAAQ,oBAAoB;IAC7C,qBAAqB,EAAE,QAAQ,0BAA0B;IACzD,QAAQ,EAAE;;AAEd;",
4
+ "sourcesContent": ["/**\n * Catalog migration: brings a model's elements and relationships in line with the current spec\n * catalog, using the generated rewrite table (relationship-migration-table.ts).\n *\n * 1. element type renames (element-type-migration.ts), so the rules below see the new types\n * 2. relationship rewrites in relationships.yaml (relationship-migration.ts)\n * 3. the same rewrites in staged changesets\n *\n * It runs inside the 0.9.0 -> 0.10.0 and 0.10.0 -> 0.11.0 migrations, and on its own when `dr upgrade` finds a model\n * already at the current spec version that still names retired types or relationships: the\n * catalog can change between spec version bumps, and a model stamped with the version before\n * a change would otherwise never receive it. Every step only rewrites what the table matches,\n * so running it again on a migrated model changes nothing.\n */\nimport type { Model } from \"./model.js\";\nimport type { Relationship } from \"./relationships.js\";\nimport { migrateElementTypes, summarizeElementTypeChanges, type ElementTypeRenameChange } from \"./element-type-migration.js\";\nimport {\n migrateRelationshipsFile,\n migrateStagedChangesets,\n summarizeRelationshipChanges,\n type RelationshipRewriteChange,\n type StagedRelationshipRewriteChange,\n} from \"./relationship-migration.js\";\n\nexport interface CatalogMigrationResult {\n filesModified: number;\n /** One phrase per kind of change, for the migration description */\n summaries: string[];\n errors: string[];\n details: {\n elementTypeChanges: ElementTypeRenameChange[];\n relationshipChanges: RelationshipRewriteChange[];\n stagedRelationshipChanges: StagedRelationshipRewriteChange[];\n /**\n * relationships.yaml as the migration leaves it (as it would, in a dry run). Absent when the\n * relationships were not migrated (an element-type error, an unreadable file).\n */\n committedRelationshipsAfter?: Relationship[];\n };\n}\n\nexport async function applyCatalogMigrations(\n model: Model,\n options: { dryRun?: boolean } = {}\n): Promise<CatalogMigrationResult> {\n const dryRun = options.dryRun ?? false;\n // Renamed element types first (product.capability -> product.softwarecapability), so the\n // relationship rules, which name the new types, resolve the renamed elements.\n const types = await migrateElementTypes(model, undefined, { dryRun });\n\n const relationships = types.errors.length\n ? { filesModified: 0, changes: [], committed: { before: [], after: [] }, error: undefined }\n : await migrateRelationshipsFile(model, undefined, { dryRun });\n const staged = relationships.error\n ? { changesetsModified: 0, changes: [], errors: [] }\n : await migrateStagedChangesets(model, relationships.committed, undefined, { dryRun });\n\n const summaries = [\n ...(types.changes.length > 0 ? [`element types renamed: ${summarizeElementTypeChanges(types.changes)}`] : []),\n relationships.changes.length > 0 ? summarizeRelationshipChanges(relationships.changes) : \"no relationships to rewrite\",\n ...(staged.changes.length > 0 ? [summarizeRelationshipChanges(staged.changes, \"staged relationship changes\")] : []),\n ];\n\n return {\n filesModified: types.filesModified + types.changesetsModified + relationships.filesModified + staged.changesetsModified,\n summaries,\n errors: [...types.errors, ...(relationships.error ? [relationships.error] : []), ...staged.errors],\n details: {\n elementTypeChanges: types.changes,\n relationshipChanges: relationships.changes,\n stagedRelationshipChanges: staged.changes,\n ...(!types.errors.length && !relationships.error ? { committedRelationshipsAfter: relationships.committed.after } : {}),\n },\n };\n}\n\n/** How many catalog changes the model still needs; all zero when it is in line with the catalog */\nexport async function pendingCatalogMigrations(\n model: Model\n): Promise<{ elementTypes: number; relationships: number; stagedRelationships: number; errors: string[] }> {\n const r = await applyCatalogMigrations(model, { dryRun: true });\n return {\n elementTypes: r.details.elementTypeChanges.length,\n relationships: r.details.relationshipChanges.length,\n stagedRelationships: r.details.stagedRelationshipChanges.length,\n errors: r.errors,\n };\n}\n"],
5
+ "mappings": "AAgBA,SAAS,qBAAqB,mCAAiE;AAC/F,SACE,0BACA,yBACA,oCAGK;AAmBP,eAAsB,uBACpB,OACA,UAAgC,CAAA,GAAE;AAElC,QAAM,SAAS,QAAQ,UAAU,IAG3B,QAAQ,MAAM,oBAAoB,OAAO,QAAW,EAAE,OAAM,CAAE,GAE9D,gBAAgB,MAAM,OAAO,SAC/B,EAAE,eAAe,GAAG,SAAS,CAAA,GAAI,WAAW,EAAE,QAAQ,CAAA,GAAI,OAAO,CAAA,EAAE,GAAI,OAAO,OAAS,IACvF,MAAM,yBAAyB,OAAO,QAAW,EAAE,OAAM,CAAE,GACzD,SAAS,cAAc,QACzB,EAAE,oBAAoB,GAAG,SAAS,CAAA,GAAI,QAAQ,CAAA,EAAE,IAChD,MAAM,wBAAwB,OAAO,cAAc,WAAW,QAAW,EAAE,OAAM,CAAE,GAEjF,YAAY;IAChB,GAAI,MAAM,QAAQ,SAAS,IAAI,CAAC,0BAA0B,4BAA4B,MAAM,OAAO,CAAC,EAAE,IAAI,CAAA;IAC1G,cAAc,QAAQ,SAAS,IAAI,6BAA6B,cAAc,OAAO,IAAI;IACzF,GAAI,OAAO,QAAQ,SAAS,IAAI,CAAC,6BAA6B,OAAO,SAAS,6BAA6B,CAAC,IAAI,CAAA;;AAGlH,SAAO;IACL,eAAe,MAAM,gBAAgB,MAAM,qBAAqB,cAAc,gBAAgB,OAAO;IACrG;IACA,QAAQ,CAAC,GAAG,MAAM,QAAQ,GAAI,cAAc,QAAQ,CAAC,cAAc,KAAK,IAAI,CAAA,GAAK,GAAG,OAAO,MAAM;IACjG,SAAS;MACP,oBAAoB,MAAM;MAC1B,qBAAqB,cAAc;MACnC,2BAA2B,OAAO;MAClC,GAAI,CAAC,MAAM,OAAO,UAAU,CAAC,cAAc,QAAQ,EAAE,6BAA6B,cAAc,UAAU,MAAK,IAAK,CAAA;;;AAG1H;AAGA,eAAsB,yBACpB,OAAY;AAEZ,QAAM,IAAI,MAAM,uBAAuB,OAAO,EAAE,QAAQ,GAAI,CAAE;AAC9D,SAAO;IACL,cAAc,EAAE,QAAQ,mBAAmB;IAC3C,eAAe,EAAE,QAAQ,oBAAoB;IAC7C,qBAAqB,EAAE,QAAQ,0BAA0B;IACzD,QAAQ,EAAE;;AAEd;",
6
6
  "names": []
7
7
  }
@@ -107,6 +107,13 @@ async function describeDrift(model, graph, hash) {
107
107
  const op = elementOps.get(id);
108
108
  (op === "update" || op === "delete") && overlapping.elements.push({ id, reason: "changed-in-base" });
109
109
  }
110
+ const changedSet = new Set(elementsChanged), removedSet = new Set(elementsRemoved), listed = new Set(overlapping.relationships.map((r) => relationshipKey(r)));
111
+ for (const key of net.keys()) {
112
+ if (listed.has(key))
113
+ continue;
114
+ const ref = parseKey(key), ends = [ref.source, ref.target].filter((id, i, all) => all.indexOf(id) === i), removed = ends.filter((id) => removedSet.has(id) && !addedHere.has(id)), changed = ends.filter((id) => changedSet.has(id) && !elementOps.has(id));
115
+ removed.length > 0 ? overlapping.relationships.push({ ...ref, reason: "endpoint-removed-in-base", endpoints: removed }) : changed.length > 0 && overlapping.relationships.push({ ...ref, reason: "endpoint-changed-in-base", endpoints: changed });
116
+ }
110
117
  const cut = (list) => list.slice(0, DRIFT_LIST_LIMIT);
111
118
  baseChanges = {
112
119
  counts: {