@documentation-robotics/cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +12 -2
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +31 -1
- package/dist/coding-agents/default-permissions.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +3 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +117 -14
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +43 -22
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +10 -4
- package/dist/commands/delete.js.map +2 -2
- package/dist/commands/farm.js +176 -60
- package/dist/commands/farm.js.map +2 -2
- package/dist/commands/mcp.js +19 -4
- package/dist/commands/mcp.js.map +2 -2
- package/dist/commands/relationship.js +207 -26
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/update.js +42 -23
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +12 -6
- package/dist/core/farm-manifest.js.map +2 -2
- package/dist/core/farm-sync-engine.js +1 -1
- package/dist/core/farm-sync-engine.js.map +2 -2
- package/dist/core/farm-sync-state.js +10 -2
- package/dist/core/farm-sync-state.js.map +2 -2
- package/dist/core/migration-registry.js +75 -46
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +82 -30
- package/dist/core/model.js.map +2 -2
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +13 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2236 -7790
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +26 -2
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +48 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +14 -1
- package/dist/mcp/api-key-manager.js.map +2 -2
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/tool-registry.js +19 -3
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +17 -1
- package/dist/mcp/tools/changeset-show.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +19 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +6 -5
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-delete.js +2 -2
- package/dist/mcp/tools/model-delete.js.map +2 -2
- package/dist/mcp/tools/model-search.js +14 -0
- package/dist/mcp/tools/model-search.js.map +2 -2
- package/dist/mcp/tools/model-show.js +29 -2
- package/dist/mcp/tools/model-show.js.map +2 -2
- package/dist/mcp/tools/model-trace.js +93 -30
- package/dist/mcp/tools/model-trace.js.map +2 -2
- package/dist/mcp/tools/model-update.js +35 -19
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +25 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +735 -371
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +395 -411
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +218 -34
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/utils/ansi-suppressor.js +6 -8
- package/dist/utils/ansi-suppressor.js.map +2 -2
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +9 -3
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/project-paths.js +26 -1
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/source-reference.js +14 -12
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +2 -2
- package/dist/utils/spec-version.js.map +1 -1
- package/dist/validators/composed-reference-validator.js +5 -27
- package/dist/validators/composed-reference-validator.js.map +2 -2
- package/dist/validators/reference-validator.js +0 -7
- package/dist/validators/reference-validator.js.map +2 -2
- package/dist/validators/relationship-schema-validator.js +3 -3
- package/dist/validators/relationship-schema-validator.js.map +2 -2
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +2 -1
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/report-data-model.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of relationships following higher\u2192lower layer rule\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: proportion of relationships going higher -> lower\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score (directional compliance)\n * Measures adherence to the higher -> lower layer reference rule\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n // Only count relationships with known (valid) layer names\n const validRelationships = analysis.classified.filter((rel) => {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n return sourceOrder > 0 && targetOrder > 0;\n });\n\n if (validRelationships.length === 0) return 100;\n\n let compliantCount = 0;\n\n for (const rel of validRelationships) {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n\n // Compliant if source >= target (higher layer -> lower/same layer)\n if (sourceOrder >= targetOrder) {\n compliantCount++;\n }\n }\n\n return Math.round((compliantCount / validRelationships.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
|
|
5
|
-
"mappings": "AAQA,SAAS,sBAAkC;AAC3C,SAAS,2BAAkE;AAC3E,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of cross-layer relationships whose predicate is in the catalog (direction is\n * defined by the predicate, not by layer order; see spec/DESIGN_CHARTER.md)\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: cross-layer relationships whose predicate is in the catalog\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score: the share of cross-layer relationships between known\n * layers whose predicate is in the catalog. There is no layer-direction rule; direction is a\n * property of the predicate (spec/DESIGN_CHARTER.md).\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n const crossLayer = analysis.classified.filter(\n (rel) =>\n rel.isCrossLayer && getLayerOrder(rel.sourceLayer) > 0 && getLayerOrder(rel.targetLayer) > 0\n );\n\n if (crossLayer.length === 0) return 100;\n\n const compliantCount = crossLayer.filter((rel) => rel.isSpecCompliant).length;\n return Math.round((compliantCount / crossLayer.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,8 +4,25 @@ import { FileLock } from "../utils/file-lock.js";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import yaml from "yaml";
|
|
6
6
|
import ansis from "ansis";
|
|
7
|
-
import { Changeset } from "./changeset.js";
|
|
7
|
+
import { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from "./changeset.js";
|
|
8
8
|
import { getErrorMessage } from "../utils/errors.js";
|
|
9
|
+
const MAX_UNSTAGED_TOMBSTONES = 200;
|
|
10
|
+
function readTombstones(value, id) {
|
|
11
|
+
if (value == null)
|
|
12
|
+
return;
|
|
13
|
+
if (!Array.isArray(value)) {
|
|
14
|
+
console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const valid = value.filter((t) => !!t && typeof t == "object" && typeof t.sequence == "number" && typeof t.removedAt == "number" && !!t.change && typeof t.change == "object" && typeof t.change.type == "string" && typeof t.change.elementId == "string");
|
|
18
|
+
return valid.length < value.length && console.warn(ansis.yellow(`\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`)), valid;
|
|
19
|
+
}
|
|
20
|
+
function addTombstones(changeset, tombstones) {
|
|
21
|
+
if (changeset.unstaged = [...changeset.unstaged, ...tombstones], changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {
|
|
22
|
+
const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);
|
|
23
|
+
changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES), changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
9
26
|
class StagedChangesetStorage {
|
|
10
27
|
changesetsDir;
|
|
11
28
|
constructor(rootPath) {
|
|
@@ -64,9 +81,11 @@ This may indicate disk failure or incomplete write. Check disk health and restor
|
|
|
64
81
|
status: metadata.status,
|
|
65
82
|
baseSnapshot: metadata.baseSnapshot,
|
|
66
83
|
changes,
|
|
67
|
-
stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 }
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },
|
|
85
|
+
...typeof metadata.lastSequence == "number" ? { lastSequence: metadata.lastSequence } : {},
|
|
86
|
+
...typeof metadata.unstagedPrunedThrough == "number" ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough } : {}
|
|
87
|
+
}, unstaged = readTombstones(metadata.unstaged, id);
|
|
88
|
+
return unstaged && (data.unstaged = unstaged), Changeset.fromJSON(data);
|
|
70
89
|
} catch (error) {
|
|
71
90
|
throw new Error(`Failed to load changeset '${id}': YAML parsing failed.
|
|
72
91
|
Error: ${getErrorMessage(error)}
|
|
@@ -85,7 +104,7 @@ This indicates file corruption. Try:
|
|
|
85
104
|
throw new Error("Changeset must have an id to be saved in YAML format");
|
|
86
105
|
const sanitizedId = this.sanitizeId(changeset.id), changesetPath = path.join(this.changesetsDir, sanitizedId);
|
|
87
106
|
await ensureDir(changesetPath);
|
|
88
|
-
const metadata = {
|
|
107
|
+
const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset)), lastSequence = changeset.lastSequence !== void 0 ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber)) : void 0, metadata = {
|
|
89
108
|
id: changeset.id,
|
|
90
109
|
name: changeset.name,
|
|
91
110
|
description: changeset.description,
|
|
@@ -93,9 +112,13 @@ This indicates file corruption. Try:
|
|
|
93
112
|
modified: changeset.modified,
|
|
94
113
|
status: changeset.status,
|
|
95
114
|
baseSnapshot: changeset.baseSnapshot,
|
|
96
|
-
stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 }
|
|
115
|
+
stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },
|
|
116
|
+
// Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.
|
|
117
|
+
...lastSequence !== void 0 ? { lastSequence } : {},
|
|
118
|
+
...changeset.unstagedPrunedThrough !== void 0 ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough } : {},
|
|
119
|
+
...changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}
|
|
97
120
|
};
|
|
98
|
-
await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(
|
|
121
|
+
await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(numbered), "utf-8");
|
|
99
122
|
}
|
|
100
123
|
/**
|
|
101
124
|
* List all staged changesets
|
|
@@ -154,11 +177,11 @@ Run 'dr changeset list' to see all changesets.`);
|
|
|
154
177
|
const changeset = await this.load(id);
|
|
155
178
|
if (!changeset)
|
|
156
179
|
throw new Error(`Changeset '${id}' not found`);
|
|
157
|
-
const sequenceNumber = changeset
|
|
180
|
+
const sequenceNumber = nextSequenceNumber(changeset), stagedChange = {
|
|
158
181
|
...change,
|
|
159
182
|
sequenceNumber
|
|
160
183
|
};
|
|
161
|
-
return changeset.changes.push(stagedChange), changeset.updateModified(), await this.save(changeset), sequenceNumber;
|
|
184
|
+
return changeset.changes = withSequenceNumbers(changeset.changes), changeset.changes.push(stagedChange), changeset.lastSequence = sequenceNumber, changeset.updateModified(), await this.save(changeset), sequenceNumber;
|
|
162
185
|
});
|
|
163
186
|
} catch (error) {
|
|
164
187
|
const errorMessage = getErrorMessage(error);
|
|
@@ -194,11 +217,16 @@ Changeset path: ${changesetPath}`);
|
|
|
194
217
|
const changeset = await this.load(id);
|
|
195
218
|
if (!changeset)
|
|
196
219
|
throw new Error(`Changeset '${id}' not found`);
|
|
197
|
-
const
|
|
198
|
-
changeset.changes =
|
|
199
|
-
...
|
|
200
|
-
|
|
201
|
-
|
|
220
|
+
const numbered = withSequenceNumbers(changeset.changes), removed = numbered.filter((c) => c.elementId === elementId);
|
|
221
|
+
if (changeset.changes = numbered.filter((c) => c.elementId !== elementId), removed.length > 0) {
|
|
222
|
+
const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });
|
|
223
|
+
changeset.lastSequence = removedAt, addTombstones(changeset, removed.map((c, index) => ({
|
|
224
|
+
sequence: sequenceOf(c, index),
|
|
225
|
+
removedAt,
|
|
226
|
+
change: compactChange(c)
|
|
227
|
+
})));
|
|
228
|
+
}
|
|
229
|
+
changeset.updateModified(), await this.save(changeset);
|
|
202
230
|
});
|
|
203
231
|
} catch (error) {
|
|
204
232
|
const errorMessage = getErrorMessage(error);
|
|
@@ -239,11 +267,14 @@ Changeset path: ${changesetPath}`);
|
|
|
239
267
|
const index = changeset.changes.findIndex((c) => c.elementId === elementId);
|
|
240
268
|
if (index === -1)
|
|
241
269
|
throw new Error(`No change for element '${elementId}' in changeset '${id}'`);
|
|
242
|
-
|
|
243
|
-
changeset.changes[index] =
|
|
270
|
+
changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));
|
|
271
|
+
const existing = changeset.changes[index], rewrittenAt = nextSequenceNumber(changeset);
|
|
272
|
+
changeset.lastSequence = rewrittenAt, addTombstones(changeset, [
|
|
273
|
+
{ sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: !0 }
|
|
274
|
+
]), changeset.changes[index] = {
|
|
244
275
|
...existing,
|
|
245
276
|
...updated,
|
|
246
|
-
sequenceNumber: existing.sequenceNumber
|
|
277
|
+
sequenceNumber: existing.sequenceNumber
|
|
247
278
|
}, changeset.updateModified(), await this.save(changeset);
|
|
248
279
|
});
|
|
249
280
|
} catch (error) {
|
|
@@ -252,6 +283,21 @@ Changeset path: ${changesetPath}`);
|
|
|
252
283
|
Changeset path: ${changesetPath}`);
|
|
253
284
|
}
|
|
254
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Change a changeset's name and/or description in place, under the changeset's file lock so a
|
|
288
|
+
* concurrent stage cannot be lost. Staged changes are untouched.
|
|
289
|
+
*
|
|
290
|
+
* @returns The updated changeset
|
|
291
|
+
* @throws {Error} If the changeset is not found, or on lock or I/O failure
|
|
292
|
+
*/
|
|
293
|
+
async updateMetadata(id, patch) {
|
|
294
|
+
return new FileLock(this.getChangesetPath(id)).withLock(async () => {
|
|
295
|
+
const changeset = await this.load(id);
|
|
296
|
+
if (!changeset)
|
|
297
|
+
throw new Error(`Changeset '${id}' not found`);
|
|
298
|
+
return patch.name !== void 0 && (changeset.name = patch.name), patch.description !== void 0 && (changeset.description = patch.description || void 0), changeset.updateModified(), await this.save(changeset), changeset;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
255
301
|
/**
|
|
256
302
|
* Get the storage path for a changeset
|
|
257
303
|
*/
|
|
@@ -277,6 +323,7 @@ Changeset path: ${changesetPath}`);
|
|
|
277
323
|
}
|
|
278
324
|
}
|
|
279
325
|
export {
|
|
326
|
+
MAX_UNSTAGED_TOMBSTONES,
|
|
280
327
|
StagedChangesetStorage
|
|
281
328
|
};
|
|
282
329
|
//# sourceMappingURL=staged-changeset-storage.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/staged-changeset-storage.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * StagedChangesetStorage - Handles YAML-based storage for staged changesets\n *\n * Manages the new storage structure:\n * documentation-robotics/changesets/{changeset-id}/\n * \u251C\u2500\u2500 metadata.yaml # Name, description, dates, status, base snapshot\n * \u2514\u2500\u2500 changes.yaml # Delta-only change records\n */\n\nimport { readFile, writeFile, readdir, rm } from \"fs/promises\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport ansis from \"ansis\";\nimport type { StagedChangesetData, StagedChange } from \"./changeset.js\";\nimport { Changeset } from \"./changeset.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * Storage manager for staged changesets using YAML format\n */\nexport class StagedChangesetStorage {\n private changesetsDir: string;\n\n constructor(rootPath: string) {\n // Store changesets in documentation-robotics/changesets/ directory\n this.changesetsDir = path.join(rootPath, \"documentation-robotics\", \"changesets\");\n }\n\n /**\n * Create a new staged changeset directory structure\n */\n async create(\n id: string,\n name: string,\n description: string | undefined,\n baseSnapshot: string\n ): Promise<Changeset> {\n const now = new Date().toISOString();\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n // Create directory structure\n await ensureDir(changesetPath);\n\n // Create metadata\n const metadata: Omit<StagedChangesetData, \"changes\"> = {\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n stats: {\n additions: 0,\n modifications: 0,\n deletions: 0,\n },\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write empty changes.yaml\n await writeFile(path.join(changesetPath, \"changes.yaml\"), yaml.stringify([]), \"utf-8\");\n\n // Return constructed changeset object\n return new Changeset({\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n changes: [],\n stats: metadata.stats,\n });\n }\n\n /**\n * Load a staged changeset from YAML files\n */\n async load(id: string): Promise<Changeset | null> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n const metadataPath = path.join(changesetPath, \"metadata.yaml\");\n const changesPath = path.join(changesetPath, \"changes.yaml\");\n\n // Check if changeset directory exists\n if (!(await fileExists(changesetPath))) {\n return null; // Changeset doesn't exist - this is OK\n }\n\n // Changeset directory exists - files SHOULD be present\n const metadataExists = await fileExists(metadataPath);\n const changesExists = await fileExists(changesPath);\n\n if (!metadataExists || !changesExists) {\n // Changeset directory exists but required files are missing - this is corruption\n throw new Error(\n `Changeset '${id}' is corrupted (missing ${!metadataExists ? \"metadata.yaml\" : \"changes.yaml\"}). ` +\n `Location: ${changesetPath}\\n` +\n `This may indicate disk failure or incomplete write. Check disk health and restore from backup if available.`\n );\n }\n\n try {\n // Load metadata\n const metadataContent = await readFile(metadataPath, \"utf-8\");\n const metadata = yaml.parse(metadataContent);\n\n // Load changes\n const changesContent = await readFile(changesPath, \"utf-8\");\n const changes = yaml.parse(changesContent) || [];\n\n // Construct changeset object\n const data: StagedChangesetData = {\n id: metadata.id,\n name: metadata.name,\n description: metadata.description,\n created: metadata.created,\n modified: metadata.modified,\n status: metadata.status,\n baseSnapshot: metadata.baseSnapshot,\n changes: changes as StagedChange[],\n stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },\n };\n\n return Changeset.fromJSON(data);\n } catch (error) {\n // YAML parsing or file read error - provide detailed guidance\n throw new Error(\n `Failed to load changeset '${id}': YAML parsing failed.\\n` +\n `Error: ${getErrorMessage(error)}\\n` +\n `Location: ${metadataPath}\\n` +\n `This indicates file corruption. Try:\\n` +\n ` 1. Validate YAML syntax with: yamllint ${metadataPath}\\n` +\n ` 2. Restore from backup if available\\n` +\n ` 3. Manually edit to fix syntax errors`\n );\n }\n }\n\n /**\n * Save a staged changeset to YAML files\n */\n async save(changeset: Changeset): Promise<void> {\n if (!changeset.id) {\n throw new Error(\"Changeset must have an id to be saved in YAML format\");\n }\n\n const sanitizedId = this.sanitizeId(changeset.id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n await ensureDir(changesetPath);\n\n // Prepare metadata (all fields except changes)\n const metadata = {\n id: changeset.id,\n name: changeset.name,\n description: changeset.description,\n created: changeset.created,\n modified: changeset.modified,\n status: changeset.status,\n baseSnapshot: changeset.baseSnapshot,\n stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write changes.yaml\n await writeFile(\n path.join(changesetPath, \"changes.yaml\"),\n yaml.stringify(changeset.changes),\n \"utf-8\"\n );\n }\n\n /**\n * List all staged changesets\n */\n async list(): Promise<Changeset[]> {\n if (!(await fileExists(this.changesetsDir))) {\n return [];\n }\n\n try {\n const entries = await readdir(this.changesetsDir, { withFileTypes: true });\n const changesets: Changeset[] = [];\n let skippedCount = 0;\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n try {\n const changeset = await this.load(entry.name);\n if (changeset) {\n changesets.push(changeset);\n } else {\n // load() returned null - directory exists but no valid changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Changeset directory '${entry.name}' exists but could not be loaded (may be corrupted)`\n )\n );\n skippedCount++;\n }\n } catch (loadError) {\n // load() threw an error - this is a corrupted changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Failed to load changeset '${entry.name}': ${loadError instanceof Error ? loadError.message : String(loadError)}`\n )\n );\n skippedCount++;\n }\n }\n }\n\n // Show summary if any were skipped\n if (skippedCount > 0) {\n console.warn(\n ansis.yellow(`\\n\u26A0 ${skippedCount} changeset(s) could not be loaded and were skipped.`) +\n ansis.dim(`\\nRun 'dr changeset validate' to check for corruption and repair options.`)\n );\n }\n\n return changesets;\n } catch (error) {\n throw new Error(\n `Failed to list changesets: ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Delete a staged changeset\n */\n async delete(id: string): Promise<void> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n if (!(await fileExists(changesetPath))) {\n // List available changesets to help user\n const available = await this.list();\n const availableIds = available.map((cs) => cs.id).join(\", \");\n\n throw new Error(\n `Changeset '${id}' not found.\\n` +\n `Available changesets: ${availableIds || \"(none)\"}\\n` +\n `Run 'dr changeset list' to see all changesets.`\n );\n }\n\n try {\n await rm(changesetPath, { recursive: true, force: true });\n } catch (error) {\n throw new Error(\n `Failed to delete changeset '${id}': ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Add or update a change in the changeset's changes.yaml\n *\n * Uses file locking to prevent concurrent modification race conditions\n * Assigns sequence number atomically inside the lock to prevent duplicates\n *\n * @returns The assigned sequence number for the change\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async addChange(id: string, change: Omit<StagedChange, \"sequenceNumber\">): Promise<number> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n return await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Assign sequence number atomically inside the lock\n // This prevents race conditions when multiple concurrent operations\n // try to add changes at the same time\n const sequenceNumber = changeset.changes.length;\n const stagedChange: StagedChange = {\n ...change,\n sequenceNumber,\n };\n\n changeset.changes.push(stagedChange);\n changeset.updateModified();\n\n await this.save(changeset);\n\n return sequenceNumber;\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to add change to changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to add change to changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Remove a change by element ID\n *\n * Uses file locking to prevent concurrent modification race conditions\n *\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async removeChange(id: string, elementId: string): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Filter out changes for this element\n const filtered = changeset.changes.filter((c) => c.elementId !== elementId);\n\n // Reconstruct changes with updated sequence numbers\n changeset.changes = filtered.map((c, idx) => ({\n ...c,\n sequenceNumber: idx,\n }));\n\n changeset.updateModified();\n\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to remove change from changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to remove change from changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Update an existing change in-place within the changeset's changes.yaml\n *\n * Atomically replaces the change for the given elementId under a single file lock,\n * preventing data loss if the process is interrupted between a remove and add.\n * The original sequence number is preserved so commit-ordering is unaffected.\n *\n * @throws {Error} If changeset not found, elementId not found in changes, or I/O error\n */\n async updateChange(\n id: string,\n elementId: string,\n updated: Omit<StagedChange, \"sequenceNumber\">\n ): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n const index = changeset.changes.findIndex((c) => c.elementId === elementId);\n if (index === -1) {\n throw new Error(`No change for element '${elementId}' in changeset '${id}'`);\n }\n\n // Spread existing entry first so runtime fields like sequenceNumber and timestamp\n // are preserved. Then apply the updated fields. Since `updated` is\n // Omit<StagedChange, \"sequenceNumber\">, it will not overwrite sequenceNumber.\n const existing = changeset.changes[index] as any;\n changeset.changes[index] = {\n ...existing,\n ...updated,\n sequenceNumber: existing.sequenceNumber ?? index,\n } as any;\n\n changeset.updateModified();\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n throw new Error(\n `Failed to update change in changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Get the storage path for a changeset\n */\n getChangesetPath(id: string): string {\n const sanitizedId = this.sanitizeId(id);\n return path.join(this.changesetsDir, sanitizedId);\n }\n\n /**\n * Get the root changesets directory\n */\n getChangesetsDir(): string {\n return this.changesetsDir;\n }\n\n /**\n * Sanitize changeset ID for safe use in file paths\n * Prevents path traversal by removing special characters and path separators\n */\n private sanitizeId(id: string): string {\n const sanitized = id\n .replace(/\\.\\./g, \"\") // Remove .. (parent directory)\n .replace(/^\\/+/, \"\") // Remove leading slashes\n .replace(/^[A-Z]:\\\\/g, \"\") // Remove Windows drive letters\n .toLowerCase()\n .replace(/\\s+/g, \"-\") // Replace spaces with hyphens\n .replace(/[^a-z0-9-]/g, \"\"); // Keep only alphanumeric and hyphens\n\n if (!sanitized) {\n throw new Error(\n `Invalid changeset ID '${id}': results in empty string after sanitization. ` +\n `ID must contain at least one alphanumeric character.`\n );\n }\n\n return sanitized;\n }\n}\n"],
|
|
5
|
-
"mappings": "AASA,SAAS,UAAU,WAAW,SAAS,UAAU;AACjD,SAAS,YAAY,iBAAiB;AACtC,SAAS,gBAAgB;AACzB,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,OAAO,WAAW;AAElB,SAAS,
|
|
4
|
+
"sourcesContent": ["/**\n * StagedChangesetStorage - Handles YAML-based storage for staged changesets\n *\n * Manages the new storage structure:\n * documentation-robotics/changesets/{changeset-id}/\n * \u251C\u2500\u2500 metadata.yaml # Name, description, dates, status, base snapshot\n * \u2514\u2500\u2500 changes.yaml # Delta-only change records\n */\n\nimport { readFile, writeFile, readdir, rm } from \"fs/promises\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport ansis from \"ansis\";\nimport type { StagedChangesetData, StagedChange, UnstagedChange } from \"./changeset.js\";\nimport { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from \"./changeset.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * How many unstaged changes a changeset keeps as tombstones. Older ones are dropped (and\n * `unstagedPrunedThrough` records how far), so a delta reaching back past them says it is\n * approximate.\n */\nexport const MAX_UNSTAGED_TOMBSTONES = 200;\n\n/**\n * Reads the tombstone list from metadata.yaml, keeping the well-formed entries. Malformed entries\n * are reported, not silently dropped: the changeset still loads, and its changes are untouched.\n */\nfunction readTombstones(value: unknown, id: string): UnstagedChange[] | undefined {\n if (value === undefined || value === null) return undefined;\n if (!Array.isArray(value)) {\n console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));\n return undefined;\n }\n const valid = value.filter(\n (t): t is UnstagedChange =>\n !!t &&\n typeof t === \"object\" &&\n typeof t.sequence === \"number\" &&\n typeof t.removedAt === \"number\" &&\n !!t.change &&\n typeof t.change === \"object\" &&\n typeof t.change.type === \"string\" &&\n typeof t.change.elementId === \"string\"\n );\n if (valid.length < value.length) {\n console.warn(\n ansis.yellow(\n `\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`\n )\n );\n }\n return valid;\n}\n\n/** Appends tombstones, keeping the newest MAX_UNSTAGED_TOMBSTONES and recording how far it trimmed. */\nfunction addTombstones(changeset: Changeset, tombstones: UnstagedChange[]): void {\n changeset.unstaged = [...changeset.unstaged, ...tombstones];\n if (changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {\n const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);\n changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES);\n changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));\n }\n}\n\n/**\n * Storage manager for staged changesets using YAML format\n */\nexport class StagedChangesetStorage {\n private changesetsDir: string;\n\n constructor(rootPath: string) {\n // Store changesets in documentation-robotics/changesets/ directory\n this.changesetsDir = path.join(rootPath, \"documentation-robotics\", \"changesets\");\n }\n\n /**\n * Create a new staged changeset directory structure\n */\n async create(\n id: string,\n name: string,\n description: string | undefined,\n baseSnapshot: string\n ): Promise<Changeset> {\n const now = new Date().toISOString();\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n // Create directory structure\n await ensureDir(changesetPath);\n\n // Create metadata\n const metadata: Omit<StagedChangesetData, \"changes\"> = {\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n stats: {\n additions: 0,\n modifications: 0,\n deletions: 0,\n },\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write empty changes.yaml\n await writeFile(path.join(changesetPath, \"changes.yaml\"), yaml.stringify([]), \"utf-8\");\n\n // Return constructed changeset object\n return new Changeset({\n id: sanitizedId,\n name,\n description,\n created: now,\n modified: now,\n status: \"staged\",\n baseSnapshot,\n changes: [],\n stats: metadata.stats,\n });\n }\n\n /**\n * Load a staged changeset from YAML files\n */\n async load(id: string): Promise<Changeset | null> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n const metadataPath = path.join(changesetPath, \"metadata.yaml\");\n const changesPath = path.join(changesetPath, \"changes.yaml\");\n\n // Check if changeset directory exists\n if (!(await fileExists(changesetPath))) {\n return null; // Changeset doesn't exist - this is OK\n }\n\n // Changeset directory exists - files SHOULD be present\n const metadataExists = await fileExists(metadataPath);\n const changesExists = await fileExists(changesPath);\n\n if (!metadataExists || !changesExists) {\n // Changeset directory exists but required files are missing - this is corruption\n throw new Error(\n `Changeset '${id}' is corrupted (missing ${!metadataExists ? \"metadata.yaml\" : \"changes.yaml\"}). ` +\n `Location: ${changesetPath}\\n` +\n `This may indicate disk failure or incomplete write. Check disk health and restore from backup if available.`\n );\n }\n\n try {\n // Load metadata\n const metadataContent = await readFile(metadataPath, \"utf-8\");\n const metadata = yaml.parse(metadataContent);\n\n // Load changes\n const changesContent = await readFile(changesPath, \"utf-8\");\n const changes = yaml.parse(changesContent) || [];\n\n // Construct changeset object\n const data: StagedChangesetData = {\n id: metadata.id,\n name: metadata.name,\n description: metadata.description,\n created: metadata.created,\n modified: metadata.modified,\n status: metadata.status,\n baseSnapshot: metadata.baseSnapshot,\n changes: changes as StagedChange[],\n stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },\n ...(typeof metadata.lastSequence === \"number\" ? { lastSequence: metadata.lastSequence } : {}),\n ...(typeof metadata.unstagedPrunedThrough === \"number\"\n ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough }\n : {}),\n };\n const unstaged = readTombstones(metadata.unstaged, id);\n if (unstaged) data.unstaged = unstaged;\n\n return Changeset.fromJSON(data);\n } catch (error) {\n // YAML parsing or file read error - provide detailed guidance\n throw new Error(\n `Failed to load changeset '${id}': YAML parsing failed.\\n` +\n `Error: ${getErrorMessage(error)}\\n` +\n `Location: ${metadataPath}\\n` +\n `This indicates file corruption. Try:\\n` +\n ` 1. Validate YAML syntax with: yamllint ${metadataPath}\\n` +\n ` 2. Restore from backup if available\\n` +\n ` 3. Manually edit to fix syntax errors`\n );\n }\n }\n\n /**\n * Save a staged changeset to YAML files\n */\n async save(changeset: Changeset): Promise<void> {\n if (!changeset.id) {\n throw new Error(\"Changeset must have an id to be saved in YAML format\");\n }\n\n const sanitizedId = this.sanitizeId(changeset.id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n await ensureDir(changesetPath);\n\n // A change staged in memory without a number gets the next unused one (never one an unstaged\n // change or unstage event took); lastSequence then covers it.\n const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const lastSequence =\n changeset.lastSequence !== undefined\n ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber))\n : undefined;\n\n // Prepare metadata (all fields except changes)\n const metadata = {\n id: changeset.id,\n name: changeset.name,\n description: changeset.description,\n created: changeset.created,\n modified: changeset.modified,\n status: changeset.status,\n baseSnapshot: changeset.baseSnapshot,\n stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },\n // Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.\n ...(lastSequence !== undefined ? { lastSequence } : {}),\n ...(changeset.unstagedPrunedThrough !== undefined\n ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough }\n : {}),\n ...(changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}),\n };\n\n // Write metadata.yaml\n await writeFile(path.join(changesetPath, \"metadata.yaml\"), yaml.stringify(metadata), \"utf-8\");\n\n // Write changes.yaml\n await writeFile(\n path.join(changesetPath, \"changes.yaml\"),\n yaml.stringify(numbered),\n \"utf-8\"\n );\n }\n\n /**\n * List all staged changesets\n */\n async list(): Promise<Changeset[]> {\n if (!(await fileExists(this.changesetsDir))) {\n return [];\n }\n\n try {\n const entries = await readdir(this.changesetsDir, { withFileTypes: true });\n const changesets: Changeset[] = [];\n let skippedCount = 0;\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n try {\n const changeset = await this.load(entry.name);\n if (changeset) {\n changesets.push(changeset);\n } else {\n // load() returned null - directory exists but no valid changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Changeset directory '${entry.name}' exists but could not be loaded (may be corrupted)`\n )\n );\n skippedCount++;\n }\n } catch (loadError) {\n // load() threw an error - this is a corrupted changeset\n console.warn(\n ansis.yellow(\n `\u26A0 Warning: Failed to load changeset '${entry.name}': ${loadError instanceof Error ? loadError.message : String(loadError)}`\n )\n );\n skippedCount++;\n }\n }\n }\n\n // Show summary if any were skipped\n if (skippedCount > 0) {\n console.warn(\n ansis.yellow(`\\n\u26A0 ${skippedCount} changeset(s) could not be loaded and were skipped.`) +\n ansis.dim(`\\nRun 'dr changeset validate' to check for corruption and repair options.`)\n );\n }\n\n return changesets;\n } catch (error) {\n throw new Error(\n `Failed to list changesets: ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Delete a staged changeset\n */\n async delete(id: string): Promise<void> {\n const sanitizedId = this.sanitizeId(id);\n const changesetPath = path.join(this.changesetsDir, sanitizedId);\n\n if (!(await fileExists(changesetPath))) {\n // List available changesets to help user\n const available = await this.list();\n const availableIds = available.map((cs) => cs.id).join(\", \");\n\n throw new Error(\n `Changeset '${id}' not found.\\n` +\n `Available changesets: ${availableIds || \"(none)\"}\\n` +\n `Run 'dr changeset list' to see all changesets.`\n );\n }\n\n try {\n await rm(changesetPath, { recursive: true, force: true });\n } catch (error) {\n throw new Error(\n `Failed to delete changeset '${id}': ${getErrorMessage(error)}`\n );\n }\n }\n\n /**\n * Add or update a change in the changeset's changes.yaml\n *\n * Uses file locking to prevent concurrent modification race conditions\n * Assigns sequence number atomically inside the lock to prevent duplicates\n *\n * @returns The assigned sequence number for the change\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async addChange(id: string, change: Omit<StagedChange, \"sequenceNumber\">): Promise<number> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n return await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Assign sequence number atomically inside the lock\n // This prevents race conditions when multiple concurrent operations\n // try to add changes at the same time. One past every number used so far (unstaged\n // changes and unstage events included), so a number is never reused and a revision\n // always names the same prefix of the plan.\n const sequenceNumber = nextSequenceNumber(changeset);\n const stagedChange: StagedChange = {\n ...change,\n sequenceNumber,\n };\n\n changeset.changes = withSequenceNumbers(changeset.changes);\n changeset.changes.push(stagedChange);\n changeset.lastSequence = sequenceNumber;\n changeset.updateModified();\n\n await this.save(changeset);\n\n return sequenceNumber;\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to add change to changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to add change to changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Remove a change by element ID\n *\n * Uses file locking to prevent concurrent modification race conditions\n *\n * @throws {Error} If lock acquisition, changeset load, save, or lock release fails\n */\n async removeChange(id: string, elementId: string): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n // Number any change that carries none by its position first, so the ones that stay keep\n // the numbers they had. Remaining changes are not renumbered.\n const numbered = withSequenceNumbers(changeset.changes);\n const removed = numbered.filter((c) => c.elementId === elementId);\n changeset.changes = numbered.filter((c) => c.elementId !== elementId);\n\n if (removed.length > 0) {\n // The unstage is a revision of its own: it takes the next sequence number, and each\n // removed change is kept as a tombstone so the plan before it can be rebuilt.\n const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });\n changeset.lastSequence = removedAt;\n addTombstones(\n changeset,\n removed.map((c, index) => ({\n sequence: sequenceOf(c, index),\n removedAt,\n change: compactChange(c),\n }))\n );\n }\n\n changeset.updateModified();\n\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n\n // Detect lock-specific errors\n if (errorMessage.includes(\"lock\") || errorMessage.includes(\"timeout\")) {\n const lockFilePath = path.join(changesetPath, \".lock\");\n\n throw new Error(\n `Failed to remove change from changeset '${id}': Lock acquisition failed.\\n` +\n `Error: ${errorMessage}\\n\\n` +\n `This usually means another dr process is modifying this changeset.\\n` +\n `If no other processes are running, a stale lock file may exist.\\n\\n` +\n `To resolve:\\n` +\n ` 1. Wait for other operations to complete\\n` +\n ` 2. Check for running dr processes: ps aux | grep dr\\n` +\n ` 3. If no processes found, remove stale lock: rm \"${lockFilePath}\"\\n` +\n ` 4. Retry the operation`\n );\n }\n\n // Generic error with context\n throw new Error(\n `Failed to remove change from changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Update an existing change in-place within the changeset's changes.yaml\n *\n * Atomically replaces the change for the given elementId under a single file lock,\n * preventing data loss if the process is interrupted between a remove and add.\n * The original sequence number is preserved so commit-ordering is unaffected.\n *\n * @throws {Error} If changeset not found, elementId not found in changes, or I/O error\n */\n async updateChange(\n id: string,\n elementId: string,\n updated: Omit<StagedChange, \"sequenceNumber\">\n ): Promise<void> {\n const changesetPath = this.getChangesetPath(id);\n const lock = new FileLock(changesetPath);\n\n try {\n await lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n\n const index = changeset.changes.findIndex((c) => c.elementId === elementId);\n if (index === -1) {\n throw new Error(`No change for element '${elementId}' in changeset '${id}'`);\n }\n\n // Number every change first (an older changes.yaml may carry none), so the list is never\n // left half numbered. Spread the existing entry so runtime fields like sequenceNumber and\n // timestamp are preserved; `updated` omits sequenceNumber and cannot overwrite it.\n changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));\n const existing = changeset.changes[index] as StagedChange;\n // The rewrite is a revision of its own, like an unstage: it takes the next sequence number,\n // and the version the change had is kept as a tombstone so the plan before it can be rebuilt.\n const rewrittenAt = nextSequenceNumber(changeset);\n changeset.lastSequence = rewrittenAt;\n addTombstones(changeset, [\n { sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: true },\n ]);\n changeset.changes[index] = {\n ...existing,\n ...updated,\n sequenceNumber: existing.sequenceNumber,\n } as StagedChange;\n\n changeset.updateModified();\n await this.save(changeset);\n });\n } catch (error) {\n const errorMessage = getErrorMessage(error);\n throw new Error(\n `Failed to update change in changeset '${id}': ${errorMessage}\\n` +\n `Changeset path: ${changesetPath}`\n );\n }\n }\n\n /**\n * Change a changeset's name and/or description in place, under the changeset's file lock so a\n * concurrent stage cannot be lost. Staged changes are untouched.\n *\n * @returns The updated changeset\n * @throws {Error} If the changeset is not found, or on lock or I/O failure\n */\n async updateMetadata(\n id: string,\n patch: { name?: string; description?: string }\n ): Promise<Changeset> {\n const lock = new FileLock(this.getChangesetPath(id));\n return lock.withLock(async () => {\n const changeset = await this.load(id);\n if (!changeset) {\n throw new Error(`Changeset '${id}' not found`);\n }\n if (patch.name !== undefined) changeset.name = patch.name;\n if (patch.description !== undefined) changeset.description = patch.description || undefined;\n changeset.updateModified();\n await this.save(changeset);\n return changeset;\n });\n }\n\n /**\n * Get the storage path for a changeset\n */\n getChangesetPath(id: string): string {\n const sanitizedId = this.sanitizeId(id);\n return path.join(this.changesetsDir, sanitizedId);\n }\n\n /**\n * Get the root changesets directory\n */\n getChangesetsDir(): string {\n return this.changesetsDir;\n }\n\n /**\n * Sanitize changeset ID for safe use in file paths\n * Prevents path traversal by removing special characters and path separators\n */\n private sanitizeId(id: string): string {\n const sanitized = id\n .replace(/\\.\\./g, \"\") // Remove .. (parent directory)\n .replace(/^\\/+/, \"\") // Remove leading slashes\n .replace(/^[A-Z]:\\\\/g, \"\") // Remove Windows drive letters\n .toLowerCase()\n .replace(/\\s+/g, \"-\") // Replace spaces with hyphens\n .replace(/[^a-z0-9-]/g, \"\"); // Keep only alphanumeric and hyphens\n\n if (!sanitized) {\n throw new Error(\n `Invalid changeset ID '${id}': results in empty string after sanitization. ` +\n `ID must contain at least one alphanumeric character.`\n );\n }\n\n return sanitized;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const LEGACY_KEY = "sourceReference", KEY = "source_reference";
|
|
2
|
+
function setStagedSourceReference(snapshot, ref) {
|
|
3
|
+
delete snapshot[LEGACY_KEY], snapshot[KEY] = ref ?? null;
|
|
4
|
+
}
|
|
5
|
+
function readStagedSourceReference(snapshot) {
|
|
6
|
+
if (!snapshot)
|
|
7
|
+
return { present: !1, value: void 0 };
|
|
8
|
+
for (const key of [LEGACY_KEY, KEY])
|
|
9
|
+
if (Object.prototype.hasOwnProperty.call(snapshot, key)) {
|
|
10
|
+
const raw = snapshot[key];
|
|
11
|
+
return { present: !0, value: raw && typeof raw == "object" ? raw : void 0 };
|
|
12
|
+
}
|
|
13
|
+
return { present: !1, value: void 0 };
|
|
14
|
+
}
|
|
15
|
+
function stagedSourceReference(snapshot) {
|
|
16
|
+
return readStagedSourceReference(snapshot).value;
|
|
17
|
+
}
|
|
18
|
+
function canonicalStagedSnapshot(snapshot) {
|
|
19
|
+
const sourceReference = readStagedSourceReference(snapshot), data = { ...snapshot };
|
|
20
|
+
return delete data[LEGACY_KEY], delete data[KEY], sourceReference.value && (data[KEY] = sourceReference.value), { data, sourceReference };
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
canonicalStagedSnapshot,
|
|
24
|
+
readStagedSourceReference,
|
|
25
|
+
setStagedSourceReference,
|
|
26
|
+
stagedSourceReference
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=staged-source-reference.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/staged-source-reference.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Source references inside staged element snapshots.\n *\n * A staged change's `before`/`after` is an element snapshot in the on-disk shape\n * (`Element.toJSON()`), so its source reference lives under the snake_case key\n * `source_reference`. A cleared reference is written as `source_reference: null`:\n * an update's `after` starts as a copy of `before`, so leaving the key out would\n * keep the old reference instead of clearing it.\n *\n * Earlier builds of `dr update` and the MCP `model_update` tool wrote the new\n * reference under a camelCase `sourceReference` key instead, next to the stale\n * snake_case copy. Those changes.yaml files exist, so readers go through\n * {@link readStagedSourceReference}, which honours the camelCase key when present.\n */\nimport type { SourceReference } from \"../types/source-reference.js\";\n\nconst LEGACY_KEY = \"sourceReference\";\nconst KEY = \"source_reference\";\n\n/**\n * Record a source-reference change on a staged element snapshot.\n * `undefined` clears the reference.\n */\nexport function setStagedSourceReference(\n snapshot: Record<string, unknown>,\n ref: SourceReference | undefined\n): void {\n delete snapshot[LEGACY_KEY];\n snapshot[KEY] = ref ?? null;\n}\n\n/**\n * Read a staged snapshot's source reference.\n *\n * `present` is false when the snapshot says nothing about the reference (neither\n * key is set), so a caller can keep whatever the element already has; when it is\n * true, `value` is the reference to use, `undefined` meaning cleared.\n */\nexport function readStagedSourceReference(\n snapshot: Record<string, unknown> | undefined | null\n): { present: boolean; value: SourceReference | undefined } {\n if (!snapshot) return { present: false, value: undefined };\n // The legacy camelCase key carries the intended new value; its snake_case\n // neighbour in such files is the stale copy of `before`.\n for (const key of [LEGACY_KEY, KEY]) {\n if (Object.prototype.hasOwnProperty.call(snapshot, key)) {\n const raw = snapshot[key];\n const value = raw && typeof raw === \"object\" ? (raw as SourceReference) : undefined;\n return { present: true, value };\n }\n }\n return { present: false, value: undefined };\n}\n\n/** The snapshot's source reference, or undefined when it has none or it is cleared. */\nexport function stagedSourceReference(\n snapshot: Record<string, unknown> | undefined | null\n): SourceReference | undefined {\n return readStagedSourceReference(snapshot).value;\n}\n\n/**\n * A copy of the snapshot in the canonical shape: the legacy camelCase key folded\n * into `source_reference`, and a cleared reference left out. Used where a\n * snapshot becomes an element (commit), so no stray keys reach the model.\n */\nexport function canonicalStagedSnapshot(snapshot: Record<string, unknown>): {\n data: Record<string, unknown>;\n sourceReference: { present: boolean; value: SourceReference | undefined };\n} {\n const sourceReference = readStagedSourceReference(snapshot);\n const data = { ...snapshot };\n delete data[LEGACY_KEY];\n delete data[KEY];\n if (sourceReference.value) data[KEY] = sourceReference.value;\n return { data, sourceReference };\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,MAAM,aAAa,mBACb,MAAM;AAMN,SAAU,yBACd,UACA,KAAgC;AAEhC,SAAO,SAAS,UAAU,GAC1B,SAAS,GAAG,IAAI,OAAO;AACzB;AASM,SAAU,0BACd,UAAoD;AAEpD,MAAI,CAAC;AAAU,WAAO,EAAE,SAAS,IAAO,OAAO,OAAS;AAGxD,aAAW,OAAO,CAAC,YAAY,GAAG;AAChC,QAAI,OAAO,UAAU,eAAe,KAAK,UAAU,GAAG,GAAG;AACvD,YAAM,MAAM,SAAS,GAAG;AAExB,aAAO,EAAE,SAAS,IAAM,OADV,OAAO,OAAO,OAAQ,WAAY,MAA0B,OAC7C;IAC/B;AAEF,SAAO,EAAE,SAAS,IAAO,OAAO,OAAS;AAC3C;AAGM,SAAU,sBACd,UAAoD;AAEpD,SAAO,0BAA0B,QAAQ,EAAE;AAC7C;AAOM,SAAU,wBAAwB,UAAiC;AAIvE,QAAM,kBAAkB,0BAA0B,QAAQ,GACpD,OAAO,EAAE,GAAG,SAAQ;AAC1B,gBAAO,KAAK,UAAU,GACtB,OAAO,KAAK,GAAG,GACX,gBAAgB,UAAO,KAAK,GAAG,IAAI,gBAAgB,QAChD,EAAE,MAAM,gBAAe;AAChC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|