@documentation-robotics/cli 0.1.0 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +466 -693
- package/dist/analysis/data-model-analyzer.js +189 -0
- package/dist/analysis/data-model-analyzer.js.map +7 -0
- package/dist/analysis/index.js +3 -0
- package/dist/analysis/index.js.map +7 -0
- package/dist/analysis/relationship-classifier.js +181 -0
- package/dist/analysis/relationship-classifier.js.map +7 -0
- package/dist/analyzers/base-analyzer.js +1 -0
- package/dist/analyzers/base-analyzer.js.map +7 -0
- package/dist/analyzers/call-graph-utils.js +39 -0
- package/dist/analyzers/call-graph-utils.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +1205 -0
- package/dist/analyzers/cbm-analyzer.js.map +7 -0
- package/dist/analyzers/heuristic-utils.js +55 -0
- package/dist/analyzers/heuristic-utils.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +146 -0
- package/dist/analyzers/mapping-loader.js.map +7 -0
- package/dist/analyzers/registry.js +124 -0
- package/dist/analyzers/registry.js.map +7 -0
- package/dist/analyzers/session-state.js +176 -0
- package/dist/analyzers/session-state.js.map +7 -0
- package/dist/analyzers/stdio-client.js +208 -0
- package/dist/analyzers/stdio-client.js.map +7 -0
- package/dist/analyzers/types.js +15 -0
- package/dist/analyzers/types.js.map +7 -0
- package/dist/analyzers/verify-engine.js +209 -0
- package/dist/analyzers/verify-engine.js.map +7 -0
- package/dist/analyzers/verify-ignore.js +130 -0
- package/dist/analyzers/verify-ignore.js.map +7 -0
- package/dist/analyzers/version.js +8 -0
- package/dist/analyzers/version.js.map +7 -0
- package/dist/audit/ai/runner.js +41 -0
- package/dist/audit/ai/runner.js.map +7 -0
- package/dist/audit/differential-analyzer.js +183 -0
- package/dist/audit/differential-analyzer.js.map +7 -0
- package/dist/audit/index.js +32 -0
- package/dist/audit/index.js.map +7 -0
- package/dist/audit/nodes/ai/evaluator.js +49 -0
- package/dist/audit/nodes/ai/evaluator.js.map +7 -0
- package/dist/audit/nodes/ai/parser.js +55 -0
- package/dist/audit/nodes/ai/parser.js.map +7 -0
- package/dist/audit/nodes/ai/prompts.js +103 -0
- package/dist/audit/nodes/ai/prompts.js.map +7 -0
- package/dist/audit/nodes/analysis/node-completeness-checker.js +32 -0
- package/dist/audit/nodes/analysis/node-completeness-checker.js.map +7 -0
- package/dist/audit/nodes/analysis/node-definition-analyzer.js +47 -0
- package/dist/audit/nodes/analysis/node-definition-analyzer.js.map +7 -0
- package/dist/audit/nodes/analysis/node-overlap-detector.js +79 -0
- package/dist/audit/nodes/analysis/node-overlap-detector.js.map +7 -0
- package/dist/audit/nodes/model/orchestrator.js +82 -0
- package/dist/audit/nodes/model/orchestrator.js.map +7 -0
- package/dist/audit/nodes/spec/loader.js +116 -0
- package/dist/audit/nodes/spec/loader.js.map +7 -0
- package/dist/audit/nodes/spec/orchestrator.js +68 -0
- package/dist/audit/nodes/spec/orchestrator.js.map +7 -0
- package/dist/audit/nodes/types.js +1 -0
- package/dist/audit/nodes/types.js.map +7 -0
- package/dist/audit/pipeline/index.js +5 -0
- package/dist/audit/pipeline/index.js.map +7 -0
- package/dist/audit/pipeline/pipeline-orchestrator.js +218 -0
- package/dist/audit/pipeline/pipeline-orchestrator.js.map +7 -0
- package/dist/audit/relationships/ai/evaluator.js +235 -0
- package/dist/audit/relationships/ai/evaluator.js.map +7 -0
- package/dist/audit/relationships/ai/parser.js +197 -0
- package/dist/audit/relationships/ai/parser.js.map +7 -0
- package/dist/audit/relationships/ai/prompts.js +274 -0
- package/dist/audit/relationships/ai/prompts.js.map +7 -0
- package/dist/audit/relationships/analysis/balance-assessor.js +115 -0
- package/dist/audit/relationships/analysis/balance-assessor.js.map +7 -0
- package/dist/audit/relationships/analysis/coverage-analyzer.js +99 -0
- package/dist/audit/relationships/analysis/coverage-analyzer.js.map +7 -0
- package/dist/audit/relationships/analysis/duplicate-detector.js +95 -0
- package/dist/audit/relationships/analysis/duplicate-detector.js.map +7 -0
- package/dist/audit/relationships/analysis/gap-analyzer.js +99 -0
- package/dist/audit/relationships/analysis/gap-analyzer.js.map +7 -0
- package/dist/audit/relationships/analysis/layer-templates.js +294 -0
- package/dist/audit/relationships/analysis/layer-templates.js.map +7 -0
- package/dist/audit/relationships/graph/connectivity.js +129 -0
- package/dist/audit/relationships/graph/connectivity.js.map +7 -0
- package/dist/audit/relationships/graph/relationship-graph.js +179 -0
- package/dist/audit/relationships/graph/relationship-graph.js.map +7 -0
- package/dist/audit/relationships/model/balance-assessor.js +49 -0
- package/dist/audit/relationships/model/balance-assessor.js.map +7 -0
- package/dist/audit/relationships/model/coverage-analyzer.js +34 -0
- package/dist/audit/relationships/model/coverage-analyzer.js.map +7 -0
- package/dist/audit/relationships/model/duplicate-detector.js +68 -0
- package/dist/audit/relationships/model/duplicate-detector.js.map +7 -0
- package/dist/audit/relationships/model/gap-analyzer.js +41 -0
- package/dist/audit/relationships/model/gap-analyzer.js.map +7 -0
- package/dist/audit/relationships/model/orchestrator.js +126 -0
- package/dist/audit/relationships/model/orchestrator.js.map +7 -0
- package/dist/audit/relationships/spec/orchestrator.js +157 -0
- package/dist/audit/relationships/spec/orchestrator.js.map +7 -0
- package/dist/audit/reports/index.js +5 -0
- package/dist/audit/reports/index.js.map +7 -0
- package/dist/audit/reports/report-generator.js +118 -0
- package/dist/audit/reports/report-generator.js.map +7 -0
- package/dist/audit/snapshot-storage.js +163 -0
- package/dist/audit/snapshot-storage.js.map +7 -0
- package/dist/audit/types.js +21 -0
- package/dist/audit/types.js.map +7 -0
- package/dist/cli.js +223 -92
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +147 -0
- package/dist/coding-agents/agents/claude-code.js.map +7 -0
- package/dist/coding-agents/agents/index.js +5 -0
- package/dist/coding-agents/agents/index.js.map +7 -0
- package/dist/coding-agents/agents/types.js +1 -0
- package/dist/coding-agents/agents/types.js.map +7 -0
- package/dist/coding-agents/base-chat-client.js +45 -0
- package/dist/coding-agents/base-chat-client.js.map +7 -0
- package/dist/coding-agents/chat-utils.js +23 -0
- package/dist/coding-agents/chat-utils.js.map +7 -0
- package/dist/coding-agents/claude-code-client.js +246 -0
- package/dist/coding-agents/claude-code-client.js.map +7 -0
- package/dist/coding-agents/copilot-client.js +209 -0
- package/dist/coding-agents/copilot-client.js.map +7 -0
- package/dist/commands/add.js +95 -33
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +511 -0
- package/dist/commands/analyzer.js.map +7 -0
- package/dist/commands/audit-diff.js +49 -0
- package/dist/commands/audit-diff.js.map +7 -0
- package/dist/commands/audit-snapshots.js +62 -0
- package/dist/commands/audit-snapshots.js.map +7 -0
- package/dist/commands/audit.js +81 -0
- package/dist/commands/audit.js.map +7 -0
- package/dist/commands/catalog.js +248 -0
- package/dist/commands/catalog.js.map +7 -0
- package/dist/commands/changeset.js +577 -66
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/chat.js +106 -83
- package/dist/commands/chat.js.map +2 -2
- package/dist/commands/claude.js +74 -0
- package/dist/commands/claude.js.map +7 -0
- package/dist/commands/conformance.js +71 -68
- package/dist/commands/conformance.js.map +2 -2
- package/dist/commands/copilot.js +75 -0
- package/dist/commands/copilot.js.map +7 -0
- package/dist/commands/delete.js +125 -11
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/discover-logic.js +44 -0
- package/dist/commands/discover-logic.js.map +7 -0
- package/dist/commands/docs.js +87 -0
- package/dist/commands/docs.js.map +7 -0
- package/dist/commands/element.js +14 -20
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/export.js +13 -3
- package/dist/commands/export.js.map +2 -2
- package/dist/commands/import.js +51 -0
- package/dist/commands/import.js.map +7 -0
- package/dist/commands/info.js +30 -24
- package/dist/commands/info.js.map +2 -2
- package/dist/commands/init.js +27 -11
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/list.js +63 -13
- package/dist/commands/list.js.map +3 -3
- package/dist/commands/relationship.js +225 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/report.js +54 -0
- package/dist/commands/report.js.map +7 -0
- package/dist/commands/reports.js +45 -0
- package/dist/commands/reports.js.map +7 -0
- package/dist/commands/schema.js +135 -0
- package/dist/commands/schema.js.map +7 -0
- package/dist/commands/search.js +40 -12
- package/dist/commands/search.js.map +2 -2
- package/dist/commands/show.js +58 -27
- package/dist/commands/show.js.map +2 -2
- package/dist/commands/stats.js +35 -0
- package/dist/commands/stats.js.map +7 -0
- package/dist/commands/trace.js +39 -16
- package/dist/commands/trace.js.map +2 -2
- package/dist/commands/update.js +141 -14
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +210 -46
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +142 -23
- package/dist/commands/validate.js.map +3 -3
- package/dist/commands/version.js +8 -12
- package/dist/commands/version.js.map +2 -2
- package/dist/commands/visualize.js +162 -25
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/base-snapshot-manager.js +84 -0
- package/dist/core/base-snapshot-manager.js.map +7 -0
- package/dist/core/changeset-exporter.js +305 -0
- package/dist/core/changeset-exporter.js.map +7 -0
- package/dist/core/changeset-validator.js +41 -0
- package/dist/core/changeset-validator.js.map +7 -0
- package/dist/core/changeset.js +88 -173
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/dependency-tracker.js +4 -2
- package/dist/core/dependency-tracker.js.map +2 -2
- package/dist/core/element.js +98 -27
- package/dist/core/element.js.map +2 -2
- package/dist/core/golden-copy-cache.js +290 -0
- package/dist/core/golden-copy-cache.js.map +7 -0
- package/dist/core/graph-model.js +266 -0
- package/dist/core/graph-model.js.map +7 -0
- package/dist/core/layer.js +159 -16
- package/dist/core/layer.js.map +2 -2
- package/dist/core/layers.js +32 -0
- package/dist/core/layers.js.map +7 -0
- package/dist/core/manifest.js +32 -11
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +56 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +424 -217
- package/dist/core/model.js.map +3 -3
- package/dist/core/mutation-handler.js +179 -0
- package/dist/core/mutation-handler.js.map +7 -0
- package/dist/core/reference-registry.js +29 -21
- package/dist/core/reference-registry.js.map +2 -2
- package/dist/core/relationship-catalog.js +189 -0
- package/dist/core/relationship-catalog.js.map +7 -0
- package/dist/core/relationship-registry.js.map +2 -2
- package/dist/core/relationships.js.map +1 -1
- package/dist/core/report-data-model.js +285 -0
- package/dist/core/report-data-model.js.map +7 -0
- package/dist/core/spec-data-service.js +185 -0
- package/dist/core/spec-data-service.js.map +7 -0
- package/dist/core/spec-loader-types.js +1 -0
- package/dist/core/spec-loader-types.js.map +7 -0
- package/dist/core/spec-loader.js +234 -0
- package/dist/core/spec-loader.js.map +7 -0
- package/dist/core/staged-changeset-storage.js +282 -0
- package/dist/core/staged-changeset-storage.js.map +7 -0
- package/dist/core/staging-area.js +1002 -0
- package/dist/core/staging-area.js.map +7 -0
- package/dist/core/stats-collector.js +169 -0
- package/dist/core/stats-collector.js.map +7 -0
- package/dist/core/virtual-projection.js +443 -0
- package/dist/core/virtual-projection.js.map +7 -0
- package/dist/export/archimate-exporter.js +99 -60
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/export/audit-formatters.js +479 -0
- package/dist/export/audit-formatters.js.map +7 -0
- package/dist/export/diff-formatters.js +99 -0
- package/dist/export/diff-formatters.js.map +7 -0
- package/dist/export/export-manager.js.map +2 -2
- package/dist/export/graphml-exporter.js +44 -27
- package/dist/export/graphml-exporter.js.map +2 -2
- package/dist/export/index.js +4 -0
- package/dist/export/index.js.map +2 -2
- package/dist/export/json-schema-exporter.js +55 -42
- package/dist/export/json-schema-exporter.js.map +2 -2
- package/dist/export/markdown-exporter.js +92 -70
- package/dist/export/markdown-exporter.js.map +2 -2
- package/dist/export/markdown-generator.js +248 -0
- package/dist/export/markdown-generator.js.map +7 -0
- package/dist/export/markdown-utils.js +30 -0
- package/dist/export/markdown-utils.js.map +7 -0
- package/dist/export/mermaid-markdown-exporter.js +37 -0
- package/dist/export/mermaid-markdown-exporter.js.map +7 -0
- package/dist/export/openapi-exporter.js +119 -71
- package/dist/export/openapi-exporter.js.map +2 -2
- package/dist/export/plantuml-exporter.js +104 -39
- package/dist/export/plantuml-exporter.js.map +2 -2
- package/dist/export/report-formatters.js +111 -0
- package/dist/export/report-formatters.js.map +7 -0
- package/dist/export/stats-formatters.js +98 -0
- package/dist/export/stats-formatters.js.map +7 -0
- package/dist/export/types.js +2 -14
- package/dist/export/types.js.map +2 -2
- package/dist/export/verify-formatters.js +153 -0
- package/dist/export/verify-formatters.js.map +7 -0
- package/dist/generated/compiled-validators.js +17 -0
- package/dist/generated/compiled-validators.js.map +7 -0
- package/dist/generated/index.js +31 -0
- package/dist/generated/index.js.map +7 -0
- package/dist/generated/layer-registry.js +190 -0
- package/dist/generated/layer-registry.js.map +7 -0
- package/dist/generated/layer-types.js +7 -0
- package/dist/generated/layer-types.js.map +7 -0
- package/dist/generated/node-types.js +1719 -0
- package/dist/generated/node-types.js.map +7 -0
- package/dist/generated/relationship-index.js +14552 -0
- package/dist/generated/relationship-index.js.map +7 -0
- package/dist/import/archimate-importer.js +166 -0
- package/dist/import/archimate-importer.js.map +7 -0
- package/dist/import/index.js +47 -0
- package/dist/import/index.js.map +7 -0
- package/dist/import/openapi-importer.js +143 -0
- package/dist/import/openapi-importer.js.map +7 -0
- package/dist/import/types.js +1 -0
- package/dist/import/types.js.map +7 -0
- package/dist/integrations/base-manager.js +311 -0
- package/dist/integrations/base-manager.js.map +7 -0
- package/dist/integrations/claude-manager.js +360 -0
- package/dist/integrations/claude-manager.js.map +7 -0
- package/dist/integrations/claude_code/agents/dr-advisor.md +253 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +278 -0
- package/dist/integrations/claude_code/agents/dr-extractor.md +286 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +657 -0
- package/dist/integrations/claude_code/commands/dr-design.md +373 -0
- package/dist/integrations/claude_code/commands/dr-info.md +132 -0
- package/dist/integrations/claude_code/commands/dr-init.md +251 -0
- package/dist/integrations/claude_code/commands/dr-map.md +936 -0
- package/dist/integrations/claude_code/commands/dr-model.md +493 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +814 -0
- package/dist/integrations/claude_code/commands/dr-sync.md +279 -0
- package/dist/integrations/claude_code/commands/dr-validate.md +654 -0
- package/dist/integrations/claude_code/commands/dr-verify.md +559 -0
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +903 -0
- package/dist/integrations/claude_code/docs/QUICK_START.md +271 -0
- package/dist/integrations/claude_code/docs/README.md +347 -0
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +550 -0
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +620 -0
- package/dist/integrations/claude_code/skills/dr_03_security_layer/SKILL.md +642 -0
- package/dist/integrations/claude_code/skills/dr_04_application_layer/SKILL.md +911 -0
- package/dist/integrations/claude_code/skills/dr_05_technology_layer/SKILL.md +836 -0
- package/dist/integrations/claude_code/skills/dr_06_api_layer/SKILL.md +933 -0
- package/dist/integrations/claude_code/skills/dr_07_data_model_layer/SKILL.md +210 -0
- package/dist/integrations/claude_code/skills/dr_08_dataset_layer/SKILL.md +319 -0
- package/dist/integrations/claude_code/skills/dr_09_ux_layer/SKILL.md +319 -0
- package/dist/integrations/claude_code/skills/dr_10_navigation_layer/SKILL.md +318 -0
- package/dist/integrations/claude_code/skills/dr_11_apm_layer/SKILL.md +273 -0
- package/dist/integrations/claude_code/skills/dr_12_testing_layer/SKILL.md +273 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +187 -0
- package/dist/integrations/claude_code/skills/dr_codebase_memory/SKILL.md +590 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +295 -0
- package/dist/integrations/copilot-manager.js +328 -0
- package/dist/integrations/copilot-manager.js.map +7 -0
- package/dist/integrations/github_copilot/agents/dr-advisor.md +243 -0
- package/dist/integrations/github_copilot/agents/dr-architect.md +1567 -0
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +491 -0
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +539 -0
- package/dist/integrations/github_copilot/skills/dr_03_security_layer/SKILL.md +667 -0
- package/dist/integrations/github_copilot/skills/dr_04_application_layer/SKILL.md +670 -0
- package/dist/integrations/github_copilot/skills/dr_05_technology_layer/SKILL.md +660 -0
- package/dist/integrations/github_copilot/skills/dr_06_api_layer/SKILL.md +855 -0
- package/dist/integrations/github_copilot/skills/dr_07_data_model_layer/SKILL.md +171 -0
- package/dist/integrations/github_copilot/skills/dr_08_dataset_layer/SKILL.md +216 -0
- package/dist/integrations/github_copilot/skills/dr_09_ux_layer/SKILL.md +230 -0
- package/dist/integrations/github_copilot/skills/dr_10_navigation_layer/SKILL.md +228 -0
- package/dist/integrations/github_copilot/skills/dr_11_apm_layer/SKILL.md +234 -0
- package/dist/integrations/github_copilot/skills/dr_12_testing_layer/SKILL.md +230 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +187 -0
- package/dist/integrations/hash-utils.js +51 -0
- package/dist/integrations/hash-utils.js.map +7 -0
- package/dist/integrations/types.js +16 -0
- package/dist/integrations/types.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +278 -0
- package/dist/reports/model-layer-report-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +57 -0
- package/dist/reports/model-report-data.js.map +7 -0
- package/dist/reports/model-report-orchestrator.js +103 -0
- package/dist/reports/model-report-orchestrator.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +595 -0
- package/dist/schemas/bundled/analyzers/manifest.json +9 -0
- package/dist/schemas/bundled/api.json +3411 -0
- package/dist/schemas/bundled/apm.json +2371 -0
- package/dist/schemas/bundled/application.json +1253 -0
- package/dist/schemas/bundled/base.json +1622 -0
- package/dist/schemas/bundled/business.json +1279 -0
- package/dist/schemas/bundled/data-model.json +1298 -0
- package/dist/schemas/bundled/data-store.json +2025 -0
- package/dist/schemas/bundled/manifest.json +93 -0
- package/dist/schemas/bundled/motivation.json +1360 -0
- package/dist/schemas/bundled/navigation.json +1874 -0
- package/dist/schemas/bundled/security.json +3488 -0
- package/dist/schemas/bundled/technology.json +2067 -0
- package/dist/schemas/bundled/testing.json +3120 -0
- package/dist/schemas/bundled/ux.json +3592 -0
- package/dist/server/schemas.js +202 -0
- package/dist/server/schemas.js.map +7 -0
- package/dist/server/server.js +1336 -252
- package/dist/server/server.js.map +3 -3
- package/dist/server/telemetry-middleware.js +22 -15
- package/dist/server/telemetry-middleware.js.map +2 -2
- package/dist/server-entry.js +65 -0
- package/dist/server-entry.js.map +7 -0
- package/dist/telemetry/config.js +89 -0
- package/dist/telemetry/config.js.map +7 -0
- package/dist/telemetry/console-interceptor.js.map +1 -1
- package/dist/telemetry/detect.js +24 -0
- package/dist/telemetry/detect.js.map +7 -0
- package/dist/telemetry/fetch-otlp-exporter.js +112 -0
- package/dist/telemetry/fetch-otlp-exporter.js.map +7 -0
- package/dist/telemetry/index.js +153 -3
- package/dist/telemetry/index.js.map +2 -2
- package/dist/telemetry/resilient-exporter.js +22 -8
- package/dist/telemetry/resilient-exporter.js.map +2 -2
- package/dist/telemetry/resilient-log-exporter.js +17 -8
- package/dist/telemetry/resilient-log-exporter.js.map +2 -2
- package/dist/telemetry/test-instrumentation.js.map +2 -2
- package/dist/types/index.js +84 -0
- package/dist/types/index.js.map +3 -3
- package/dist/types/source-reference.js +1 -0
- package/dist/types/source-reference.js.map +7 -0
- package/dist/utils/chat-logger.js +292 -0
- package/dist/utils/chat-logger.js.map +7 -0
- package/dist/utils/claude-stream.js +8 -0
- package/dist/utils/claude-stream.js.map +7 -0
- package/dist/utils/date-utils.js +7 -0
- package/dist/utils/date-utils.js.map +7 -0
- package/dist/utils/element-utils.js.map +1 -1
- package/dist/utils/errors.js +88 -15
- package/dist/utils/errors.js.map +3 -3
- package/dist/utils/export-python-annotations.js +82 -0
- package/dist/utils/export-python-annotations.js.map +7 -0
- package/dist/utils/file-io.js +7 -2
- package/dist/utils/file-io.js.map +2 -2
- package/dist/utils/file-lock.js +156 -0
- package/dist/utils/file-lock.js.map +7 -0
- package/dist/utils/id-generator.js +18 -0
- package/dist/utils/id-generator.js.map +7 -0
- package/dist/utils/layer-name-formatter.js +16 -0
- package/dist/utils/layer-name-formatter.js.map +7 -0
- package/dist/utils/markdown-anchor.js +7 -0
- package/dist/utils/markdown-anchor.js.map +7 -0
- package/dist/utils/markdown-link.js +12 -0
- package/dist/utils/markdown-link.js.map +7 -0
- package/dist/utils/markdown-table.js +17 -0
- package/dist/utils/markdown-table.js.map +7 -0
- package/dist/utils/mermaid-utils.js +11 -0
- package/dist/utils/mermaid-utils.js.map +7 -0
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +52 -0
- package/dist/utils/source-reference.js.map +7 -0
- package/dist/utils/spec-installer.js +40 -53
- package/dist/utils/spec-installer.js.map +2 -2
- package/dist/utils/spec-version.js +5 -1
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/utils/string-utils.js +12 -0
- package/dist/utils/string-utils.js.map +7 -0
- package/dist/utils/table-formatting.js +19 -0
- package/dist/utils/table-formatting.js.map +7 -0
- package/dist/validators/index.js +4 -0
- package/dist/validators/index.js.map +2 -2
- package/dist/validators/naming-validator.js +20 -23
- package/dist/validators/naming-validator.js.map +2 -2
- package/dist/validators/reference-validator.js +23 -38
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +243 -0
- package/dist/validators/relationship-schema-validator.js.map +7 -0
- package/dist/validators/schema-validator.js +168 -68
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/semantic-validator.js +36 -60
- package/dist/validators/semantic-validator.js.map +2 -2
- package/dist/validators/types.js.map +2 -2
- package/dist/validators/validation-formatter.js +159 -0
- package/dist/validators/validation-formatter.js.map +7 -0
- package/dist/validators/validator.js +24 -38
- package/dist/validators/validator.js.map +2 -2
- package/dist/viewer/README.md +139 -0
- package/dist/viewer/assets/main-CY0Xufuj.css +1 -0
- package/dist/viewer/assets/main-DtgErnwF.js +81 -0
- package/dist/viewer/assets/vendor-CxKvDp2x.js +17 -0
- package/dist/viewer/favicon.svg +11 -0
- package/dist/viewer/fonts/inter/Inter-Black.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Bold.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-ExtraBold.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Light.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Medium.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-Regular.woff2 +0 -0
- package/dist/viewer/fonts/inter/Inter-SemiBold.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
- package/dist/viewer/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
- package/dist/viewer/index.html +15 -0
- package/dist/viewer/manifest.json +99 -0
- package/dist/viewer/version.json +1 -0
- package/package.json +42 -16
- package/PYTHON_CLI_PARITY.md +0 -265
- package/PYTHON_CLI_STRUCTURE_UPDATE.md +0 -246
- package/biome.json +0 -42
- package/bunfig.toml +0 -3
- package/dist/ai/claude-client.js +0 -71
- package/dist/ai/claude-client.js.map +0 -7
- package/dist/ai/context-provider.js +0 -129
- package/dist/ai/context-provider.js.map +0 -7
- package/dist/ai/tools.js +0 -231
- package/dist/ai/tools.js.map +0 -7
- package/dist/commands/migrate.js +0 -36
- package/dist/commands/migrate.js.map +0 -7
- package/dist/commands/project.js +0 -74
- package/dist/commands/project.js.map +0 -7
- package/dist/core/active-changeset.js +0 -52
- package/dist/core/active-changeset.js.map +0 -7
- package/dist/core/projection-engine.js +0 -288
- package/dist/core/projection-engine.js.map +0 -7
- package/dist/schemas/bundled/01-motivation-layer.schema.json +0 -942
- package/dist/schemas/bundled/02-business-layer.schema.json +0 -1293
- package/dist/schemas/bundled/03-security-layer.schema.json +0 -2177
- package/dist/schemas/bundled/04-application-layer.schema.json +0 -1328
- package/dist/schemas/bundled/05-technology-layer.schema.json +0 -1465
- package/dist/schemas/bundled/06-api-layer.schema.json +0 -1743
- package/dist/schemas/bundled/07-data-model-layer.schema.json +0 -1253
- package/dist/schemas/bundled/08-datastore-layer.schema.json +0 -892
- package/dist/schemas/bundled/09-ux-layer.schema.json +0 -1506
- package/dist/schemas/bundled/10-navigation-layer.schema.json +0 -843
- package/dist/schemas/bundled/11-apm-observability-layer.schema.json +0 -1430
- package/dist/schemas/bundled/12-testing-layer.schema.json +0 -1091
- package/dist/schemas/bundled/common/layer-extensions.schema.json +0 -266
- package/dist/schemas/bundled/common/predicates.schema.json +0 -493
- package/dist/schemas/bundled/common/relationships.schema.json +0 -791
- package/dist/schemas/bundled/common/source-references.schema.json +0 -71
- package/dist/schemas/bundled/link-registry.json +0 -1839
- package/dist/schemas/bundled/link-registry.schema.json +0 -339
- package/dist/schemas/bundled/relationship-catalog.json +0 -1302
- package/dist/schemas/bundled/relationship-type.schema.json +0 -221
- package/dist/utils/model-path.js +0 -22
- package/dist/utils/model-path.js.map +0 -7
- package/docs/TEST-INSTRUMENTATION.md +0 -405
- package/docs/telemetry.md +0 -427
- package/test-debug.js +0 -33
- package/test-special.mjs +0 -46
- package/test_runner.ts +0 -7
package/dist/server/server.js
CHANGED
|
@@ -1,60 +1,298 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { upgradeWebSocket, websocket } from "hono/bun";
|
|
1
|
+
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
|
|
3
2
|
import { cors } from "hono/cors";
|
|
4
|
-
import {
|
|
3
|
+
import { swaggerUI } from "@hono/swagger-ui";
|
|
4
|
+
import { randomBytes } from "crypto";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import path from "path";
|
|
8
|
+
let upgradeWebSocket, websocket;
|
|
9
|
+
const require2 = createRequire(import.meta.url);
|
|
5
10
|
import { Model } from "../core/model.js";
|
|
11
|
+
import { StagedChangesetStorage } from "../core/staged-changeset-storage.js";
|
|
6
12
|
import { telemetryMiddleware } from "./telemetry-middleware.js";
|
|
13
|
+
import { ClaudeCodeClient } from "../coding-agents/claude-code-client.js";
|
|
14
|
+
import { CopilotClient } from "../coding-agents/copilot-client.js";
|
|
15
|
+
import { detectAvailableClients, selectChatClient } from "../coding-agents/chat-utils.js";
|
|
16
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
17
|
+
import { AnnotationCreateSchema, AnnotationUpdateSchema, AnnotationReplyCreateSchema, LayerNameSchema, IdSchema, AnnotationFilterSchema, ErrorResponseSchema, HealthResponseSchema, AnnotationSchema, AnnotationReplySchema, AnnotationsListSchema, ChangesetsListSchema, ChangesetDetailSchema, ModelResponseSchema, LayerResponseSchema, SpecResponseSchema, AnnotationRepliesSchema, WSMessageSchema } from "./schemas.js";
|
|
18
|
+
const JSONRPC_ERRORS = {
|
|
19
|
+
// Standard JSON-RPC errors
|
|
20
|
+
INVALID_PARAMS: -32602,
|
|
21
|
+
// Invalid method parameters
|
|
22
|
+
METHOD_NOT_FOUND: -32601,
|
|
23
|
+
// Method does not exist
|
|
24
|
+
INTERNAL_ERROR: -32603,
|
|
25
|
+
// Internal server error
|
|
26
|
+
// Custom application errors
|
|
27
|
+
NO_CLIENT_AVAILABLE: -32001
|
|
28
|
+
// No chat client available
|
|
29
|
+
};
|
|
7
30
|
class VisualizationServer {
|
|
8
31
|
app;
|
|
9
32
|
model;
|
|
10
33
|
_server;
|
|
34
|
+
// Bun.serve return type, loaded dynamically
|
|
11
35
|
clients = /* @__PURE__ */ new Set();
|
|
12
36
|
watcher;
|
|
13
37
|
annotations = /* @__PURE__ */ new Map();
|
|
14
38
|
// annotationId -> annotation
|
|
15
39
|
annotationsByElement = /* @__PURE__ */ new Map();
|
|
16
40
|
// elementId -> Set<annotationId>
|
|
41
|
+
replies = /* @__PURE__ */ new Map();
|
|
42
|
+
// annotationId -> replies[]
|
|
17
43
|
changesets = /* @__PURE__ */ new Map();
|
|
18
44
|
// changesetId -> changeset
|
|
45
|
+
changesetStorage;
|
|
19
46
|
authToken;
|
|
20
47
|
authEnabled = !0;
|
|
21
|
-
|
|
48
|
+
withDanger = !1;
|
|
49
|
+
// Danger mode disabled by default
|
|
50
|
+
viewerPath;
|
|
51
|
+
// Optional custom viewer path
|
|
22
52
|
activeChatProcesses = /* @__PURE__ */ new Map();
|
|
23
53
|
// conversationId -> Bun.spawn process
|
|
24
54
|
chatConversationCounter = 0;
|
|
55
|
+
selectedChatClient;
|
|
56
|
+
// Selected chat client for server
|
|
57
|
+
chatInitializationError;
|
|
58
|
+
// Store initialization error for status endpoint
|
|
59
|
+
/**
|
|
60
|
+
* Valid spec node IDs per layer
|
|
61
|
+
* Dynamically loaded from compiled spec (spec/dist/{layer}.json at module load time)
|
|
62
|
+
* Used to validate and resolve viewer spec node IDs
|
|
63
|
+
* Prevents silent drift when node types are added to spec/schemas/nodes/
|
|
64
|
+
*/
|
|
65
|
+
static VALID_SPEC_NODE_IDS = VisualizationServer.loadValidSpecNodeIds();
|
|
66
|
+
/**
|
|
67
|
+
* Load valid spec node IDs dynamically from bundled spec files
|
|
68
|
+
* Extracts node_types arrays from each layer's compiled schema file
|
|
69
|
+
* This ensures the list is always in sync with the spec build output
|
|
70
|
+
* If any file cannot be loaded, falls back to a minimal safe set
|
|
71
|
+
*/
|
|
72
|
+
static loadValidSpecNodeIds() {
|
|
73
|
+
const result = {}, requiredLayers = [
|
|
74
|
+
"motivation",
|
|
75
|
+
"business",
|
|
76
|
+
"security",
|
|
77
|
+
"application",
|
|
78
|
+
"technology",
|
|
79
|
+
"api",
|
|
80
|
+
"data-model",
|
|
81
|
+
"data-store",
|
|
82
|
+
"ux",
|
|
83
|
+
"navigation",
|
|
84
|
+
"apm",
|
|
85
|
+
"testing"
|
|
86
|
+
], __dirname = path.dirname(fileURLToPath(import.meta.url)), bundledDir = path.join(__dirname, "..", "schemas", "bundled");
|
|
87
|
+
for (const layer of requiredLayers)
|
|
88
|
+
try {
|
|
89
|
+
const specFilePath = path.join(bundledDir, `${layer}.json`), nodeTypes = require2(specFilePath).layer?.node_types;
|
|
90
|
+
Array.isArray(nodeTypes) && nodeTypes.length > 0 ? result[layer] = nodeTypes : (console.warn(`[SPEC-LOAD-001] Layer ${layer} has no node_types in spec file`), result[layer] = []);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`[SPEC-LOAD-002] Failed to load spec node IDs for layer ${layer}: ${getErrorMessage(error)}`), result[layer] = [];
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Fallback spec node IDs used when type extraction from elementId fails
|
|
98
|
+
* These are used when no valid spec node ID can be extracted from the elementId
|
|
99
|
+
* or found through other resolution strategies
|
|
100
|
+
*/
|
|
101
|
+
static LAYER_FALLBACK = {
|
|
102
|
+
security: "business.businessprocess",
|
|
103
|
+
technology: "application.applicationservice",
|
|
104
|
+
api: "business.businessprocess",
|
|
105
|
+
"data-model": "data-store.database",
|
|
106
|
+
"data-store": "data-store.database",
|
|
107
|
+
ux: "application.applicationcomponent",
|
|
108
|
+
navigation: "application.applicationcomponent",
|
|
109
|
+
apm: "application.applicationservice",
|
|
110
|
+
testing: "business.businessprocess"
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* All valid spec node IDs across all layers (cached Set for O(1) lookup)
|
|
114
|
+
*/
|
|
115
|
+
static ALL_VALID_IDS = (() => {
|
|
116
|
+
const ids = /* @__PURE__ */ new Set();
|
|
117
|
+
return Object.values(VisualizationServer.VALID_SPEC_NODE_IDS).forEach((layerIds) => {
|
|
118
|
+
layerIds.forEach((id) => ids.add(id));
|
|
119
|
+
}), ids;
|
|
120
|
+
})();
|
|
25
121
|
constructor(model, options) {
|
|
26
|
-
this.app = new
|
|
122
|
+
this.app = new OpenAPIHono(), this.model = model, this.loadBunAdapters(), this.authEnabled = options?.authEnabled ?? process.env.DR_AUTH_ENABLED !== "false", this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken(), this.withDanger = options?.withDanger || !1, this.viewerPath = options?.viewerPath || new URL("../viewer", import.meta.url).pathname, !this.authEnabled && options?.authToken && console.warn("[VisualizationServer] Warning: authToken provided but authEnabled is false. The token will be generated but not used for authentication. Set authEnabled: true to enable authentication."), this.app.use("/*", cors()), this.app.use("/*", telemetryMiddleware), this.app.use("/api/*", async (c, next) => {
|
|
27
123
|
if (!this.authEnabled)
|
|
28
124
|
return next();
|
|
29
125
|
const authHeader = c.req.header("Authorization"), queryToken = c.req.query("token");
|
|
30
126
|
let providedToken = null;
|
|
31
127
|
return authHeader && authHeader.startsWith("Bearer ") ? providedToken = authHeader.substring(7) : queryToken && (providedToken = queryToken), providedToken ? providedToken !== this.authToken ? c.json({ error: "Invalid authentication token" }, 403) : next() : c.json({ error: "Authentication required. Please provide a valid token." }, 401);
|
|
32
|
-
}), this.setupRoutes(), this.authEnabled && process.env.VERBOSE && console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`)
|
|
128
|
+
}), this.setupRoutes(), this.authEnabled && process.env.VERBOSE && console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`), this.initializeChatClients().catch((error) => {
|
|
129
|
+
this.chatInitializationError = error instanceof Error ? error : new Error(String(error)), console.error("[Chat] Failed to initialize chat clients:", error);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Load Bun-specific WebSocket adapters if available
|
|
134
|
+
* This is called in the constructor to support WebSocket functionality in Bun runtime
|
|
135
|
+
* In Node.js/tsx environments (e.g., generating OpenAPI spec), these imports are skipped
|
|
136
|
+
*/
|
|
137
|
+
loadBunAdapters() {
|
|
138
|
+
if (typeof global.Bun < "u")
|
|
139
|
+
try {
|
|
140
|
+
const honoModule = require2("hono/bun");
|
|
141
|
+
upgradeWebSocket = honoModule.upgradeWebSocket, websocket = honoModule.websocket;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
console.warn(`[Server] WARNING: Could not load WebSocket adapters from hono/bun: ${message}. WebSocket endpoints (/ws) will not be available. Please ensure 'hono' is installed and the Bun runtime is properly configured.`), process.env.VERBOSE && error instanceof Error && error.stack && console.debug("[Server] Stack trace:", error.stack);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Detect and initialize available chat clients
|
|
149
|
+
*/
|
|
150
|
+
async initializeChatClients() {
|
|
151
|
+
const clients = await detectAvailableClients();
|
|
152
|
+
this.selectedChatClient = selectChatClient(clients, null), this.selectedChatClient && process.env.VERBOSE ? console.log(`[Chat] Using chat client: ${this.selectedChatClient.getClientName()}`) : clients.length === 0 && process.env.VERBOSE && console.log("[Chat] No chat clients available");
|
|
33
153
|
}
|
|
34
154
|
/**
|
|
35
155
|
* Generate a random auth token
|
|
36
156
|
*/
|
|
37
157
|
generateAuthToken() {
|
|
38
|
-
return `dr-${
|
|
158
|
+
return `dr-${randomBytes(24).toString("base64url")}`;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Shared annotation update logic for PUT/PATCH handlers
|
|
162
|
+
* Performs partial updates with atomic operations
|
|
163
|
+
*/
|
|
164
|
+
async updateAnnotation(annotationId, body) {
|
|
165
|
+
const annotation = this.annotations.get(annotationId);
|
|
166
|
+
if (!annotation)
|
|
167
|
+
return null;
|
|
168
|
+
const updatedAnnotation = {
|
|
169
|
+
...annotation,
|
|
170
|
+
content: body.content ?? annotation.content,
|
|
171
|
+
tags: body.tags ?? annotation.tags,
|
|
172
|
+
resolved: body.resolved ?? annotation.resolved,
|
|
173
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
174
|
+
};
|
|
175
|
+
return this.annotations.set(annotationId, updatedAnnotation), await this.broadcastMessage({
|
|
176
|
+
type: "annotation.updated",
|
|
177
|
+
annotationId: updatedAnnotation.id,
|
|
178
|
+
timestamp: updatedAnnotation.updatedAt
|
|
179
|
+
}), updatedAnnotation;
|
|
39
180
|
}
|
|
40
181
|
/**
|
|
41
182
|
* Setup HTTP routes and WebSocket handler
|
|
42
183
|
*/
|
|
43
184
|
setupRoutes() {
|
|
44
|
-
this.app.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
185
|
+
this.app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
|
|
186
|
+
type: "http",
|
|
187
|
+
scheme: "bearer",
|
|
188
|
+
description: "API token (shown at server startup, pass as Authorization: Bearer <token>)"
|
|
189
|
+
}), this.app.get("/", (c) => this.viewerPath ? this.serveCustomViewer("index.html") : c.html(this.getViewerHTML())), this.viewerPath && this.app.get("/*", async (c, next) => {
|
|
190
|
+
const requestPath = c.req.path;
|
|
191
|
+
return process.env.VERBOSE && console.log(`[ROUTE] Catch-all matched: ${requestPath}`), requestPath.startsWith("/api/") || requestPath === "/ws" || requestPath === "/health" || requestPath === "/api-spec.yaml" || requestPath === "/api-docs" ? (process.env.VERBOSE && console.log(`[ROUTE] Catch-all delegating to next handler for: ${requestPath}`), next()) : (process.env.VERBOSE && console.log(`[ROUTE] Catch-all serving custom viewer file: ${requestPath}`), this.serveCustomViewer(requestPath.substring(1)));
|
|
192
|
+
});
|
|
193
|
+
const healthRoute = createRoute({
|
|
194
|
+
method: "get",
|
|
195
|
+
path: "/health",
|
|
196
|
+
tags: ["Health"],
|
|
197
|
+
summary: "Health check",
|
|
198
|
+
description: "Check if the server is running and healthy",
|
|
199
|
+
responses: {
|
|
200
|
+
200: {
|
|
201
|
+
description: "Server is healthy",
|
|
202
|
+
content: {
|
|
203
|
+
"application/json": {
|
|
204
|
+
schema: HealthResponseSchema
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
this.app.openapi(healthRoute, (c) => {
|
|
211
|
+
const response = {
|
|
212
|
+
status: this.chatInitializationError ? "degraded" : "ok",
|
|
213
|
+
version: "0.1.0"
|
|
214
|
+
};
|
|
215
|
+
return this.chatInitializationError && (response.warnings = [
|
|
216
|
+
`Chat service unavailable: ${getErrorMessage(this.chatInitializationError)}`
|
|
217
|
+
]), c.json(response);
|
|
218
|
+
});
|
|
219
|
+
const getModelRoute = createRoute({
|
|
220
|
+
method: "get",
|
|
221
|
+
path: "/api/model",
|
|
222
|
+
tags: ["Model"],
|
|
223
|
+
summary: "Get full model",
|
|
224
|
+
description: "Retrieve the complete architecture model across all layers",
|
|
225
|
+
security: [{ bearerAuth: [] }],
|
|
226
|
+
responses: {
|
|
227
|
+
200: {
|
|
228
|
+
description: "Model data retrieved successfully",
|
|
229
|
+
content: {
|
|
230
|
+
"application/json": {
|
|
231
|
+
schema: ModelResponseSchema
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
500: {
|
|
236
|
+
description: "Server error",
|
|
237
|
+
content: {
|
|
238
|
+
"application/json": {
|
|
239
|
+
schema: ErrorResponseSchema
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
this.app.openapi(getModelRoute, async (c) => {
|
|
246
|
+
process.env.VERBOSE && console.log("[ROUTE] /api/model handler called");
|
|
48
247
|
try {
|
|
248
|
+
process.env.VERBOSE && console.log("[ROUTE] /api/model serializing model...");
|
|
49
249
|
const modelData = await this.serializeModel();
|
|
50
|
-
return c.json(modelData);
|
|
250
|
+
return process.env.VERBOSE && console.log(`[ROUTE] /api/model returning ${Object.keys(modelData.layers || {}).length} layers`), c.json(modelData);
|
|
51
251
|
} catch (error) {
|
|
52
|
-
const message =
|
|
53
|
-
return c.json({ error: message }, 500);
|
|
252
|
+
const message = getErrorMessage(error);
|
|
253
|
+
return console.error(`[ROUTE] /api/model error: ${message}`), c.json({ error: message }, 500);
|
|
54
254
|
}
|
|
55
|
-
})
|
|
255
|
+
});
|
|
256
|
+
const getLayerRoute = createRoute({
|
|
257
|
+
method: "get",
|
|
258
|
+
path: "/api/layers/:layerName",
|
|
259
|
+
tags: ["Model"],
|
|
260
|
+
summary: "Get layer",
|
|
261
|
+
description: "Retrieve a specific layer with all its elements",
|
|
262
|
+
security: [{ bearerAuth: [] }],
|
|
263
|
+
request: {
|
|
264
|
+
params: z.object({ layerName: LayerNameSchema })
|
|
265
|
+
},
|
|
266
|
+
responses: {
|
|
267
|
+
200: {
|
|
268
|
+
description: "Layer retrieved successfully",
|
|
269
|
+
content: {
|
|
270
|
+
"application/json": {
|
|
271
|
+
schema: LayerResponseSchema
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
404: {
|
|
276
|
+
description: "Layer not found",
|
|
277
|
+
content: {
|
|
278
|
+
"application/json": {
|
|
279
|
+
schema: ErrorResponseSchema
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
500: {
|
|
284
|
+
description: "Server error",
|
|
285
|
+
content: {
|
|
286
|
+
"application/json": {
|
|
287
|
+
schema: ErrorResponseSchema
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
this.app.openapi(getLayerRoute, async (c) => {
|
|
56
294
|
try {
|
|
57
|
-
const layerName = c.req.
|
|
295
|
+
const { layerName } = c.req.valid("param"), layer = await this.model.getLayer(layerName);
|
|
58
296
|
if (!layer)
|
|
59
297
|
return c.json({ error: "Layer not found" }, 404);
|
|
60
298
|
const elements = layer.listElements().map((e) => e.toJSON());
|
|
@@ -64,21 +302,37 @@ class VisualizationServer {
|
|
|
64
302
|
elementCount: elements.length
|
|
65
303
|
});
|
|
66
304
|
} catch (error) {
|
|
67
|
-
const message =
|
|
305
|
+
const message = getErrorMessage(error);
|
|
68
306
|
return c.json({ error: message }, 500);
|
|
69
307
|
}
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
308
|
+
});
|
|
309
|
+
const getSpecRoute = createRoute({
|
|
310
|
+
method: "get",
|
|
311
|
+
path: "/api/spec",
|
|
312
|
+
tags: ["Schema"],
|
|
313
|
+
summary: "Get JSON schemas",
|
|
314
|
+
description: "Retrieve all JSON Schema definitions used by the system",
|
|
315
|
+
security: [{ bearerAuth: [] }],
|
|
316
|
+
responses: {
|
|
317
|
+
200: {
|
|
318
|
+
description: "Schemas retrieved successfully",
|
|
319
|
+
content: {
|
|
320
|
+
"application/json": {
|
|
321
|
+
schema: SpecResponseSchema
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
500: {
|
|
326
|
+
description: "Server error",
|
|
327
|
+
content: {
|
|
328
|
+
"application/json": {
|
|
329
|
+
schema: ErrorResponseSchema
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
80
333
|
}
|
|
81
|
-
})
|
|
334
|
+
});
|
|
335
|
+
this.app.openapi(getSpecRoute, async (c) => {
|
|
82
336
|
try {
|
|
83
337
|
const schemas = await this.loadSchemas();
|
|
84
338
|
return c.json({
|
|
@@ -90,19 +344,33 @@ class VisualizationServer {
|
|
|
90
344
|
schemaCount: Object.keys(schemas).length
|
|
91
345
|
});
|
|
92
346
|
} catch (error) {
|
|
93
|
-
const message =
|
|
347
|
+
const message = getErrorMessage(error);
|
|
94
348
|
return c.json({ error: message }, 500);
|
|
95
349
|
}
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
350
|
+
});
|
|
351
|
+
const getAnnotationsRoute = createRoute({
|
|
352
|
+
method: "get",
|
|
353
|
+
path: "/api/annotations",
|
|
354
|
+
tags: ["Annotations"],
|
|
355
|
+
summary: "Get annotations",
|
|
356
|
+
description: "Retrieve all annotations, optionally filtered by element",
|
|
357
|
+
security: [{ bearerAuth: [] }],
|
|
358
|
+
request: {
|
|
359
|
+
query: AnnotationFilterSchema
|
|
360
|
+
},
|
|
361
|
+
responses: {
|
|
362
|
+
200: {
|
|
363
|
+
description: "Annotations retrieved successfully",
|
|
364
|
+
content: {
|
|
365
|
+
"application/json": {
|
|
366
|
+
schema: AnnotationsListSchema
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
103
370
|
}
|
|
104
|
-
})
|
|
105
|
-
|
|
371
|
+
});
|
|
372
|
+
this.app.openapi(getAnnotationsRoute, (c) => {
|
|
373
|
+
const elementId = c.req.valid("query").elementId;
|
|
106
374
|
if (elementId) {
|
|
107
375
|
const annotationIds = this.annotationsByElement.get(elementId) || /* @__PURE__ */ new Set(), annotations = Array.from(annotationIds).map((id) => this.annotations.get(id)).filter((a) => a !== void 0);
|
|
108
376
|
return c.json({ annotations });
|
|
@@ -110,22 +378,72 @@ class VisualizationServer {
|
|
|
110
378
|
const annotations = Array.from(this.annotations.values());
|
|
111
379
|
return c.json({ annotations });
|
|
112
380
|
}
|
|
113
|
-
})
|
|
381
|
+
});
|
|
382
|
+
const createAnnotationRoute = createRoute({
|
|
383
|
+
method: "post",
|
|
384
|
+
path: "/api/annotations",
|
|
385
|
+
tags: ["Annotations"],
|
|
386
|
+
summary: "Create annotation",
|
|
387
|
+
description: "Create a new annotation on a model element",
|
|
388
|
+
security: [{ bearerAuth: [] }],
|
|
389
|
+
request: {
|
|
390
|
+
body: {
|
|
391
|
+
content: {
|
|
392
|
+
"application/json": {
|
|
393
|
+
schema: AnnotationCreateSchema
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
responses: {
|
|
399
|
+
201: {
|
|
400
|
+
description: "Annotation created successfully",
|
|
401
|
+
content: {
|
|
402
|
+
"application/json": {
|
|
403
|
+
schema: AnnotationSchema
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
400: {
|
|
408
|
+
description: "Invalid request body",
|
|
409
|
+
content: {
|
|
410
|
+
"application/json": {
|
|
411
|
+
schema: ErrorResponseSchema
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
404: {
|
|
416
|
+
description: "Element not found",
|
|
417
|
+
content: {
|
|
418
|
+
"application/json": {
|
|
419
|
+
schema: ErrorResponseSchema
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
500: {
|
|
424
|
+
description: "Server error",
|
|
425
|
+
content: {
|
|
426
|
+
"application/json": {
|
|
427
|
+
schema: ErrorResponseSchema
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
this.app.openapi(createAnnotationRoute, async (c) => {
|
|
114
434
|
try {
|
|
115
|
-
const body =
|
|
116
|
-
if (!body.elementId || !body.content || !body.author)
|
|
117
|
-
return c.json({
|
|
118
|
-
error: "Missing required fields: elementId, content, author"
|
|
119
|
-
}, 400);
|
|
435
|
+
const body = c.req.valid("json");
|
|
120
436
|
if (!await this.findElement(body.elementId))
|
|
121
437
|
return c.json({ error: "Element not found" }, 404);
|
|
122
438
|
const annotation = {
|
|
123
439
|
id: this.generateAnnotationId(),
|
|
124
|
-
elementId:
|
|
125
|
-
author:
|
|
126
|
-
content:
|
|
440
|
+
elementId: body.elementId,
|
|
441
|
+
author: body.author,
|
|
442
|
+
content: body.content,
|
|
127
443
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
128
|
-
tags: body.tags
|
|
444
|
+
tags: body.tags,
|
|
445
|
+
resolved: !1
|
|
446
|
+
// Default to unresolved
|
|
129
447
|
};
|
|
130
448
|
return this.annotations.set(annotation.id, annotation), this.annotationsByElement.has(annotation.elementId) || this.annotationsByElement.set(annotation.elementId, /* @__PURE__ */ new Set()), this.annotationsByElement.get(annotation.elementId).add(annotation.id), await this.broadcastMessage({
|
|
131
449
|
type: "annotation.added",
|
|
@@ -134,30 +452,200 @@ class VisualizationServer {
|
|
|
134
452
|
timestamp: annotation.createdAt
|
|
135
453
|
}), c.json(annotation, 201);
|
|
136
454
|
} catch (error) {
|
|
137
|
-
const message =
|
|
455
|
+
const message = getErrorMessage(error);
|
|
138
456
|
return c.json({ error: message }, 500);
|
|
139
457
|
}
|
|
140
|
-
})
|
|
458
|
+
});
|
|
459
|
+
const getAnnotationRoute = createRoute({
|
|
460
|
+
method: "get",
|
|
461
|
+
path: "/api/annotations/:annotationId",
|
|
462
|
+
tags: ["Annotations"],
|
|
463
|
+
summary: "Get annotation by ID",
|
|
464
|
+
description: "Retrieve a specific annotation by its ID",
|
|
465
|
+
security: [{ bearerAuth: [] }],
|
|
466
|
+
request: {
|
|
467
|
+
params: z.object({ annotationId: IdSchema })
|
|
468
|
+
},
|
|
469
|
+
responses: {
|
|
470
|
+
200: {
|
|
471
|
+
description: "Annotation retrieved successfully",
|
|
472
|
+
content: {
|
|
473
|
+
"application/json": {
|
|
474
|
+
schema: AnnotationSchema
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
404: {
|
|
479
|
+
description: "Annotation not found",
|
|
480
|
+
content: {
|
|
481
|
+
"application/json": {
|
|
482
|
+
schema: ErrorResponseSchema
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
500: {
|
|
487
|
+
description: "Server error",
|
|
488
|
+
content: {
|
|
489
|
+
"application/json": {
|
|
490
|
+
schema: ErrorResponseSchema
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
this.app.openapi(getAnnotationRoute, async (c) => {
|
|
141
497
|
try {
|
|
142
|
-
const annotationId = c.req.
|
|
143
|
-
|
|
144
|
-
return c.json({ error: "Annotation not found" }, 404);
|
|
145
|
-
const body = await c.req.json();
|
|
146
|
-
return body.content !== void 0 && (annotation.content = String(body.content)), body.tags !== void 0 && (annotation.tags = body.tags), annotation.updatedAt = (/* @__PURE__ */ new Date()).toISOString(), await this.broadcastMessage({
|
|
147
|
-
type: "annotation.updated",
|
|
148
|
-
annotationId: annotation.id,
|
|
149
|
-
timestamp: annotation.updatedAt
|
|
150
|
-
}), c.json(annotation);
|
|
498
|
+
const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
|
|
499
|
+
return annotation ? c.json(annotation) : c.json({ error: "Annotation not found" }, 404);
|
|
151
500
|
} catch (error) {
|
|
152
|
-
const message =
|
|
501
|
+
const message = getErrorMessage(error);
|
|
502
|
+
return c.json({ error: message }, 500);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
const putAnnotationRoute = createRoute({
|
|
506
|
+
method: "put",
|
|
507
|
+
path: "/api/annotations/:annotationId",
|
|
508
|
+
tags: ["Annotations"],
|
|
509
|
+
summary: "Update annotation (partial)",
|
|
510
|
+
description: "Update an existing annotation with partial fields (NOTE: partial updates, use PATCH for recommended behavior)",
|
|
511
|
+
security: [{ bearerAuth: [] }],
|
|
512
|
+
request: {
|
|
513
|
+
params: z.object({ annotationId: IdSchema }),
|
|
514
|
+
body: {
|
|
515
|
+
content: {
|
|
516
|
+
"application/json": {
|
|
517
|
+
schema: AnnotationUpdateSchema
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
},
|
|
522
|
+
responses: {
|
|
523
|
+
200: {
|
|
524
|
+
description: "Annotation updated successfully",
|
|
525
|
+
content: {
|
|
526
|
+
"application/json": {
|
|
527
|
+
schema: AnnotationSchema
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
404: {
|
|
532
|
+
description: "Annotation not found",
|
|
533
|
+
content: {
|
|
534
|
+
"application/json": {
|
|
535
|
+
schema: ErrorResponseSchema
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
500: {
|
|
540
|
+
description: "Server error",
|
|
541
|
+
content: {
|
|
542
|
+
"application/json": {
|
|
543
|
+
schema: ErrorResponseSchema
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
this.app.openapi(putAnnotationRoute, async (c) => {
|
|
550
|
+
try {
|
|
551
|
+
const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), updatedAnnotation = await this.updateAnnotation(annotationId, body);
|
|
552
|
+
return updatedAnnotation ? c.json(updatedAnnotation) : c.json({ error: "Annotation not found" }, 404);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
const message = getErrorMessage(error);
|
|
555
|
+
return c.json({ error: message }, 500);
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
const patchAnnotationRoute = createRoute({
|
|
559
|
+
method: "patch",
|
|
560
|
+
path: "/api/annotations/:annotationId",
|
|
561
|
+
tags: ["Annotations"],
|
|
562
|
+
summary: "Partially update annotation",
|
|
563
|
+
description: "Partially update an existing annotation (recommended over PUT)",
|
|
564
|
+
security: [{ bearerAuth: [] }],
|
|
565
|
+
request: {
|
|
566
|
+
params: z.object({ annotationId: IdSchema }),
|
|
567
|
+
body: {
|
|
568
|
+
content: {
|
|
569
|
+
"application/json": {
|
|
570
|
+
schema: AnnotationUpdateSchema
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
responses: {
|
|
576
|
+
200: {
|
|
577
|
+
description: "Annotation updated successfully",
|
|
578
|
+
content: {
|
|
579
|
+
"application/json": {
|
|
580
|
+
schema: AnnotationSchema
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
404: {
|
|
585
|
+
description: "Annotation not found",
|
|
586
|
+
content: {
|
|
587
|
+
"application/json": {
|
|
588
|
+
schema: ErrorResponseSchema
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
},
|
|
592
|
+
500: {
|
|
593
|
+
description: "Server error",
|
|
594
|
+
content: {
|
|
595
|
+
"application/json": {
|
|
596
|
+
schema: ErrorResponseSchema
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
this.app.openapi(patchAnnotationRoute, async (c) => {
|
|
603
|
+
try {
|
|
604
|
+
const { annotationId } = c.req.valid("param"), body = c.req.valid("json"), updatedAnnotation = await this.updateAnnotation(annotationId, body);
|
|
605
|
+
return updatedAnnotation ? c.json(updatedAnnotation) : c.json({ error: "Annotation not found" }, 404);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const message = getErrorMessage(error);
|
|
153
608
|
return c.json({ error: message }, 500);
|
|
154
609
|
}
|
|
155
|
-
})
|
|
610
|
+
});
|
|
611
|
+
const deleteAnnotationRoute = createRoute({
|
|
612
|
+
method: "delete",
|
|
613
|
+
path: "/api/annotations/:annotationId",
|
|
614
|
+
tags: ["Annotations"],
|
|
615
|
+
summary: "Delete annotation",
|
|
616
|
+
description: "Delete an existing annotation",
|
|
617
|
+
security: [{ bearerAuth: [] }],
|
|
618
|
+
request: {
|
|
619
|
+
params: z.object({ annotationId: IdSchema })
|
|
620
|
+
},
|
|
621
|
+
responses: {
|
|
622
|
+
204: {
|
|
623
|
+
description: "Annotation deleted successfully"
|
|
624
|
+
},
|
|
625
|
+
404: {
|
|
626
|
+
description: "Annotation not found",
|
|
627
|
+
content: {
|
|
628
|
+
"application/json": {
|
|
629
|
+
schema: ErrorResponseSchema
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
500: {
|
|
634
|
+
description: "Server error",
|
|
635
|
+
content: {
|
|
636
|
+
"application/json": {
|
|
637
|
+
schema: ErrorResponseSchema
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
this.app.openapi(deleteAnnotationRoute, async (c) => {
|
|
156
644
|
try {
|
|
157
|
-
const annotationId = c.req.
|
|
645
|
+
const { annotationId } = c.req.valid("param"), annotation = this.annotations.get(annotationId);
|
|
158
646
|
if (!annotation)
|
|
159
647
|
return c.json({ error: "Annotation not found" }, 404);
|
|
160
|
-
this.annotations.delete(annotationId);
|
|
648
|
+
this.annotations.delete(annotationId), this.replies.delete(annotationId);
|
|
161
649
|
const elementAnnotations = this.annotationsByElement.get(annotation.elementId);
|
|
162
650
|
return elementAnnotations && elementAnnotations.delete(annotationId), await this.broadcastMessage({
|
|
163
651
|
type: "annotation.deleted",
|
|
@@ -165,142 +653,478 @@ class VisualizationServer {
|
|
|
165
653
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
166
654
|
}), c.body(null, 204);
|
|
167
655
|
} catch (error) {
|
|
168
|
-
const message =
|
|
656
|
+
const message = getErrorMessage(error);
|
|
657
|
+
return c.json({ error: message }, 500);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
const getAnnotationRepliesRoute = createRoute({
|
|
661
|
+
method: "get",
|
|
662
|
+
path: "/api/annotations/:annotationId/replies",
|
|
663
|
+
tags: ["Annotations"],
|
|
664
|
+
summary: "Get annotation replies",
|
|
665
|
+
description: "Retrieve all replies to an annotation",
|
|
666
|
+
security: [{ bearerAuth: [] }],
|
|
667
|
+
request: {
|
|
668
|
+
params: z.object({ annotationId: IdSchema })
|
|
669
|
+
},
|
|
670
|
+
responses: {
|
|
671
|
+
200: {
|
|
672
|
+
description: "Replies retrieved successfully",
|
|
673
|
+
content: {
|
|
674
|
+
"application/json": {
|
|
675
|
+
schema: AnnotationRepliesSchema
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
404: {
|
|
680
|
+
description: "Annotation not found",
|
|
681
|
+
content: {
|
|
682
|
+
"application/json": {
|
|
683
|
+
schema: ErrorResponseSchema
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
this.app.openapi(getAnnotationRepliesRoute, (c) => {
|
|
690
|
+
const { annotationId } = c.req.valid("param");
|
|
691
|
+
if (!this.annotations.get(annotationId))
|
|
692
|
+
return c.json({ error: "Annotation not found" }, 404);
|
|
693
|
+
const replies = this.replies.get(annotationId) || [];
|
|
694
|
+
return c.json({ replies });
|
|
695
|
+
});
|
|
696
|
+
const createAnnotationReplyRoute = createRoute({
|
|
697
|
+
method: "post",
|
|
698
|
+
path: "/api/annotations/:annotationId/replies",
|
|
699
|
+
tags: ["Annotations"],
|
|
700
|
+
summary: "Create annotation reply",
|
|
701
|
+
description: "Add a reply to an annotation",
|
|
702
|
+
security: [{ bearerAuth: [] }],
|
|
703
|
+
request: {
|
|
704
|
+
params: z.object({ annotationId: IdSchema }),
|
|
705
|
+
body: {
|
|
706
|
+
content: {
|
|
707
|
+
"application/json": {
|
|
708
|
+
schema: AnnotationReplyCreateSchema
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
responses: {
|
|
714
|
+
201: {
|
|
715
|
+
description: "Reply created successfully",
|
|
716
|
+
content: {
|
|
717
|
+
"application/json": {
|
|
718
|
+
schema: AnnotationReplySchema
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
404: {
|
|
723
|
+
description: "Annotation not found",
|
|
724
|
+
content: {
|
|
725
|
+
"application/json": {
|
|
726
|
+
schema: ErrorResponseSchema
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
500: {
|
|
731
|
+
description: "Server error",
|
|
732
|
+
content: {
|
|
733
|
+
"application/json": {
|
|
734
|
+
schema: ErrorResponseSchema
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
this.app.openapi(createAnnotationReplyRoute, async (c) => {
|
|
741
|
+
try {
|
|
742
|
+
const { annotationId } = c.req.valid("param");
|
|
743
|
+
if (!this.annotations.get(annotationId))
|
|
744
|
+
return c.json({ error: "Annotation not found" }, 404);
|
|
745
|
+
const body = c.req.valid("json"), reply = {
|
|
746
|
+
id: this.generateReplyId(),
|
|
747
|
+
author: body.author,
|
|
748
|
+
content: body.content,
|
|
749
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
750
|
+
};
|
|
751
|
+
return this.replies.has(annotationId) || this.replies.set(annotationId, []), this.replies.get(annotationId).push(reply), await this.broadcastMessage({
|
|
752
|
+
type: "annotation.reply.added",
|
|
753
|
+
annotationId,
|
|
754
|
+
replyId: reply.id,
|
|
755
|
+
timestamp: reply.createdAt
|
|
756
|
+
}), c.json(reply, 201);
|
|
757
|
+
} catch (error) {
|
|
758
|
+
const message = getErrorMessage(error);
|
|
169
759
|
return c.json({ error: message }, 500);
|
|
170
760
|
}
|
|
171
|
-
})
|
|
761
|
+
});
|
|
762
|
+
const getChangesetsRoute = createRoute({
|
|
763
|
+
method: "get",
|
|
764
|
+
path: "/api/changesets",
|
|
765
|
+
tags: ["Changesets"],
|
|
766
|
+
summary: "Get changesets",
|
|
767
|
+
description: "Retrieve a list of all changesets",
|
|
768
|
+
security: [{ bearerAuth: [] }],
|
|
769
|
+
responses: {
|
|
770
|
+
200: {
|
|
771
|
+
description: "Changesets retrieved successfully",
|
|
772
|
+
content: {
|
|
773
|
+
"application/json": {
|
|
774
|
+
schema: ChangesetsListSchema
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
this.app.openapi(getChangesetsRoute, (c) => {
|
|
172
781
|
const changesets = {};
|
|
173
782
|
for (const [id, changeset] of this.changesets)
|
|
174
783
|
changesets[id] = {
|
|
175
|
-
name: changeset.
|
|
176
|
-
status: changeset.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
elements_count: changeset.changes.changes.length
|
|
784
|
+
name: changeset.name,
|
|
785
|
+
status: changeset.status,
|
|
786
|
+
created: changeset.created,
|
|
787
|
+
changes_count: changeset.changes.length
|
|
180
788
|
};
|
|
181
789
|
return c.json({
|
|
182
790
|
version: "1.0.0",
|
|
183
791
|
changesets
|
|
184
792
|
});
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
} catch (error) {
|
|
214
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
-
return c.json({ error: message }, 500);
|
|
793
|
+
});
|
|
794
|
+
const getChangesetRoute = createRoute({
|
|
795
|
+
method: "get",
|
|
796
|
+
path: "/api/changesets/:changesetId",
|
|
797
|
+
tags: ["Changesets"],
|
|
798
|
+
summary: "Get changeset",
|
|
799
|
+
description: "Retrieve a specific changeset with all its changes",
|
|
800
|
+
security: [{ bearerAuth: [] }],
|
|
801
|
+
request: {
|
|
802
|
+
params: z.object({ changesetId: IdSchema })
|
|
803
|
+
},
|
|
804
|
+
responses: {
|
|
805
|
+
200: {
|
|
806
|
+
description: "Changeset retrieved successfully",
|
|
807
|
+
content: {
|
|
808
|
+
"application/json": {
|
|
809
|
+
schema: ChangesetDetailSchema
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
404: {
|
|
814
|
+
description: "Changeset not found",
|
|
815
|
+
content: {
|
|
816
|
+
"application/json": {
|
|
817
|
+
schema: ErrorResponseSchema
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
216
821
|
}
|
|
217
|
-
})
|
|
822
|
+
});
|
|
823
|
+
this.app.openapi(getChangesetRoute, (c) => {
|
|
824
|
+
const { changesetId } = c.req.valid("param"), changeset = this.changesets.get(changesetId);
|
|
825
|
+
return changeset ? c.json({
|
|
826
|
+
id: changeset.id,
|
|
827
|
+
name: changeset.name,
|
|
828
|
+
description: changeset.description,
|
|
829
|
+
status: changeset.status,
|
|
830
|
+
created: changeset.created,
|
|
831
|
+
modified: changeset.modified,
|
|
832
|
+
baseSnapshot: changeset.baseSnapshot,
|
|
833
|
+
stats: changeset.stats,
|
|
834
|
+
changes: changeset.changes
|
|
835
|
+
}) : c.json({ error: "Changeset not found" }, 404);
|
|
836
|
+
}), this.app.doc("/api-spec.yaml", {
|
|
837
|
+
openapi: "3.1.0",
|
|
838
|
+
info: {
|
|
839
|
+
title: "Documentation Robotics Visualization Server API",
|
|
840
|
+
version: "0.1.0",
|
|
841
|
+
description: "API specification for the DR CLI visualization server",
|
|
842
|
+
contact: {
|
|
843
|
+
name: "Documentation Robotics",
|
|
844
|
+
url: "https://github.com/tinkermonkey/documentation_robotics"
|
|
845
|
+
},
|
|
846
|
+
license: {
|
|
847
|
+
name: "ISC"
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
servers: [
|
|
851
|
+
{ url: "http://localhost:8080", description: "Local development server" }
|
|
852
|
+
],
|
|
853
|
+
tags: [
|
|
854
|
+
{ name: "Health", description: "Server health and status" },
|
|
855
|
+
{ name: "Schema", description: "JSON Schema specifications" },
|
|
856
|
+
{ name: "Model", description: "Architecture model data" },
|
|
857
|
+
{ name: "Changesets", description: "Model changesets and history" },
|
|
858
|
+
{ name: "Annotations", description: "User annotations on model elements" },
|
|
859
|
+
{ name: "WebSocket", description: "Real-time updates via WebSocket" }
|
|
860
|
+
]
|
|
861
|
+
}), this.app.get("/api-docs", swaggerUI({ url: "/api-spec.yaml" })), upgradeWebSocket && this.app.get("/ws", async (c, next) => {
|
|
218
862
|
if (this.authEnabled) {
|
|
219
|
-
const queryToken = c.req.query("token");
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
863
|
+
const queryToken = c.req.query("token"), wsProtocol = c.req.header("Sec-WebSocket-Protocol");
|
|
864
|
+
let protocolToken = null;
|
|
865
|
+
if (wsProtocol) {
|
|
866
|
+
const protocols = wsProtocol.split(",").map((p) => p.trim()), tokenIndex = protocols.indexOf("token");
|
|
867
|
+
tokenIndex !== -1 && protocols.length > tokenIndex + 1 && (protocolToken = protocols[tokenIndex + 1]);
|
|
868
|
+
}
|
|
869
|
+
const authHeader = c.req.header("Authorization"), bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.substring(7) : null, token = queryToken || protocolToken || bearerToken;
|
|
870
|
+
if (!token)
|
|
871
|
+
return c.json({
|
|
872
|
+
error: 'Authentication required. Provide token via: 1) Query param (?token=YOUR_TOKEN), 2) Sec-WebSocket-Protocol header (browser: new WebSocket(url, ["token", "YOUR_TOKEN"])), or 3) Authorization header (Bearer YOUR_TOKEN)'
|
|
873
|
+
}, 401);
|
|
874
|
+
if (token !== this.authToken)
|
|
223
875
|
return c.json({ error: "Invalid authentication token" }, 403);
|
|
876
|
+
protocolToken && c.header("Sec-WebSocket-Protocol", "token");
|
|
224
877
|
}
|
|
225
878
|
return next();
|
|
226
879
|
}, upgradeWebSocket(() => ({
|
|
227
|
-
onOpen: (_evt, ws) => {
|
|
228
|
-
this.
|
|
880
|
+
onOpen: async (_evt, ws) => {
|
|
881
|
+
await this.recordWebSocketEvent("ws.connection.open", {
|
|
882
|
+
"ws.client_count": this.clients.size + 1
|
|
883
|
+
}), this.clients.add(ws), ws.send(JSON.stringify({
|
|
229
884
|
type: "connected",
|
|
230
885
|
version: "0.1.0",
|
|
231
886
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
232
887
|
})), process.env.VERBOSE && console.log(`[WebSocket] Client connected (total: ${this.clients.size})`);
|
|
233
888
|
},
|
|
234
|
-
onClose: (_evt, ws) => {
|
|
235
|
-
this.
|
|
889
|
+
onClose: async (_evt, ws) => {
|
|
890
|
+
await this.recordWebSocketEvent("ws.connection.close", {
|
|
891
|
+
"ws.client_count": this.clients.size - 1
|
|
892
|
+
}), this.clients.delete(ws), process.env.VERBOSE && console.log(`[WebSocket] Client disconnected (total: ${this.clients.size})`);
|
|
236
893
|
},
|
|
237
894
|
onMessage: async (message, ws) => {
|
|
895
|
+
const messageStartTime = Date.now();
|
|
896
|
+
let messageType = "unknown";
|
|
238
897
|
try {
|
|
239
898
|
const rawData = message.data ?? message;
|
|
240
899
|
let msgStr;
|
|
241
900
|
typeof rawData == "string" ? msgStr = rawData : rawData instanceof Buffer || rawData instanceof Uint8Array ? msgStr = new TextDecoder().decode(rawData) : msgStr = String(rawData);
|
|
242
|
-
const
|
|
243
|
-
|
|
901
|
+
const rawMessage = JSON.parse(msgStr), validationResult = WSMessageSchema.safeParse(rawMessage);
|
|
902
|
+
if (!validationResult.success) {
|
|
903
|
+
const errorMsg = validationResult.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
904
|
+
console.error("Invalid WebSocket message format:", errorMsg), ws.send(JSON.stringify({ error: "Invalid message format", details: errorMsg }));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const data = validationResult.data;
|
|
908
|
+
"jsonrpc" in data && data.jsonrpc === "2.0" ? "method" in data && typeof data.method == "string" ? messageType = data.method : messageType = data.id?.toString() || "jsonrpc-response" : "type" in data ? messageType = data.type : messageType = "unknown", await this.recordWebSocketEvent("ws.message.received", {
|
|
909
|
+
"ws.message.type": messageType,
|
|
910
|
+
"ws.message.size_bytes": msgStr.length
|
|
911
|
+
}), "jsonrpc" in data && data.jsonrpc === "2.0" ? await this.handleJSONRPCMessage(ws, data) : await this.handleWSMessage(ws, data);
|
|
912
|
+
const durationMs = Date.now() - messageStartTime;
|
|
913
|
+
await this.recordWebSocketEvent("ws.message.processed", {
|
|
914
|
+
"ws.message.type": messageType,
|
|
915
|
+
"ws.message.duration_ms": durationMs,
|
|
916
|
+
"ws.message.status": "success"
|
|
917
|
+
});
|
|
244
918
|
} catch (error) {
|
|
245
|
-
const errorMsg = error
|
|
246
|
-
|
|
919
|
+
const errorMsg = getErrorMessage(error), durationMs = Date.now() - messageStartTime;
|
|
920
|
+
await this.recordWebSocketEvent("ws.message.error", {
|
|
921
|
+
"ws.message.type": messageType,
|
|
922
|
+
"ws.message.duration_ms": durationMs,
|
|
923
|
+
"ws.message.status": "error",
|
|
924
|
+
"error.message": errorMsg
|
|
925
|
+
}), console.warn(`[WebSocket] Error handling ${messageType} message (${durationMs}ms): ${errorMsg}`), ws.send(JSON.stringify({
|
|
247
926
|
type: "error",
|
|
248
927
|
message: errorMsg
|
|
249
928
|
}));
|
|
250
929
|
}
|
|
251
930
|
},
|
|
252
|
-
onError: (error) => {
|
|
253
|
-
const message =
|
|
254
|
-
|
|
931
|
+
onError: async (error) => {
|
|
932
|
+
const message = getErrorMessage(error);
|
|
933
|
+
await this.recordWebSocketEvent("ws.error", {
|
|
934
|
+
"error.type": error instanceof Error ? error.constructor.name : "unknown",
|
|
935
|
+
"error.message": message
|
|
936
|
+
}), console.error("[WebSocket] Error:", error);
|
|
255
937
|
}
|
|
256
938
|
})));
|
|
257
939
|
}
|
|
258
940
|
/**
|
|
259
|
-
*
|
|
941
|
+
* Get OpenAPI specification document
|
|
942
|
+
* Provides a stable public API for OpenAPI spec generation.
|
|
943
|
+
*
|
|
944
|
+
* BEHAVIOR: Returns an OpenAPI spec with the version specified in `config.openapi`.
|
|
945
|
+
* The returned document will have whatever OpenAPI version was passed in the config.
|
|
946
|
+
*
|
|
947
|
+
* NOTE: Hono's getOpenAPI31Document() passes the config directly through and
|
|
948
|
+
* uses the specified version in the output. There is no forced version conversion.
|
|
949
|
+
*/
|
|
950
|
+
getOpenAPIDocument(config) {
|
|
951
|
+
return this.app.getOpenAPI31Document(config);
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Serialize model to graph format for client consumption.
|
|
955
|
+
*
|
|
956
|
+
* Returns { nodes, links } where:
|
|
957
|
+
* - nodes: flat array of all elements across all layers
|
|
958
|
+
* - links: intra-layer relationships (carry layer_id) and cross-layer
|
|
959
|
+
* references (carry source_layer_id + target_layer_id)
|
|
960
|
+
*
|
|
961
|
+
* Links are deduplicated: if the same relationship appears on multiple
|
|
962
|
+
* elements (e.g. stored on both source and target), only one entry is kept.
|
|
260
963
|
*/
|
|
261
964
|
async serializeModel() {
|
|
262
|
-
const
|
|
263
|
-
for (const [
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
965
|
+
const nodes = [], linksMap = /* @__PURE__ */ new Map(), elementLayerMap = /* @__PURE__ */ new Map();
|
|
966
|
+
for (const [layerName, layer] of this.model.layers)
|
|
967
|
+
for (const e of layer.listElements()) {
|
|
968
|
+
const layerId = e.layer_id || layerName;
|
|
969
|
+
e.id && elementLayerMap.set(e.id, layerId), e.path && e.path !== e.id && elementLayerMap.set(e.path, layerId);
|
|
970
|
+
}
|
|
971
|
+
for (const [layerName, layer] of this.model.layers)
|
|
972
|
+
for (const e of layer.listElements()) {
|
|
973
|
+
const layerId = e.layer_id || layerName, viewerSpecNodeId = this.resolveViewerSpecNodeId(e.id, layerId, e.type, e.spec_node_id), node = {
|
|
974
|
+
id: e.id,
|
|
975
|
+
spec_node_id: viewerSpecNodeId,
|
|
976
|
+
type: e.type,
|
|
977
|
+
layer_id: layerId,
|
|
978
|
+
name: e.name
|
|
269
979
|
};
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
980
|
+
e.description && (node.description = e.description), e.attributes && Object.keys(e.attributes).length > 0 && (node.attributes = e.attributes), e.source_reference && (node.source_reference = e.source_reference), e.metadata && (node.metadata = e.metadata), nodes.push(node);
|
|
981
|
+
for (const rel of e.relationships) {
|
|
982
|
+
const linkId = `rel:${rel.source}:${rel.target}:${rel.predicate}`;
|
|
983
|
+
linksMap.has(linkId) || linksMap.set(linkId, {
|
|
984
|
+
id: linkId,
|
|
985
|
+
source: rel.source,
|
|
986
|
+
target: rel.target,
|
|
987
|
+
type: rel.predicate,
|
|
988
|
+
layer_id: layerId
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
for (const ref of e.references) {
|
|
992
|
+
const linkId = `ref:${ref.source}:${ref.target}:${ref.type}`;
|
|
993
|
+
linksMap.has(linkId) || linksMap.set(linkId, {
|
|
994
|
+
id: linkId,
|
|
995
|
+
source: ref.source,
|
|
996
|
+
target: ref.target,
|
|
997
|
+
type: ref.type,
|
|
998
|
+
source_layer_id: elementLayerMap.get(ref.source) || layerId,
|
|
999
|
+
target_layer_id: elementLayerMap.get(ref.target) || "unknown"
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
for (const rel of this.model.relationships.getAll()) {
|
|
1004
|
+
const linkId = `rel:${rel.source}:${rel.target}:${rel.predicate}`;
|
|
1005
|
+
if (!linksMap.has(linkId)) {
|
|
1006
|
+
const layerId = rel.layer || elementLayerMap.get(rel.source);
|
|
1007
|
+
if (!layerId)
|
|
1008
|
+
continue;
|
|
1009
|
+
const link = {
|
|
1010
|
+
id: linkId,
|
|
1011
|
+
source: rel.source,
|
|
1012
|
+
target: rel.target,
|
|
1013
|
+
type: rel.predicate
|
|
1014
|
+
};
|
|
1015
|
+
rel.targetLayer ? (link.source_layer_id = layerId, link.target_layer_id = rel.targetLayer) : link.layer_id = layerId, linksMap.set(linkId, link);
|
|
1016
|
+
}
|
|
276
1017
|
}
|
|
277
1018
|
return {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
totalElements: Array.from(this.model.layers.values()).reduce((sum, layer) => sum + layer.listElements().length, 0)
|
|
1019
|
+
nodes,
|
|
1020
|
+
links: Array.from(linksMap.values())
|
|
281
1021
|
};
|
|
282
1022
|
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve viewer spec node ID with fallback logic
|
|
1025
|
+
* Handles multiple resolution strategies:
|
|
1026
|
+
* 1. Valid pass-through: Return specNodeId if it's already a valid spec node ID
|
|
1027
|
+
* 2. Dot-format extraction: Extract type from elementId in "layer.type.name" format and construct candidate
|
|
1028
|
+
* 3. Hyphen-format extraction: Extract type from elementId in "layer-type-name" format and construct candidate
|
|
1029
|
+
* 4. Type-parameter fallback: If extracted type differs from type parameter, retry with type parameter
|
|
1030
|
+
* 5. Layer-specific fallback: Use LAYER_FALLBACK map for layers with limited viewer support
|
|
1031
|
+
*
|
|
1032
|
+
* @param elementId - The element ID (can be in dot-format or hyphen-format)
|
|
1033
|
+
* @param layer - The layer name (canonical form)
|
|
1034
|
+
* @param type - The element type field
|
|
1035
|
+
* @param specNodeId - The spec node ID if already known
|
|
1036
|
+
* @returns The resolved spec node ID string
|
|
1037
|
+
*/
|
|
1038
|
+
resolveViewerSpecNodeId(elementId, layer, type, specNodeId) {
|
|
1039
|
+
if (specNodeId && VisualizationServer.ALL_VALID_IDS.has(specNodeId))
|
|
1040
|
+
return specNodeId;
|
|
1041
|
+
let extractedType = null;
|
|
1042
|
+
if (elementId) {
|
|
1043
|
+
if (elementId.includes(".")) {
|
|
1044
|
+
const parts = elementId.split(".");
|
|
1045
|
+
parts.length >= 2 && (extractedType = parts[1]);
|
|
1046
|
+
}
|
|
1047
|
+
if (!extractedType && elementId.includes("-"))
|
|
1048
|
+
if (layer === "data-model" || layer === "data-store") {
|
|
1049
|
+
const expectedPrefix = layer + "-";
|
|
1050
|
+
if (elementId.startsWith(expectedPrefix)) {
|
|
1051
|
+
const parts = elementId.substring(expectedPrefix.length).split("-");
|
|
1052
|
+
parts.length >= 1 && (extractedType = parts[0]);
|
|
1053
|
+
}
|
|
1054
|
+
} else {
|
|
1055
|
+
const parts = elementId.split("-");
|
|
1056
|
+
parts.length >= 2 && (extractedType = parts[1]);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
if (extractedType) {
|
|
1060
|
+
const candidate = `${layer}.${extractedType}`;
|
|
1061
|
+
if ((VisualizationServer.VALID_SPEC_NODE_IDS[layer] || []).includes(candidate))
|
|
1062
|
+
return candidate;
|
|
1063
|
+
}
|
|
1064
|
+
if (type && (!extractedType || extractedType !== type)) {
|
|
1065
|
+
const candidate = `${layer}.${type}`;
|
|
1066
|
+
if ((VisualizationServer.VALID_SPEC_NODE_IDS[layer] || []).includes(candidate))
|
|
1067
|
+
return candidate;
|
|
1068
|
+
}
|
|
1069
|
+
return layer in VisualizationServer.LAYER_FALLBACK ? VisualizationServer.LAYER_FALLBACK[layer] : "data-store.database";
|
|
1070
|
+
}
|
|
283
1071
|
/**
|
|
284
1072
|
* Generate unique annotation ID
|
|
1073
|
+
* Uses cryptographically secure random bytes for collision-resistant IDs
|
|
285
1074
|
*/
|
|
286
1075
|
generateAnnotationId() {
|
|
287
|
-
return `ann-${Date.now()}-${
|
|
1076
|
+
return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Generate unique reply ID
|
|
1080
|
+
* Uses cryptographically secure random bytes for collision-resistant IDs
|
|
1081
|
+
*/
|
|
1082
|
+
generateReplyId() {
|
|
1083
|
+
return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
288
1084
|
}
|
|
289
1085
|
/**
|
|
290
1086
|
* Broadcast message to all connected WebSocket clients
|
|
291
1087
|
*/
|
|
292
1088
|
async broadcastMessage(message) {
|
|
293
|
-
const messageStr = JSON.stringify(message);
|
|
1089
|
+
const messageStr = JSON.stringify(message), messageType = message.type || "unknown";
|
|
1090
|
+
await this.recordWebSocketEvent("ws.broadcast.start", {
|
|
1091
|
+
"ws.broadcast.type": messageType,
|
|
1092
|
+
"ws.broadcast.client_count": this.clients.size,
|
|
1093
|
+
"ws.broadcast.message_size_bytes": messageStr.length
|
|
1094
|
+
});
|
|
294
1095
|
let failureCount = 0;
|
|
1096
|
+
const failedClients = [];
|
|
295
1097
|
for (const client of this.clients)
|
|
296
1098
|
try {
|
|
297
1099
|
client.send(messageStr);
|
|
298
1100
|
} catch (error) {
|
|
299
|
-
failureCount
|
|
300
|
-
const msg =
|
|
301
|
-
|
|
1101
|
+
failureCount++, failedClients.push(client);
|
|
1102
|
+
const msg = getErrorMessage(error);
|
|
1103
|
+
console.warn(`[WebSocket] Failed to send message to client: ${msg}`);
|
|
302
1104
|
}
|
|
303
|
-
|
|
1105
|
+
for (const client of failedClients)
|
|
1106
|
+
this.clients.delete(client);
|
|
1107
|
+
if (await this.recordWebSocketEvent("ws.broadcast.complete", {
|
|
1108
|
+
"ws.broadcast.type": messageType,
|
|
1109
|
+
"ws.broadcast.success_count": this.clients.size - failureCount,
|
|
1110
|
+
"ws.broadcast.failure_count": failureCount
|
|
1111
|
+
}), failureCount > 0) {
|
|
1112
|
+
const failureRate = this.clients.size > 0 ? failureCount / this.clients.size * 100 : 0;
|
|
1113
|
+
(failureRate >= 10 || failureCount > 5) && console.warn(`[WebSocket] Broadcast completed with ${failureCount}/${this.clients.size} client failures (${failureRate.toFixed(1)}% failure rate)`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Record WebSocket telemetry event
|
|
1118
|
+
* Creates spans for WebSocket operations when telemetry is enabled
|
|
1119
|
+
*/
|
|
1120
|
+
async recordWebSocketEvent(eventName, attributes) {
|
|
1121
|
+
return;
|
|
1122
|
+
try {
|
|
1123
|
+
const { startSpan, endSpan } = await import("../telemetry/index.js"), span = startSpan(eventName, attributes);
|
|
1124
|
+
endSpan(span);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
console.warn(`[Telemetry] Failed to record WebSocket event: ${getErrorMessage(error)}`);
|
|
1127
|
+
}
|
|
304
1128
|
}
|
|
305
1129
|
/**
|
|
306
1130
|
* Find an element across all layers
|
|
@@ -317,9 +1141,10 @@ class VisualizationServer {
|
|
|
317
1141
|
* Handle WebSocket messages
|
|
318
1142
|
*/
|
|
319
1143
|
async handleWSMessage(ws, data) {
|
|
320
|
-
|
|
1144
|
+
const simpleMsg = data;
|
|
1145
|
+
switch (simpleMsg.type) {
|
|
321
1146
|
case "subscribe":
|
|
322
|
-
const topics =
|
|
1147
|
+
const topics = simpleMsg.topics || ["model", "annotations"];
|
|
323
1148
|
ws.send(JSON.stringify({
|
|
324
1149
|
type: "subscribed",
|
|
325
1150
|
topics,
|
|
@@ -339,15 +1164,15 @@ class VisualizationServer {
|
|
|
339
1164
|
}));
|
|
340
1165
|
break;
|
|
341
1166
|
case "annotate":
|
|
342
|
-
|
|
1167
|
+
simpleMsg.annotation && await this.broadcastMessage({
|
|
343
1168
|
type: "annotation",
|
|
344
|
-
data:
|
|
1169
|
+
data: simpleMsg.annotation
|
|
345
1170
|
});
|
|
346
1171
|
break;
|
|
347
1172
|
default:
|
|
348
1173
|
ws.send(JSON.stringify({
|
|
349
1174
|
type: "error",
|
|
350
|
-
message: `Unknown message type: ${
|
|
1175
|
+
message: `Unknown message type: ${simpleMsg.type}`
|
|
351
1176
|
}));
|
|
352
1177
|
}
|
|
353
1178
|
}
|
|
@@ -355,7 +1180,12 @@ class VisualizationServer {
|
|
|
355
1180
|
* Handle JSON-RPC 2.0 messages for chat functionality
|
|
356
1181
|
*/
|
|
357
1182
|
async handleJSONRPCMessage(ws, data) {
|
|
358
|
-
|
|
1183
|
+
if (!("method" in data)) {
|
|
1184
|
+
const responseType = "result" in data ? "success" : "error" in data ? "error" : "unknown", msgId = "id" in data ? data.id : "unknown";
|
|
1185
|
+
console.warn(`[WebSocket] Received JSON-RPC ${responseType} response from client (ID: ${msgId}), ignoring`);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
const rpcMsg = data, { method, params, id } = rpcMsg, sendResponse = (result) => {
|
|
359
1189
|
ws.send(JSON.stringify({
|
|
360
1190
|
jsonrpc: "2.0",
|
|
361
1191
|
result,
|
|
@@ -371,24 +1201,25 @@ class VisualizationServer {
|
|
|
371
1201
|
try {
|
|
372
1202
|
switch (method) {
|
|
373
1203
|
case "chat.status":
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
1204
|
+
const hasClient = this.selectedChatClient !== void 0;
|
|
1205
|
+
let statusError = null;
|
|
1206
|
+
this.chatInitializationError ? statusError = `Chat initialization failed: ${this.chatInitializationError.message}` : hasClient || (statusError = "No chat client available. Install Claude Code or GitHub Copilot."), sendResponse({
|
|
1207
|
+
sdk_available: hasClient,
|
|
1208
|
+
sdk_version: hasClient ? this.selectedChatClient.getClientName() : null,
|
|
1209
|
+
error_message: statusError
|
|
379
1210
|
});
|
|
380
1211
|
break;
|
|
381
1212
|
case "chat.send":
|
|
382
|
-
if (!params || !params.message) {
|
|
383
|
-
sendError(
|
|
1213
|
+
if (!params || typeof params != "object" || !("message" in params) || typeof params.message != "string" || params.message.trim() === "") {
|
|
1214
|
+
sendError(JSONRPC_ERRORS.INVALID_PARAMS, "Message cannot be empty");
|
|
384
1215
|
return;
|
|
385
1216
|
}
|
|
386
|
-
if (!
|
|
387
|
-
sendError(
|
|
1217
|
+
if (!this.selectedChatClient) {
|
|
1218
|
+
sendError(JSONRPC_ERRORS.NO_CLIENT_AVAILABLE, "No chat client available. Install Claude Code or GitHub Copilot to enable chat.");
|
|
388
1219
|
return;
|
|
389
1220
|
}
|
|
390
1221
|
const conversationId = `conv-${++this.chatConversationCounter}-${Date.now()}`;
|
|
391
|
-
await this.
|
|
1222
|
+
await this.launchChat(ws, conversationId, params.message, id);
|
|
392
1223
|
break;
|
|
393
1224
|
case "chat.cancel":
|
|
394
1225
|
let cancelled = !1, cancelledConvId = null;
|
|
@@ -396,7 +1227,8 @@ class VisualizationServer {
|
|
|
396
1227
|
try {
|
|
397
1228
|
process2.kill(), this.activeChatProcesses.delete(convId), cancelled = !0, cancelledConvId = convId;
|
|
398
1229
|
break;
|
|
399
|
-
} catch {
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
console.warn(`[Chat] Failed to cancel process for conversation ${convId}: ${getErrorMessage(error)}`);
|
|
400
1232
|
}
|
|
401
1233
|
sendResponse({
|
|
402
1234
|
cancelled,
|
|
@@ -404,43 +1236,46 @@ class VisualizationServer {
|
|
|
404
1236
|
});
|
|
405
1237
|
break;
|
|
406
1238
|
default:
|
|
407
|
-
sendError(
|
|
1239
|
+
sendError(JSONRPC_ERRORS.METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
408
1240
|
}
|
|
409
1241
|
} catch (error) {
|
|
410
|
-
const errorMsg =
|
|
411
|
-
sendError(
|
|
1242
|
+
const errorMsg = getErrorMessage(error);
|
|
1243
|
+
sendError(JSONRPC_ERRORS.INTERNAL_ERROR, "Internal error", errorMsg);
|
|
412
1244
|
}
|
|
413
1245
|
}
|
|
414
1246
|
/**
|
|
415
|
-
*
|
|
1247
|
+
* Launch chat with selected client and stream responses via WebSocket
|
|
416
1248
|
*/
|
|
417
|
-
async
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
1249
|
+
async launchChat(ws, conversationId, message, requestId) {
|
|
1250
|
+
if (!this.selectedChatClient) {
|
|
1251
|
+
ws.send(JSON.stringify({
|
|
1252
|
+
jsonrpc: "2.0",
|
|
1253
|
+
error: {
|
|
1254
|
+
code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
|
|
1255
|
+
message: "No chat client available"
|
|
1256
|
+
},
|
|
1257
|
+
id: requestId
|
|
1258
|
+
}));
|
|
1259
|
+
return;
|
|
426
1260
|
}
|
|
1261
|
+
this.selectedChatClient instanceof ClaudeCodeClient ? await this.launchClaudeCodeChat(ws, conversationId, message, requestId) : this.selectedChatClient instanceof CopilotClient ? await this.launchCopilotChat(ws, conversationId, message, requestId) : ws.send(JSON.stringify({
|
|
1262
|
+
jsonrpc: "2.0",
|
|
1263
|
+
error: {
|
|
1264
|
+
code: JSONRPC_ERRORS.NO_CLIENT_AVAILABLE,
|
|
1265
|
+
message: "Unknown chat client type"
|
|
1266
|
+
},
|
|
1267
|
+
id: requestId
|
|
1268
|
+
}));
|
|
427
1269
|
}
|
|
428
1270
|
/**
|
|
429
1271
|
* Launch Claude Code CLI with dr-architect agent and stream responses
|
|
430
1272
|
*/
|
|
431
1273
|
async launchClaudeCodeChat(ws, conversationId, message, requestId) {
|
|
432
1274
|
try {
|
|
1275
|
+
const cmd = ["claude", "--agent", "dr-architect", "--print"];
|
|
1276
|
+
this.withDanger && cmd.push("--dangerously-skip-permissions"), cmd.push("--verbose", "--output-format", "stream-json");
|
|
433
1277
|
const proc = Bun.spawn({
|
|
434
|
-
cmd
|
|
435
|
-
"claude",
|
|
436
|
-
"--agent",
|
|
437
|
-
"dr-architect",
|
|
438
|
-
"--print",
|
|
439
|
-
"--dangerously-skip-permissions",
|
|
440
|
-
"--verbose",
|
|
441
|
-
"--output-format",
|
|
442
|
-
"stream-json"
|
|
443
|
-
],
|
|
1278
|
+
cmd,
|
|
444
1279
|
cwd: this.model.rootPath,
|
|
445
1280
|
stdin: "pipe",
|
|
446
1281
|
stdout: "pipe",
|
|
@@ -495,8 +1330,8 @@ class VisualizationServer {
|
|
|
495
1330
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
496
1331
|
}
|
|
497
1332
|
}));
|
|
498
|
-
} catch {
|
|
499
|
-
ws.send(JSON.stringify({
|
|
1333
|
+
} catch (parseError) {
|
|
1334
|
+
process.env.DEBUG && console.debug(`[Chat] Failed to parse JSON chunk: ${getErrorMessage(parseError)}`), ws.send(JSON.stringify({
|
|
500
1335
|
jsonrpc: "2.0",
|
|
501
1336
|
method: "chat.response.chunk",
|
|
502
1337
|
params: {
|
|
@@ -532,96 +1367,197 @@ class VisualizationServer {
|
|
|
532
1367
|
id: requestId
|
|
533
1368
|
}));
|
|
534
1369
|
} catch (error) {
|
|
535
|
-
const errorMsg =
|
|
1370
|
+
const errorMsg = getErrorMessage(error);
|
|
536
1371
|
ws.send(JSON.stringify({
|
|
537
1372
|
jsonrpc: "2.0",
|
|
538
1373
|
error: {
|
|
539
|
-
code:
|
|
1374
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
540
1375
|
message: `Chat failed: ${errorMsg}`
|
|
541
1376
|
},
|
|
542
1377
|
id: requestId
|
|
543
1378
|
})), this.activeChatProcesses.delete(conversationId);
|
|
544
1379
|
try {
|
|
545
|
-
|
|
546
|
-
} catch {
|
|
1380
|
+
await this.killProcessWithRetry(proc, conversationId, "Claude Code");
|
|
1381
|
+
} catch (error2) {
|
|
1382
|
+
console.error(`[Claude Code] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
|
|
547
1383
|
}
|
|
548
1384
|
}
|
|
549
|
-
})()
|
|
1385
|
+
})().catch((err) => {
|
|
1386
|
+
const errorMsg = getErrorMessage(err);
|
|
1387
|
+
console.error(`[Claude Code] Stream error: ${errorMsg}`);
|
|
1388
|
+
try {
|
|
1389
|
+
ws.send(JSON.stringify({
|
|
1390
|
+
jsonrpc: "2.0",
|
|
1391
|
+
error: {
|
|
1392
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
1393
|
+
message: `Stream error: ${errorMsg}`
|
|
1394
|
+
},
|
|
1395
|
+
id: requestId
|
|
1396
|
+
}));
|
|
1397
|
+
} catch (sendError) {
|
|
1398
|
+
console.error(`[Claude Code] Failed to send stream error notification: ${getErrorMessage(sendError)}`);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
550
1401
|
} catch (error) {
|
|
551
|
-
const errorMsg =
|
|
1402
|
+
const errorMsg = getErrorMessage(error);
|
|
552
1403
|
ws.send(JSON.stringify({
|
|
553
1404
|
jsonrpc: "2.0",
|
|
554
1405
|
error: {
|
|
555
|
-
code:
|
|
1406
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
556
1407
|
message: `Failed to launch Claude Code: ${errorMsg}`
|
|
557
1408
|
},
|
|
558
1409
|
id: requestId
|
|
559
1410
|
}));
|
|
560
1411
|
}
|
|
561
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Launch GitHub Copilot CLI and stream responses via WebSocket
|
|
1415
|
+
*/
|
|
1416
|
+
async launchCopilotChat(ws, conversationId, message, requestId) {
|
|
1417
|
+
try {
|
|
1418
|
+
let cmd;
|
|
1419
|
+
Bun.spawnSync({
|
|
1420
|
+
cmd: ["gh", "copilot", "--version"],
|
|
1421
|
+
stdout: "pipe",
|
|
1422
|
+
stderr: "pipe"
|
|
1423
|
+
}).exitCode === 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));
|
|
1424
|
+
const proc = Bun.spawn({
|
|
1425
|
+
cmd,
|
|
1426
|
+
cwd: this.model.rootPath,
|
|
1427
|
+
stdin: "pipe",
|
|
1428
|
+
stdout: "pipe",
|
|
1429
|
+
stderr: "pipe"
|
|
1430
|
+
});
|
|
1431
|
+
this.activeChatProcesses.set(conversationId, proc);
|
|
1432
|
+
const stdoutReader = proc.stdout.getReader(), decoder = new TextDecoder();
|
|
1433
|
+
(async () => {
|
|
1434
|
+
let accumulatedText = "";
|
|
1435
|
+
try {
|
|
1436
|
+
for (; ; ) {
|
|
1437
|
+
const { done, value } = await stdoutReader.read();
|
|
1438
|
+
if (done)
|
|
1439
|
+
break;
|
|
1440
|
+
const chunk = decoder.decode(value, { stream: !0 });
|
|
1441
|
+
accumulatedText += chunk, ws.send(JSON.stringify({
|
|
1442
|
+
jsonrpc: "2.0",
|
|
1443
|
+
method: "chat.response.chunk",
|
|
1444
|
+
params: {
|
|
1445
|
+
conversation_id: conversationId,
|
|
1446
|
+
content: chunk,
|
|
1447
|
+
is_final: !1,
|
|
1448
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1449
|
+
}
|
|
1450
|
+
}));
|
|
1451
|
+
}
|
|
1452
|
+
const exitCode = await proc.exited;
|
|
1453
|
+
this.activeChatProcesses.delete(conversationId), ws.send(JSON.stringify({
|
|
1454
|
+
jsonrpc: "2.0",
|
|
1455
|
+
result: {
|
|
1456
|
+
conversation_id: conversationId,
|
|
1457
|
+
status: exitCode === 0 ? "complete" : "error",
|
|
1458
|
+
exit_code: exitCode,
|
|
1459
|
+
full_response: accumulatedText,
|
|
1460
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1461
|
+
},
|
|
1462
|
+
id: requestId
|
|
1463
|
+
}));
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
const errorMsg = getErrorMessage(error);
|
|
1466
|
+
ws.send(JSON.stringify({
|
|
1467
|
+
jsonrpc: "2.0",
|
|
1468
|
+
error: {
|
|
1469
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
1470
|
+
message: `Chat failed: ${errorMsg}`
|
|
1471
|
+
},
|
|
1472
|
+
id: requestId
|
|
1473
|
+
})), this.activeChatProcesses.delete(conversationId);
|
|
1474
|
+
try {
|
|
1475
|
+
await this.killProcessWithRetry(proc, conversationId, "Copilot");
|
|
1476
|
+
} catch (error2) {
|
|
1477
|
+
console.error(`[Copilot] Failed to kill process for conversation ${conversationId}: ${getErrorMessage(error2)}`);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
})().catch((err) => {
|
|
1481
|
+
console.error(`[Copilot] Stream error: ${getErrorMessage(err)}`);
|
|
1482
|
+
});
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
const errorMsg = getErrorMessage(error);
|
|
1485
|
+
ws.send(JSON.stringify({
|
|
1486
|
+
jsonrpc: "2.0",
|
|
1487
|
+
error: {
|
|
1488
|
+
code: JSONRPC_ERRORS.INTERNAL_ERROR,
|
|
1489
|
+
message: `Failed to launch GitHub Copilot: ${errorMsg}`
|
|
1490
|
+
},
|
|
1491
|
+
id: requestId
|
|
1492
|
+
}));
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
562
1495
|
/**
|
|
563
1496
|
* Setup file watcher for model changes
|
|
564
1497
|
*/
|
|
565
1498
|
setupFileWatcher() {
|
|
566
|
-
const
|
|
1499
|
+
const modelPath = `${this.model.rootPath}/documentation-robotics/model`, bunWatch = globalThis.Bun?.watch;
|
|
567
1500
|
if (!bunWatch) {
|
|
568
1501
|
console.warn("[Watcher] Bun.watch not available, file watching disabled");
|
|
569
1502
|
return;
|
|
570
1503
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const msg =
|
|
588
|
-
console.
|
|
1504
|
+
try {
|
|
1505
|
+
this.watcher = bunWatch(modelPath, {
|
|
1506
|
+
recursive: !0,
|
|
1507
|
+
onChange: async (_event, path2) => {
|
|
1508
|
+
process.env.VERBOSE && console.log(`[Watcher] File changed: ${path2}`);
|
|
1509
|
+
try {
|
|
1510
|
+
this.model = await Model.load(this.model.rootPath, { lazyLoad: !1 }), await this.loadChangesetsFromDisk();
|
|
1511
|
+
const modelData = await this.serializeModel(), message = JSON.stringify({
|
|
1512
|
+
type: "model.updated",
|
|
1513
|
+
data: modelData,
|
|
1514
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1515
|
+
});
|
|
1516
|
+
for (const client of this.clients)
|
|
1517
|
+
try {
|
|
1518
|
+
client.send(message);
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
const msg = getErrorMessage(error);
|
|
1521
|
+
console.warn(`[Watcher] Failed to send update: ${msg}`);
|
|
589
1522
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
const message = getErrorMessage(error);
|
|
1525
|
+
console.error(`[Watcher] Failed to reload model: ${message}`);
|
|
1526
|
+
}
|
|
594
1527
|
}
|
|
595
|
-
}
|
|
596
|
-
})
|
|
1528
|
+
});
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
console.warn(`[Watcher] Could not watch ${modelPath}: ${err}`);
|
|
1531
|
+
}
|
|
597
1532
|
}
|
|
598
1533
|
/**
|
|
599
|
-
* Load all JSON schemas from bundled directory
|
|
1534
|
+
* Load all JSON schemas from bundled directory (recursive)
|
|
1535
|
+
* Loads schemas from all subdirectories: base/, nodes/, relationships/, etc.
|
|
600
1536
|
*/
|
|
601
1537
|
async loadSchemas() {
|
|
602
1538
|
const schemasPath = new URL("../schemas/bundled/", import.meta.url).pathname, schemas = {};
|
|
603
1539
|
try {
|
|
604
|
-
const fs = await import("fs/promises"),
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
|
|
1540
|
+
const fs = await import("fs/promises"), path2 = await import("path"), walkDirectory = async (dir, prefix = "") => {
|
|
1541
|
+
const entries = await fs.readdir(dir, { withFileTypes: !0 });
|
|
1542
|
+
for (const entry of entries) {
|
|
1543
|
+
const fullPath = path2.join(dir, entry.name), schemaKey = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1544
|
+
if (entry.isDirectory())
|
|
1545
|
+
await walkDirectory(fullPath, schemaKey);
|
|
1546
|
+
else if (entry.isFile() && (entry.name.endsWith(".schema.json") || entry.name.endsWith(".json"))) {
|
|
1547
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
1548
|
+
try {
|
|
1549
|
+
schemas[schemaKey] = JSON.parse(content);
|
|
1550
|
+
} catch (parseError) {
|
|
1551
|
+
const parseErrorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
1552
|
+
throw new Error(`Invalid JSON in schema file ${fullPath}: ${parseErrorMsg}`);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
609
1555
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
throw console.error("Failed to load schemas:", error), new Error("Failed to load schema files");
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Load link registry from bundled directory
|
|
617
|
-
*/
|
|
618
|
-
async loadLinkRegistry() {
|
|
619
|
-
const linkRegistryPath = new URL("../schemas/bundled/link-registry.json", import.meta.url).pathname;
|
|
620
|
-
try {
|
|
621
|
-
const content = await (await import("fs/promises")).readFile(linkRegistryPath, "utf-8");
|
|
622
|
-
return JSON.parse(content);
|
|
1556
|
+
};
|
|
1557
|
+
return await walkDirectory(schemasPath), schemas;
|
|
623
1558
|
} catch (error) {
|
|
624
|
-
|
|
1559
|
+
const errorMsg = getErrorMessage(error);
|
|
1560
|
+
throw console.error("Failed to load schemas:", error), new Error(`Failed to load schema files: ${errorMsg}`);
|
|
625
1561
|
}
|
|
626
1562
|
}
|
|
627
1563
|
/**
|
|
@@ -967,7 +1903,27 @@ class VisualizationServer {
|
|
|
967
1903
|
return div.innerHTML;
|
|
968
1904
|
}
|
|
969
1905
|
|
|
970
|
-
|
|
1906
|
+
// Extract token from URL query parameters if present
|
|
1907
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
1908
|
+
const token = urlParams.get('token');
|
|
1909
|
+
|
|
1910
|
+
// Log token status for debugging
|
|
1911
|
+
if (!token) {
|
|
1912
|
+
console.warn('[WebSocket] No authentication token found in URL. If authentication is enabled, connection will fail.');
|
|
1913
|
+
console.warn('[WebSocket] Expected URL format: http://localhost:PORT?token=YOUR_TOKEN');
|
|
1914
|
+
} else {
|
|
1915
|
+
console.log('[WebSocket] Found authentication token in URL');
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// Build WebSocket URL with token if available
|
|
1919
|
+
let wsUrl = \`ws://\${window.location.host}/ws\`;
|
|
1920
|
+
if (token) {
|
|
1921
|
+
wsUrl += \`?token=\${encodeURIComponent(token)}\`;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
console.log('[WebSocket] Connecting to:', wsUrl.replace(/token=[^&]+/, 'token=***'));
|
|
1925
|
+
|
|
1926
|
+
const ws = new WebSocket(wsUrl);
|
|
971
1927
|
|
|
972
1928
|
ws.addEventListener('open', () => {
|
|
973
1929
|
console.log('WebSocket connected');
|
|
@@ -993,16 +1949,45 @@ class VisualizationServer {
|
|
|
993
1949
|
}
|
|
994
1950
|
});
|
|
995
1951
|
|
|
996
|
-
ws.addEventListener('close', () => {
|
|
997
|
-
console.log('WebSocket disconnected');
|
|
1952
|
+
ws.addEventListener('close', (event) => {
|
|
1953
|
+
console.log('WebSocket disconnected', event.code, event.reason);
|
|
998
1954
|
updateStatus(false);
|
|
1955
|
+
|
|
1956
|
+
// If closed immediately with auth error codes, show helpful message
|
|
1957
|
+
if (!token && (event.code === 1002 || event.code === 1008 || event.code === 1006)) {
|
|
1958
|
+
showAuthError();
|
|
1959
|
+
}
|
|
999
1960
|
});
|
|
1000
1961
|
|
|
1001
1962
|
ws.addEventListener('error', (error) => {
|
|
1002
1963
|
console.error('WebSocket error:', error);
|
|
1003
1964
|
updateStatus(false);
|
|
1965
|
+
|
|
1966
|
+
// If we don't have a token, likely an auth issue
|
|
1967
|
+
if (!token) {
|
|
1968
|
+
showAuthError();
|
|
1969
|
+
}
|
|
1004
1970
|
});
|
|
1005
1971
|
|
|
1972
|
+
function showAuthError() {
|
|
1973
|
+
const errorDiv = document.createElement('div');
|
|
1974
|
+
errorDiv.style.cssText = 'position: fixed; top: 80px; left: 50%; transform: translateX(-50%); background: #f44336; color: white; padding: 16px 24px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 10000; max-width: 500px; text-align: center;';
|
|
1975
|
+
errorDiv.innerHTML = \`
|
|
1976
|
+
<strong>\u26A0\uFE0F Authentication Required</strong><br>
|
|
1977
|
+
<span style="font-size: 14px; margin-top: 8px; display: inline-block;">
|
|
1978
|
+
Please use the full URL with authentication token from the terminal.
|
|
1979
|
+
</span>
|
|
1980
|
+
\`;
|
|
1981
|
+
document.body.appendChild(errorDiv);
|
|
1982
|
+
|
|
1983
|
+
// Auto-dismiss after 10 seconds
|
|
1984
|
+
setTimeout(() => {
|
|
1985
|
+
errorDiv.style.transition = 'opacity 0.5s';
|
|
1986
|
+
errorDiv.style.opacity = '0';
|
|
1987
|
+
setTimeout(() => errorDiv.remove(), 500);
|
|
1988
|
+
}, 10000);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1006
1991
|
function updateStatus(connected) {
|
|
1007
1992
|
if (connected) {
|
|
1008
1993
|
statusEl.className = 'status-badge connected';
|
|
@@ -1044,12 +2029,13 @@ class VisualizationServer {
|
|
|
1044
2029
|
for (const element of layerData.elements) {
|
|
1045
2030
|
const elemDiv = document.createElement('div');
|
|
1046
2031
|
elemDiv.className = 'element-item';
|
|
1047
|
-
|
|
2032
|
+
const elementSlug = element.path || element.id;
|
|
2033
|
+
if (elementSlug === selectedElementId) {
|
|
1048
2034
|
elemDiv.classList.add('selected');
|
|
1049
2035
|
}
|
|
1050
|
-
elemDiv.innerHTML = \`\${escapeHtml(
|
|
2036
|
+
elemDiv.innerHTML = \`\${escapeHtml(elementSlug)}<br><span style="font-size: 11px; color: #999;">\${escapeHtml(element.name)}</span>\`;
|
|
1051
2037
|
elemDiv.addEventListener('click', () => {
|
|
1052
|
-
selectedElementId =
|
|
2038
|
+
selectedElementId = elementSlug;
|
|
1053
2039
|
document.querySelectorAll('.element-item').forEach(el => el.classList.remove('selected'));
|
|
1054
2040
|
elemDiv.classList.add('selected');
|
|
1055
2041
|
renderSelectedElement();
|
|
@@ -1073,7 +2059,7 @@ class VisualizationServer {
|
|
|
1073
2059
|
// Find element in model
|
|
1074
2060
|
let element = null;
|
|
1075
2061
|
for (const layer of Object.values(currentModel.layers)) {
|
|
1076
|
-
element = layer.elements.find(e => e.id === selectedElementId);
|
|
2062
|
+
element = layer.elements.find(e => (e.path || e.id) === selectedElementId);
|
|
1077
2063
|
if (element) break;
|
|
1078
2064
|
}
|
|
1079
2065
|
|
|
@@ -1086,7 +2072,7 @@ class VisualizationServer {
|
|
|
1086
2072
|
<div class="detail-header">
|
|
1087
2073
|
<div class="element-type">\${escapeHtml(element.type)}</div>
|
|
1088
2074
|
<h2>\${escapeHtml(element.name)}</h2>
|
|
1089
|
-
<div style="font-size: 12px; color: #666; margin-top: 4px;"><code>\${escapeHtml(element.id)}</code></div>
|
|
2075
|
+
<div style="font-size: 12px; color: #666; margin-top: 4px;"><code>\${escapeHtml(element.path || element.id)}</code></div>
|
|
1090
2076
|
</div>
|
|
1091
2077
|
\`;
|
|
1092
2078
|
|
|
@@ -1128,11 +2114,12 @@ class VisualizationServer {
|
|
|
1128
2114
|
html += \`
|
|
1129
2115
|
<div class="detail-section">
|
|
1130
2116
|
<h3>Add Annotation</h3>
|
|
1131
|
-
<form class="annotation-form" onsubmit="addAnnotation(event, '\${escapeHtml(element.id)}')">
|
|
2117
|
+
<form class="annotation-form" onsubmit="addAnnotation(event, '\${escapeHtml(element.path || element.id)}')">
|
|
1132
2118
|
<input type="text" placeholder="Author name" id="ann-author" required>
|
|
1133
2119
|
<textarea placeholder="Annotation text" id="ann-text" required></textarea>
|
|
1134
2120
|
<button type="submit">Add Annotation</button>
|
|
1135
2121
|
</form>
|
|
2122
|
+
<div id="ann-error" style="margin-top: 8px; min-height: 1.2em;"></div>
|
|
1136
2123
|
</div>
|
|
1137
2124
|
\`;
|
|
1138
2125
|
|
|
@@ -1147,30 +2134,101 @@ class VisualizationServer {
|
|
|
1147
2134
|
|
|
1148
2135
|
if (!author || !text) return;
|
|
1149
2136
|
|
|
1150
|
-
// Send to server
|
|
1151
|
-
fetch(
|
|
2137
|
+
// Send to server using new annotation API
|
|
2138
|
+
fetch('/api/annotations', {
|
|
1152
2139
|
method: 'POST',
|
|
1153
2140
|
headers: { 'Content-Type': 'application/json' },
|
|
1154
|
-
body: JSON.stringify({ author, text })
|
|
2141
|
+
body: JSON.stringify({ elementId, author, content: text })
|
|
1155
2142
|
})
|
|
1156
|
-
.then(r => r.json())
|
|
1157
|
-
.then(data => {
|
|
1158
|
-
if (data.
|
|
2143
|
+
.then(r => r.json().then(data => ({ ok: r.ok, data })))
|
|
2144
|
+
.then(({ ok, data }) => {
|
|
2145
|
+
if (ok && data && data.id) {
|
|
1159
2146
|
document.getElementById('ann-author').value = '';
|
|
1160
2147
|
document.getElementById('ann-text').value = '';
|
|
2148
|
+
const errorDiv = document.getElementById('ann-error');
|
|
2149
|
+
if (errorDiv) errorDiv.textContent = '';
|
|
2150
|
+
} else if (!ok) {
|
|
2151
|
+
const errorMsg = (data && data.error) ? data.error : 'Failed to create annotation';
|
|
2152
|
+
const errorDiv = document.getElementById('ann-error');
|
|
2153
|
+
if (errorDiv) {
|
|
2154
|
+
errorDiv.textContent = 'Error: ' + errorMsg;
|
|
2155
|
+
errorDiv.style.color = '#d32f2f';
|
|
2156
|
+
errorDiv.style.marginTop = '8px';
|
|
2157
|
+
}
|
|
1161
2158
|
}
|
|
1162
2159
|
})
|
|
1163
|
-
.catch(err =>
|
|
2160
|
+
.catch(err => {
|
|
2161
|
+
const errorDiv = document.getElementById('ann-error');
|
|
2162
|
+
if (errorDiv) {
|
|
2163
|
+
errorDiv.textContent = 'Error: Failed to add annotation: ' + err.message;
|
|
2164
|
+
errorDiv.style.color = '#d32f2f';
|
|
2165
|
+
errorDiv.style.marginTop = '8px';
|
|
2166
|
+
}
|
|
2167
|
+
});
|
|
1164
2168
|
}
|
|
1165
2169
|
</script>
|
|
1166
2170
|
</body>
|
|
1167
2171
|
</html>`;
|
|
1168
2172
|
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Serve a file from the custom viewer path
|
|
2175
|
+
*/
|
|
2176
|
+
async serveCustomViewer(filePath) {
|
|
2177
|
+
if (process.env.VERBOSE && console.log(`[VIEWER] serveCustomViewer called for: ${filePath}`), !this.viewerPath)
|
|
2178
|
+
return console.error("[VIEWER] Custom viewer path not configured"), new Response("Custom viewer path not configured", { status: 500 });
|
|
2179
|
+
try {
|
|
2180
|
+
const path2 = await import("path"), fs = await import("fs/promises"), fullPath = path2.resolve(this.viewerPath, filePath);
|
|
2181
|
+
if (process.env.VERBOSE && console.log(`[VIEWER] Resolved path: ${fullPath}`), !fullPath.startsWith(path2.resolve(this.viewerPath)))
|
|
2182
|
+
return console.error("[VIEWER] Security check failed - path outside viewer directory"), new Response("Forbidden", { status: 403 });
|
|
2183
|
+
process.env.VERBOSE && console.log(`[VIEWER] Reading file: ${fullPath}`);
|
|
2184
|
+
const content = await fs.readFile(fullPath), ext = path2.extname(fullPath).toLowerCase(), contentType = {
|
|
2185
|
+
".html": "text/html",
|
|
2186
|
+
".css": "text/css",
|
|
2187
|
+
".js": "application/javascript",
|
|
2188
|
+
".json": "application/json",
|
|
2189
|
+
".png": "image/png",
|
|
2190
|
+
".jpg": "image/jpeg",
|
|
2191
|
+
".jpeg": "image/jpeg",
|
|
2192
|
+
".gif": "image/gif",
|
|
2193
|
+
".svg": "image/svg+xml",
|
|
2194
|
+
".ico": "image/x-icon",
|
|
2195
|
+
".woff": "font/woff",
|
|
2196
|
+
".woff2": "font/woff2",
|
|
2197
|
+
".ttf": "font/ttf",
|
|
2198
|
+
".eot": "application/vnd.ms-fontobject"
|
|
2199
|
+
}[ext] || "application/octet-stream";
|
|
2200
|
+
return process.env.VERBOSE && console.log(`[VIEWER] Successfully read file, returning with Content-Type: ${contentType}`), new Response(content, {
|
|
2201
|
+
status: 200,
|
|
2202
|
+
headers: {
|
|
2203
|
+
"Content-Type": contentType
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
} catch (error) {
|
|
2207
|
+
return error.code === "ENOENT" ? (console.error(`[VIEWER] File not found: ${filePath}`), new Response("File not found", { status: 404 })) : (console.error(`[VIEWER] Error serving custom viewer file ${filePath}:`, error), new Response("Internal server error", { status: 500 }));
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
/**
|
|
2211
|
+
* Load all changesets from disk into the in-memory map
|
|
2212
|
+
*/
|
|
2213
|
+
async loadChangesetsFromDisk() {
|
|
2214
|
+
if (this.changesetStorage)
|
|
2215
|
+
try {
|
|
2216
|
+
const changesets = await this.changesetStorage.list();
|
|
2217
|
+
this.changesets.clear();
|
|
2218
|
+
for (const cs of changesets)
|
|
2219
|
+
this.changesets.set(cs.id, cs);
|
|
2220
|
+
process.env.VERBOSE && console.log(`[Changesets] Loaded ${changesets.length} changeset(s) from disk`);
|
|
2221
|
+
} catch (err) {
|
|
2222
|
+
console.warn(`[Changesets] Warning: Failed to load changesets from disk: ${err}`);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
1169
2225
|
/**
|
|
1170
2226
|
* Start the server
|
|
1171
2227
|
*/
|
|
1172
2228
|
async start(port = 8080) {
|
|
1173
|
-
this.
|
|
2229
|
+
this.changesetStorage = new StagedChangesetStorage(this.model.rootPath), await this.loadChangesetsFromDisk(), this.setupFileWatcher();
|
|
2230
|
+
const { serve } = await import("bun");
|
|
2231
|
+
this._server = serve({
|
|
1174
2232
|
port,
|
|
1175
2233
|
fetch: this.app.fetch,
|
|
1176
2234
|
websocket
|
|
@@ -1189,6 +2247,30 @@ class VisualizationServer {
|
|
|
1189
2247
|
isAuthEnabled() {
|
|
1190
2248
|
return this.authEnabled;
|
|
1191
2249
|
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Kill a process with retry logic and exponential backoff
|
|
2252
|
+
* Attempts graceful SIGTERM first, then force SIGKILL if needed
|
|
2253
|
+
*/
|
|
2254
|
+
async killProcessWithRetry(proc, conversationId, label) {
|
|
2255
|
+
let attempt = 0;
|
|
2256
|
+
const baseDelay = 100;
|
|
2257
|
+
for (; attempt < 3; ) {
|
|
2258
|
+
try {
|
|
2259
|
+
if (proc.killed || (proc.kill("SIGTERM"), await new Promise((resolve) => setTimeout(resolve, 500)), proc.killed))
|
|
2260
|
+
return;
|
|
2261
|
+
} catch {
|
|
2262
|
+
}
|
|
2263
|
+
if (attempt++, attempt < 3) {
|
|
2264
|
+
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
2265
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
try {
|
|
2269
|
+
proc.killed || (proc.kill("SIGKILL"), console.warn(`[${label}] Process for conversation ${conversationId} did not respond to SIGTERM, force killed with SIGKILL`));
|
|
2270
|
+
} catch (error) {
|
|
2271
|
+
throw new Error(`Failed to kill process: ${error instanceof Error ? error.message : String(error)}`);
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
1192
2274
|
/**
|
|
1193
2275
|
* Stop the server
|
|
1194
2276
|
*/
|
|
@@ -1196,7 +2278,9 @@ class VisualizationServer {
|
|
|
1196
2278
|
for (const client of this.clients)
|
|
1197
2279
|
try {
|
|
1198
2280
|
client.close();
|
|
1199
|
-
} catch {
|
|
2281
|
+
} catch (error) {
|
|
2282
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2283
|
+
errorMessage.includes("already closed") || console.warn(`[SERVER] Unexpected error closing WebSocket client: ${errorMessage}`);
|
|
1200
2284
|
}
|
|
1201
2285
|
this.clients.clear(), this.watcher && this.watcher.close?.(), this._server && this._server.stop();
|
|
1202
2286
|
}
|