@documentation-robotics/cli 0.1.11 → 0.1.13

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/core/reference-registry.ts"],
4
- "sourcesContent": ["import Graph from \"graphology\";\nimport type { Reference, Element } from \"../types/index.js\";\n\n\n/**\n * Reference registry - tracks and validates cross-layer references\n *\n * NOTE: Phase 3 Deferred - Registry Consolidation\n * This registry will be consolidated into the GraphModel query API in a future phase.\n * The graph model provides the foundation with graph-based reference tracking.\n * Current approach maintains backward compatibility while the transition occurs.\n *\n * Future: Use GraphModel.getEdgesFrom/To/Between with predicate filtering\n * instead of maintaining separate reference registry.\n *\n * See: https://github.com/tinkermonkey/documentation_robotics/discussions/317\n */\nexport class ReferenceRegistry {\n private references: Map<string, Reference[]>;\n private targetIndex: Map<string, Reference[]>;\n private typeIndex: Map<string, Reference[]>;\n\n // Known reference property names from Python CLI\n private static readonly KNOWN_REF_PROPERTIES = new Set([\n \"realizes\",\n \"realizedBy\",\n \"serves\",\n \"servedBy\",\n \"accesses\",\n \"accessedBy\",\n \"uses\",\n \"usedBy\",\n \"composedOf\",\n \"partOf\",\n \"flows\",\n \"triggers\",\n \"archimateRef\",\n \"businessActorRef\",\n \"stakeholderRef\",\n \"motivationGoalRef\",\n \"dataObjectRef\",\n \"apiOperationRef\",\n \"applicationServiceRef\",\n \"schemaRef\",\n ]);\n\n constructor() {\n this.references = new Map();\n this.targetIndex = new Map();\n this.typeIndex = new Map();\n }\n\n /**\n * Add a reference to the registry\n */\n addReference(reference: Reference): void {\n // Add to source index\n const sourceKey = reference.source;\n if (!this.references.has(sourceKey)) {\n this.references.set(sourceKey, []);\n }\n this.references.get(sourceKey)!.push(reference);\n\n // Add to target index\n const targetKey = reference.target;\n if (!this.targetIndex.has(targetKey)) {\n this.targetIndex.set(targetKey, []);\n }\n this.targetIndex.get(targetKey)!.push(reference);\n\n // Add to type index\n const typeKey = reference.type;\n if (!this.typeIndex.has(typeKey)) {\n this.typeIndex.set(typeKey, []);\n }\n this.typeIndex.get(typeKey)!.push(reference);\n }\n\n /**\n * Get all references from a source element\n */\n getReferencesFrom(sourceId: string): Reference[] {\n return this.references.get(sourceId) ?? [];\n }\n\n /**\n * Get all references to a target element\n */\n getReferencesTo(targetId: string): Reference[] {\n return this.targetIndex.get(targetId) ?? [];\n }\n\n /**\n * Get all references of a specific type\n */\n getReferencesByType(type: string): Reference[] {\n return this.typeIndex.get(type) ?? [];\n }\n\n /**\n * Check if a reference exists\n */\n hasReference(sourceId: string, targetId: string): boolean {\n const refs = this.references.get(sourceId) ?? [];\n return refs.some((ref) => ref.target === targetId);\n }\n\n /**\n * Get all references\n */\n getAllReferences(): Reference[] {\n const allRefs: Reference[] = [];\n for (const refs of this.references.values()) {\n allRefs.push(...refs);\n }\n return allRefs;\n }\n\n /**\n * Clear all references\n */\n clear(): void {\n this.references.clear();\n this.targetIndex.clear();\n this.typeIndex.clear();\n }\n\n /**\n * Register an element and all its references\n * Scans element properties for known reference names\n */\n registerElement(element: Element): void {\n // Validate required element properties\n if (!element) {\n return;\n }\n\n // Use element's path (slug) as the registry key; fall back to id for legacy elements\n const elementId = element.path || element.id;\n\n // Skip if element has no ID\n if (!elementId) {\n return;\n }\n\n // Extract references from explicit references array\n if (element.references) {\n for (const ref of element.references) {\n // Rewrite source to use consistent ID\n this.addReference({\n ...ref,\n source: elementId,\n });\n }\n }\n\n // Scan attributes for known reference properties\n if (element.attributes) {\n for (const [propName, propValue] of Object.entries(element.attributes)) {\n if (ReferenceRegistry.KNOWN_REF_PROPERTIES.has(propName)) {\n // Handle string value\n if (typeof propValue === \"string\") {\n this.addReference({\n source: elementId,\n target: propValue,\n type: propName,\n });\n }\n // Handle array of strings\n else if (Array.isArray(propValue)) {\n for (const target of propValue) {\n if (typeof target === \"string\") {\n this.addReference({\n source: elementId,\n target: target,\n type: propName,\n });\n }\n }\n }\n }\n }\n\n // Scan nested attributes for *Ref/*Reference properties\n const nestedRefs = this.scanNestedReferences(elementId, element.attributes);\n for (const ref of nestedRefs) {\n this.addReference(ref);\n }\n }\n }\n\n /**\n * Recursively scan nested structures for reference properties\n * Matches Python CLI behavior: looks for properties ending in \"Ref\" or \"Reference\"\n */\n private scanNestedReferences(sourceId: string, data: any, path: string = \"\"): Reference[] {\n const references: Reference[] = [];\n\n if (!data || typeof data !== \"object\") {\n return references;\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n for (let i = 0; i < data.length; i++) {\n const itemPath = path ? `${path}[${i}]` : `[${i}]`;\n references.push(...this.scanNestedReferences(sourceId, data[i], itemPath));\n }\n return references;\n }\n\n // Scan object properties\n for (const [key, value] of Object.entries(data)) {\n const currentPath = path ? `${path}.${key}` : key;\n\n // Skip if this is a top-level known reference property (already handled)\n if (!path && ReferenceRegistry.KNOWN_REF_PROPERTIES.has(key)) {\n continue;\n }\n\n // Check if property name ends with \"Ref\" or \"Reference\"\n if (key.endsWith(\"Ref\") || key.endsWith(\"Reference\")) {\n // Handle string value\n if (typeof value === \"string\") {\n references.push({\n source: sourceId,\n target: value,\n type: key,\n description: `Nested reference at ${currentPath}`,\n });\n }\n // Handle array of strings\n else if (Array.isArray(value)) {\n for (const target of value) {\n if (typeof target === \"string\") {\n references.push({\n source: sourceId,\n target: target,\n type: key,\n description: `Nested reference at ${currentPath}`,\n });\n }\n }\n }\n }\n // Recurse into nested objects\n else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n references.push(...this.scanNestedReferences(sourceId, value, currentPath));\n }\n // Recurse into arrays\n else if (Array.isArray(value)) {\n references.push(...this.scanNestedReferences(sourceId, value, currentPath));\n }\n }\n\n return references;\n }\n\n /**\n * Find broken references (references to non-existent elements)\n */\n findBrokenReferences(validIds: Set<string>): Reference[] {\n return this.getAllReferences().filter((ref) => !validIds.has(ref.target));\n }\n\n /**\n * Get dependency graph as a directed graph using graphology\n * Nodes are element IDs, edges represent references\n */\n getDependencyGraph(): Graph {\n const graph = new Graph({ type: \"directed\" });\n\n // Add all unique nodes\n const nodes = new Set<string>();\n for (const ref of this.getAllReferences()) {\n nodes.add(ref.source);\n nodes.add(ref.target);\n }\n\n for (const node of nodes) {\n graph.addNode(node);\n }\n\n // Add edges for all references\n for (const ref of this.getAllReferences()) {\n // Use reference type as edge data\n graph.addEdge(ref.source, ref.target, {\n type: ref.type,\n description: ref.description,\n });\n }\n\n return graph;\n }\n\n /**\n * Get statistics about registered references\n */\n getStats(): {\n totalReferences: number;\n uniqueSources: number;\n uniqueTargets: number;\n referenceTypes: string[];\n } {\n return {\n totalReferences: this.getAllReferences().length,\n uniqueSources: this.references.size,\n uniqueTargets: this.targetIndex.size,\n referenceTypes: Array.from(this.typeIndex.keys()),\n };\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import Graph from \"graphology\";\nimport type { Reference, Element } from \"../types/index.js\";\nimport { looksLikeElementReference } from \"../utils/element-reference-shape.js\";\n\n\n/**\n * Reference registry - tracks and validates cross-layer references\n *\n * NOTE: Phase 3 Deferred - Registry Consolidation\n * This registry will be consolidated into the GraphModel query API in a future phase.\n * The graph model provides the foundation with graph-based reference tracking.\n * Current approach maintains backward compatibility while the transition occurs.\n *\n * Future: Use GraphModel.getEdgesFrom/To/Between with predicate filtering\n * instead of maintaining separate reference registry.\n *\n * See: https://github.com/tinkermonkey/documentation_robotics/discussions/317\n */\nexport class ReferenceRegistry {\n private references: Map<string, Reference[]>;\n private targetIndex: Map<string, Reference[]>;\n private typeIndex: Map<string, Reference[]>;\n\n // Known reference property names from Python CLI. NOTE: matching one of these names (or the\n // *Ref/*Reference naming convention scanNestedReferences uses) is necessary but not\n // sufficient \u2014 the attribute's VALUE also has to look like an element id\n // (looksLikeElementReference) before we register it as a reference. Name alone isn't\n // reliable: e.g. api.link.operationRef is a schema-declared OpenAPI URL fragment, not a DR\n // element reference, and matches \"*Ref\" purely by naming coincidence.\n private static readonly KNOWN_REF_PROPERTIES = new Set([\n \"realizes\",\n \"realizedBy\",\n \"serves\",\n \"servedBy\",\n \"accesses\",\n \"accessedBy\",\n \"uses\",\n \"usedBy\",\n \"composedOf\",\n \"partOf\",\n \"flows\",\n \"triggers\",\n \"archimateRef\",\n \"businessActorRef\",\n \"stakeholderRef\",\n \"motivationGoalRef\",\n \"dataObjectRef\",\n \"apiOperationRef\",\n \"applicationServiceRef\",\n \"schemaRef\",\n ]);\n\n constructor() {\n this.references = new Map();\n this.targetIndex = new Map();\n this.typeIndex = new Map();\n }\n\n /**\n * Add a reference to the registry\n */\n addReference(reference: Reference): void {\n // Add to source index\n const sourceKey = reference.source;\n if (!this.references.has(sourceKey)) {\n this.references.set(sourceKey, []);\n }\n this.references.get(sourceKey)!.push(reference);\n\n // Add to target index\n const targetKey = reference.target;\n if (!this.targetIndex.has(targetKey)) {\n this.targetIndex.set(targetKey, []);\n }\n this.targetIndex.get(targetKey)!.push(reference);\n\n // Add to type index\n const typeKey = reference.type;\n if (!this.typeIndex.has(typeKey)) {\n this.typeIndex.set(typeKey, []);\n }\n this.typeIndex.get(typeKey)!.push(reference);\n }\n\n /**\n * Get all references from a source element\n */\n getReferencesFrom(sourceId: string): Reference[] {\n return this.references.get(sourceId) ?? [];\n }\n\n /**\n * Get all references to a target element\n */\n getReferencesTo(targetId: string): Reference[] {\n return this.targetIndex.get(targetId) ?? [];\n }\n\n /**\n * Get all references of a specific type\n */\n getReferencesByType(type: string): Reference[] {\n return this.typeIndex.get(type) ?? [];\n }\n\n /**\n * Check if a reference exists\n */\n hasReference(sourceId: string, targetId: string): boolean {\n const refs = this.references.get(sourceId) ?? [];\n return refs.some((ref) => ref.target === targetId);\n }\n\n /**\n * Get all references\n */\n getAllReferences(): Reference[] {\n const allRefs: Reference[] = [];\n for (const refs of this.references.values()) {\n allRefs.push(...refs);\n }\n return allRefs;\n }\n\n /**\n * Clear all references\n */\n clear(): void {\n this.references.clear();\n this.targetIndex.clear();\n this.typeIndex.clear();\n }\n\n /**\n * Register an element and all its references\n * Scans element properties for known reference names\n */\n registerElement(element: Element): void {\n // Validate required element properties\n if (!element) {\n return;\n }\n\n // Use element's path (slug) as the registry key; fall back to id for legacy elements\n const elementId = element.path || element.id;\n\n // Skip if element has no ID\n if (!elementId) {\n return;\n }\n\n // Extract references from explicit references array\n if (element.references) {\n for (const ref of element.references) {\n // Rewrite source to use consistent ID\n this.addReference({\n ...ref,\n source: elementId,\n });\n }\n }\n\n // Scan attributes for known reference properties\n if (element.attributes) {\n for (const [propName, propValue] of Object.entries(element.attributes)) {\n if (ReferenceRegistry.KNOWN_REF_PROPERTIES.has(propName)) {\n // Handle string value\n if (typeof propValue === \"string\") {\n if (looksLikeElementReference(propValue)) {\n this.addReference({\n source: elementId,\n target: propValue,\n type: propName,\n });\n }\n }\n // Handle array of strings\n else if (Array.isArray(propValue)) {\n for (const target of propValue) {\n if (typeof target === \"string\" && looksLikeElementReference(target)) {\n this.addReference({\n source: elementId,\n target: target,\n type: propName,\n });\n }\n }\n }\n }\n }\n\n // Scan nested attributes for *Ref/*Reference properties\n const nestedRefs = this.scanNestedReferences(elementId, element.attributes);\n for (const ref of nestedRefs) {\n this.addReference(ref);\n }\n }\n }\n\n /**\n * Recursively scan nested structures for reference properties\n * Matches Python CLI behavior: looks for properties ending in \"Ref\" or \"Reference\"\n */\n private scanNestedReferences(sourceId: string, data: any, path: string = \"\"): Reference[] {\n const references: Reference[] = [];\n\n if (!data || typeof data !== \"object\") {\n return references;\n }\n\n // Handle arrays\n if (Array.isArray(data)) {\n for (let i = 0; i < data.length; i++) {\n const itemPath = path ? `${path}[${i}]` : `[${i}]`;\n references.push(...this.scanNestedReferences(sourceId, data[i], itemPath));\n }\n return references;\n }\n\n // Scan object properties\n for (const [key, value] of Object.entries(data)) {\n const currentPath = path ? `${path}.${key}` : key;\n\n // Skip if this is a top-level known reference property (already handled)\n if (!path && ReferenceRegistry.KNOWN_REF_PROPERTIES.has(key)) {\n continue;\n }\n\n // Check if property name ends with \"Ref\" or \"Reference\"\n if (key.endsWith(\"Ref\") || key.endsWith(\"Reference\")) {\n // Handle string value\n if (typeof value === \"string\") {\n if (looksLikeElementReference(value)) {\n references.push({\n source: sourceId,\n target: value,\n type: key,\n description: `Nested reference at ${currentPath}`,\n });\n }\n }\n // Handle array of strings\n else if (Array.isArray(value)) {\n for (const target of value) {\n if (typeof target === \"string\" && looksLikeElementReference(target)) {\n references.push({\n source: sourceId,\n target: target,\n type: key,\n description: `Nested reference at ${currentPath}`,\n });\n }\n }\n }\n }\n // Recurse into nested objects\n else if (value && typeof value === \"object\" && !Array.isArray(value)) {\n references.push(...this.scanNestedReferences(sourceId, value, currentPath));\n }\n // Recurse into arrays\n else if (Array.isArray(value)) {\n references.push(...this.scanNestedReferences(sourceId, value, currentPath));\n }\n }\n\n return references;\n }\n\n /**\n * Find broken references (references to non-existent elements)\n */\n findBrokenReferences(validIds: Set<string>): Reference[] {\n return this.getAllReferences().filter((ref) => !validIds.has(ref.target));\n }\n\n /**\n * Get dependency graph as a directed graph using graphology\n * Nodes are element IDs, edges represent references\n */\n getDependencyGraph(): Graph {\n const graph = new Graph({ type: \"directed\" });\n\n // Add all unique nodes\n const nodes = new Set<string>();\n for (const ref of this.getAllReferences()) {\n nodes.add(ref.source);\n nodes.add(ref.target);\n }\n\n for (const node of nodes) {\n graph.addNode(node);\n }\n\n // Add edges for all references\n for (const ref of this.getAllReferences()) {\n // Use reference type as edge data\n graph.addEdge(ref.source, ref.target, {\n type: ref.type,\n description: ref.description,\n });\n }\n\n return graph;\n }\n\n /**\n * Get statistics about registered references\n */\n getStats(): {\n totalReferences: number;\n uniqueSources: number;\n uniqueTargets: number;\n referenceTypes: string[];\n } {\n return {\n totalReferences: this.getAllReferences().length,\n uniqueSources: this.references.size,\n uniqueTargets: this.targetIndex.size,\n referenceTypes: Array.from(this.typeIndex.keys()),\n };\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { ALL_LAYERS, LAYER_COLORS } from "./types.js";
2
2
  import { isTelemetryEnabled, startSpan, endSpan } from "../telemetry/index.js";
3
3
  import { getErrorMessage } from "../utils/errors.js";
4
+ import { getNodeType } from "../generated/node-types.js";
4
5
  class PlantUMLExporter {
5
6
  name = "PlantUML";
6
7
  supportedLayers = ALL_LAYERS;
@@ -96,7 +97,8 @@ class PlantUMLExporter {
96
97
  if (!node)
97
98
  return null;
98
99
  let sourceRef = node.source_reference;
99
- if (sourceRef || (sourceRef = node.properties?.["x-source-reference"]), !sourceRef && node.properties?.source?.reference && (sourceRef = node.properties.source.reference), !sourceRef)
100
+ const nodeType = node.spec_node_id ? getNodeType(node.spec_node_id) : void 0, declaresAttribute = (key) => !!nodeType && (nodeType.requiredAttributes.includes(key) || nodeType.optionalAttributes.includes(key));
101
+ if (!sourceRef && !declaresAttribute("x-source-reference") && (sourceRef = node.properties?.["x-source-reference"]), !sourceRef && !declaresAttribute("source") && node.properties?.source?.reference && (sourceRef = node.properties.source.reference), !sourceRef)
100
102
  return null;
101
103
  const parts = [];
102
104
  if (Array.isArray(sourceRef.locations) && sourceRef.locations.length > 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/export/plantuml-exporter.ts"],
4
- "sourcesContent": ["import type { Model } from \"../core/model.js\";\nimport type { Exporter, ExportOptions } from \"./types.js\";\nimport { ALL_LAYERS, LAYER_COLORS } from \"./types.js\";\nimport { isTelemetryEnabled, startSpan, endSpan } from \"../telemetry/index.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * PlantUML element entry for diagram generation\n * Contains element metadata for rendering from graph nodes\n */\ninterface PlantUMLElementEntry {\n id: string;\n name: string;\n type: string;\n description?: string;\n}\n\n/**\n * PlantUML Exporter - generates PlantUML syntax for diagram visualization\n * Supports all 13 layers\n *\n * Exports the model as PlantUML component diagram syntax with layers,\n * elements, relationships, and optional source references.\n */\nexport class PlantUMLExporter implements Exporter {\n name = \"PlantUML\";\n supportedLayers = ALL_LAYERS;\n\n async export(model: Model, options: ExportOptions = {}): Promise<string> {\n const span = isTelemetryEnabled\n ? startSpan(\"export.format.plantuml\", {\n \"export.layerCount\": options.layers?.length || this.supportedLayers.length,\n \"export.includeSources\": options.includeSources === true,\n })\n : null;\n\n try {\n const lines: string[] = [];\n\n lines.push(\"@startuml\");\n lines.push(`title \"${this.escapeQuotes(model.manifest.name)}\"`);\n lines.push(\"\");\n\n if (model.manifest.description) {\n lines.push(`note top : \"${this.escapeQuotes(model.manifest.description)}\"`);\n lines.push(\"\");\n }\n\n const layersToExport = options.layers || this.supportedLayers;\n const elementsByLayer = new Map<string, PlantUMLElementEntry[]>();\n const nodesByLayer = new Map<string, any[]>();\n // Map from raw graph node ID \u2192 sanitized PlantUML ID for edge rendering\n const rawToPlantUMLId = new Map<string, string>();\n\n // Collect nodes by layer from graph model\n for (const layerName of layersToExport) {\n const nodes = model.graph.getNodesByLayer(layerName);\n if (nodes.length === 0) continue;\n\n const layerElements: PlantUMLElementEntry[] = [];\n nodesByLayer.set(layerName, nodes);\n\n for (const node of nodes) {\n const pumlId = this.toPlantUMLId(node.id);\n rawToPlantUMLId.set(node.id, pumlId);\n layerElements.push({\n id: pumlId,\n name: node.name,\n type: node.type,\n description: node.description,\n });\n }\n elementsByLayer.set(layerName, layerElements);\n }\n\n // Create packages for each layer\n for (const layerName of layersToExport) {\n const elements = elementsByLayer.get(layerName);\n if (!elements || elements.length === 0) continue;\n\n const color = LAYER_COLORS[layerName] || \"FFFFFF\";\n lines.push(`package \"${layerName}\" #${color} {`);\n\n const nodes = nodesByLayer.get(layerName) || [];\n for (let i = 0; i < elements.length; i++) {\n const element = elements[i];\n lines.push(` component \"${this.escapeQuotes(element.name)}\" as ${element.id}`);\n\n // Add source reference as note if includeSources option is enabled\n if (options.includeSources) {\n const node = nodes[i];\n const sourceRef = this.extractSourceReference(node);\n if (sourceRef) {\n lines.push(` note right of ${element.id}`);\n lines.push(` ${this.escapeQuotes(sourceRef)}`);\n lines.push(` end note`);\n }\n }\n }\n\n lines.push(\"}\");\n lines.push(\"\");\n }\n\n // Add relationships from graph edges\n const edges = model.graph.getAllEdges();\n\n for (const edge of edges) {\n // Only include edges between exported nodes (check raw IDs)\n const srcPuml = rawToPlantUMLId.get(edge.source);\n const dstPuml = rawToPlantUMLId.get(edge.destination);\n if (srcPuml && dstPuml) {\n const arrow = this.getArrowType(edge.predicate);\n lines.push(\n `${srcPuml} ${arrow} ${dstPuml} : ${this.escapeQuotes(edge.predicate)}`\n );\n }\n }\n\n lines.push(\"\");\n lines.push(\"@enduml\");\n\n const result = lines.join(\"\\n\") + \"\\n\";\n\n if (isTelemetryEnabled && span) {\n // Count total elements and relationships\n let totalElements = 0;\n let totalEdges = 0;\n\n for (const elements of elementsByLayer.values()) {\n totalElements += elements.length;\n }\n\n for (const edge of edges) {\n if (rawToPlantUMLId.has(edge.source) && rawToPlantUMLId.has(edge.destination)) {\n totalEdges++;\n }\n }\n\n (span as any).setAttribute(\"export.elementCount\", totalElements);\n (span as any).setAttribute(\"export.relationshipCount\", totalEdges);\n (span as any).setAttribute(\"export.size\", result.length);\n (span as any).setStatus({ code: 0 });\n }\n\n return result;\n } catch (error) {\n if (isTelemetryEnabled && span) {\n (span as any).recordException(error as Error);\n (span as any).setStatus({\n code: 2,\n message: getErrorMessage(error),\n });\n }\n throw error;\n } finally {\n endSpan(span);\n }\n }\n\n /**\n * Get PlantUML arrow type based on predicate\n */\n private getArrowType(predicate: string): string {\n const arrowMap: Record<string, string> = {\n composes: \"*--\",\n aggregates: \"o--\",\n supports: \"-->\",\n realizes: \"..|>\",\n \"flows-to\": \"-->\",\n \"refers-to\": \"..>\",\n };\n return arrowMap[predicate] || \"-->\";\n }\n\n /**\n * Extract and format source reference from node properties\n */\n private extractSourceReference(node: any): string | null {\n if (!node) return null;\n\n // Try node.source_reference first (primary field on GraphNode)\n let sourceRef = node.source_reference;\n\n // Fall back to x-source-reference in properties\n if (!sourceRef) {\n sourceRef = node.properties?.[\"x-source-reference\"];\n }\n\n // Fall back to nested source.reference format\n if (!sourceRef && node.properties?.source?.reference) {\n sourceRef = node.properties.source.reference;\n }\n\n if (!sourceRef) return null;\n\n // Format source reference for display\n const parts: string[] = [];\n\n if (Array.isArray(sourceRef.locations) && sourceRef.locations.length > 0) {\n const location = sourceRef.locations[0];\n if (location.file) {\n parts.push(`Source: ${location.file}`);\n }\n if (location.symbol) {\n parts.push(`Symbol: ${location.symbol}`);\n }\n }\n\n if (sourceRef.repository?.url) {\n parts.push(`Repo: ${sourceRef.repository.url}`);\n if (sourceRef.repository.commit) {\n parts.push(`Commit: ${sourceRef.repository.commit.substring(0, 8)}`);\n }\n }\n\n return parts.length > 0 ? parts.join(\"\\\\n\") : null;\n }\n\n /**\n * Escape quotes for PlantUML syntax\n */\n private escapeQuotes(str: string): string {\n // Escape double quotes within strings for PlantUML\n return str.replace(/\"/g, '\\\\\"');\n }\n\n /**\n * Convert a raw element ID (path or UUID) to a valid PlantUML identifier.\n * PlantUML identifiers cannot contain dots or hyphens, so replace with underscores.\n */\n private toPlantUMLId(id: string): string {\n return id.replace(/[.\\-]/g, \"_\");\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Model } from \"../core/model.js\";\nimport type { Exporter, ExportOptions } from \"./types.js\";\nimport { ALL_LAYERS, LAYER_COLORS } from \"./types.js\";\nimport { isTelemetryEnabled, startSpan, endSpan } from \"../telemetry/index.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { getNodeType, type SpecNodeId } from \"../generated/node-types.js\";\n\n/**\n * PlantUML element entry for diagram generation\n * Contains element metadata for rendering from graph nodes\n */\ninterface PlantUMLElementEntry {\n id: string;\n name: string;\n type: string;\n description?: string;\n}\n\n/**\n * PlantUML Exporter - generates PlantUML syntax for diagram visualization\n * Supports all 13 layers\n *\n * Exports the model as PlantUML component diagram syntax with layers,\n * elements, relationships, and optional source references.\n */\nexport class PlantUMLExporter implements Exporter {\n name = \"PlantUML\";\n supportedLayers = ALL_LAYERS;\n\n async export(model: Model, options: ExportOptions = {}): Promise<string> {\n const span = isTelemetryEnabled\n ? startSpan(\"export.format.plantuml\", {\n \"export.layerCount\": options.layers?.length || this.supportedLayers.length,\n \"export.includeSources\": options.includeSources === true,\n })\n : null;\n\n try {\n const lines: string[] = [];\n\n lines.push(\"@startuml\");\n lines.push(`title \"${this.escapeQuotes(model.manifest.name)}\"`);\n lines.push(\"\");\n\n if (model.manifest.description) {\n lines.push(`note top : \"${this.escapeQuotes(model.manifest.description)}\"`);\n lines.push(\"\");\n }\n\n const layersToExport = options.layers || this.supportedLayers;\n const elementsByLayer = new Map<string, PlantUMLElementEntry[]>();\n const nodesByLayer = new Map<string, any[]>();\n // Map from raw graph node ID \u2192 sanitized PlantUML ID for edge rendering\n const rawToPlantUMLId = new Map<string, string>();\n\n // Collect nodes by layer from graph model\n for (const layerName of layersToExport) {\n const nodes = model.graph.getNodesByLayer(layerName);\n if (nodes.length === 0) continue;\n\n const layerElements: PlantUMLElementEntry[] = [];\n nodesByLayer.set(layerName, nodes);\n\n for (const node of nodes) {\n const pumlId = this.toPlantUMLId(node.id);\n rawToPlantUMLId.set(node.id, pumlId);\n layerElements.push({\n id: pumlId,\n name: node.name,\n type: node.type,\n description: node.description,\n });\n }\n elementsByLayer.set(layerName, layerElements);\n }\n\n // Create packages for each layer\n for (const layerName of layersToExport) {\n const elements = elementsByLayer.get(layerName);\n if (!elements || elements.length === 0) continue;\n\n const color = LAYER_COLORS[layerName] || \"FFFFFF\";\n lines.push(`package \"${layerName}\" #${color} {`);\n\n const nodes = nodesByLayer.get(layerName) || [];\n for (let i = 0; i < elements.length; i++) {\n const element = elements[i];\n lines.push(` component \"${this.escapeQuotes(element.name)}\" as ${element.id}`);\n\n // Add source reference as note if includeSources option is enabled\n if (options.includeSources) {\n const node = nodes[i];\n const sourceRef = this.extractSourceReference(node);\n if (sourceRef) {\n lines.push(` note right of ${element.id}`);\n lines.push(` ${this.escapeQuotes(sourceRef)}`);\n lines.push(` end note`);\n }\n }\n }\n\n lines.push(\"}\");\n lines.push(\"\");\n }\n\n // Add relationships from graph edges\n const edges = model.graph.getAllEdges();\n\n for (const edge of edges) {\n // Only include edges between exported nodes (check raw IDs)\n const srcPuml = rawToPlantUMLId.get(edge.source);\n const dstPuml = rawToPlantUMLId.get(edge.destination);\n if (srcPuml && dstPuml) {\n const arrow = this.getArrowType(edge.predicate);\n lines.push(\n `${srcPuml} ${arrow} ${dstPuml} : ${this.escapeQuotes(edge.predicate)}`\n );\n }\n }\n\n lines.push(\"\");\n lines.push(\"@enduml\");\n\n const result = lines.join(\"\\n\") + \"\\n\";\n\n if (isTelemetryEnabled && span) {\n // Count total elements and relationships\n let totalElements = 0;\n let totalEdges = 0;\n\n for (const elements of elementsByLayer.values()) {\n totalElements += elements.length;\n }\n\n for (const edge of edges) {\n if (rawToPlantUMLId.has(edge.source) && rawToPlantUMLId.has(edge.destination)) {\n totalEdges++;\n }\n }\n\n (span as any).setAttribute(\"export.elementCount\", totalElements);\n (span as any).setAttribute(\"export.relationshipCount\", totalEdges);\n (span as any).setAttribute(\"export.size\", result.length);\n (span as any).setStatus({ code: 0 });\n }\n\n return result;\n } catch (error) {\n if (isTelemetryEnabled && span) {\n (span as any).recordException(error as Error);\n (span as any).setStatus({\n code: 2,\n message: getErrorMessage(error),\n });\n }\n throw error;\n } finally {\n endSpan(span);\n }\n }\n\n /**\n * Get PlantUML arrow type based on predicate\n */\n private getArrowType(predicate: string): string {\n const arrowMap: Record<string, string> = {\n composes: \"*--\",\n aggregates: \"o--\",\n supports: \"-->\",\n realizes: \"..|>\",\n \"flows-to\": \"-->\",\n \"refers-to\": \"..>\",\n };\n return arrowMap[predicate] || \"-->\";\n }\n\n /**\n * Extract and format source reference from node properties\n */\n private extractSourceReference(node: any): string | null {\n if (!node) return null;\n\n // Try node.source_reference first (primary, structurally-typed field on GraphNode)\n let sourceRef = node.source_reference;\n\n // Legacy fallback for pre-migration data that stored provenance directly under\n // properties[\"x-source-reference\"] / properties.source.reference instead of the\n // first-class source_reference field. Only trust these fallbacks when the node's OWN\n // schema does NOT declare \"x-source-reference\"/\"source\" as a real attribute \u2014 4 node\n // types (data-store.accesspattern/collection/field/storedlogic) declare\n // \"x-source-reference\", and 37 more declare \"source\", as genuine domain string\n // attributes (e.g. security.accesscondition.source); for those, node.properties[...]\n // holds real domain data, not source-code provenance, and reading it here would\n // silently mislabel it as \"Source:\"/\"Repo:\" in the generated diagram.\n const nodeType = node.spec_node_id ? getNodeType(node.spec_node_id as SpecNodeId) : undefined;\n const declaresAttribute = (key: string): boolean =>\n !!nodeType && (nodeType.requiredAttributes.includes(key) || nodeType.optionalAttributes.includes(key));\n\n if (!sourceRef && !declaresAttribute(\"x-source-reference\")) {\n sourceRef = node.properties?.[\"x-source-reference\"];\n }\n\n if (!sourceRef && !declaresAttribute(\"source\") && node.properties?.source?.reference) {\n sourceRef = node.properties.source.reference;\n }\n\n if (!sourceRef) return null;\n\n // Format source reference for display\n const parts: string[] = [];\n\n if (Array.isArray(sourceRef.locations) && sourceRef.locations.length > 0) {\n const location = sourceRef.locations[0];\n if (location.file) {\n parts.push(`Source: ${location.file}`);\n }\n if (location.symbol) {\n parts.push(`Symbol: ${location.symbol}`);\n }\n }\n\n if (sourceRef.repository?.url) {\n parts.push(`Repo: ${sourceRef.repository.url}`);\n if (sourceRef.repository.commit) {\n parts.push(`Commit: ${sourceRef.repository.commit.substring(0, 8)}`);\n }\n }\n\n return parts.length > 0 ? parts.join(\"\\\\n\") : null;\n }\n\n /**\n * Escape quotes for PlantUML syntax\n */\n private escapeQuotes(str: string): string {\n // Escape double quotes within strings for PlantUML\n return str.replace(/\"/g, '\\\\\"');\n }\n\n /**\n * Convert a raw element ID (path or UUID) to a valid PlantUML identifier.\n * PlantUML identifiers cannot contain dots or hyphens, so replace with underscores.\n */\n private toPlantUMLId(id: string): string {\n return id.replace(/[.\\-]/g, \"_\");\n }\n}\n"],
5
+ "mappings": "AAEA,SAAS,YAAY,oBAAoB;AACzC,SAAS,oBAAoB,WAAW,eAAe;AACvD,SAAS,uBAAuB;AAChC,SAAS,mBAAoC;AAoBvC,MAAO,iBAAgB;EAC3B,OAAO;EACP,kBAAkB;EAElB,MAAM,OAAO,OAAc,UAAyB,CAAA,GAAE;AACpD,UAAM,OAAO,qBACT,UAAU,0BAA0B;MAClC,qBAAqB,QAAQ,QAAQ,UAAU,KAAK,gBAAgB;MACpE,yBAAyB,QAAQ,mBAAmB;KACrD,IACD;AAEJ,QAAI;AACF,YAAM,QAAkB,CAAA;AAExB,YAAM,KAAK,WAAW,GACtB,MAAM,KAAK,UAAU,KAAK,aAAa,MAAM,SAAS,IAAI,CAAC,GAAG,GAC9D,MAAM,KAAK,EAAE,GAET,MAAM,SAAS,gBACjB,MAAM,KAAK,eAAe,KAAK,aAAa,MAAM,SAAS,WAAW,CAAC,GAAG,GAC1E,MAAM,KAAK,EAAE;AAGf,YAAM,iBAAiB,QAAQ,UAAU,KAAK,iBACxC,kBAAkB,oBAAI,IAAG,GACzB,eAAe,oBAAI,IAAG,GAEtB,kBAAkB,oBAAI,IAAG;AAG/B,iBAAW,aAAa,gBAAgB;AACtC,cAAM,QAAQ,MAAM,MAAM,gBAAgB,SAAS;AACnD,YAAI,MAAM,WAAW;AAAG;AAExB,cAAM,gBAAwC,CAAA;AAC9C,qBAAa,IAAI,WAAW,KAAK;AAEjC,mBAAW,QAAQ,OAAO;AACxB,gBAAM,SAAS,KAAK,aAAa,KAAK,EAAE;AACxC,0BAAgB,IAAI,KAAK,IAAI,MAAM,GACnC,cAAc,KAAK;YACjB,IAAI;YACJ,MAAM,KAAK;YACX,MAAM,KAAK;YACX,aAAa,KAAK;WACnB;QACH;AACA,wBAAgB,IAAI,WAAW,aAAa;MAC9C;AAGA,iBAAW,aAAa,gBAAgB;AACtC,cAAM,WAAW,gBAAgB,IAAI,SAAS;AAC9C,YAAI,CAAC,YAAY,SAAS,WAAW;AAAG;AAExC,cAAM,QAAQ,aAAa,SAAS,KAAK;AACzC,cAAM,KAAK,YAAY,SAAS,MAAM,KAAK,IAAI;AAE/C,cAAM,QAAQ,aAAa,IAAI,SAAS,KAAK,CAAA;AAC7C,iBAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,gBAAM,UAAU,SAAS,CAAC;AAI1B,cAHA,MAAM,KAAK,gBAAgB,KAAK,aAAa,QAAQ,IAAI,CAAC,QAAQ,QAAQ,EAAE,EAAE,GAG1E,QAAQ,gBAAgB;AAC1B,kBAAM,OAAO,MAAM,CAAC,GACd,YAAY,KAAK,uBAAuB,IAAI;AAClD,YAAI,cACF,MAAM,KAAK,mBAAmB,QAAQ,EAAE,EAAE,GAC1C,MAAM,KAAK,OAAO,KAAK,aAAa,SAAS,CAAC,EAAE,GAChD,MAAM,KAAK,YAAY;UAE3B;QACF;AAEA,cAAM,KAAK,GAAG,GACd,MAAM,KAAK,EAAE;MACf;AAGA,YAAM,QAAQ,MAAM,MAAM,YAAW;AAErC,iBAAW,QAAQ,OAAO;AAExB,cAAM,UAAU,gBAAgB,IAAI,KAAK,MAAM,GACzC,UAAU,gBAAgB,IAAI,KAAK,WAAW;AACpD,YAAI,WAAW,SAAS;AACtB,gBAAM,QAAQ,KAAK,aAAa,KAAK,SAAS;AAC9C,gBAAM,KACJ,GAAG,OAAO,IAAI,KAAK,IAAI,OAAO,MAAM,KAAK,aAAa,KAAK,SAAS,CAAC,EAAE;QAE3E;MACF;AAEA,YAAM,KAAK,EAAE,GACb,MAAM,KAAK,SAAS;AAEpB,YAAM,SAAS,MAAM,KAAK;CAAI,IAAI;;AAElC,UAAI,sBAAsB,MAAM;AAE9B,YAAI,gBAAgB,GAChB,aAAa;AAEjB,mBAAW,YAAY,gBAAgB,OAAM;AAC3C,2BAAiB,SAAS;AAG5B,mBAAW,QAAQ;AACjB,UAAI,gBAAgB,IAAI,KAAK,MAAM,KAAK,gBAAgB,IAAI,KAAK,WAAW,KAC1E;AAIH,aAAa,aAAa,uBAAuB,aAAa,GAC9D,KAAa,aAAa,4BAA4B,UAAU,GAChE,KAAa,aAAa,eAAe,OAAO,MAAM,GACtD,KAAa,UAAU,EAAE,MAAM,EAAC,CAAE;MACrC;AAEA,aAAO;IACT,SAAS,OAAO;AACd,YAAI,sBAAsB,SACvB,KAAa,gBAAgB,KAAc,GAC3C,KAAa,UAAU;QACtB,MAAM;QACN,SAAS,gBAAgB,KAAK;OAC/B,IAEG;IACR;AACE,cAAQ,IAAI;IACd;EACF;;;;EAKQ,aAAa,WAAiB;AASpC,WARyC;MACvC,UAAU;MACV,YAAY;MACZ,UAAU;MACV,UAAU;MACV,YAAY;MACZ,aAAa;MAEC,SAAS,KAAK;EAChC;;;;EAKQ,uBAAuB,MAAS;AACtC,QAAI,CAAC;AAAM,aAAO;AAGlB,QAAI,YAAY,KAAK;AAWrB,UAAM,WAAW,KAAK,eAAe,YAAY,KAAK,YAA0B,IAAI,QAC9E,oBAAoB,CAAC,QACzB,CAAC,CAAC,aAAa,SAAS,mBAAmB,SAAS,GAAG,KAAK,SAAS,mBAAmB,SAAS,GAAG;AAUtG,QARI,CAAC,aAAa,CAAC,kBAAkB,oBAAoB,MACvD,YAAY,KAAK,aAAa,oBAAoB,IAGhD,CAAC,aAAa,CAAC,kBAAkB,QAAQ,KAAK,KAAK,YAAY,QAAQ,cACzE,YAAY,KAAK,WAAW,OAAO,YAGjC,CAAC;AAAW,aAAO;AAGvB,UAAM,QAAkB,CAAA;AAExB,QAAI,MAAM,QAAQ,UAAU,SAAS,KAAK,UAAU,UAAU,SAAS,GAAG;AACxE,YAAM,WAAW,UAAU,UAAU,CAAC;AACtC,MAAI,SAAS,QACX,MAAM,KAAK,WAAW,SAAS,IAAI,EAAE,GAEnC,SAAS,UACX,MAAM,KAAK,WAAW,SAAS,MAAM,EAAE;IAE3C;AAEA,WAAI,UAAU,YAAY,QACxB,MAAM,KAAK,SAAS,UAAU,WAAW,GAAG,EAAE,GAC1C,UAAU,WAAW,UACvB,MAAM,KAAK,WAAW,UAAU,WAAW,OAAO,UAAU,GAAG,CAAC,CAAC,EAAE,IAIhE,MAAM,SAAS,IAAI,MAAM,KAAK,KAAK,IAAI;EAChD;;;;EAKQ,aAAa,KAAW;AAE9B,WAAO,IAAI,QAAQ,MAAM,KAAK;EAChC;;;;;EAMQ,aAAa,IAAU;AAC7B,WAAO,GAAG,QAAQ,UAAU,GAAG;EACjC;;",
6
6
  "names": []
7
7
  }
@@ -62,6 +62,8 @@ class VisualizationServer {
62
62
  // Selected chat client for server
63
63
  chatInitializationError;
64
64
  // Store initialization error for status endpoint
65
+ _testClaudeCmdOverride;
66
+ _testCopilotCmdOverride;
65
67
  /**
66
68
  * Valid spec node IDs per layer
67
69
  * Dynamically loaded from compiled spec (spec/dist/{layer}.json at module load time)
@@ -1269,7 +1271,7 @@ class VisualizationServer {
1269
1271
  async launchClaudeCodeChat(ws, conversationId, message, requestId) {
1270
1272
  try {
1271
1273
  const cmd = ["claude", "--agent", "dr-architect", "--print"];
1272
- this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json");
1274
+ this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json"), this._testClaudeCmdOverride && cmd.splice(0, cmd.length, ...this._testClaudeCmdOverride);
1273
1275
  const proc = spawn(cmd[0], cmd.slice(1), {
1274
1276
  cwd: this.model.rootPath,
1275
1277
  stdio: ["pipe", "pipe", "pipe"]
@@ -1424,10 +1426,14 @@ class VisualizationServer {
1424
1426
  async launchCopilotChat(ws, conversationId, message, requestId) {
1425
1427
  try {
1426
1428
  let cmd;
1427
- const ghResult = spawnSync("gh", ["copilot", "--version"], {
1428
- stdio: ["ignore", "pipe", "pipe"]
1429
- });
1430
- ghResult.error ? console.warn(`[Copilot] 'gh copilot --version' probe failed to spawn: ${getErrorMessage(ghResult.error)}`) : ghResult.status !== 0 && process.env.VERBOSE && console.log(`[Copilot] 'gh copilot' unavailable (exit ${ghResult.status}), falling back to standalone 'copilot'`), ghResult.status === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
1429
+ if (this._testCopilotCmdOverride)
1430
+ cmd = [...this._testCopilotCmdOverride];
1431
+ else {
1432
+ const ghResult = spawnSync("gh", ["copilot", "--version"], {
1433
+ stdio: ["ignore", "pipe", "pipe"]
1434
+ });
1435
+ ghResult.error ? console.warn(`[Copilot] 'gh copilot --version' probe failed to spawn: ${getErrorMessage(ghResult.error)}`) : ghResult.status !== 0 && process.env.VERBOSE && console.log(`[Copilot] 'gh copilot' unavailable (exit ${ghResult.status}), falling back to standalone 'copilot'`), ghResult.status === 0 ? (cmd = ["gh", "copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message)) : (cmd = ["copilot", "explain"], this.withDanger && cmd.push("--allow-all-tools"), cmd.push(message));
1436
+ }
1431
1437
  const proc = spawn(cmd[0], cmd.slice(1), {
1432
1438
  cwd: this.model.rootPath,
1433
1439
  stdio: ["pipe", "pipe", "pipe"]
@@ -1497,7 +1503,20 @@ class VisualizationServer {
1497
1503
  }
1498
1504
  }
1499
1505
  })().catch((err) => {
1500
- console.error(`[Copilot] Stream error: ${getErrorMessage(err)}`);
1506
+ const errorMsg = getErrorMessage(err);
1507
+ console.error(`[Copilot] Stream error: ${errorMsg}`);
1508
+ try {
1509
+ ws.send(JSON.stringify({
1510
+ jsonrpc: "2.0",
1511
+ error: {
1512
+ code: JSONRPC_ERRORS.INTERNAL_ERROR,
1513
+ message: `Stream error: ${errorMsg}`
1514
+ },
1515
+ id: requestId
1516
+ }));
1517
+ } catch (sendError) {
1518
+ console.error(`[Copilot] Failed to send stream error notification: ${getErrorMessage(sendError)}`);
1519
+ }
1501
1520
  });
1502
1521
  } catch (error) {
1503
1522
  const errorMsg = getErrorMessage(error);