@documentation-robotics/cli 0.1.0 → 0.1.5
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 +206 -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 +155 -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 +37 -13
- 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
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import ansis from "ansis";
|
|
3
|
+
import { StagedChangesetStorage } from "./staged-changeset-storage.js";
|
|
4
|
+
import { BaseSnapshotManager } from "./base-snapshot-manager.js";
|
|
5
|
+
import { ChangesetValidator } from "./changeset-validator.js";
|
|
6
|
+
import { fileExists } from "../utils/file-io.js";
|
|
7
|
+
import { createSha256Hash } from "../types/index.js";
|
|
8
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
9
|
+
import { Layer } from "./layer.js";
|
|
10
|
+
import { isValidLayerName } from "./layers.js";
|
|
11
|
+
import { isTelemetryEnabled, startSpan, startActiveSpan, endSpan, emitLog, SeverityNumber } from "../telemetry/index.js";
|
|
12
|
+
import { ModelReportOrchestrator } from "../reports/model-report-orchestrator.js";
|
|
13
|
+
class StagingAreaManager {
|
|
14
|
+
storage;
|
|
15
|
+
snapshotManager;
|
|
16
|
+
validator;
|
|
17
|
+
projectionEngine = null;
|
|
18
|
+
rootPath;
|
|
19
|
+
model = null;
|
|
20
|
+
constructor(rootPath, model) {
|
|
21
|
+
this.rootPath = rootPath, this.model = model || null, this.storage = new StagedChangesetStorage(rootPath), this.snapshotManager = new BaseSnapshotManager(), this.validator = new ChangesetValidator(rootPath), this.projectionEngine = null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate changeset ID format and content
|
|
25
|
+
* Ensures ID is safe and provides early validation before file operations
|
|
26
|
+
*
|
|
27
|
+
* Validation rules:
|
|
28
|
+
* - Must be a non-empty string
|
|
29
|
+
* - Cannot contain path separators (.., /, \)
|
|
30
|
+
* - Cannot contain special characters (<>:"|?*)
|
|
31
|
+
* - During storage, the ID will be normalized: lowercase, spaces→hyphens, non-alphanumeric→stripped
|
|
32
|
+
*
|
|
33
|
+
* @param changesetId - The changeset ID to validate
|
|
34
|
+
* @throws Error if ID contains invalid characters or structure
|
|
35
|
+
*/
|
|
36
|
+
validateChangesetId(changesetId) {
|
|
37
|
+
if (!changesetId || typeof changesetId != "string")
|
|
38
|
+
throw new Error("Changeset ID must be a non-empty string");
|
|
39
|
+
if (changesetId.trim() === "")
|
|
40
|
+
throw new Error("Changeset ID cannot be empty or whitespace-only");
|
|
41
|
+
if (changesetId.includes("..") || changesetId.includes("/") || changesetId.includes("\\"))
|
|
42
|
+
throw new Error("Changeset ID cannot contain path separators or traversal sequences (..)");
|
|
43
|
+
if (/[<>:"|?*]/.test(changesetId))
|
|
44
|
+
throw new Error('Changeset ID contains invalid special characters (<>:"|?*). Only alphanumeric characters and hyphens are preserved. Spaces are converted to hyphens, and other characters are removed during storage.');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get or create projection engine instance (lazy initialization).
|
|
48
|
+
* Deferred initialization avoids circular dependency issues at module load time.
|
|
49
|
+
* @returns VirtualProjectionEngine instance for computing merged model views
|
|
50
|
+
*/
|
|
51
|
+
async getProjectionEngine() {
|
|
52
|
+
if (!this.projectionEngine) {
|
|
53
|
+
const { VirtualProjectionEngine } = await import("./virtual-projection.js");
|
|
54
|
+
this.projectionEngine = new VirtualProjectionEngine(this.rootPath);
|
|
55
|
+
}
|
|
56
|
+
return this.projectionEngine;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create a new staged changeset
|
|
60
|
+
*/
|
|
61
|
+
async create(name, description) {
|
|
62
|
+
if (!this.model)
|
|
63
|
+
throw new Error("Model required for creating changesets");
|
|
64
|
+
const id = this.generateChangesetId(), baseSnapshotId = await this.snapshotManager.captureSnapshot(this.model), changeset = await this.storage.create(id, name, description, baseSnapshotId);
|
|
65
|
+
return changeset.id = id, changeset.markStaged(), await this.storage.save(changeset), changeset;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load a changeset by name or ID
|
|
69
|
+
*/
|
|
70
|
+
async load(name) {
|
|
71
|
+
let changeset = await this.storage.load(name);
|
|
72
|
+
return changeset || (await this.storage.list()).find((cs) => cs.name === name) || null;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* List all staged changesets
|
|
76
|
+
*/
|
|
77
|
+
async list() {
|
|
78
|
+
return this.storage.list();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Delete a changeset
|
|
82
|
+
* If the changeset being deleted is the currently active changeset,
|
|
83
|
+
* this method will also clear the active changeset marker.
|
|
84
|
+
*/
|
|
85
|
+
async delete(name) {
|
|
86
|
+
const changeset = await this.load(name);
|
|
87
|
+
if (!changeset)
|
|
88
|
+
throw new Error(`Changeset '${name}' not found`);
|
|
89
|
+
if (!changeset.id)
|
|
90
|
+
throw new Error(`Changeset '${name}' has no ID and cannot be deleted`);
|
|
91
|
+
const activeChangeset = await this.getActive();
|
|
92
|
+
activeChangeset && activeChangeset.id === changeset.id && await this.clearActive(), await this.storage.delete(changeset.id);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Stage a change in the changeset.
|
|
96
|
+
* Records the change in the staging area without applying to the base model.
|
|
97
|
+
* The changeset must be in 'staged' status to accept new changes.
|
|
98
|
+
*
|
|
99
|
+
* @param changesetId - ID of the changeset to stage the change in
|
|
100
|
+
* @param change - The change to stage (type, elementId, layerName, before/after snapshots)
|
|
101
|
+
* @throws Error if changeset ID is invalid, changeset not found, or not in 'staged' status
|
|
102
|
+
*/
|
|
103
|
+
async stage(changesetId, change) {
|
|
104
|
+
this.validateChangesetId(changesetId);
|
|
105
|
+
const changeset = await this.load(changesetId);
|
|
106
|
+
if (!changeset)
|
|
107
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
108
|
+
if (changeset.status !== "staged")
|
|
109
|
+
throw new Error(`Cannot stage changes on changeset with status '${changeset.status}'. Changeset must have status 'staged' to accept new changes.`);
|
|
110
|
+
const stagedChange = {
|
|
111
|
+
...change,
|
|
112
|
+
timestamp: change.timestamp || (/* @__PURE__ */ new Date()).toISOString()
|
|
113
|
+
};
|
|
114
|
+
await this.storage.addChange(changesetId, stagedChange), (await this.getProjectionEngine()).invalidateOnStage(changesetId, change.layerName);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Unstage a specific element from the changeset.
|
|
118
|
+
* Removes all changes associated with the element.
|
|
119
|
+
* The changeset must be in 'staged' status.
|
|
120
|
+
*
|
|
121
|
+
* @param changesetId - ID of the changeset to unstage from
|
|
122
|
+
* @param elementId - ID of the element to remove all changes for
|
|
123
|
+
* @throws Error if changeset ID is invalid, changeset not found, or not in 'staged' status
|
|
124
|
+
*/
|
|
125
|
+
async unstage(changesetId, elementId) {
|
|
126
|
+
this.validateChangesetId(changesetId);
|
|
127
|
+
const changeset = await this.load(changesetId);
|
|
128
|
+
if (!changeset)
|
|
129
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
130
|
+
if (changeset.status !== "staged")
|
|
131
|
+
throw new Error(`Cannot unstage changes on changeset with status '${changeset.status}'. Changeset must have status 'staged'.`);
|
|
132
|
+
await this.storage.removeChange(changesetId, elementId), await (await this.getProjectionEngine()).invalidateOnUnstage(changesetId, elementId);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Discard all changes in the changeset.
|
|
136
|
+
* Clears all staged changes and marks the changeset as 'discarded'.
|
|
137
|
+
* This operation is irreversible for the changeset but does not affect the base model.
|
|
138
|
+
*
|
|
139
|
+
* @param changesetId - ID of the changeset to discard
|
|
140
|
+
* @throws Error if changeset ID is invalid or changeset not found
|
|
141
|
+
*/
|
|
142
|
+
async discard(changesetId) {
|
|
143
|
+
this.validateChangesetId(changesetId);
|
|
144
|
+
const changeset = await this.load(changesetId);
|
|
145
|
+
if (!changeset)
|
|
146
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
147
|
+
changeset.changes = [], changeset.status = "discarded", changeset.updateModified(), await this.storage.save(changeset), (await this.getProjectionEngine()).invalidateOnDiscard(changesetId);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Capture a base snapshot of the current model state for drift detection.
|
|
151
|
+
* Stores a hash of the model that can be compared later to detect changes.
|
|
152
|
+
*
|
|
153
|
+
* @param changesetId - ID of the changeset to capture snapshot for
|
|
154
|
+
* @returns SHA256 hash of the current model state
|
|
155
|
+
* @throws Error if changeset not found or model not available
|
|
156
|
+
*/
|
|
157
|
+
async captureBaseSnapshot(changesetId) {
|
|
158
|
+
if (this.validateChangesetId(changesetId), !this.model)
|
|
159
|
+
throw new Error("Model required for capturing base snapshot");
|
|
160
|
+
const changeset = await this.load(changesetId);
|
|
161
|
+
if (!changeset)
|
|
162
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
163
|
+
const snapshotId = await this.snapshotManager.captureSnapshot(this.model);
|
|
164
|
+
return changeset.baseSnapshot = snapshotId, changeset.updateModified(), await this.storage.save(changeset), snapshotId;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Detect drift by comparing base snapshot hash to current model state.
|
|
168
|
+
* Identifies whether the base model has changed since the changeset was created.
|
|
169
|
+
*
|
|
170
|
+
* @param changesetId - ID of the changeset to check for drift
|
|
171
|
+
* @returns DriftReport with isDrifted flag, snapshot hashes, and warnings
|
|
172
|
+
* @throws Error if changeset ID is invalid, changeset not found, or model not available
|
|
173
|
+
*
|
|
174
|
+
* @remarks
|
|
175
|
+
* Drift detection uses hash-based comparison of the entire model snapshot.
|
|
176
|
+
* Element-level change tracking is not implemented - only the isDrifted flag
|
|
177
|
+
* indicates if the model has changed. Detailed diff detection would require
|
|
178
|
+
* storing complete snapshot data, not just hashes.
|
|
179
|
+
* Returns isDrifted=false and warnings if no base snapshot was captured.
|
|
180
|
+
*/
|
|
181
|
+
async detectDrift(changesetId) {
|
|
182
|
+
if (this.validateChangesetId(changesetId), !this.model)
|
|
183
|
+
throw new Error("Model required for drift detection");
|
|
184
|
+
const changeset = await this.load(changesetId);
|
|
185
|
+
if (!changeset)
|
|
186
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
187
|
+
if (!changeset.baseSnapshot)
|
|
188
|
+
return {
|
|
189
|
+
isDrifted: !1,
|
|
190
|
+
baseSnapshotId: "unknown",
|
|
191
|
+
currentSnapshotId: "unknown",
|
|
192
|
+
warnings: ["No base snapshot available for drift detection"]
|
|
193
|
+
};
|
|
194
|
+
const currentSnapshotId = await this.snapshotManager.captureSnapshot(this.model), isDrifted = changeset.baseSnapshot !== currentSnapshotId, warnings = [];
|
|
195
|
+
return isDrifted && warnings.push("Model has drifted since changeset creation (base snapshot hash differs from current)"), {
|
|
196
|
+
isDrifted,
|
|
197
|
+
baseSnapshotId: changeset.baseSnapshot,
|
|
198
|
+
currentSnapshotId,
|
|
199
|
+
warnings
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Commit a changeset to the base model with atomic guarantee.
|
|
204
|
+
* Applies all staged changes and persists them to the base model.
|
|
205
|
+
*
|
|
206
|
+
* Implements atomic commit with the following steps:
|
|
207
|
+
* 1. Detect drift in base model (fail unless force=true)
|
|
208
|
+
* 2. Validate projected model (schema, references, semantics)
|
|
209
|
+
* 3. Create backup of current model state
|
|
210
|
+
* 4. Apply all changes in sequence order
|
|
211
|
+
* 5. Save all modified layers to disk
|
|
212
|
+
* 6. Update changeset status and manifest history
|
|
213
|
+
* 7. Cleanup backup on success, restore on failure
|
|
214
|
+
*
|
|
215
|
+
* @param model - The model to commit changes to
|
|
216
|
+
* @param changesetId - ID of the changeset to commit
|
|
217
|
+
* @param options - Commit options (force, validate, dryRun)
|
|
218
|
+
* @returns CommitResult with count of committed/failed changes and validation status
|
|
219
|
+
* @throws Error if validation fails, drift detected without --force, or commit fails
|
|
220
|
+
*
|
|
221
|
+
* @remarks
|
|
222
|
+
* Validation errors always block commit (force flag does not override).
|
|
223
|
+
* Drift warnings can be bypassed with --force flag.
|
|
224
|
+
* Dry-run mode performs all checks but does not persist changes.
|
|
225
|
+
* On commit failure, model state is restored from backup; rollback failure triggers CRITICAL error.
|
|
226
|
+
*/
|
|
227
|
+
async commit(model, changesetId, options = {}) {
|
|
228
|
+
return startActiveSpan("staging.commit", async (span) => {
|
|
229
|
+
this.validateChangesetId(changesetId);
|
|
230
|
+
const changeset = await this.load(changesetId);
|
|
231
|
+
if (!changeset)
|
|
232
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
233
|
+
span.setAttribute("changeset.name", changeset.name), span.setAttribute("changeset.changeCount", changeset.changes.length);
|
|
234
|
+
const isDryRun = options.dryRun === !0, shouldValidate = options.validate !== !1, driftSpan = isTelemetryEnabled ? startSpan("staging.detectDrift") : null, drift = await this.detectDrift(changesetId);
|
|
235
|
+
if (isTelemetryEnabled && driftSpan && (driftSpan.setAttribute("drift.detected", drift.isDrifted), driftSpan.setAttribute("drift.baseSnapshotId", drift.baseSnapshotId), driftSpan.setAttribute("drift.currentSnapshotId", drift.currentSnapshotId), driftSpan.setStatus({ code: 0 })), endSpan(driftSpan), drift.isDrifted)
|
|
236
|
+
if (span.setAttribute("commit.driftDetected", !0), options.force)
|
|
237
|
+
emitLog(SeverityNumber.WARN, "Drift detected but overridden with --force", {
|
|
238
|
+
"drift.baseSnapshot": drift.baseSnapshotId,
|
|
239
|
+
"drift.currentSnapshot": drift.currentSnapshotId
|
|
240
|
+
});
|
|
241
|
+
else
|
|
242
|
+
throw emitLog(SeverityNumber.ERROR, "Drift detected, commit blocked", {
|
|
243
|
+
"drift.baseSnapshot": drift.baseSnapshotId,
|
|
244
|
+
"drift.currentSnapshot": drift.currentSnapshotId
|
|
245
|
+
}), new Error("Base model has drifted since changeset creation. Use --force to commit anyway.");
|
|
246
|
+
const result = {
|
|
247
|
+
changeset: changeset.name,
|
|
248
|
+
committed: 0,
|
|
249
|
+
skipped: 0,
|
|
250
|
+
skippedDetails: [],
|
|
251
|
+
failed: 0,
|
|
252
|
+
validation: {
|
|
253
|
+
passed: !0,
|
|
254
|
+
errors: []
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
if (shouldValidate && await startActiveSpan("staging.validate", async (validationSpan) => {
|
|
258
|
+
const validation = await this.validator.validateChangeset(model, changesetId);
|
|
259
|
+
if (validationSpan.setAttribute("validation.passed", validation.isValid()), validationSpan.setAttribute("validation.errorCount", validation.errors.length), validationSpan.setStatus({ code: 0 }), !validation.isValid())
|
|
260
|
+
throw result.validation.passed = !1, result.validation.errors = validation.errors.map((e) => e.message), emitLog(SeverityNumber.ERROR, "Validation failed", {
|
|
261
|
+
"validation.errorCount": result.validation.errors.length,
|
|
262
|
+
"validation.errors": result.validation.errors.slice(0, 10)
|
|
263
|
+
// First 10 errors
|
|
264
|
+
}), new Error(`Validation failed with ${result.validation.errors.length} error(s):
|
|
265
|
+
` + result.validation.errors.map((e) => ` - ${e}`).join(`
|
|
266
|
+
`) + `
|
|
267
|
+
|
|
268
|
+
Fix these validation errors before committing.`);
|
|
269
|
+
}), isDryRun)
|
|
270
|
+
return result.committed = changeset.changes.length, drift.isDrifted && (result.driftWarning = drift), result;
|
|
271
|
+
let backup;
|
|
272
|
+
try {
|
|
273
|
+
backup = await this.backupModel(model);
|
|
274
|
+
} catch (backupError) {
|
|
275
|
+
throw new Error(`Failed to create backup before commit. No changes have been applied. ${backupError instanceof Error ? backupError.message : String(backupError)}`);
|
|
276
|
+
}
|
|
277
|
+
const sortedChanges = [...changeset.changes].sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
278
|
+
try {
|
|
279
|
+
const applySpan = isTelemetryEnabled ? startSpan("staging.apply", {
|
|
280
|
+
"apply.changeCount": changeset.changes.length
|
|
281
|
+
}) : null;
|
|
282
|
+
try {
|
|
283
|
+
for (const change of sortedChanges)
|
|
284
|
+
try {
|
|
285
|
+
let layer = await model.getBaseLayer(change.layerName);
|
|
286
|
+
if (layer || (layer = new Layer(change.layerName), model.addLayer(layer)), change.type === "relationship-add" && change.after) {
|
|
287
|
+
const rel = change.after;
|
|
288
|
+
model.relationships.find(rel.source, rel.target, rel.predicate).length === 0 && model.relationships.add({
|
|
289
|
+
source: rel.source,
|
|
290
|
+
target: rel.target,
|
|
291
|
+
predicate: rel.predicate,
|
|
292
|
+
layer: rel.layer,
|
|
293
|
+
...rel.targetLayer ? { targetLayer: rel.targetLayer } : {},
|
|
294
|
+
category: rel.category ?? "structural",
|
|
295
|
+
...rel.properties ? { properties: rel.properties } : {}
|
|
296
|
+
}), result.committed++;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (change.type === "relationship-delete" && change.before) {
|
|
300
|
+
const rel = change.before;
|
|
301
|
+
model.relationships.delete(rel.source, rel.target, rel.predicate), result.committed++;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (change.type === "add" && change.after) {
|
|
305
|
+
const { Element } = await import("./element.js"), elementData = change.after, elementId = elementData.id;
|
|
306
|
+
if (layer.listElements().some((e) => e.id === elementId))
|
|
307
|
+
result.skipped = (result.skipped ?? 0) + 1, result.skippedDetails = result.skippedDetails ?? [], result.skippedDetails.push(`'${elementId}' in '${change.layerName}' already exists \u2014 deduplicated`), emitLog(SeverityNumber.WARN, "Element deduplicated during commit", {
|
|
308
|
+
"change.elementId": change.elementId,
|
|
309
|
+
"change.layer": change.layerName
|
|
310
|
+
});
|
|
311
|
+
else {
|
|
312
|
+
const element = new Element(elementData);
|
|
313
|
+
layer.addElement(element), result.committed++;
|
|
314
|
+
}
|
|
315
|
+
} else if (change.type === "update" && change.after) {
|
|
316
|
+
const element = layer.getElement(change.elementId);
|
|
317
|
+
if (element) {
|
|
318
|
+
const elementData = change.after;
|
|
319
|
+
Object.assign(element, elementData), layer.updateElement(element), result.committed++;
|
|
320
|
+
} else
|
|
321
|
+
result.failed++, result.validation.passed = !1, result.validation.errors.push(`Cannot update '${change.elementId}' in '${change.layerName}': element not found in base model`);
|
|
322
|
+
} else if (change.type === "delete") {
|
|
323
|
+
const element = layer.getElement(change.elementId);
|
|
324
|
+
element ? (layer.deleteElement(element.path || element.id), result.committed++) : (result.skipped = (result.skipped ?? 0) + 1, result.skippedDetails = result.skippedDetails ?? [], result.skippedDetails.push(`'${change.elementId}' in '${change.layerName}' not found \u2014 already deleted`));
|
|
325
|
+
} else
|
|
326
|
+
result.failed++, result.validation.passed = !1, result.validation.errors.push(`Cannot apply change '${change.type}' for '${change.elementId}': missing payload or unrecognised type`);
|
|
327
|
+
} catch (error) {
|
|
328
|
+
throw result.failed++, result.validation.passed = !1, result.validation.errors.push(`Failed to apply change to ${change.elementId}: ${getErrorMessage(error)}`), emitLog(SeverityNumber.ERROR, "Failed to apply change", {
|
|
329
|
+
"change.elementId": change.elementId,
|
|
330
|
+
"change.type": change.type,
|
|
331
|
+
"change.layer": change.layerName,
|
|
332
|
+
"error.message": getErrorMessage(error)
|
|
333
|
+
}), new Error(`Atomic commit failed at change ${sortedChanges.indexOf(change) + 1}/${sortedChanges.length}: ${getErrorMessage(error)}`);
|
|
334
|
+
}
|
|
335
|
+
isTelemetryEnabled && applySpan && (applySpan.setAttribute("apply.committed", result.committed), applySpan.setAttribute("apply.failed", result.failed), applySpan.setStatus({ code: 0 }));
|
|
336
|
+
} finally {
|
|
337
|
+
endSpan(applySpan);
|
|
338
|
+
}
|
|
339
|
+
const modifiedLayers = /* @__PURE__ */ new Set();
|
|
340
|
+
let hasRelationshipChanges = !1;
|
|
341
|
+
for (const change of sortedChanges)
|
|
342
|
+
change.type === "relationship-add" || change.type === "relationship-delete" ? hasRelationshipChanges = !0 : modifiedLayers.add(change.layerName);
|
|
343
|
+
for (const layerName of modifiedLayers)
|
|
344
|
+
try {
|
|
345
|
+
await model.saveLayer(layerName);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw new Error(`Failed to save layer '${layerName}': ${getErrorMessage(error)}`);
|
|
348
|
+
}
|
|
349
|
+
if (hasRelationshipChanges)
|
|
350
|
+
try {
|
|
351
|
+
await model.saveRelationships();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Failed to save relationships after commit: ${getErrorMessage(error)}`);
|
|
354
|
+
}
|
|
355
|
+
changeset.status = "committed", changeset.updateModified();
|
|
356
|
+
try {
|
|
357
|
+
await this.storage.save(changeset);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw new Error(`Failed to save changeset status after commit: ${getErrorMessage(error)}. This is a critical error that requires rollback to maintain consistency between model state and changeset metadata.`);
|
|
360
|
+
}
|
|
361
|
+
model.manifest.changeset_history || (model.manifest.changeset_history = []), model.manifest.changeset_history.push({
|
|
362
|
+
name: changeset.name,
|
|
363
|
+
committed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
364
|
+
action: "committed"
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
await model.saveManifest();
|
|
368
|
+
} catch (manifestError) {
|
|
369
|
+
throw new Error(`Failed to save manifest with changeset history: ${manifestError instanceof Error ? manifestError.message : String(manifestError)}. Model will be rolled back to maintain consistency.`);
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const orchestrator = new ModelReportOrchestrator(model, model.rootPath), affectedLayers = /* @__PURE__ */ new Set();
|
|
373
|
+
for (const change of sortedChanges)
|
|
374
|
+
if (isValidLayerName(change.layerName))
|
|
375
|
+
for (const layer of orchestrator.computeAffectedLayers(change.layerName))
|
|
376
|
+
affectedLayers.add(layer);
|
|
377
|
+
await orchestrator.regenerate(affectedLayers);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
emitLog(SeverityNumber.WARN, "Failed to regenerate layer reports after changeset commit", {
|
|
380
|
+
"changeset.id": changesetId,
|
|
381
|
+
"changeset.name": changeset.name,
|
|
382
|
+
"changeset.changeCount": sortedChanges.length,
|
|
383
|
+
"error.message": getErrorMessage(error)
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
return await this.cleanupBackup(backup), span.setAttribute("commit.success", !0), span.setAttribute("commit.committed", result.committed), drift.isDrifted && span.setAttribute("commit.driftOverridden", !0), span.setStatus({ code: 0 }), result;
|
|
387
|
+
} catch (commitError) {
|
|
388
|
+
let rollbackSucceeded = !1, rollbackError = null;
|
|
389
|
+
const rollbackSpan = isTelemetryEnabled ? startSpan("staging.rollback") : null;
|
|
390
|
+
try {
|
|
391
|
+
emitLog(SeverityNumber.WARN, "Commit failed, attempting rollback", {
|
|
392
|
+
"commit.error": commitError instanceof Error ? commitError.message : String(commitError)
|
|
393
|
+
}), await this.restoreModel(model, backup), rollbackSucceeded = !0, isTelemetryEnabled && rollbackSpan && (rollbackSpan.setAttribute("rollback.success", !0), rollbackSpan.setStatus({ code: 0 })), emitLog(SeverityNumber.INFO, "Rollback succeeded, model restored", {
|
|
394
|
+
"backup.path": backup
|
|
395
|
+
});
|
|
396
|
+
} catch (err) {
|
|
397
|
+
rollbackError = err instanceof Error ? err : new Error(String(err)), isTelemetryEnabled && rollbackSpan && (rollbackSpan.setAttribute("rollback.success", !1), rollbackSpan.recordException(rollbackError), rollbackSpan.setStatus({ code: 2, message: rollbackError.message })), emitLog(SeverityNumber.ERROR, "Rollback failed, model may be corrupted", {
|
|
398
|
+
"rollback.error": rollbackError.message,
|
|
399
|
+
"backup.path": backup
|
|
400
|
+
});
|
|
401
|
+
} finally {
|
|
402
|
+
endSpan(rollbackSpan);
|
|
403
|
+
}
|
|
404
|
+
let backupHealth, backupValidationError = null, validationProcessFailed = !1;
|
|
405
|
+
try {
|
|
406
|
+
backupHealth = await this.validateBackupIntegrity(backup);
|
|
407
|
+
} catch (validationErr) {
|
|
408
|
+
validationProcessFailed = !0, backupValidationError = validationErr instanceof Error ? validationErr.message : String(validationErr), backupHealth = {
|
|
409
|
+
isValid: !1,
|
|
410
|
+
filesChecked: 0,
|
|
411
|
+
errors: [
|
|
412
|
+
`Validation process failed: ${backupValidationError}`,
|
|
413
|
+
"This indicates a critical issue with the backup infrastructure, not the backup content.",
|
|
414
|
+
"Contact support before attempting manual restoration."
|
|
415
|
+
]
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
let compositeMessage;
|
|
419
|
+
const suggestions = [];
|
|
420
|
+
if (rollbackSucceeded)
|
|
421
|
+
throw compositeMessage = `Commit failed but rollback succeeded. Model restored to pre-commit state.
|
|
422
|
+
|
|
423
|
+
`, compositeMessage += `Error during commit:
|
|
424
|
+
`, compositeMessage += ` ${commitError instanceof Error ? commitError.message : String(commitError)}
|
|
425
|
+
|
|
426
|
+
`, compositeMessage += `Backup location (for reference): ${backup}`, await this.cleanupBackup(backup), span.setAttribute("commit.success", !1), span.setAttribute("commit.rollbackSuccess", !0), span.recordException(commitError), span.setStatus({
|
|
427
|
+
code: 2,
|
|
428
|
+
message: commitError instanceof Error ? commitError.message : String(commitError)
|
|
429
|
+
}), new Error(compositeMessage);
|
|
430
|
+
{
|
|
431
|
+
if (compositeMessage = `Commit failed AND rollback failed. Model may be in corrupted state.
|
|
432
|
+
|
|
433
|
+
Original commit error:
|
|
434
|
+
${commitError instanceof Error ? commitError.message : String(commitError)}
|
|
435
|
+
|
|
436
|
+
Rollback error:
|
|
437
|
+
${rollbackError ? rollbackError.message : "Unknown error"}
|
|
438
|
+
|
|
439
|
+
Backup location: ${backup}
|
|
440
|
+
`, compositeMessage += `Backup integrity check:
|
|
441
|
+
`, validationProcessFailed)
|
|
442
|
+
compositeMessage += ` \u2717 Backup integrity issues found:
|
|
443
|
+
`, compositeMessage += ` - Validation process failed: ${backupValidationError}
|
|
444
|
+
`, compositeMessage += `
|
|
445
|
+
\u26A0 CRITICAL: Backup validation process crashed.
|
|
446
|
+
`, compositeMessage += `This indicates a system-level issue (filesystem, memory, permissions).
|
|
447
|
+
`, compositeMessage += `DO NOT attempt manual restoration until this is resolved.
|
|
448
|
+
`;
|
|
449
|
+
else if (backupHealth.isValid)
|
|
450
|
+
compositeMessage += ` \u2713 Backup is valid (${backupHealth.filesChecked} files checked)
|
|
451
|
+
`;
|
|
452
|
+
else {
|
|
453
|
+
compositeMessage += ` \u2717 Backup integrity issues found:
|
|
454
|
+
`;
|
|
455
|
+
for (const error of backupHealth.errors)
|
|
456
|
+
compositeMessage += ` - ${error}
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
compositeMessage += `
|
|
460
|
+
`, backupHealth.isValid ? suggestions.push(`Backup is valid. Manually restore from backup: ${backup}`, `Copy manifest.json/manifest.yaml from backup to ${path.join(model.rootPath, "documentation-robotics")}`, `Copy layer files from ${path.join(backup, "layers")} to ${path.join(model.rootPath, "documentation-robotics", "layers")}`, "Run 'dr validate' after manual restoration", "Contact support if issue persists") : suggestions.push("\u26A0 Backup integrity is compromised. Do NOT use this backup for recovery.", `Check if backup files exist at: ${backup}`, "Verify disk space and file permissions", "Contact support immediately - manual data recovery may be required", "Do not attempt manual restoration with a corrupted backup"), compositeMessage += "Manual recovery may be required.", span.setAttribute("commit.success", !1), span.setAttribute("commit.rollbackSuccess", !1), span.recordException(commitError), span.setStatus({ code: 2, message: "Commit and rollback both failed" });
|
|
461
|
+
const { CLIError } = await import("../utils/errors.js");
|
|
462
|
+
throw new CLIError(compositeMessage, 1, suggestions);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}, {
|
|
466
|
+
"changeset.id": changesetId,
|
|
467
|
+
"commit.validate": options.validate !== !1,
|
|
468
|
+
"commit.force": options.force === !0,
|
|
469
|
+
"commit.dryRun": options.dryRun === !0
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Check if a changeset is currently active.
|
|
474
|
+
* Active changesets intercept mutations to stage changes instead of applying to base model.
|
|
475
|
+
*
|
|
476
|
+
* @param changesetId - ID of the changeset to check
|
|
477
|
+
* @returns true if changeset is marked as active, false otherwise
|
|
478
|
+
* @throws Error if changeset ID is invalid
|
|
479
|
+
*/
|
|
480
|
+
async isActive(changesetId) {
|
|
481
|
+
this.validateChangesetId(changesetId);
|
|
482
|
+
const activePath = path.join(this.rootPath, "documentation-robotics", "changesets", ".active");
|
|
483
|
+
if (!await fileExists(activePath))
|
|
484
|
+
return !1;
|
|
485
|
+
const { readFile } = await import("../utils/file-io.js");
|
|
486
|
+
return (await readFile(activePath)).trim() === changesetId;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Activate a changeset to intercept model mutations.
|
|
490
|
+
* Marks the changeset as active and transitions status to 'staged'.
|
|
491
|
+
* Only one changeset can be active at a time.
|
|
492
|
+
*
|
|
493
|
+
* @param changesetId - ID of the changeset to activate
|
|
494
|
+
* @throws Error if changeset ID is invalid or changeset not found
|
|
495
|
+
*
|
|
496
|
+
* @remarks
|
|
497
|
+
* When a changeset is active, add/update/delete commands will stage changes
|
|
498
|
+
* instead of applying them directly to the base model.
|
|
499
|
+
*/
|
|
500
|
+
async setActive(changesetId) {
|
|
501
|
+
this.validateChangesetId(changesetId);
|
|
502
|
+
const changeset = await this.load(changesetId);
|
|
503
|
+
if (!changeset)
|
|
504
|
+
throw new Error(`Changeset '${changesetId}' not found`);
|
|
505
|
+
changeset.status !== "staged" && (changeset.markStaged(), await this.storage.save(changeset));
|
|
506
|
+
const { writeFile, ensureDir } = await import("../utils/file-io.js"), changesetDir = path.join(this.rootPath, "documentation-robotics", "changesets");
|
|
507
|
+
await ensureDir(changesetDir);
|
|
508
|
+
const activePath = path.join(changesetDir, ".active");
|
|
509
|
+
if (!changeset.id)
|
|
510
|
+
throw new Error("Changeset has no ID, cannot activate");
|
|
511
|
+
await writeFile(activePath, changeset.id);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Clear the active changeset marker.
|
|
515
|
+
* Allows a new changeset to be activated or model mutations to proceed normally.
|
|
516
|
+
*/
|
|
517
|
+
async clearActive() {
|
|
518
|
+
const { writeFile } = await import("../utils/file-io.js"), activePath = path.join(this.rootPath, "documentation-robotics", "changesets", ".active");
|
|
519
|
+
await fileExists(activePath) && await writeFile(activePath, "");
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Get the currently active changeset.
|
|
523
|
+
* Returns null if no changeset is active.
|
|
524
|
+
*
|
|
525
|
+
* @returns The active changeset or null if none is active
|
|
526
|
+
*/
|
|
527
|
+
async getActive() {
|
|
528
|
+
const activePath = path.join(this.rootPath, "documentation-robotics", "changesets", ".active");
|
|
529
|
+
if (!await fileExists(activePath))
|
|
530
|
+
return null;
|
|
531
|
+
const { readFile } = await import("../utils/file-io.js"), id = (await readFile(activePath)).trim();
|
|
532
|
+
return id ? this.storage.load(id) : null;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get the ID of the active changeset without loading the full changeset object.
|
|
536
|
+
* Useful for status display and quick active changeset checks.
|
|
537
|
+
*
|
|
538
|
+
* @returns The active changeset ID or null if no active changeset exists
|
|
539
|
+
*/
|
|
540
|
+
async getActiveId() {
|
|
541
|
+
const activePath = path.join(this.rootPath, "documentation-robotics", "changesets", ".active");
|
|
542
|
+
if (!await fileExists(activePath))
|
|
543
|
+
return null;
|
|
544
|
+
const { readFile } = await import("../utils/file-io.js");
|
|
545
|
+
return (await readFile(activePath)).trim() || null;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Apply a changeset to the base model.
|
|
549
|
+
* Alias for commit() to match legacy command terminology.
|
|
550
|
+
*
|
|
551
|
+
* @param model - The model to apply changes to
|
|
552
|
+
* @param changesetId - ID of the changeset to apply
|
|
553
|
+
* @param options - Commit options (force, validate, dryRun)
|
|
554
|
+
* @returns CommitResult with count of applied changes and validation status
|
|
555
|
+
* @throws Error if validation fails, drift detected without --force, or apply fails
|
|
556
|
+
*
|
|
557
|
+
* @remarks
|
|
558
|
+
* This method provides backward compatibility with changeset commands that use
|
|
559
|
+
* "apply" instead of "commit" terminology. All logic is delegated to commit().
|
|
560
|
+
*/
|
|
561
|
+
async apply(model, changesetId, options = {}) {
|
|
562
|
+
return this.commit(model, changesetId, options);
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Revert a changeset without applying changes.
|
|
566
|
+
* Alias for discard() to match legacy command terminology.
|
|
567
|
+
*
|
|
568
|
+
* @param changesetId - ID of the changeset to revert
|
|
569
|
+
* @throws Error if changeset ID is invalid or changeset not found
|
|
570
|
+
*
|
|
571
|
+
* @remarks
|
|
572
|
+
* This method provides backward compatibility with changeset commands that use
|
|
573
|
+
* "revert" instead of "discard" terminology. All logic is delegated to discard().
|
|
574
|
+
*/
|
|
575
|
+
async revert(changesetId) {
|
|
576
|
+
return this.discard(changesetId);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Backup the current model state for atomic rollback on commit failure.
|
|
580
|
+
* Creates a temporary backup directory with copies of all layer files and manifest.
|
|
581
|
+
* Validates backup completeness and calculates checksums for integrity verification.
|
|
582
|
+
*
|
|
583
|
+
* @param model - The model to backup
|
|
584
|
+
* @returns Path to the backup directory
|
|
585
|
+
* @throws Error if backup creation or validation fails
|
|
586
|
+
*
|
|
587
|
+
* @remarks
|
|
588
|
+
* Backup includes:
|
|
589
|
+
* - manifest.yaml or manifest.json
|
|
590
|
+
* - All layer YAML files
|
|
591
|
+
* - Backup manifest with checksums and metadata
|
|
592
|
+
* Backup is removed after successful commit; retained if commit fails for recovery.
|
|
593
|
+
*/
|
|
594
|
+
async backupModel(model) {
|
|
595
|
+
const backupDir = path.join(this.rootPath, "documentation-robotics", ".backups", `backup-${Date.now()}`), { ensureDir, readFile, atomicWrite } = await import("../utils/file-io.js"), { createHash } = await import("crypto");
|
|
596
|
+
await ensureDir(backupDir);
|
|
597
|
+
const backupFiles = [], timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
598
|
+
try {
|
|
599
|
+
let manifestPath = path.join(model.rootPath, "documentation-robotics", "model", "manifest.yaml");
|
|
600
|
+
if (await fileExists(manifestPath) || (manifestPath = path.join(model.rootPath, "documentation-robotics", "manifest.json")), !await fileExists(manifestPath))
|
|
601
|
+
throw new Error("Manifest file not found in expected locations");
|
|
602
|
+
const manifestContent = await readFile(manifestPath), manifestChecksum = createSha256Hash(createHash("sha256").update(manifestContent).digest("hex")), manifestFilename = path.basename(manifestPath);
|
|
603
|
+
await atomicWrite(path.join(backupDir, manifestFilename), manifestContent), backupFiles.push({
|
|
604
|
+
path: manifestFilename,
|
|
605
|
+
checksum: manifestChecksum,
|
|
606
|
+
size: manifestContent.length
|
|
607
|
+
});
|
|
608
|
+
const layersDir = path.join(backupDir, "layers");
|
|
609
|
+
await ensureDir(layersDir);
|
|
610
|
+
const fs = await import("fs/promises");
|
|
611
|
+
for (const layer of model.layers.values()) {
|
|
612
|
+
const layerBackupDir = path.join(layersDir, layer.name);
|
|
613
|
+
await ensureDir(layerBackupDir);
|
|
614
|
+
const layerDirPath = await this.findLayerDirectory(model.rootPath, layer.name);
|
|
615
|
+
if (!layerDirPath)
|
|
616
|
+
continue;
|
|
617
|
+
const files = await fs.readdir(layerDirPath);
|
|
618
|
+
for (const file of files)
|
|
619
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
|
620
|
+
const filePath = path.join(layerDirPath, file), fileContent = await readFile(filePath), fileChecksum = createSha256Hash(createHash("sha256").update(fileContent).digest("hex"));
|
|
621
|
+
await atomicWrite(path.join(layerBackupDir, file), fileContent), backupFiles.push({
|
|
622
|
+
path: `layers/${layer.name}/${file}`,
|
|
623
|
+
checksum: fileChecksum,
|
|
624
|
+
size: fileContent.length
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const backupManifest = {
|
|
629
|
+
files: backupFiles,
|
|
630
|
+
timestamp
|
|
631
|
+
};
|
|
632
|
+
return await atomicWrite(path.join(backupDir, ".backup-manifest.json"), JSON.stringify(backupManifest, null, 2)), await this.validateBackup(backupDir, backupManifest), backupDir;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
const cleanupError = await this.forceRemoveBackupDir(backupDir);
|
|
635
|
+
throw cleanupError ? new Error(`Backup creation failed AND cleanup failed.
|
|
636
|
+
Original error: ${getErrorMessage(error)}
|
|
637
|
+
Cleanup error: ${cleanupError}
|
|
638
|
+
|
|
639
|
+
Orphaned backup directory: ${backupDir}
|
|
640
|
+
This directory will accumulate and consume disk space.
|
|
641
|
+
|
|
642
|
+
REQUIRED ACTIONS:
|
|
643
|
+
1. Manually remove: rm -rf "${backupDir}"
|
|
644
|
+
2. Check disk space: df -h
|
|
645
|
+
3. Check permissions: ls -la "${path.dirname(backupDir)}"
|
|
646
|
+
|
|
647
|
+
Cannot proceed with commit until this is resolved.`) : new Error(`Failed to create backup: ${getErrorMessage(error)}`);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Validate backup completeness and file integrity.
|
|
652
|
+
* Verifies that all files in the manifest exist in the backup directory
|
|
653
|
+
* with matching checksums and sizes.
|
|
654
|
+
*
|
|
655
|
+
* @param backupDir - Path to the backup directory
|
|
656
|
+
* @param manifest - Backup manifest with file list and checksums
|
|
657
|
+
* @throws Error if any file is missing, checksum mismatches, or size differs
|
|
658
|
+
*/
|
|
659
|
+
async validateBackup(backupDir, manifest) {
|
|
660
|
+
const { readFile, fileExists: fileExists2 } = await import("../utils/file-io.js"), { createHash } = await import("crypto");
|
|
661
|
+
for (const file of manifest.files) {
|
|
662
|
+
const backupFilePath = path.join(backupDir, file.path);
|
|
663
|
+
if (!await fileExists2(backupFilePath))
|
|
664
|
+
throw new Error(`Backup validation failed: ${file.path} not found in backup`);
|
|
665
|
+
const content = await readFile(backupFilePath), checksum = createHash("sha256").update(content).digest("hex");
|
|
666
|
+
if (checksum !== file.checksum)
|
|
667
|
+
throw new Error(`Backup validation failed: ${file.path} checksum mismatch (expected ${file.checksum}, got ${checksum})`);
|
|
668
|
+
if (content.length !== file.size)
|
|
669
|
+
throw new Error(`Backup validation failed: ${file.path} size mismatch (expected ${file.size}, got ${content.length})`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Validate backup integrity by checking manifest and file checksums.
|
|
674
|
+
* This is called before suggesting manual restoration to ensure the backup is usable.
|
|
675
|
+
* @param backupDir - Path to the backup directory
|
|
676
|
+
* @returns Promise resolving to backup health status or throwing error with details
|
|
677
|
+
* @throws Error with details about any integrity issues found
|
|
678
|
+
*/
|
|
679
|
+
async validateBackupIntegrity(backupDir) {
|
|
680
|
+
const { readFile, fileExists: fileExists2 } = await import("../utils/file-io.js"), { createHash } = await import("crypto"), errors = [];
|
|
681
|
+
let filesChecked = 0;
|
|
682
|
+
try {
|
|
683
|
+
if (!await fileExists2(backupDir))
|
|
684
|
+
throw new Error(`Backup directory not found: ${backupDir}`);
|
|
685
|
+
const manifestPath = path.join(backupDir, ".backup-manifest.json");
|
|
686
|
+
if (!await fileExists2(manifestPath))
|
|
687
|
+
throw new Error(`Backup manifest not found at ${manifestPath}`);
|
|
688
|
+
let rawManifest;
|
|
689
|
+
try {
|
|
690
|
+
const manifestContent = await readFile(manifestPath);
|
|
691
|
+
if (!manifestContent || manifestContent.length === 0)
|
|
692
|
+
throw new Error("Backup manifest is empty");
|
|
693
|
+
rawManifest = JSON.parse(manifestContent.toString());
|
|
694
|
+
} catch (err) {
|
|
695
|
+
throw new Error(`Failed to read backup manifest: ${err instanceof Error ? err.message : String(err)}`);
|
|
696
|
+
}
|
|
697
|
+
if (!rawManifest || typeof rawManifest != "object")
|
|
698
|
+
throw new Error("Backup manifest must be an object");
|
|
699
|
+
if (!rawManifest.files || !Array.isArray(rawManifest.files))
|
|
700
|
+
throw new Error("Backup manifest must have a files array");
|
|
701
|
+
for (let index = 0; index < rawManifest.files.length; index++) {
|
|
702
|
+
const file = rawManifest.files[index];
|
|
703
|
+
if (filesChecked++, !file || typeof file != "object") {
|
|
704
|
+
errors.push(`Backup manifest file entry ${index} must be an object`);
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
if (typeof file.path != "string" || !file.path) {
|
|
708
|
+
errors.push(`Backup manifest file entry ${index} must have a non-empty path string`);
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (typeof file.checksum != "string") {
|
|
712
|
+
errors.push(`Backup manifest file entry ${index} must have a checksum string`);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (typeof file.size != "number" || file.size < 0) {
|
|
716
|
+
errors.push(`Backup manifest file entry ${index} must have a non-negative size number`);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const backupFilePath = path.join(backupDir, file.path);
|
|
720
|
+
if (!await fileExists2(backupFilePath)) {
|
|
721
|
+
errors.push(`Missing: ${file.path}`);
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
const content = await readFile(backupFilePath), checksum = createHash("sha256").update(content).digest("hex");
|
|
726
|
+
if (checksum !== file.checksum) {
|
|
727
|
+
errors.push(`Checksum mismatch: ${file.path} (expected ${file.checksum}, got ${checksum})`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
content.length !== file.size && errors.push(`Size mismatch: ${file.path} (expected ${file.size}, got ${content.length})`);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
errors.push(`Failed to validate: ${file.path} (${err instanceof Error ? err.message : String(err)})`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
isValid: errors.length === 0,
|
|
737
|
+
filesChecked,
|
|
738
|
+
errors
|
|
739
|
+
};
|
|
740
|
+
} catch (err) {
|
|
741
|
+
throw new Error(`Backup integrity check failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Clean up a backup directory after successful commit.
|
|
746
|
+
* Removes the backup directory and all its contents to prevent disk space accumulation.
|
|
747
|
+
*
|
|
748
|
+
* @param backupDir - Path to the backup directory to remove
|
|
749
|
+
*
|
|
750
|
+
* @remarks
|
|
751
|
+
* Failures during cleanup are logged with ERROR severity but do not throw.
|
|
752
|
+
* Successful commits should not fail due to cleanup errors, but users need to
|
|
753
|
+
* know about cleanup failures to avoid disk space accumulation.
|
|
754
|
+
* Includes actionable guidance for common failure scenarios.
|
|
755
|
+
*/
|
|
756
|
+
async cleanupBackup(backupDir) {
|
|
757
|
+
try {
|
|
758
|
+
const { rm } = await import("node:fs/promises");
|
|
759
|
+
await rm(backupDir, { recursive: !0, force: !0 });
|
|
760
|
+
} catch (error) {
|
|
761
|
+
const errorMsg = getErrorMessage(error), errorCode = error?.code;
|
|
762
|
+
let suggestions = "";
|
|
763
|
+
if (errorCode === "ENOENT")
|
|
764
|
+
return;
|
|
765
|
+
if (errorCode === "ENOSPC")
|
|
766
|
+
throw new Error(`Backup cleanup failed due to disk full.
|
|
767
|
+
Original error: ${errorMsg}
|
|
768
|
+
|
|
769
|
+
Orphaned backup directory: ${backupDir}
|
|
770
|
+
This directory will accumulate and consume disk space.
|
|
771
|
+
|
|
772
|
+
REQUIRED ACTIONS:
|
|
773
|
+
1. Manually remove: rm -rf "${backupDir}"
|
|
774
|
+
2. Free up disk space: df -h
|
|
775
|
+
3. Check disk usage: du -sh ${path.dirname(backupDir)}/*
|
|
776
|
+
|
|
777
|
+
Cannot proceed with future commits until disk space is freed.`);
|
|
778
|
+
errorCode === "EACCES" ? suggestions = `
|
|
779
|
+
|
|
780
|
+
[PERMISSION ERROR] Cannot delete backup directory.
|
|
781
|
+
Directory: ${backupDir}
|
|
782
|
+
Check file permissions or try manual cleanup:
|
|
783
|
+
sudo rm -rf "${backupDir}"` : suggestions = `
|
|
784
|
+
|
|
785
|
+
[ACTION REQUIRED] Backup cleanup failed due to file system error.
|
|
786
|
+
Directory: ${backupDir}
|
|
787
|
+
Manual cleanup: rm -rf "${backupDir}"
|
|
788
|
+
Note: Backup directories accumulate over time and can fill disk space.`, console.error(`[ERROR] Failed to clean up backup directory after commit: ${errorMsg}` + suggestions);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Force remove a backup directory with enhanced error handling.
|
|
793
|
+
* Attempts multiple strategies to ensure removal even with permission issues.
|
|
794
|
+
*
|
|
795
|
+
* @param backupDir - Path to the backup directory to remove
|
|
796
|
+
* @returns Error message if removal failed, null if successful
|
|
797
|
+
*
|
|
798
|
+
* @remarks
|
|
799
|
+
* This is used during backup creation failures to ensure cleanup of partial state.
|
|
800
|
+
* Returns error details instead of throwing so caller can log and continue.
|
|
801
|
+
*/
|
|
802
|
+
async forceRemoveBackupDir(backupDir) {
|
|
803
|
+
const fs = await import("fs/promises");
|
|
804
|
+
try {
|
|
805
|
+
return await fs.rm(backupDir, { recursive: !0, force: !0 }), null;
|
|
806
|
+
} catch {
|
|
807
|
+
try {
|
|
808
|
+
const removalFailures = [], entries = await fs.readdir(backupDir, { withFileTypes: !0 });
|
|
809
|
+
for (const entry of entries) {
|
|
810
|
+
const fullPath = path.join(backupDir, entry.name);
|
|
811
|
+
try {
|
|
812
|
+
entry.isDirectory() ? await fs.rm(fullPath, { recursive: !0, force: !0 }) : await fs.unlink(fullPath);
|
|
813
|
+
} catch (itemError) {
|
|
814
|
+
removalFailures.push({
|
|
815
|
+
path: fullPath,
|
|
816
|
+
error: itemError instanceof Error ? itemError.message : String(itemError)
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
return await fs.rmdir(backupDir), null;
|
|
822
|
+
} catch {
|
|
823
|
+
if (removalFailures.length > 0) {
|
|
824
|
+
const failureDetails = removalFailures.map((f) => ` - ${f.path}: ${f.error}`).join(`
|
|
825
|
+
`);
|
|
826
|
+
return `Could not remove ${removalFailures.length} items from backup directory:
|
|
827
|
+
` + failureDetails + `
|
|
828
|
+
|
|
829
|
+
Manual cleanup required: rm -rf "${backupDir}"`;
|
|
830
|
+
}
|
|
831
|
+
return `Could not remove directory ${backupDir} - may contain leftover files`;
|
|
832
|
+
}
|
|
833
|
+
} catch (aggressiveError) {
|
|
834
|
+
return `Backup cleanup failed: ${aggressiveError instanceof Error ? aggressiveError.message : String(aggressiveError)}`;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
/**
|
|
839
|
+
* Restore model state from backup after failed commit.
|
|
840
|
+
* Restores manifest first (critical), then all layer files with defensive error handling.
|
|
841
|
+
* Provides detailed reporting of restoration progress and failures.
|
|
842
|
+
*
|
|
843
|
+
* @param model - The model to restore
|
|
844
|
+
* @param backupDir - Path to the backup directory
|
|
845
|
+
* @throws Error with detailed restoration summary if restoration fails
|
|
846
|
+
*
|
|
847
|
+
* @remarks
|
|
848
|
+
* Restoration strategy:
|
|
849
|
+
* 1. Restore manifest first (critical for model integrity)
|
|
850
|
+
* 2. Restore all layer files with per-file error handling
|
|
851
|
+
* 3. Continue restoring remaining files even if some fail
|
|
852
|
+
* 4. Throw detailed error if critical files fail to restore
|
|
853
|
+
* Both manifest and layer restoration failures are critical and will throw errors.
|
|
854
|
+
*/
|
|
855
|
+
async restoreModel(model, backupDir) {
|
|
856
|
+
const { readFile, fileExists: fileExists2, atomicWrite } = await import("../utils/file-io.js"), restoredFiles = [], failedFiles = [], criticalLayers = /* @__PURE__ */ new Set();
|
|
857
|
+
try {
|
|
858
|
+
let backupManifestFile = path.join(backupDir, "manifest.yaml"), manifestDestPath = path.join(model.rootPath, "documentation-robotics", "model", "manifest.yaml");
|
|
859
|
+
await fileExists2(backupManifestFile) || (backupManifestFile = path.join(backupDir, "manifest.json"), manifestDestPath = path.join(model.rootPath, "documentation-robotics", "manifest.json"));
|
|
860
|
+
try {
|
|
861
|
+
if (!await fileExists2(backupManifestFile))
|
|
862
|
+
throw new Error("Backup manifest file not found (tried .yaml and .json)");
|
|
863
|
+
const manifestContent = await readFile(backupManifestFile);
|
|
864
|
+
await atomicWrite(manifestDestPath, manifestContent), restoredFiles.push(path.basename(backupManifestFile));
|
|
865
|
+
} catch (error) {
|
|
866
|
+
throw failedFiles.push({
|
|
867
|
+
path: path.basename(backupManifestFile),
|
|
868
|
+
error: getErrorMessage(error),
|
|
869
|
+
isCritical: !0
|
|
870
|
+
}), new Error(`Failed to restore ${path.basename(backupManifestFile)}: ${getErrorMessage(error)}`);
|
|
871
|
+
}
|
|
872
|
+
const backupLayersDir = path.join(backupDir, "layers"), fs = await import("fs/promises");
|
|
873
|
+
for (const layer of model.layers.values()) {
|
|
874
|
+
const backupLayerDir = path.join(backupLayersDir, layer.name);
|
|
875
|
+
try {
|
|
876
|
+
if (!await fileExists2(backupLayerDir)) {
|
|
877
|
+
const errorMsg = `Layer '${layer.name}' not found in backup (backup incomplete or corrupted)`;
|
|
878
|
+
failedFiles.push({
|
|
879
|
+
path: `layers/${layer.name}`,
|
|
880
|
+
error: errorMsg,
|
|
881
|
+
isCritical: !0
|
|
882
|
+
}), criticalLayers.add(layer.name);
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
const layerDirPath = await this.findLayerDirectory(model.rootPath, layer.name);
|
|
886
|
+
if (!layerDirPath) {
|
|
887
|
+
const errorMsg = `Destination directory not found for layer '${layer.name}' (permissions issue or directory structure changed)`;
|
|
888
|
+
failedFiles.push({
|
|
889
|
+
path: `layers/${layer.name}`,
|
|
890
|
+
error: errorMsg,
|
|
891
|
+
isCritical: !0
|
|
892
|
+
}), criticalLayers.add(layer.name);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const backupFiles = await fs.readdir(backupLayerDir);
|
|
896
|
+
let filesRestoredInLayer = 0;
|
|
897
|
+
for (const file of backupFiles)
|
|
898
|
+
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
|
899
|
+
const backupFilePath = path.join(backupLayerDir, file), destFilePath = path.join(layerDirPath, file);
|
|
900
|
+
try {
|
|
901
|
+
const fileContent = await readFile(backupFilePath);
|
|
902
|
+
await atomicWrite(destFilePath, fileContent), restoredFiles.push(`layers/${layer.name}/${file}`), filesRestoredInLayer++;
|
|
903
|
+
} catch (fileError) {
|
|
904
|
+
const errorMsg = fileError instanceof Error ? fileError.message : String(fileError);
|
|
905
|
+
console.error(ansis.red(`\u2717 Failed to restore ${layer.name}/${file}: ${errorMsg}`)), failedFiles.push({
|
|
906
|
+
path: `layers/${layer.name}/${file}`,
|
|
907
|
+
error: errorMsg,
|
|
908
|
+
isCritical: !0
|
|
909
|
+
}), criticalLayers.add(layer.name);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
} catch (error) {
|
|
913
|
+
const errorMessage = getErrorMessage(error);
|
|
914
|
+
failedFiles.push({
|
|
915
|
+
path: `layers/${layer.name}`,
|
|
916
|
+
error: errorMessage,
|
|
917
|
+
isCritical: !0
|
|
918
|
+
}), criticalLayers.add(layer.name);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (model.layers.clear(), criticalLayers.size > 0) {
|
|
922
|
+
const failedLayersList = Array.from(criticalLayers).join(", "), detailedErrors = failedFiles.map((f) => ` - ${f.path}: ${f.error}`).join(`
|
|
923
|
+
`), errorSummary = `Model restoration FAILED - ${failedFiles.length} critical layer(s) could not be restored:
|
|
924
|
+
Failed layers: ${failedLayersList}
|
|
925
|
+
|
|
926
|
+
Detailed errors:
|
|
927
|
+
${detailedErrors}
|
|
928
|
+
|
|
929
|
+
Successfully restored: ${restoredFiles.length} files
|
|
930
|
+
Failed to restore: ${failedFiles.length} files
|
|
931
|
+
|
|
932
|
+
Possible causes:
|
|
933
|
+
1. Backup directory corrupted or incomplete
|
|
934
|
+
2. Insufficient permissions to write to model directory
|
|
935
|
+
3. Disk full during restoration
|
|
936
|
+
4. Layer directory structure changed since backup
|
|
937
|
+
|
|
938
|
+
Backup location: ${backupDir}
|
|
939
|
+
|
|
940
|
+
ACTION REQUIRED: Manually restore from backup or contact support.`;
|
|
941
|
+
throw new Error(errorSummary);
|
|
942
|
+
}
|
|
943
|
+
} catch (error) {
|
|
944
|
+
const summary = `Model restoration failed: ${getErrorMessage(error)}
|
|
945
|
+
|
|
946
|
+
Restoration progress:
|
|
947
|
+
Successfully restored: ${restoredFiles.length} files
|
|
948
|
+
Failed to restore: ${failedFiles.length} files (all critical)
|
|
949
|
+
|
|
950
|
+
Restored files:
|
|
951
|
+
${restoredFiles.map((f) => ` \u2713 ${f}`).join(`
|
|
952
|
+
`)}
|
|
953
|
+
|
|
954
|
+
Failed files:
|
|
955
|
+
${failedFiles.map((f) => ` ${f.isCritical ? "\u2717 [CRITICAL]" : "\u26A0 [WARNING]"} ${f.path}: ${f.error}`).join(`
|
|
956
|
+
`)}
|
|
957
|
+
|
|
958
|
+
Backup location: ${backupDir}`;
|
|
959
|
+
throw new Error(summary);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Find the directory path for a layer in the model.
|
|
964
|
+
* Uses same discovery logic as Model.loadLayer to locate layer directories.
|
|
965
|
+
* Handles naming convention: `{PREFIX}_{layerName}` where PREFIX is numeric (01_motivation, etc).
|
|
966
|
+
*
|
|
967
|
+
* @param rootPath - Root path of the model
|
|
968
|
+
* @param layerName - Name of the layer to find
|
|
969
|
+
* @returns Path to the layer directory or null if not found
|
|
970
|
+
*/
|
|
971
|
+
async findLayerDirectory(rootPath, layerName) {
|
|
972
|
+
const fs = await import("fs/promises"), modelDir = path.join(rootPath, "documentation-robotics", "model");
|
|
973
|
+
try {
|
|
974
|
+
const layerDir = (await fs.readdir(modelDir, { withFileTypes: !0 })).find((e) => e.isDirectory() && e.name.match(/^\d{2}_/) && e.name.replace(/^\d{2}_/, "") === layerName);
|
|
975
|
+
return layerDir ? path.join(modelDir, layerDir.name) : null;
|
|
976
|
+
} catch (error) {
|
|
977
|
+
const errorCode = error?.code;
|
|
978
|
+
if (errorCode === "ENOENT")
|
|
979
|
+
return null;
|
|
980
|
+
throw new Error(`Cannot access model directory for layer '${layerName}': ${getErrorMessage(error)}
|
|
981
|
+
Location: ${modelDir}
|
|
982
|
+
Error code: ${errorCode}
|
|
983
|
+
This indicates a filesystem or permissions issue. Check:
|
|
984
|
+
1. Directory permissions: ls -la ${path.dirname(modelDir)}
|
|
985
|
+
2. Disk health: df -h
|
|
986
|
+
3. Filesystem errors: dmesg | tail`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Generate a unique changeset ID.
|
|
991
|
+
* Combines timestamp and random suffix to ensure uniqueness across concurrent operations.
|
|
992
|
+
*
|
|
993
|
+
* @returns A unique changeset ID in format `changeset-{timestamp}-{random}`
|
|
994
|
+
*/
|
|
995
|
+
generateChangesetId() {
|
|
996
|
+
return `changeset-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
export {
|
|
1000
|
+
StagingAreaManager
|
|
1001
|
+
};
|
|
1002
|
+
//# sourceMappingURL=staging-area.js.map
|