@documentation-robotics/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -16
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- 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 +191 -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/extract.js +138 -0
- package/dist/analyzers/cards/extract.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/routes.js +96 -0
- package/dist/analyzers/cards/routes.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 +270 -174
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +342 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +49 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/extraction-plan.js +541 -0
- package/dist/analyzers/extraction-plan.js.map +7 -0
- package/dist/analyzers/index-job.js +229 -0
- package/dist/analyzers/index-job.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 +23 -11
- 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 +269 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +7 -4
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +143 -40
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +63 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +80 -29
- 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/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +40 -21
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +208 -37
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +3 -3
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +52 -32
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -103
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +19 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +1 -1
- package/dist/commands/visualize.js +34 -15
- package/dist/commands/visualize.js.map +2 -2
- package/dist/core/annotation-store.js +15 -5
- package/dist/core/annotation-store.js.map +2 -2
- package/dist/core/catalog-migration.js +34 -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 +160 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.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 +848 -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/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.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 +88 -47
- 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/project-init.js +53 -0
- package/dist/core/project-init.js.map +7 -0
- 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 +161 -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 +145 -18
- 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 +166 -24
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/upgrade-plan.js +117 -0
- package/dist/core/upgrade-plan.js.map +7 -0
- 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 +2468 -7913
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/.claude-plugin/plugin.json +24 -0
- package/dist/integrations/claude_code/agents/dr-architect.md +30 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +8 -2
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +20 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +66 -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 +47 -26
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +68 -2
- package/dist/integrations/claude_code/plugin-agents/dr-advisor.md +255 -0
- package/dist/integrations/claude_code/plugin-agents/dr-architect.md +306 -0
- 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 +95 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +178 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +8 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +6 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +3 -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 +225 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +177 -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 +45 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/project-root.js +85 -0
- package/dist/mcp/project-root.js.map +7 -0
- package/dist/mcp/server-instructions.js +27 -0
- package/dist/mcp/server-instructions.js.map +7 -0
- package/dist/mcp/strict-schema.js +52 -0
- package/dist/mcp/strict-schema.js.map +7 -0
- package/dist/mcp/tool-registry.js +55 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +98 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-commit.js +224 -0
- package/dist/mcp/tools/changeset-commit.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +260 -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 +49 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/codebase-extract.js +836 -0
- package/dist/mcp/tools/codebase-extract.js.map +7 -0
- package/dist/mcp/tools/codebase-index.js +125 -0
- package/dist/mcp/tools/codebase-index.js.map +7 -0
- package/dist/mcp/tools/codebase-shared.js +265 -0
- package/dist/mcp/tools/codebase-shared.js.map +7 -0
- package/dist/mcp/tools/codebase-verify.js +48 -0
- package/dist/mcp/tools/codebase-verify.js.map +7 -0
- package/dist/mcp/tools/index.js +34 -1
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +191 -74
- 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 +46 -26
- package/dist/mcp/tools/model-update.js.map +2 -2
- package/dist/mcp/tools/model-validate.js +46 -4
- package/dist/mcp/tools/model-validate.js.map +2 -2
- package/dist/mcp/tools/project-status-collectors.js +381 -0
- package/dist/mcp/tools/project-status-collectors.js.map +7 -0
- package/dist/mcp/tools/project-status.js +237 -0
- package/dist/mcp/tools/project-status.js.map +7 -0
- package/dist/mcp/tools/project-tools.js +505 -0
- package/dist/mcp/tools/project-tools.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +167 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/review-open.js +210 -0
- package/dist/mcp/tools/review-open.js.map +7 -0
- package/dist/mcp/tools/shared.js +35 -4
- 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/reports/model-report-orchestrator.js +31 -2
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/schemas/bundled/analyzers/cbm.json +1200 -70
- 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 +865 -1646
- 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 +686 -231
- package/dist/server/server.js.map +3 -3
- package/dist/server/viewer-registry.js +264 -0
- package/dist/server/viewer-registry.js.map +7 -0
- 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/id-generator.js +5 -2
- package/dist/utils/id-generator.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 +64 -31
- 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/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- 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-CUOQGGRA.css +1 -0
- package/dist/viewer/assets/main-DvH3gYrI.js +84 -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/farm-sync-engine.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Farm Sync Engine - Orchestrates headless sync pipeline\n *\n * Implements:\n * - Git pull for tracked codebases\n * - Diff computation between sync commits\n * - File-to-element mapping via source references\n * - Changeset generation for affected elements\n * - Staged changeset integration\n */\n\nimport path from \"path\";\nimport { execSync, execFileSync } from \"child_process\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FarmSyncState } from \"./farm-sync-state.js\";\nimport { Model } from \"./model.js\";\nimport { StagingAreaManager } from \"./staging-area.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport type { FarmProject } from \"./farm-manifest.js\";\n\n/**\n * Options for sync operation\n */\nexport interface SyncOptions {\n dryRun?: boolean; // Preview without creating changeset\n force?: boolean; // Proceed despite ambiguities\n verbose?: boolean; // Detailed output\n}\n\n/**\n * Result of file diff analysis\n */\nexport interface FileDiffResult {\n added: string[];\n modified: string[];\n deleted: string[];\n}\n\n/**\n * Mapping of file path to model elements\n */\nexport interface FileElementMapping {\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number; // 0-100\n sourceRef: {\n file: string;\n symbol?: string;\n };\n }>;\n}\n\n/**\n * Result of a sync operation\n */\nexport interface SyncResult {\n status: \"success\" | \"partial\" | \"failed\";\n projectName: string;\n commitsBefore: string;\n commitsAfter: string;\n filesChanged: FileDiffResult;\n elementMappings: FileElementMapping[];\n ambiguities: FileElementMapping[]; // Mappings with low confidence\n changesetId?: string; // ID of generated changeset if applicable\n changeCount: number; // Total staged changes\n notes: string[];\n dryRun?: boolean; // True if this was a dry run (no state changed)\n}\n\n/**\n * FarmSyncEngine - Orchestrates the sync pipeline\n * Model parameter is optional - only required for sync operations, not for pull\n */\nexport class FarmSyncEngine {\n private farmRoot: string;\n private model: Model | null;\n\n constructor(farmRoot: string, model?: Model) {\n this.farmRoot = farmRoot;\n this.model = model || null;\n }\n\n /**\n * Pull latest changes from remote for a codebase\n * @param codebasePath - Relative path to codebase folder\n * @returns Updated commit SHA\n */\n async pullCodebase(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n // Try to fetch latest if remote exists\n try {\n execSync(\"git fetch --prune\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (fetchError) {\n const fetchMsg = getErrorMessage(fetchError);\n // Only treat \"no remote\" as acceptable; re-throw auth, merge, and network errors\n const isNoRemote = fetchMsg.includes(\"No remote repository specified\") ||\n fetchMsg.includes(\"no remote\");\n if (!isNoRemote) {\n throw new Error(`git fetch failed: ${fetchMsg}`);\n }\n }\n\n // Try to pull if remote exists\n try {\n execSync(\"git pull --no-rebase\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (pullError) {\n const pullMsg = getErrorMessage(pullError);\n // Only treat \"no remote\" or \"no tracking information\" as acceptable; re-throw auth, merge, and network errors\n const isAcceptable = pullMsg.includes(\"No remote repository specified\") ||\n pullMsg.includes(\"no remote\") ||\n pullMsg.includes(\"no tracking information\");\n if (!isAcceptable) {\n throw new Error(`git pull failed: ${pullMsg}`);\n }\n }\n\n // Get current HEAD commit\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n\n return commit;\n } catch (error) {\n throw new Error(`Failed to pull codebase '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Get the current HEAD commit SHA for a codebase\n */\n async getCurrentCommit(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n return commit;\n } catch (error) {\n throw new Error(`Failed to get current commit for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Compute diff between two commits\n * Commit SHAs are validated before use to prevent command injection\n */\n async computeDiff(codebasePath: string, fromCommit: string, toCommit: string): Promise<FileDiffResult> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n // Validate commit SHAs to prevent command injection\n const commitShaRegex = /^[0-9a-f]{7,40}$/i;\n if (!commitShaRegex.test(fromCommit) || !commitShaRegex.test(toCommit)) {\n throw new Error(\n `Invalid commit SHA format. Expected 7-40 character hex string. Got: ${fromCommit}, ${toCommit}`\n );\n }\n\n try {\n // Get diff status between commits using execFileSync to avoid shell interpretation\n const diffOutput = execFileSync(\"git\", [\"diff\", \"--name-status\", `${fromCommit}...${toCommit}`], {\n cwd: fullPath,\n encoding: \"utf-8\",\n });\n\n const added: string[] = [];\n const modified: string[] = [];\n const deleted: string[] = [];\n\n for (const line of diffOutput.trim().split(\"\\n\")) {\n if (!line) continue;\n\n const [status, ...fileParts] = line.split(\"\\t\");\n const filePath = fileParts.join(\"\\t\"); // Handle paths with tabs\n\n switch (status) {\n case \"A\":\n added.push(filePath);\n break;\n case \"M\":\n modified.push(filePath);\n break;\n case \"D\":\n deleted.push(filePath);\n break;\n default:\n if (status.startsWith(\"R\")) {\n // Handle renames (R100, R087, R095, etc. - any similarity index)\n // Report as both a delete of the old name and add of the new name\n const [oldPath, newPath] = fileParts;\n deleted.push(oldPath);\n added.push(newPath);\n } else if (status.startsWith(\"C\")) {\n // Handle copies (C100, C087, etc. - any similarity index)\n // Report as add of new file\n added.push(fileParts[1]);\n }\n break;\n // Ignore other statuses\n }\n }\n\n return { added, modified, deleted };\n } catch (error) {\n throw new Error(`Failed to compute diff for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Map changed files to model elements using source references\n * Looks at each model element's source references and matches against changed files\n * Returns empty results if model is not available\n */\n async mapFilesToElements(): Promise<{ confident: FileElementMapping[]; ambiguous: FileElementMapping[] }> {\n if (!this.model) {\n return { confident: [], ambiguous: [] };\n }\n\n const confident: Map<string, FileElementMapping> = new Map();\n const ambiguous: Map<string, FileElementMapping> = new Map();\n\n // Iterate through all layers and elements\n for (const layer of this.model.layers.values()) {\n for (const element of layer.listElements()) {\n if (!element.source_reference) continue;\n\n const sourceRef = element.source_reference;\n if (!sourceRef.locations || sourceRef.locations.length === 0) continue;\n\n // Match each source location against potential changed files\n for (const location of sourceRef.locations) {\n // Use file path as key\n const filePath = location.file;\n\n if (!confident.has(filePath) && !ambiguous.has(filePath)) {\n const mapping: FileElementMapping = {\n filePath,\n possibleElements: [\n {\n elementId: element.id,\n layer: layer.name,\n confidence: 100, // Exact match from source reference\n sourceRef: location,\n },\n ],\n };\n confident.set(filePath, mapping);\n } else if (confident.has(filePath)) {\n // Multiple elements reference the same file - mark as ambiguous\n const existing = confident.get(filePath)!;\n confident.delete(filePath);\n\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n\n ambiguous.set(filePath, existing);\n } else {\n // Already ambiguous, add another possibility\n const existing = ambiguous.get(filePath)!;\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n }\n }\n }\n }\n\n return {\n confident: Array.from(confident.values()),\n ambiguous: Array.from(ambiguous.values()),\n };\n }\n\n /**\n * Generate a changeset from file-to-element mappings\n * Creates add/update/delete changes based on the diff and mappings\n * Stages changes into the changeset for review before commit\n */\n async generateChangeset(\n project: FarmProject,\n diff: FileDiffResult,\n mappings: { confident: FileElementMapping[]; ambiguous: FileElementMapping[] },\n options: SyncOptions = {}\n ): Promise<{ changesetId: string; changeCount: number; warnings: string[] }> {\n if (!this.model) {\n throw new Error(\"Model is required for changeset generation\");\n }\n\n const projectModelPath = path.join(this.farmRoot, project.model);\n const stagingManager = new StagingAreaManager(projectModelPath, this.model);\n\n // Create a new changeset\n const changesetName = `farm-sync-${project.name}-${Date.now()}`;\n const changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);\n\n if (!changeset.id) {\n throw new Error(\"Failed to create changeset - no ID assigned\");\n }\n\n const warnings: string[] = [];\n let changeCount = 0;\n\n // Create a map of file path -> confident mapping for quick lookup\n const confidentMap = new Map(mappings.confident.map((m) => [m.filePath, m]));\n\n // Process added files\n for (const filePath of diff.added) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"add\",\n elementId: element.elementId,\n layerName: element.layer,\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(\n `File added: ${filePath} -> ${element.elementId} (staged for review, verify content accuracy)`\n );\n changeCount++;\n }\n }\n\n // Process modified files\n for (const filePath of diff.modified) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"update\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File modified: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Process deleted files\n for (const filePath of diff.deleted) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"delete\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File deleted: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Flag ambiguities\n if (mappings.ambiguous.length > 0 && !options.force) {\n for (const ambiguity of mappings.ambiguous) {\n warnings.push(\n `Ambiguous mapping: ${ambiguity.filePath} -> ${ambiguity.possibleElements.map((e) => e.elementId).join(\", \")} (skipped, use --force to proceed)`\n );\n }\n }\n\n return {\n changesetId: changeset.id,\n changeCount,\n warnings,\n };\n }\n\n /**\n * Execute a complete sync operation for a project\n */\n async syncProject(project: FarmProject, options: SyncOptions = {}): Promise<SyncResult> {\n const codebasePath = project.source;\n const notes: string[] = [];\n\n // Step 1: Get current commit (pull only if not dry run)\n if (options.verbose && !options.dryRun) {\n notes.push(`Pulling latest from ${codebasePath}...`);\n }\n const currentCommit = options.dryRun\n ? await this.getCurrentCommit(codebasePath)\n : await this.pullCodebase(codebasePath);\n if (options.dryRun) {\n notes.push(\"(DRY RUN - skipping git pull)\");\n }\n\n // Step 2: Load sync state\n const syncStatePath = path.join(this.farmRoot, project.model, \".farm-sync.yaml\");\n await ensureDir(path.dirname(syncStatePath));\n const syncState = await FarmSyncState.loadOrCreate(syncStatePath, project.name);\n\n const previousCommit = syncState.lastSyncCommit;\n notes.push(`Current commit: ${currentCommit}`);\n notes.push(`Previous sync commit: ${previousCommit || \"none\"}`);\n\n // Step 3: If no previous sync, this is initial sync - just record state\n if (!previousCommit) {\n notes.push(\"Initial sync - recording baseline only\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"Initial sync, no changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: \"none\",\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: { added: [], modified: [], deleted: [] },\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 4: Compute diff\n if (options.verbose) {\n notes.push(`Computing diff between ${previousCommit.substring(0, 8)} and ${currentCommit.substring(0, 8)}...`);\n }\n const diff = await this.computeDiff(codebasePath, previousCommit, currentCommit);\n\n // If no changes, return early\n if (diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0) {\n notes.push(\"No changes detected\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"No changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n notes.push(\n `Files changed: ${diff.added.length} added, ${diff.modified.length} modified, ${diff.deleted.length} deleted`\n );\n\n // Step 5: Check that model is available before attempting to map files\n if (!this.model) {\n notes.push(\"CRITICAL: Model is not loaded, cannot map files to elements\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: previousCommit, // Don't advance lastSyncCommit - stay at previous commit\n status: \"failed\",\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: 0,\n notes: \"Model not loaded - file changes not synced. Load the model and retry to process these files.\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"failed\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 6: Map files to elements\n if (options.verbose) {\n notes.push(\"Mapping changed files to model elements...\");\n }\n const mappings = await this.mapFilesToElements();\n\n // Step 7: Record ambiguities in sync state\n if (mappings.ambiguous.length > 0) {\n syncState.recordAmbiguities(\n mappings.ambiguous.map((m) => ({\n filePath: m.filePath,\n possibleElements: m.possibleElements.map((e) => ({\n elementId: e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n }))\n );\n notes.push(`Ambiguities detected: ${mappings.ambiguous.length} files`);\n }\n\n // Step 8: Generate changeset\n let changesetId: string | undefined;\n let changeCount = 0;\n\n if (!options.dryRun) {\n const changesetResult = await this.generateChangeset(project, diff, mappings, options);\n changesetId = changesetResult.changesetId;\n changeCount = changesetResult.changeCount;\n notes.push(...changesetResult.warnings);\n }\n\n // Step 9: Record sync in state (skip for dry run)\n const syncStatus = mappings.ambiguous.length > 0 && !options.force ? \"partial\" : \"success\";\n\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: syncStatus,\n changeset: changesetId,\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: mappings.confident.length + mappings.ambiguous.length,\n notes: notes.join(\"\\n\"),\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: syncStatus,\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: mappings.confident,\n ambiguities: mappings.ambiguous,\n changesetId,\n changeCount,\n notes,\n dryRun: options.dryRun,\n };\n }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Farm Sync Engine - Orchestrates headless sync pipeline\n *\n * Implements:\n * - Git pull for tracked codebases\n * - Diff computation between sync commits\n * - File-to-element mapping via source references\n * - Changeset generation for affected elements\n * - Staged changeset integration\n */\n\nimport path from \"path\";\nimport { execSync, execFileSync } from \"child_process\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FarmSyncState } from \"./farm-sync-state.js\";\nimport { Model } from \"./model.js\";\nimport { StagingAreaManager } from \"./staging-area.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport type { FarmProject } from \"./farm-manifest.js\";\n\n/**\n * Options for sync operation\n */\nexport interface SyncOptions {\n dryRun?: boolean; // Preview without creating changeset\n force?: boolean; // Proceed despite ambiguities\n verbose?: boolean; // Detailed output\n}\n\n/**\n * Result of file diff analysis\n */\nexport interface FileDiffResult {\n added: string[];\n modified: string[];\n deleted: string[];\n}\n\n/**\n * Mapping of file path to model elements\n */\nexport interface FileElementMapping {\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number; // 0-100\n sourceRef: {\n file: string;\n symbol?: string;\n };\n }>;\n}\n\n/**\n * Result of a sync operation\n */\nexport interface SyncResult {\n status: \"success\" | \"partial\" | \"failed\";\n projectName: string;\n commitsBefore: string;\n commitsAfter: string;\n filesChanged: FileDiffResult;\n elementMappings: FileElementMapping[];\n ambiguities: FileElementMapping[]; // Mappings with low confidence\n changesetId?: string; // ID of generated changeset if applicable\n changeCount: number; // Total staged changes\n notes: string[];\n dryRun?: boolean; // True if this was a dry run (no state changed)\n}\n\n/**\n * FarmSyncEngine - Orchestrates the sync pipeline\n * Model parameter is optional - only required for sync operations, not for pull\n */\nexport class FarmSyncEngine {\n private farmRoot: string;\n private model: Model | null;\n\n constructor(farmRoot: string, model?: Model) {\n this.farmRoot = farmRoot;\n this.model = model || null;\n }\n\n /**\n * Pull latest changes from remote for a codebase\n * @param codebasePath - Relative path to codebase folder\n * @returns Updated commit SHA\n */\n async pullCodebase(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n // Try to fetch latest if remote exists\n try {\n execSync(\"git fetch --prune\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (fetchError) {\n const fetchMsg = getErrorMessage(fetchError);\n // Only treat \"no remote\" as acceptable; re-throw auth, merge, and network errors\n const isNoRemote = fetchMsg.includes(\"No remote repository specified\") ||\n fetchMsg.includes(\"no remote\");\n if (!isNoRemote) {\n throw new Error(`git fetch failed: ${fetchMsg}`);\n }\n }\n\n // Try to pull if remote exists\n try {\n execSync(\"git pull --no-rebase\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (pullError) {\n const pullMsg = getErrorMessage(pullError);\n // Only treat \"no remote\" or \"no tracking information\" as acceptable; re-throw auth, merge, and network errors\n const isAcceptable = pullMsg.includes(\"No remote repository specified\") ||\n pullMsg.includes(\"no remote\") ||\n pullMsg.includes(\"no tracking information\");\n if (!isAcceptable) {\n throw new Error(`git pull failed: ${pullMsg}`);\n }\n }\n\n // Get current HEAD commit\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n\n return commit;\n } catch (error) {\n throw new Error(`Failed to pull codebase '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Get the current HEAD commit SHA for a codebase\n */\n async getCurrentCommit(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n return commit;\n } catch (error) {\n throw new Error(`Failed to get current commit for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Compute diff between two commits\n * Commit SHAs are validated before use to prevent command injection\n */\n async computeDiff(codebasePath: string, fromCommit: string, toCommit: string): Promise<FileDiffResult> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n // Validate commit SHAs to prevent command injection\n const commitShaRegex = /^[0-9a-f]{7,40}$/i;\n if (!commitShaRegex.test(fromCommit) || !commitShaRegex.test(toCommit)) {\n throw new Error(\n `Invalid commit SHA format. Expected 7-40 character hex string. Got: ${fromCommit}, ${toCommit}`\n );\n }\n\n try {\n // Get diff status between commits using execFileSync to avoid shell interpretation\n const diffOutput = execFileSync(\"git\", [\"diff\", \"--name-status\", `${fromCommit}...${toCommit}`], {\n cwd: fullPath,\n encoding: \"utf-8\",\n });\n\n const added: string[] = [];\n const modified: string[] = [];\n const deleted: string[] = [];\n\n for (const line of diffOutput.trim().split(\"\\n\")) {\n if (!line) continue;\n\n const [status, ...fileParts] = line.split(\"\\t\");\n const filePath = fileParts.join(\"\\t\"); // Handle paths with tabs\n\n switch (status) {\n case \"A\":\n added.push(filePath);\n break;\n case \"M\":\n modified.push(filePath);\n break;\n case \"D\":\n deleted.push(filePath);\n break;\n default:\n if (status.startsWith(\"R\")) {\n // Handle renames (R100, R087, R095, etc. - any similarity index)\n // Report as both a delete of the old name and add of the new name\n const [oldPath, newPath] = fileParts;\n deleted.push(oldPath);\n added.push(newPath);\n } else if (status.startsWith(\"C\")) {\n // Handle copies (C100, C087, etc. - any similarity index)\n // Report as add of new file\n added.push(fileParts[1]);\n }\n break;\n // Ignore other statuses\n }\n }\n\n return { added, modified, deleted };\n } catch (error) {\n throw new Error(`Failed to compute diff for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Map changed files to model elements using source references\n * Looks at each model element's source references and matches against changed files\n * Returns empty results if model is not available\n */\n async mapFilesToElements(): Promise<{ confident: FileElementMapping[]; ambiguous: FileElementMapping[] }> {\n if (!this.model) {\n return { confident: [], ambiguous: [] };\n }\n\n const confident: Map<string, FileElementMapping> = new Map();\n const ambiguous: Map<string, FileElementMapping> = new Map();\n\n // Iterate through all layers and elements\n for (const layer of this.model.layers.values()) {\n for (const element of layer.listElements()) {\n if (!element.source_reference) continue;\n\n const sourceRef = element.source_reference;\n if (!sourceRef.locations || sourceRef.locations.length === 0) continue;\n\n // Match each source location against potential changed files\n for (const location of sourceRef.locations) {\n // Use file path as key\n const filePath = location.file;\n\n if (!confident.has(filePath) && !ambiguous.has(filePath)) {\n const mapping: FileElementMapping = {\n filePath,\n possibleElements: [\n {\n elementId: element.id,\n layer: layer.name,\n confidence: 100, // Exact match from source reference\n sourceRef: location,\n },\n ],\n };\n confident.set(filePath, mapping);\n } else if (confident.has(filePath)) {\n // Multiple elements reference the same file - mark as ambiguous\n const existing = confident.get(filePath)!;\n confident.delete(filePath);\n\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n\n ambiguous.set(filePath, existing);\n } else {\n // Already ambiguous, add another possibility\n const existing = ambiguous.get(filePath)!;\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n }\n }\n }\n }\n\n return {\n confident: Array.from(confident.values()),\n ambiguous: Array.from(ambiguous.values()),\n };\n }\n\n /**\n * Generate a changeset from file-to-element mappings\n * Creates add/update/delete changes based on the diff and mappings\n * Stages changes into the changeset for review before commit\n */\n async generateChangeset(\n project: FarmProject,\n diff: FileDiffResult,\n mappings: { confident: FileElementMapping[]; ambiguous: FileElementMapping[] },\n options: SyncOptions = {}\n ): Promise<{ changesetId: string; changeCount: number; warnings: string[] }> {\n if (!this.model) {\n throw new Error(\"Model is required for changeset generation\");\n }\n\n // The staging area lives under the model's project root. For a model the farm\n // scaffolded that is <farm>/<model>; for one adopted from a codebase it is the codebase\n // itself, so farm.yaml's `model` (the model directory) must not be used here.\n const stagingManager = new StagingAreaManager(this.model.rootPath, this.model);\n\n // Create a new changeset\n const changesetName = `farm-sync-${project.name}-${Date.now()}`;\n const changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);\n\n if (!changeset.id) {\n throw new Error(\"Failed to create changeset - no ID assigned\");\n }\n\n const warnings: string[] = [];\n let changeCount = 0;\n\n // Create a map of file path -> confident mapping for quick lookup\n const confidentMap = new Map(mappings.confident.map((m) => [m.filePath, m]));\n\n // Process added files\n for (const filePath of diff.added) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"add\",\n elementId: element.elementId,\n layerName: element.layer,\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(\n `File added: ${filePath} -> ${element.elementId} (staged for review, verify content accuracy)`\n );\n changeCount++;\n }\n }\n\n // Process modified files\n for (const filePath of diff.modified) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"update\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File modified: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Process deleted files\n for (const filePath of diff.deleted) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"delete\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File deleted: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Flag ambiguities\n if (mappings.ambiguous.length > 0 && !options.force) {\n for (const ambiguity of mappings.ambiguous) {\n warnings.push(\n `Ambiguous mapping: ${ambiguity.filePath} -> ${ambiguity.possibleElements.map((e) => e.elementId).join(\", \")} (skipped, use --force to proceed)`\n );\n }\n }\n\n return {\n changesetId: changeset.id,\n changeCount,\n warnings,\n };\n }\n\n /**\n * Execute a complete sync operation for a project\n */\n async syncProject(project: FarmProject, options: SyncOptions = {}): Promise<SyncResult> {\n const codebasePath = project.source;\n const notes: string[] = [];\n\n // Step 1: Get current commit (pull only if not dry run)\n if (options.verbose && !options.dryRun) {\n notes.push(`Pulling latest from ${codebasePath}...`);\n }\n const currentCommit = options.dryRun\n ? await this.getCurrentCommit(codebasePath)\n : await this.pullCodebase(codebasePath);\n if (options.dryRun) {\n notes.push(\"(DRY RUN - skipping git pull)\");\n }\n\n // Step 2: Load sync state\n const syncStatePath = path.join(this.farmRoot, project.model, \".farm-sync.yaml\");\n await ensureDir(path.dirname(syncStatePath));\n const syncState = await FarmSyncState.loadOrCreate(syncStatePath, project.name);\n\n const previousCommit = syncState.lastSyncCommit;\n notes.push(`Current commit: ${currentCommit}`);\n notes.push(`Previous sync commit: ${previousCommit || \"none\"}`);\n\n // Step 3: If no previous sync, this is initial sync - just record state\n if (!previousCommit) {\n notes.push(\"Initial sync - recording baseline only\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"Initial sync, no changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: \"none\",\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: { added: [], modified: [], deleted: [] },\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 4: Compute diff\n if (options.verbose) {\n notes.push(`Computing diff between ${previousCommit.substring(0, 8)} and ${currentCommit.substring(0, 8)}...`);\n }\n const diff = await this.computeDiff(codebasePath, previousCommit, currentCommit);\n\n // If no changes, return early\n if (diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0) {\n notes.push(\"No changes detected\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"No changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n notes.push(\n `Files changed: ${diff.added.length} added, ${diff.modified.length} modified, ${diff.deleted.length} deleted`\n );\n\n // Step 5: Check that model is available before attempting to map files\n if (!this.model) {\n notes.push(\"CRITICAL: Model is not loaded, cannot map files to elements\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: previousCommit, // Don't advance lastSyncCommit - stay at previous commit\n status: \"failed\",\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: 0,\n notes: \"Model not loaded - file changes not synced. Load the model and retry to process these files.\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"failed\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 6: Map files to elements\n if (options.verbose) {\n notes.push(\"Mapping changed files to model elements...\");\n }\n const mappings = await this.mapFilesToElements();\n\n // Step 7: Record ambiguities in sync state\n if (mappings.ambiguous.length > 0) {\n syncState.recordAmbiguities(\n mappings.ambiguous.map((m) => ({\n filePath: m.filePath,\n possibleElements: m.possibleElements.map((e) => ({\n elementId: e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n }))\n );\n notes.push(`Ambiguities detected: ${mappings.ambiguous.length} files`);\n }\n\n // Step 8: Generate changeset\n let changesetId: string | undefined;\n let changeCount = 0;\n\n if (!options.dryRun) {\n const changesetResult = await this.generateChangeset(project, diff, mappings, options);\n changesetId = changesetResult.changesetId;\n changeCount = changesetResult.changeCount;\n notes.push(...changesetResult.warnings);\n }\n\n // Step 9: Record sync in state (skip for dry run)\n const syncStatus = mappings.ambiguous.length > 0 && !options.force ? \"partial\" : \"success\";\n\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: syncStatus,\n changeset: changesetId,\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: mappings.confident.length + mappings.ambiguous.length,\n notes: notes.join(\"\\n\"),\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: syncStatus,\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: mappings.confident,\n ambiguities: mappings.ambiguous,\n changesetId,\n changeCount,\n notes,\n dryRun: options.dryRun,\n };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import yaml from "yaml";
|
|
2
2
|
import { readFile, writeFile, fileExists } from "../utils/file-io.js";
|
|
3
|
+
class FarmSyncStateNotFoundError extends Error {
|
|
4
|
+
code = "FARM_SYNC_STATE_NOT_FOUND";
|
|
5
|
+
filePath;
|
|
6
|
+
constructor(filePath) {
|
|
7
|
+
super(`Farm sync state file not found: ${filePath}`), this.name = "FarmSyncStateNotFoundError", this.filePath = filePath;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
3
10
|
class FarmSyncState {
|
|
4
11
|
projectName;
|
|
5
12
|
lastSyncCommit;
|
|
@@ -24,7 +31,7 @@ class FarmSyncState {
|
|
|
24
31
|
*/
|
|
25
32
|
static async load(filePath) {
|
|
26
33
|
if (!await fileExists(filePath))
|
|
27
|
-
throw new
|
|
34
|
+
throw new FarmSyncStateNotFoundError(filePath);
|
|
28
35
|
const content = await readFile(filePath), rawData = yaml.parse(content);
|
|
29
36
|
if (!rawData)
|
|
30
37
|
throw new Error(`Invalid farm sync state: file is empty or contains invalid YAML at ${filePath}`);
|
|
@@ -154,6 +161,7 @@ class FarmSyncState {
|
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
163
|
export {
|
|
157
|
-
FarmSyncState
|
|
164
|
+
FarmSyncState,
|
|
165
|
+
FarmSyncStateNotFoundError
|
|
158
166
|
};
|
|
159
167
|
//# sourceMappingURL=farm-sync-state.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/farm-sync-state.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Farm Sync State - Manages .farm-sync.yaml per project\n *\n * Tracks sync history, last sync commit, and timestamps for each project.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile, fileExists } from \"../utils/file-io.js\";\n\n/**\n * Represents a single sync record\n */\nexport interface SyncRecord {\n timestamp: string; // ISO 8601 timestamp\n commit: string; // Commit SHA where sync occurred\n changeset?: string; // ID of generated changeset\n files_changed?: number; // Number of files that changed\n elements_affected?: number; // Number of model elements affected\n status: \"success\" | \"partial\" | \"failed\"; // Status of the sync\n notes?: string; // Optional notes about the sync\n}\n\n/**\n * Farm sync state for a single project\n */\nexport interface FarmSyncStateData {\n project_name: string;\n last_sync_commit?: string; // Last successful sync commit SHA\n last_sync_timestamp?: string; // ISO 8601 timestamp\n sync_history: SyncRecord[]; // Complete history of syncs\n ambiguities?: Array<{\n file_path: string;\n possible_elements: Array<{\n element_id: string;\n layer: string;\n confidence: number; // 0-100 confidence score\n }>;\n }>; // Ambiguous file-to-element mappings flagged for review\n}\n\n/**\n * FarmSyncState class - Load, validate, and save farm sync state per project\n */\nexport class FarmSyncState {\n projectName: string;\n lastSyncCommit?: string;\n lastSyncTimestamp?: string;\n syncHistory: SyncRecord[];\n ambiguities: Array<{\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number;\n }>;\n }>;\n filePath?: string;\n\n constructor(data: FarmSyncStateData) {\n this.projectName = data.project_name;\n this.lastSyncCommit = data.last_sync_commit;\n this.lastSyncTimestamp = data.last_sync_timestamp;\n this.syncHistory = data.sync_history || [];\n this.ambiguities = data.ambiguities?.map(a => ({\n filePath: a.file_path,\n possibleElements: a.possible_elements.map(e => ({\n elementId: e.element_id,\n layer: e.layer,\n confidence: e.confidence,\n })),\n })) || [];\n }\n\n /**\n * Load farm sync state from file\n * @param filePath - Path to .farm-sync.yaml\n * @returns FarmSyncState instance\n */\n static async load(filePath: string): Promise<FarmSyncState> {\n if (!(await fileExists(filePath))) {\n throw new
|
|
5
|
-
"mappings": "AAMA,OAAO,UAAU;AACjB,SAAS,UAAU,WAAW,kBAAkB;
|
|
4
|
+
"sourcesContent": ["/**\n * Farm Sync State - Manages .farm-sync.yaml per project\n *\n * Tracks sync history, last sync commit, and timestamps for each project.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile, fileExists } from \"../utils/file-io.js\";\n\n/**\n * Represents a single sync record\n */\nexport interface SyncRecord {\n timestamp: string; // ISO 8601 timestamp\n commit: string; // Commit SHA where sync occurred\n changeset?: string; // ID of generated changeset\n files_changed?: number; // Number of files that changed\n elements_affected?: number; // Number of model elements affected\n status: \"success\" | \"partial\" | \"failed\"; // Status of the sync\n notes?: string; // Optional notes about the sync\n}\n\n/**\n * Farm sync state for a single project\n */\nexport interface FarmSyncStateData {\n project_name: string;\n last_sync_commit?: string; // Last successful sync commit SHA\n last_sync_timestamp?: string; // ISO 8601 timestamp\n sync_history: SyncRecord[]; // Complete history of syncs\n ambiguities?: Array<{\n file_path: string;\n possible_elements: Array<{\n element_id: string;\n layer: string;\n confidence: number; // 0-100 confidence score\n }>;\n }>; // Ambiguous file-to-element mappings flagged for review\n}\n\n/**\n * Thrown by FarmSyncState.load when the .farm-sync.yaml file does not exist.\n * Callers that treat \"never synced\" differently from other failures check `code`\n * (or `instanceof`) rather than matching the message text.\n */\nexport class FarmSyncStateNotFoundError extends Error {\n readonly code = \"FARM_SYNC_STATE_NOT_FOUND\";\n readonly filePath: string;\n\n constructor(filePath: string) {\n super(`Farm sync state file not found: ${filePath}`);\n this.name = \"FarmSyncStateNotFoundError\";\n this.filePath = filePath;\n }\n}\n\n/**\n * FarmSyncState class - Load, validate, and save farm sync state per project\n */\nexport class FarmSyncState {\n projectName: string;\n lastSyncCommit?: string;\n lastSyncTimestamp?: string;\n syncHistory: SyncRecord[];\n ambiguities: Array<{\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number;\n }>;\n }>;\n filePath?: string;\n\n constructor(data: FarmSyncStateData) {\n this.projectName = data.project_name;\n this.lastSyncCommit = data.last_sync_commit;\n this.lastSyncTimestamp = data.last_sync_timestamp;\n this.syncHistory = data.sync_history || [];\n this.ambiguities = data.ambiguities?.map(a => ({\n filePath: a.file_path,\n possibleElements: a.possible_elements.map(e => ({\n elementId: e.element_id,\n layer: e.layer,\n confidence: e.confidence,\n })),\n })) || [];\n }\n\n /**\n * Load farm sync state from file\n * @param filePath - Path to .farm-sync.yaml\n * @returns FarmSyncState instance\n */\n static async load(filePath: string): Promise<FarmSyncState> {\n if (!(await fileExists(filePath))) {\n throw new FarmSyncStateNotFoundError(filePath);\n }\n\n const content = await readFile(filePath);\n const rawData = yaml.parse(content) as any;\n\n if (!rawData) {\n throw new Error(\n `Invalid farm sync state: file is empty or contains invalid YAML at ${filePath}`\n );\n }\n\n // Handle both old (camelCase) and new (snake_case) formats for backward compatibility\n const data: FarmSyncStateData = {\n project_name: rawData.project_name || rawData.projectName,\n last_sync_commit: rawData.last_sync_commit || rawData.lastSyncCommit,\n last_sync_timestamp: rawData.last_sync_timestamp || rawData.lastSyncTimestamp,\n sync_history: (rawData.sync_history || rawData.syncHistory || []).map((record: any) => ({\n timestamp: record.timestamp,\n commit: record.commit,\n changeset: record.changeset || record.changesetId,\n files_changed: record.files_changed || record.filesChanged,\n elements_affected: record.elements_affected || record.elementsAffected,\n status: record.status,\n notes: record.notes,\n })),\n ambiguities: (rawData.ambiguities || []).map((a: any) => ({\n file_path: a.file_path || a.filePath,\n possible_elements: (a.possible_elements || a.possibleElements || []).map((e: any) => ({\n element_id: e.element_id || e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n })),\n };\n\n if (!data.project_name) {\n throw new Error(\"Farm sync state must have a 'project_name' field\");\n }\n\n const state = new FarmSyncState(data);\n state.filePath = filePath;\n return state;\n }\n\n /**\n * Create a new farm sync state\n * @param projectName - Name of the project\n * @param options - Optional initial state\n * @returns FarmSyncState instance\n */\n static create(\n projectName: string,\n options: {\n lastSyncCommit?: string;\n lastSyncTimestamp?: string;\n } = {}\n ): FarmSyncState {\n return new FarmSyncState({\n project_name: projectName,\n last_sync_commit: options.lastSyncCommit,\n last_sync_timestamp: options.lastSyncTimestamp,\n sync_history: [],\n ambiguities: [],\n });\n }\n\n /**\n * Load or create farm sync state\n * @param filePath - Path to .farm-sync.yaml\n * @param projectName - Project name (used if creating new)\n * @returns FarmSyncState instance\n */\n static async loadOrCreate(filePath: string, projectName: string): Promise<FarmSyncState> {\n if (await fileExists(filePath)) {\n return this.load(filePath);\n }\n return this.create(projectName);\n }\n\n /**\n * Save state to file\n * @param filePath - Path where to save .farm-sync.yaml\n */\n async save(filePath?: string): Promise<void> {\n const targetPath = filePath || this.filePath;\n if (!targetPath) {\n throw new Error(\"No file path specified for saving farm sync state\");\n }\n\n const data = this.toJSON();\n const yamlContent = yaml.stringify(data, { lineWidth: 0 });\n\n await writeFile(targetPath, yamlContent);\n this.filePath = targetPath;\n }\n\n /**\n * Record a sync (successful or failed)\n */\n recordSync(record: Omit<SyncRecord, \"status\"> & { status?: \"success\" | \"partial\" | \"failed\" }): void {\n const status = record.status || \"success\";\n\n // Update lastSyncCommit on successful and partial syncs to avoid reprocessing confident files.\n // Failed syncs do not advance the pointer (will retry from same point).\n // Partial syncs advance because confident files have been processed and staged;\n // ambiguous files are tracked separately for manual review.\n if (status === \"success\" || status === \"partial\") {\n this.lastSyncCommit = record.commit;\n this.lastSyncTimestamp = record.timestamp;\n }\n\n this.syncHistory.push({\n timestamp: record.timestamp,\n commit: record.commit,\n status,\n changeset: record.changeset,\n files_changed: record.files_changed,\n elements_affected: record.elements_affected,\n notes: record.notes,\n });\n\n // Keep history trimmed to last 100 syncs\n if (this.syncHistory.length > 100) {\n this.syncHistory = this.syncHistory.slice(-100);\n }\n }\n\n /**\n * Record ambiguous file-to-element mappings for review\n */\n recordAmbiguities(\n ambiguities: Array<{\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number;\n }>;\n }>\n ): void {\n this.ambiguities = ambiguities;\n }\n\n /**\n * Clear ambiguities after review\n */\n clearAmbiguities(): void {\n this.ambiguities = [];\n }\n\n /**\n * Get the last sync record if any\n */\n getLastSync(): SyncRecord | undefined {\n return this.syncHistory.length > 0 ? this.syncHistory[this.syncHistory.length - 1] : undefined;\n }\n\n /**\n * Serialize to JSON representation for saving\n */\n toJSON(): FarmSyncStateData {\n return {\n project_name: this.projectName,\n last_sync_commit: this.lastSyncCommit,\n last_sync_timestamp: this.lastSyncTimestamp,\n sync_history: this.syncHistory.map(record => ({\n timestamp: record.timestamp,\n commit: record.commit,\n changeset: record.changeset,\n files_changed: record.files_changed,\n elements_affected: record.elements_affected,\n status: record.status,\n notes: record.notes,\n })),\n ...(this.ambiguities.length > 0 && {\n ambiguities: this.ambiguities.map(a => ({\n file_path: a.filePath,\n possible_elements: a.possibleElements.map(e => ({\n element_id: e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n }))\n }),\n };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getErrorMessage } from "../utils/errors.js";
|
|
2
|
+
import { applyCatalogMigrations } from "./catalog-migration.js";
|
|
2
3
|
class MigrationRegistry {
|
|
3
4
|
migrations = [];
|
|
4
5
|
constructor() {
|
|
@@ -137,57 +138,42 @@ class MigrationRegistry {
|
|
|
137
138
|
}), this.migrations.push({
|
|
138
139
|
fromVersion: "0.9.0",
|
|
139
140
|
toVersion: "0.10.0",
|
|
140
|
-
description: "Data Model objectschema.required type change (Spec v0.10.0)",
|
|
141
|
+
description: "Data Model objectschema.required type change, element type renames and relationship catalog rewrites (Spec v0.10.0)",
|
|
142
|
+
rewritesCatalog: !0,
|
|
141
143
|
apply: async (model) => {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
await fs.access(dataModelDir);
|
|
146
|
-
} catch (error) {
|
|
147
|
-
if (error.code === "ENOENT")
|
|
148
|
-
return {
|
|
149
|
-
migrationsApplied: 1,
|
|
150
|
-
filesModified: 0,
|
|
151
|
-
description: "Spec version updated to 0.10.0 (objectschema.required type changed from string to array; no model data to migrate)"
|
|
152
|
-
};
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
155
|
-
const files = await fs.readdir(dataModelDir), migrationErrors = [];
|
|
156
|
-
for (const file of files) {
|
|
157
|
-
if (!file.endsWith(".yaml") && !file.endsWith(".yml"))
|
|
158
|
-
continue;
|
|
159
|
-
const filePath = pathModule.join(dataModelDir, file);
|
|
160
|
-
try {
|
|
161
|
-
const content = await fs.readFile(filePath, "utf-8");
|
|
162
|
-
let modified = !1;
|
|
163
|
-
const doc = YAML.parse(content);
|
|
164
|
-
if (!doc || typeof doc != "object")
|
|
165
|
-
continue;
|
|
166
|
-
for (const [, element] of Object.entries(doc)) {
|
|
167
|
-
if (!element || typeof element != "object" || element.spec_node_id !== "data-model.objectschema")
|
|
168
|
-
continue;
|
|
169
|
-
const attributes = element.attributes;
|
|
170
|
-
if (attributes && typeof attributes == "object") {
|
|
171
|
-
const required = attributes.required;
|
|
172
|
-
required && typeof required == "string" && (attributes.required = required.split(",").map((item) => item.trim()).filter((item) => item.length > 0), modified = !0);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
modified && (await fs.writeFile(filePath, YAML.stringify(doc), "utf-8"), filesModified++);
|
|
176
|
-
} catch (error) {
|
|
177
|
-
migrationErrors.push(`Failed to migrate ${file}: ${getErrorMessage(error)}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return migrationErrors.length > 0 ? {
|
|
144
|
+
const required = await migrateObjectSchemaRequired(model), catalog = await applyCatalogMigrations(model), errors = [...required.errors, ...catalog.errors];
|
|
145
|
+
return {
|
|
181
146
|
migrationsApplied: 1,
|
|
182
|
-
filesModified,
|
|
183
|
-
description:
|
|
184
|
-
error:
|
|
185
|
-
|
|
147
|
+
filesModified: required.filesModified + catalog.filesModified,
|
|
148
|
+
description: `Spec version updated to 0.10.0 (objectschema.required converted from string to array; ${catalog.summaries.join("; ")})`,
|
|
149
|
+
...errors.length > 0 && { error: errors.join("; ") },
|
|
150
|
+
details: catalog.details
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}), this.migrations.push({
|
|
154
|
+
fromVersion: "0.10.0",
|
|
155
|
+
toVersion: "0.11.0",
|
|
156
|
+
description: "Relationship catalog rewrites from the layer rules pass (Spec v0.11.0)",
|
|
157
|
+
rewritesCatalog: !0,
|
|
158
|
+
apply: async (model) => {
|
|
159
|
+
const catalog = await applyCatalogMigrations(model);
|
|
160
|
+
return {
|
|
186
161
|
migrationsApplied: 1,
|
|
187
|
-
filesModified,
|
|
188
|
-
description:
|
|
162
|
+
filesModified: catalog.filesModified,
|
|
163
|
+
description: `Spec version updated to 0.11.0 (${catalog.summaries.join("; ")})`,
|
|
164
|
+
...catalog.errors.length > 0 && { error: catalog.errors.join("; ") },
|
|
165
|
+
details: catalog.details
|
|
189
166
|
};
|
|
190
167
|
}
|
|
168
|
+
}), this.migrations.push({
|
|
169
|
+
fromVersion: "0.11.0",
|
|
170
|
+
toVersion: "0.12.0",
|
|
171
|
+
description: "Twelve test links in the testing layer (Spec v0.12.0)",
|
|
172
|
+
apply: async () => ({
|
|
173
|
+
migrationsApplied: 1,
|
|
174
|
+
filesModified: 0,
|
|
175
|
+
description: "Spec version updated to 0.12.0 (twelve test links in the testing layer)"
|
|
176
|
+
})
|
|
191
177
|
});
|
|
192
178
|
}
|
|
193
179
|
/**
|
|
@@ -271,7 +257,8 @@ class MigrationRegistry {
|
|
|
271
257
|
migrations: path.map((m) => ({
|
|
272
258
|
from: m.fromVersion,
|
|
273
259
|
to: m.toVersion,
|
|
274
|
-
description: m.description
|
|
260
|
+
description: m.description,
|
|
261
|
+
...m.rewritesCatalog ? { rewritesCatalog: !0 } : {}
|
|
275
262
|
}))
|
|
276
263
|
};
|
|
277
264
|
}
|
|
@@ -291,6 +278,60 @@ class MigrationRegistry {
|
|
|
291
278
|
return 0;
|
|
292
279
|
}
|
|
293
280
|
}
|
|
281
|
+
async function migrateObjectSchemaRequired(model) {
|
|
282
|
+
const result = await migrateObjectSchemaRequiredFiles(model);
|
|
283
|
+
return migrateLoadedObjectSchemaRequired(model), result;
|
|
284
|
+
}
|
|
285
|
+
function requiredAsArray(required) {
|
|
286
|
+
return !required || typeof required != "string" ? null : required.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
287
|
+
}
|
|
288
|
+
function migrateLoadedObjectSchemaRequired(model) {
|
|
289
|
+
const layer = model.layers.get("data-model");
|
|
290
|
+
if (layer)
|
|
291
|
+
for (const element of layer.listElements()) {
|
|
292
|
+
if (element.spec_node_id !== "data-model.objectschema")
|
|
293
|
+
continue;
|
|
294
|
+
const required = requiredAsArray(element.attributes.required);
|
|
295
|
+
required && (element.attributes = { ...element.attributes, required }, layer.updateElement(element));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async function migrateObjectSchemaRequiredFiles(model) {
|
|
299
|
+
const fs = await import("fs/promises"), pathModule = await import("path"), YAML = await import("yaml"), dataModelDir = pathModule.join(model.rootPath, "documentation-robotics/model/08_data-model");
|
|
300
|
+
let filesModified = 0;
|
|
301
|
+
try {
|
|
302
|
+
await fs.access(dataModelDir);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error.code === "ENOENT")
|
|
305
|
+
return { filesModified: 0, errors: [] };
|
|
306
|
+
throw error;
|
|
307
|
+
}
|
|
308
|
+
const files = await fs.readdir(dataModelDir), errors = [];
|
|
309
|
+
for (const file of files) {
|
|
310
|
+
if (!file.endsWith(".yaml") && !file.endsWith(".yml"))
|
|
311
|
+
continue;
|
|
312
|
+
const filePath = pathModule.join(dataModelDir, file);
|
|
313
|
+
try {
|
|
314
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
315
|
+
let modified = !1;
|
|
316
|
+
const doc = YAML.parse(content);
|
|
317
|
+
if (!doc || typeof doc != "object")
|
|
318
|
+
continue;
|
|
319
|
+
for (const [, element] of Object.entries(doc)) {
|
|
320
|
+
if (!element || typeof element != "object" || element.spec_node_id !== "data-model.objectschema")
|
|
321
|
+
continue;
|
|
322
|
+
const attributes = element.attributes;
|
|
323
|
+
if (attributes && typeof attributes == "object") {
|
|
324
|
+
const required = requiredAsArray(attributes.required);
|
|
325
|
+
required && (attributes.required = required, modified = !0);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
modified && (await fs.writeFile(filePath, YAML.stringify(doc), "utf-8"), filesModified++);
|
|
329
|
+
} catch (error) {
|
|
330
|
+
errors.push(`Failed to migrate ${file}: ${getErrorMessage(error)}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { filesModified, errors };
|
|
334
|
+
}
|
|
294
335
|
export {
|
|
295
336
|
MigrationRegistry
|
|
296
337
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/migration-registry.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Migration Registry - Manages version-based model migrations\n *\n * Handles sequential migrations from one spec version to another,\n * supporting the evolution of the architecture model specification.\n */\n\nimport type { Model } from \"./model.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\n/**\n * Represents a single version migration\n */\nexport interface Migration {\n fromVersion: string;\n toVersion: string;\n description: string;\n apply: (model: Model) => Promise<MigrationResult>;\n}\n\n/**\n * Result of applying a migration\n */\nexport interface MigrationResult {\n migrationsApplied: number;\n filesModified: number;\n description: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\n/**\n * Migration Registry managing all available migrations\n */\nexport class MigrationRegistry {\n private migrations: Migration[] = [];\n\n constructor() {\n this.registerMigrations();\n }\n\n /**\n * Register all available migrations\n */\n private registerMigrations(): void {\n // Migration from v0.5.0 to v0.6.0: Relationship Taxonomy\n this.migrations.push({\n fromVersion: \"0.5.0\",\n toVersion: \"0.6.0\",\n description: \"Relationship Taxonomy (Spec v0.6.0)\",\n apply: async () => {\n // This migration is opt-in - the new relationship taxonomy\n // is fully backward compatible with existing models\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.6.0 (Relationship Taxonomy now available)\",\n };\n },\n });\n\n // Migration from v0.6.0 to v0.7.0: Layer Schema Relationship Metadata\n this.migrations.push({\n fromVersion: \"0.6.0\",\n toVersion: \"0.7.0\",\n description: \"Layer Schema Relationship Metadata (Spec v0.7.0)\",\n apply: async () => {\n // This migration updates layer schemas with relationship metadata sections\n // - All 12 layer schemas now include layerMetadata, intraLayerRelationships, crossLayerRelationships\n // - Deprecated link-registry.json (removed in v0.8.0)\n // - Terminology change: \"cross-layer links\" \u2192 \"cross-layer relationships\"\n // Fully backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.7.0 (Layer schemas now include relationship metadata sections)\",\n };\n },\n });\n\n // Migration from v0.7.0 to v0.7.1: Source Code Reference Infrastructure\n this.migrations.push({\n fromVersion: \"0.7.0\",\n toVersion: \"0.7.1\",\n description: \"Source Code Reference Infrastructure (Spec v0.7.1)\",\n apply: async () => {\n // This migration adds source code reference infrastructure\n // - Common schema definitions in spec/schemas/common/\n // - Source references integrated across 10 layers\n // - Backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.7.1 (Source Code Reference Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.7.1 to v0.8.0: JSON Schema-Only Spec Architecture\n this.migrations.push({\n fromVersion: \"0.7.1\",\n toVersion: \"0.8.0\",\n description: \"JSON Schema-Only Spec Architecture (Spec v0.8.0)\",\n apply: async () => {\n // Spec architecture refactored to JSON Schema-only model:\n // - Node type definitions moved to spec/schemas/nodes/{layer}/*.node.schema.json\n // - Relationship definitions moved to spec/schemas/relationships/{layer}/*.relationship.schema.json\n // - Predicates relocated to spec/schemas/base/predicates.json\n // This is a spec architecture change only - model data files are fully backward compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.0 (JSON Schema-Only Spec Architecture)\",\n };\n },\n });\n\n // Migration from v0.8.0 to v0.8.1: Expanded Relationship Coverage and Node Improvements\n this.migrations.push({\n fromVersion: \"0.8.0\",\n toVersion: \"0.8.1\",\n description: \"Expanded Relationship Coverage and Node Improvements (Spec v0.8.1)\",\n apply: async () => {\n // Spec enhancements only - no model data format changes:\n // - 733 new relationship schemas added across all 12 layers\n // - Node type descriptions improved for all 12 layers\n // - Enum-masquerader node types consolidated into parent attributes\n // - Existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.1 (Expanded Relationship Coverage and Node Improvements)\",\n };\n },\n });\n\n // Migration from v0.8.1 to v0.8.2: UUID/Path identifier separation\n this.migrations.push({\n fromVersion: \"0.8.1\",\n toVersion: \"0.8.2\",\n description: \"UUID/Path identifier separation (Spec v0.8.2)\",\n apply: async () => {\n // The path field is now required on all spec nodes (spec-node.schema.json).\n // Existing elements are migrated automatically on load in model.ts (loadLayer):\n // - Elements with slug-format id: slug moved to path, deterministic UUID assigned to id\n // - Elements with UUID id but no path: path derived from layer_id.type.kebab(name)\n // No manual data file changes needed; migration runs transparently on next load/save.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.2 (UUID/Path identifier separation)\",\n };\n },\n });\n\n // Migration from v0.8.2 to v0.8.3: Inter-layer Relationship Schemas and APM Node Types\n this.migrations.push({\n fromVersion: \"0.8.2\",\n toVersion: \"0.8.3\",\n description: \"Inter-layer Relationship Schemas and APM Node Types (Spec v0.8.3)\",\n apply: async () => {\n // Major expansion of inter-layer relationship coverage and APM layer:\n // - 495 new inter-layer relationship schemas across all 12 layers\n // - Total relationship count increased from 969 to 1,447\n // - 2 new APM node types: apm.alert and apm.dashboard\n // - Backward compatible - existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.3 (Inter-layer Relationship Schemas and APM Node Types)\",\n };\n },\n });\n\n // Migration from v0.8.3 to v0.8.4: Analyzer Infrastructure (CBM Mapping)\n this.migrations.push({\n fromVersion: \"0.8.3\",\n toVersion: \"0.8.4\",\n description: \"Analyzer Infrastructure (Spec v0.8.4)\",\n apply: async () => {\n // Introduces spec/analyzers/ with codebase-memory (CBM) mapping:\n // - New analyzer adapter foundation and full analyzer subcommands\n // - dr analyzer verify with changeset awareness\n // - Claude Code integration commands (/dr-verify, /dr-map, dr-extractor)\n // - No model data format changes; existing models are fully compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.4 (Analyzer Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.8.4 to v0.9.0: Product Layer Introduction\n this.migrations.push({\n fromVersion: \"0.8.4\",\n toVersion: \"0.9.0\",\n description: \"Product Layer Introduction (Spec v0.9.0)\",\n apply: async (model: Model) => {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n\n const modelDir = `${model.rootPath}/documentation-robotics/model`;\n let filesModified = 0;\n\n try {\n // Verify model directory exists\n await fs.access(modelDir);\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT means the directory doesn't exist - nothing to migrate\n if (err.code === \"ENOENT\") {\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced; no model directories to migrate)\",\n };\n }\n // Re-throw other errors (e.g., EACCES for permission denied)\n throw error;\n }\n\n // Rename existing layer directories: shift from 03-12 to 04-13\n // Must do this in reverse order (12\u219213, 11\u219212, ..., 03\u219204) to avoid conflicts\n const renameMap = [\n [\"12_testing\", \"13_testing\"],\n [\"11_apm\", \"12_apm\"],\n [\"10_navigation\", \"11_navigation\"],\n [\"09_ux\", \"10_ux\"],\n [\"08_data-store\", \"09_data-store\"],\n [\"07_data-model\", \"08_data-model\"],\n [\"06_api\", \"07_api\"],\n [\"05_technology\", \"06_technology\"],\n [\"04_application\", \"05_application\"],\n [\"03_security\", \"04_security\"],\n ];\n\n const renameErrors: string[] = [];\n\n for (const [oldName, newName] of renameMap) {\n const oldPath = pathModule.join(modelDir, oldName);\n const newPath = pathModule.join(modelDir, newName);\n\n try {\n await fs.rename(oldPath, newPath);\n filesModified++;\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT is expected when a directory hasn't been created yet\n if (err.code === \"ENOENT\") {\n continue;\n }\n // Track non-ENOENT errors to report them in the result\n renameErrors.push(\n `Failed to rename ${oldName} to ${newName}: ${getErrorMessage(error)}`\n );\n }\n }\n\n // If there were any rename failures, include them in the error field\n if (renameErrors.length > 0) {\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n error: renameErrors.join(\"; \"),\n };\n }\n\n // Create new product layer directory (it will be empty for now)\n // recursive: true handles EEXIST, so no catch needed\n const productPath = pathModule.join(modelDir, \"03_product\");\n await fs.mkdir(productPath, { recursive: true });\n\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n };\n },\n });\n\n // Migration from v0.9.0 to v0.10.0: Data Model objectschema.required type change\n this.migrations.push({\n fromVersion: \"0.9.0\",\n toVersion: \"0.10.0\",\n description: \"Data Model objectschema.required type change (Spec v0.10.0)\",\n apply: async (model: Model) => {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n const YAML = await import(\"yaml\");\n\n const modelDir = `${model.rootPath}/documentation-robotics/model`;\n const dataModelDir = pathModule.join(modelDir, \"08_data-model\");\n let filesModified = 0;\n\n try {\n // Verify data model directory exists\n await fs.access(dataModelDir);\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT means the directory doesn't exist - nothing to migrate\n if (err.code === \"ENOENT\") {\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.10.0 (objectschema.required type changed from string to array; no model data to migrate)\",\n };\n }\n // Re-throw other errors\n throw error;\n }\n\n // Scan all YAML files in the data model directory\n const files = await fs.readdir(dataModelDir);\n const migrationErrors: string[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".yaml\") && !file.endsWith(\".yml\")) {\n continue;\n }\n\n const filePath = pathModule.join(dataModelDir, file);\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n let modified = false;\n const doc = YAML.parse(content);\n\n if (!doc || typeof doc !== \"object\") {\n continue;\n }\n\n // Iterate through all elements in the file\n for (const [, element] of Object.entries(doc)) {\n if (\n !element ||\n typeof element !== \"object\" ||\n (element as Record<string, unknown>).spec_node_id !== \"data-model.objectschema\"\n ) {\n continue;\n }\n\n const elem = element as Record<string, unknown>;\n const attributes = elem.attributes as Record<string, unknown> | undefined;\n\n if (attributes && typeof attributes === \"object\") {\n const required = attributes.required;\n\n // Check if required field exists and is a string\n if (required && typeof required === \"string\") {\n // Convert comma-separated string to array\n attributes.required = required\n .split(\",\")\n .map((item: string) => item.trim())\n .filter((item: string) => item.length > 0);\n modified = true;\n }\n }\n }\n\n // Write back if modified\n if (modified) {\n await fs.writeFile(filePath, YAML.stringify(doc), \"utf-8\");\n filesModified++;\n }\n } catch (error) {\n migrationErrors.push(\n `Failed to migrate ${file}: ${getErrorMessage(error)}`\n );\n }\n }\n\n if (migrationErrors.length > 0) {\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.10.0 (objectschema.required converted from string to array)\",\n error: migrationErrors.join(\"; \"),\n };\n }\n\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.10.0 (objectschema.required converted from string to array)\",\n };\n },\n });\n }\n\n /**\n * Get the latest available specification version\n */\n getLatestVersion(): string {\n if (this.migrations.length === 0) {\n return \"0.7.0\";\n }\n\n // Return the highest toVersion from all migrations using proper version comparison\n const versions = this.migrations.map((m) => m.toVersion);\n return versions.reduce((max, current) => {\n return this.compareVersions(current, max) > 0 ? current : max;\n });\n }\n\n /**\n * Get the sequence of migrations needed from one version to another\n */\n getMigrationPath(fromVersion: string, toVersion?: string): Migration[] {\n const targetVersion = toVersion || this.getLatestVersion();\n\n if (this.compareVersions(fromVersion, targetVersion) >= 0) {\n return [];\n }\n\n const path: Migration[] = [];\n let current = fromVersion;\n\n // Sort migrations by fromVersion\n const sortedMigrations = [...this.migrations].sort((a, b) =>\n this.compareVersions(a.fromVersion, b.fromVersion)\n );\n\n for (const migration of sortedMigrations) {\n // Check if this migration is in our path\n if (\n this.compareVersions(current, migration.fromVersion) >= 0 &&\n this.compareVersions(current, migration.toVersion) < 0 &&\n this.compareVersions(migration.toVersion, targetVersion) <= 0\n ) {\n path.push(migration);\n current = migration.toVersion;\n }\n }\n\n return path;\n }\n\n /**\n * Check if a model requires migration\n */\n requiresMigration(currentVersion: string): boolean {\n const path = this.getMigrationPath(currentVersion);\n return path.length > 0;\n }\n\n /**\n * Apply all migrations in sequence\n */\n async applyMigrations(\n model: Model,\n options: {\n fromVersion: string;\n toVersion?: string;\n dryRun?: boolean;\n validate?: boolean;\n }\n ): Promise<ApplyMigrationsResult> {\n const path = this.getMigrationPath(options.fromVersion, options.toVersion);\n const targetVersion = options.toVersion || this.getLatestVersion();\n\n if (path.length === 0) {\n return {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n }\n\n const results: ApplyMigrationsResult = {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n\n for (const migration of path) {\n if (!options.dryRun) {\n try {\n const result = await migration.apply(model);\n\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n changes: result,\n });\n\n results.totalChanges += result.migrationsApplied;\n\n // Only update model's spec version if migration succeeded (no error field)\n if (!result.error) {\n model.manifest.specVersion = migration.toVersion;\n } else {\n // If migration has errors, throw to prevent partial success state\n throw new Error(result.error);\n }\n } catch (error) {\n throw new Error(\n `Migration ${migration.fromVersion} \u2192 ${migration.toVersion} failed: ${\n getErrorMessage(error)\n }`\n );\n }\n } else {\n // Dry run - just record what would happen\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n dryRun: true,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Get a summary of migrations that would be applied\n */\n getMigrationSummary(\n fromVersion: string,\n toVersion?: string\n ): {\n currentVersion: string;\n targetVersion: string;\n migrationsNeeded: number;\n migrations: Array<{\n from: string;\n to: string;\n description: string;\n }>;\n } {\n const path = this.getMigrationPath(fromVersion, toVersion);\n const targetVersion = toVersion || this.getLatestVersion();\n\n return {\n currentVersion: fromVersion,\n targetVersion,\n migrationsNeeded: path.length,\n migrations: path.map((m) => ({\n from: m.fromVersion,\n to: m.toVersion,\n description: m.description,\n })),\n };\n }\n\n /**\n * Compare two version strings\n * Returns: -1 if a < b, 0 if a == b, 1 if a > b\n */\n private compareVersions(a: string, b: string): number {\n const aParts = a.split(\".\").map(Number);\n const bParts = b.split(\".\").map(Number);\n\n const maxLength = Math.max(aParts.length, bParts.length);\n\n for (let i = 0; i < maxLength; i++) {\n const aPart = aParts[i] || 0;\n const bPart = bParts[i] || 0;\n\n if (aPart < bPart) return -1;\n if (aPart > bPart) return 1;\n }\n\n return 0;\n }\n}\n\n/**\n * Result of applying multiple migrations\n */\nexport interface ApplyMigrationsResult {\n applied: Array<{\n from: string;\n to: string;\n description: string;\n changes?: MigrationResult;\n dryRun?: boolean;\n }>;\n currentVersion: string;\n targetVersion: string;\n totalChanges: number;\n}\n"],
|
|
5
|
-
"mappings": "AAQA,SAAS,uBAAuB;
|
|
4
|
+
"sourcesContent": ["/**\n * Migration Registry - Manages version-based model migrations\n *\n * Handles sequential migrations from one spec version to another,\n * supporting the evolution of the architecture model specification.\n */\n\nimport type { Model } from \"./model.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport { applyCatalogMigrations } from \"./catalog-migration.js\";\n\n/**\n * Represents a single version migration\n */\nexport interface Migration {\n fromVersion: string;\n toVersion: string;\n description: string;\n /** True when the step runs the catalog migration (element type renames, relationship rewrites) */\n rewritesCatalog?: boolean;\n apply: (model: Model) => Promise<MigrationResult>;\n}\n\n/**\n * Result of applying a migration\n */\nexport interface MigrationResult {\n migrationsApplied: number;\n filesModified: number;\n description: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\n/**\n * Migration Registry managing all available migrations\n */\nexport class MigrationRegistry {\n private migrations: Migration[] = [];\n\n constructor() {\n this.registerMigrations();\n }\n\n /**\n * Register all available migrations\n */\n private registerMigrations(): void {\n // Migration from v0.5.0 to v0.6.0: Relationship Taxonomy\n this.migrations.push({\n fromVersion: \"0.5.0\",\n toVersion: \"0.6.0\",\n description: \"Relationship Taxonomy (Spec v0.6.0)\",\n apply: async () => {\n // This migration is opt-in - the new relationship taxonomy\n // is fully backward compatible with existing models\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.6.0 (Relationship Taxonomy now available)\",\n };\n },\n });\n\n // Migration from v0.6.0 to v0.7.0: Layer Schema Relationship Metadata\n this.migrations.push({\n fromVersion: \"0.6.0\",\n toVersion: \"0.7.0\",\n description: \"Layer Schema Relationship Metadata (Spec v0.7.0)\",\n apply: async () => {\n // This migration updates layer schemas with relationship metadata sections\n // - All 12 layer schemas now include layerMetadata, intraLayerRelationships, crossLayerRelationships\n // - Deprecated link-registry.json (removed in v0.8.0)\n // - Terminology change: \"cross-layer links\" \u2192 \"cross-layer relationships\"\n // Fully backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.7.0 (Layer schemas now include relationship metadata sections)\",\n };\n },\n });\n\n // Migration from v0.7.0 to v0.7.1: Source Code Reference Infrastructure\n this.migrations.push({\n fromVersion: \"0.7.0\",\n toVersion: \"0.7.1\",\n description: \"Source Code Reference Infrastructure (Spec v0.7.1)\",\n apply: async () => {\n // This migration adds source code reference infrastructure\n // - Common schema definitions in spec/schemas/common/\n // - Source references integrated across 10 layers\n // - Backward compatible - existing models continue to work\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.7.1 (Source Code Reference Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.7.1 to v0.8.0: JSON Schema-Only Spec Architecture\n this.migrations.push({\n fromVersion: \"0.7.1\",\n toVersion: \"0.8.0\",\n description: \"JSON Schema-Only Spec Architecture (Spec v0.8.0)\",\n apply: async () => {\n // Spec architecture refactored to JSON Schema-only model:\n // - Node type definitions moved to spec/schemas/nodes/{layer}/*.node.schema.json\n // - Relationship definitions moved to spec/schemas/relationships/{layer}/*.relationship.schema.json\n // - Predicates relocated to spec/schemas/base/predicates.json\n // This is a spec architecture change only - model data files are fully backward compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.0 (JSON Schema-Only Spec Architecture)\",\n };\n },\n });\n\n // Migration from v0.8.0 to v0.8.1: Expanded Relationship Coverage and Node Improvements\n this.migrations.push({\n fromVersion: \"0.8.0\",\n toVersion: \"0.8.1\",\n description: \"Expanded Relationship Coverage and Node Improvements (Spec v0.8.1)\",\n apply: async () => {\n // Spec enhancements only - no model data format changes:\n // - 733 new relationship schemas added across all 12 layers\n // - Node type descriptions improved for all 12 layers\n // - Enum-masquerader node types consolidated into parent attributes\n // - Existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.1 (Expanded Relationship Coverage and Node Improvements)\",\n };\n },\n });\n\n // Migration from v0.8.1 to v0.8.2: UUID/Path identifier separation\n this.migrations.push({\n fromVersion: \"0.8.1\",\n toVersion: \"0.8.2\",\n description: \"UUID/Path identifier separation (Spec v0.8.2)\",\n apply: async () => {\n // The path field is now required on all spec nodes (spec-node.schema.json).\n // Existing elements are migrated automatically on load in model.ts (loadLayer):\n // - Elements with slug-format id: slug moved to path, deterministic UUID assigned to id\n // - Elements with UUID id but no path: path derived from layer_id.type.kebab(name)\n // No manual data file changes needed; migration runs transparently on next load/save.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.2 (UUID/Path identifier separation)\",\n };\n },\n });\n\n // Migration from v0.8.2 to v0.8.3: Inter-layer Relationship Schemas and APM Node Types\n this.migrations.push({\n fromVersion: \"0.8.2\",\n toVersion: \"0.8.3\",\n description: \"Inter-layer Relationship Schemas and APM Node Types (Spec v0.8.3)\",\n apply: async () => {\n // Major expansion of inter-layer relationship coverage and APM layer:\n // - 495 new inter-layer relationship schemas across all 12 layers\n // - Total relationship count increased from 969 to 1,447\n // - 2 new APM node types: apm.alert and apm.dashboard\n // - Backward compatible - existing models continue to work without modification\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.3 (Inter-layer Relationship Schemas and APM Node Types)\",\n };\n },\n });\n\n // Migration from v0.8.3 to v0.8.4: Analyzer Infrastructure (CBM Mapping)\n this.migrations.push({\n fromVersion: \"0.8.3\",\n toVersion: \"0.8.4\",\n description: \"Analyzer Infrastructure (Spec v0.8.4)\",\n apply: async () => {\n // Introduces spec/analyzers/ with codebase-memory (CBM) mapping:\n // - New analyzer adapter foundation and full analyzer subcommands\n // - dr analyzer verify with changeset awareness\n // - Claude Code integration commands (/dr-verify, /dr-map, dr-extractor)\n // - No model data format changes; existing models are fully compatible\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.8.4 (Analyzer Infrastructure)\",\n };\n },\n });\n\n // Migration from v0.8.4 to v0.9.0: Product Layer Introduction\n this.migrations.push({\n fromVersion: \"0.8.4\",\n toVersion: \"0.9.0\",\n description: \"Product Layer Introduction (Spec v0.9.0)\",\n apply: async (model: Model) => {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n\n const modelDir = `${model.rootPath}/documentation-robotics/model`;\n let filesModified = 0;\n\n try {\n // Verify model directory exists\n await fs.access(modelDir);\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT means the directory doesn't exist - nothing to migrate\n if (err.code === \"ENOENT\") {\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced; no model directories to migrate)\",\n };\n }\n // Re-throw other errors (e.g., EACCES for permission denied)\n throw error;\n }\n\n // Rename existing layer directories: shift from 03-12 to 04-13\n // Must do this in reverse order (12\u219213, 11\u219212, ..., 03\u219204) to avoid conflicts\n const renameMap = [\n [\"12_testing\", \"13_testing\"],\n [\"11_apm\", \"12_apm\"],\n [\"10_navigation\", \"11_navigation\"],\n [\"09_ux\", \"10_ux\"],\n [\"08_data-store\", \"09_data-store\"],\n [\"07_data-model\", \"08_data-model\"],\n [\"06_api\", \"07_api\"],\n [\"05_technology\", \"06_technology\"],\n [\"04_application\", \"05_application\"],\n [\"03_security\", \"04_security\"],\n ];\n\n const renameErrors: string[] = [];\n\n for (const [oldName, newName] of renameMap) {\n const oldPath = pathModule.join(modelDir, oldName);\n const newPath = pathModule.join(modelDir, newName);\n\n try {\n await fs.rename(oldPath, newPath);\n filesModified++;\n } catch (error) {\n const err = error as { code?: string };\n // ENOENT is expected when a directory hasn't been created yet\n if (err.code === \"ENOENT\") {\n continue;\n }\n // Track non-ENOENT errors to report them in the result\n renameErrors.push(\n `Failed to rename ${oldName} to ${newName}: ${getErrorMessage(error)}`\n );\n }\n }\n\n // If there were any rename failures, include them in the error field\n if (renameErrors.length > 0) {\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n error: renameErrors.join(\"; \"),\n };\n }\n\n // Create new product layer directory (it will be empty for now)\n // recursive: true handles EEXIST, so no catch needed\n const productPath = pathModule.join(modelDir, \"03_product\");\n await fs.mkdir(productPath, { recursive: true });\n\n return {\n migrationsApplied: 1,\n filesModified,\n description:\n \"Spec version updated to 0.9.0 (Product layer introduced at layer 3; Security through Testing shifted from layers 3\u201312 to 4\u201313)\",\n };\n },\n });\n\n // Migration from v0.9.0 to v0.10.0: objectschema.required type change and relationship catalog rewrites\n this.migrations.push({\n fromVersion: \"0.9.0\",\n toVersion: \"0.10.0\",\n description:\n \"Data Model objectschema.required type change, element type renames and relationship catalog rewrites (Spec v0.10.0)\",\n rewritesCatalog: true,\n apply: async (model: Model) => {\n const required = await migrateObjectSchemaRequired(model);\n\n // Element type renames, then relationship rewrites in relationships.yaml and in staged\n // changesets: shared with `dr upgrade` on a model already at the current version\n const catalog = await applyCatalogMigrations(model);\n const errors = [...required.errors, ...catalog.errors];\n\n return {\n migrationsApplied: 1,\n filesModified: required.filesModified + catalog.filesModified,\n description: `Spec version updated to 0.10.0 (objectschema.required converted from string to array; ${catalog.summaries.join(\"; \")})`,\n ...(errors.length > 0 && { error: errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n\n // Migration from v0.10.0 to v0.11.0: the layer rules pass retired relationship types and\n // predicates; CLI 0.2.0 stamped models 0.10.0 with the catalog from before it\n this.migrations.push({\n fromVersion: \"0.10.0\",\n toVersion: \"0.11.0\",\n description: \"Relationship catalog rewrites from the layer rules pass (Spec v0.11.0)\",\n rewritesCatalog: true,\n apply: async (model: Model) => {\n const catalog = await applyCatalogMigrations(model);\n return {\n migrationsApplied: 1,\n filesModified: catalog.filesModified,\n description: `Spec version updated to 0.11.0 (${catalog.summaries.join(\"; \")})`,\n ...(catalog.errors.length > 0 && { error: catalog.errors.join(\"; \") }),\n details: catalog.details,\n };\n },\n });\n\n // Migration from v0.11.0 to v0.12.0: twelve test links added to the testing layer\n this.migrations.push({\n fromVersion: \"0.11.0\",\n toVersion: \"0.12.0\",\n description: \"Twelve test links in the testing layer (Spec v0.12.0)\",\n apply: async () => {\n // Additive only: twelve new relationship types (a test case tests an object schema, access\n // pattern, application component and nine more; a coverage target covers a library\n // component). Nothing is retired or renamed, so existing models need no change.\n return {\n migrationsApplied: 1,\n filesModified: 0,\n description: \"Spec version updated to 0.12.0 (twelve test links in the testing layer)\",\n };\n },\n });\n }\n\n /**\n * Get the latest available specification version\n */\n getLatestVersion(): string {\n if (this.migrations.length === 0) {\n return \"0.7.0\";\n }\n\n // Return the highest toVersion from all migrations using proper version comparison\n const versions = this.migrations.map((m) => m.toVersion);\n return versions.reduce((max, current) => {\n return this.compareVersions(current, max) > 0 ? current : max;\n });\n }\n\n /**\n * Get the sequence of migrations needed from one version to another\n */\n getMigrationPath(fromVersion: string, toVersion?: string): Migration[] {\n const targetVersion = toVersion || this.getLatestVersion();\n\n if (this.compareVersions(fromVersion, targetVersion) >= 0) {\n return [];\n }\n\n const path: Migration[] = [];\n let current = fromVersion;\n\n // Sort migrations by fromVersion\n const sortedMigrations = [...this.migrations].sort((a, b) =>\n this.compareVersions(a.fromVersion, b.fromVersion)\n );\n\n for (const migration of sortedMigrations) {\n // Check if this migration is in our path\n if (\n this.compareVersions(current, migration.fromVersion) >= 0 &&\n this.compareVersions(current, migration.toVersion) < 0 &&\n this.compareVersions(migration.toVersion, targetVersion) <= 0\n ) {\n path.push(migration);\n current = migration.toVersion;\n }\n }\n\n return path;\n }\n\n /**\n * Check if a model requires migration\n */\n requiresMigration(currentVersion: string): boolean {\n const path = this.getMigrationPath(currentVersion);\n return path.length > 0;\n }\n\n /**\n * Apply all migrations in sequence\n */\n async applyMigrations(\n model: Model,\n options: {\n fromVersion: string;\n toVersion?: string;\n dryRun?: boolean;\n validate?: boolean;\n }\n ): Promise<ApplyMigrationsResult> {\n const path = this.getMigrationPath(options.fromVersion, options.toVersion);\n const targetVersion = options.toVersion || this.getLatestVersion();\n\n if (path.length === 0) {\n return {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n }\n\n const results: ApplyMigrationsResult = {\n applied: [],\n currentVersion: options.fromVersion,\n targetVersion,\n totalChanges: 0,\n };\n\n for (const migration of path) {\n if (!options.dryRun) {\n try {\n const result = await migration.apply(model);\n\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n changes: result,\n });\n\n results.totalChanges += result.migrationsApplied;\n\n // Only update model's spec version if migration succeeded (no error field)\n if (!result.error) {\n model.manifest.specVersion = migration.toVersion;\n } else {\n // If migration has errors, throw to prevent partial success state\n throw new Error(result.error);\n }\n } catch (error) {\n throw new Error(\n `Migration ${migration.fromVersion} \u2192 ${migration.toVersion} failed: ${\n getErrorMessage(error)\n }`\n );\n }\n } else {\n // Dry run - just record what would happen\n results.applied.push({\n from: migration.fromVersion,\n to: migration.toVersion,\n description: migration.description,\n dryRun: true,\n });\n }\n }\n\n return results;\n }\n\n /**\n * Get a summary of migrations that would be applied\n */\n getMigrationSummary(\n fromVersion: string,\n toVersion?: string\n ): {\n currentVersion: string;\n targetVersion: string;\n migrationsNeeded: number;\n migrations: Array<{\n from: string;\n to: string;\n description: string;\n rewritesCatalog?: boolean;\n }>;\n } {\n const path = this.getMigrationPath(fromVersion, toVersion);\n const targetVersion = toVersion || this.getLatestVersion();\n\n return {\n currentVersion: fromVersion,\n targetVersion,\n migrationsNeeded: path.length,\n migrations: path.map((m) => ({\n from: m.fromVersion,\n to: m.toVersion,\n description: m.description,\n ...(m.rewritesCatalog ? { rewritesCatalog: true } : {}),\n })),\n };\n }\n\n /**\n * Compare two version strings\n * Returns: -1 if a < b, 0 if a == b, 1 if a > b\n */\n private compareVersions(a: string, b: string): number {\n const aParts = a.split(\".\").map(Number);\n const bParts = b.split(\".\").map(Number);\n\n const maxLength = Math.max(aParts.length, bParts.length);\n\n for (let i = 0; i < maxLength; i++) {\n const aPart = aParts[i] || 0;\n const bPart = bParts[i] || 0;\n\n if (aPart < bPart) return -1;\n if (aPart > bPart) return 1;\n }\n\n return 0;\n }\n}\n\n/**\n * Result of applying multiple migrations\n */\nexport interface ApplyMigrationsResult {\n applied: Array<{\n from: string;\n to: string;\n description: string;\n changes?: MigrationResult;\n dryRun?: boolean;\n }>;\n currentVersion: string;\n targetVersion: string;\n totalChanges: number;\n}\n\n/**\n * Spec v0.10.0: data-model.objectschema `required` changed from a comma-separated string to an\n * array of property names. Converts every string value in 08_data-model/*.yaml, and in the\n * data-model layer when the model has it loaded: `dr upgrade` saves the loaded layers after\n * migrating, which would otherwise write the old strings back over the converted files.\n */\nasync function migrateObjectSchemaRequired(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const result = await migrateObjectSchemaRequiredFiles(model);\n migrateLoadedObjectSchemaRequired(model);\n return result;\n}\n\n/** A comma-separated `required` string as an array of property names; null for any other value */\nfunction requiredAsArray(required: unknown): string[] | null {\n if (!required || typeof required !== \"string\") {\n return null;\n }\n return required\n .split(\",\")\n .map((item) => item.trim())\n .filter((item) => item.length > 0);\n}\n\nfunction migrateLoadedObjectSchemaRequired(model: Model): void {\n // The committed layer: getLayer() would merge in an active changeset's staged changes\n const layer = model.layers.get(\"data-model\");\n if (!layer) {\n return;\n }\n for (const element of layer.listElements()) {\n if (element.spec_node_id !== \"data-model.objectschema\") {\n continue;\n }\n const required = requiredAsArray(element.attributes.required);\n if (required) {\n element.attributes = { ...element.attributes, required };\n layer.updateElement(element);\n }\n }\n}\n\nasync function migrateObjectSchemaRequiredFiles(\n model: Model\n): Promise<{ filesModified: number; errors: string[] }> {\n const fs = await import(\"fs/promises\");\n const pathModule = await import(\"path\");\n const YAML = await import(\"yaml\");\n\n const dataModelDir = pathModule.join(model.rootPath, \"documentation-robotics/model/08_data-model\");\n let filesModified = 0;\n\n try {\n await fs.access(dataModelDir);\n } catch (error) {\n // ENOENT means the directory doesn't exist - nothing to migrate\n if ((error as { code?: string }).code === \"ENOENT\") {\n return { filesModified: 0, errors: [] };\n }\n throw error;\n }\n\n const files = await fs.readdir(dataModelDir);\n const errors: string[] = [];\n\n for (const file of files) {\n if (!file.endsWith(\".yaml\") && !file.endsWith(\".yml\")) {\n continue;\n }\n\n const filePath = pathModule.join(dataModelDir, file);\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n let modified = false;\n const doc = YAML.parse(content);\n\n if (!doc || typeof doc !== \"object\") {\n continue;\n }\n\n for (const [, element] of Object.entries(doc)) {\n if (\n !element ||\n typeof element !== \"object\" ||\n (element as Record<string, unknown>).spec_node_id !== \"data-model.objectschema\"\n ) {\n continue;\n }\n\n const attributes = (element as Record<string, unknown>).attributes as\n | Record<string, unknown>\n | undefined;\n\n if (attributes && typeof attributes === \"object\") {\n const required = requiredAsArray(attributes.required);\n if (required) {\n attributes.required = required;\n modified = true;\n }\n }\n }\n\n if (modified) {\n await fs.writeFile(filePath, YAML.stringify(doc), \"utf-8\");\n filesModified++;\n }\n } catch (error) {\n errors.push(`Failed to migrate ${file}: ${getErrorMessage(error)}`);\n }\n }\n\n return { filesModified, errors };\n}\n"],
|
|
5
|
+
"mappings": "AAQA,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AA4BjC,MAAO,kBAAiB;EACpB,aAA0B,CAAA;EAElC,cAAA;AACE,SAAK,mBAAkB;EACzB;;;;EAKQ,qBAAkB;AAExB,SAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAGE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aACE;;KAGP,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAKE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAME;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,OAAO,UAAgB;AAC5B,cAAM,KAAK,MAAM,OAAO,aAAa,GAC/B,aAAa,MAAM,OAAO,MAAM,GAEhC,WAAW,GAAG,MAAM,QAAQ;AAClC,YAAI,gBAAgB;AAEpB,YAAI;AAEF,gBAAM,GAAG,OAAO,QAAQ;QAC1B,SAAS,OAAO;AAGd,cAFY,MAEJ,SAAS;AACf,mBAAO;cACL,mBAAmB;cACnB,eAAe;cACf,aACE;;AAIN,gBAAM;QACR;AAIA,cAAM,YAAY;UAChB,CAAC,cAAc,YAAY;UAC3B,CAAC,UAAU,QAAQ;UACnB,CAAC,iBAAiB,eAAe;UACjC,CAAC,SAAS,OAAO;UACjB,CAAC,iBAAiB,eAAe;UACjC,CAAC,iBAAiB,eAAe;UACjC,CAAC,UAAU,QAAQ;UACnB,CAAC,iBAAiB,eAAe;UACjC,CAAC,kBAAkB,gBAAgB;UACnC,CAAC,eAAe,aAAa;WAGzB,eAAyB,CAAA;AAE/B,mBAAW,CAAC,SAAS,OAAO,KAAK,WAAW;AAC1C,gBAAM,UAAU,WAAW,KAAK,UAAU,OAAO,GAC3C,UAAU,WAAW,KAAK,UAAU,OAAO;AAEjD,cAAI;AACF,kBAAM,GAAG,OAAO,SAAS,OAAO,GAChC;UACF,SAAS,OAAO;AAGd,gBAFY,MAEJ,SAAS;AACf;AAGF,yBAAa,KACX,oBAAoB,OAAO,OAAO,OAAO,KAAK,gBAAgB,KAAK,CAAC,EAAE;UAE1E;QACF;AAGA,YAAI,aAAa,SAAS;AACxB,iBAAO;YACL,mBAAmB;YACnB;YACA,aACE;YACF,OAAO,aAAa,KAAK,IAAI;;AAMjC,cAAM,cAAc,WAAW,KAAK,UAAU,YAAY;AAC1D,qBAAM,GAAG,MAAM,aAAa,EAAE,WAAW,GAAI,CAAE,GAExC;UACL,mBAAmB;UACnB;UACA,aACE;;MAEN;KACD,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aACE;MACF,iBAAiB;MACjB,OAAO,OAAO,UAAgB;AAC5B,cAAM,WAAW,MAAM,4BAA4B,KAAK,GAIlD,UAAU,MAAM,uBAAuB,KAAK,GAC5C,SAAS,CAAC,GAAG,SAAS,QAAQ,GAAG,QAAQ,MAAM;AAErD,eAAO;UACL,mBAAmB;UACnB,eAAe,SAAS,gBAAgB,QAAQ;UAChD,aAAa,yFAAyF,QAAQ,UAAU,KAAK,IAAI,CAAC;UAClI,GAAI,OAAO,SAAS,KAAK,EAAE,OAAO,OAAO,KAAK,IAAI,EAAC;UACnD,SAAS,QAAQ;;MAErB;KACD,GAID,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,iBAAiB;MACjB,OAAO,OAAO,UAAgB;AAC5B,cAAM,UAAU,MAAM,uBAAuB,KAAK;AAClD,eAAO;UACL,mBAAmB;UACnB,eAAe,QAAQ;UACvB,aAAa,mCAAmC,QAAQ,UAAU,KAAK,IAAI,CAAC;UAC5E,GAAI,QAAQ,OAAO,SAAS,KAAK,EAAE,OAAO,QAAQ,OAAO,KAAK,IAAI,EAAC;UACnE,SAAS,QAAQ;;MAErB;KACD,GAGD,KAAK,WAAW,KAAK;MACnB,aAAa;MACb,WAAW;MACX,aAAa;MACb,OAAO,aAIE;QACL,mBAAmB;QACnB,eAAe;QACf,aAAa;;KAGlB;EACH;;;;EAKA,mBAAgB;AACd,WAAI,KAAK,WAAW,WAAW,IACtB,UAIQ,KAAK,WAAW,IAAI,CAAC,MAAM,EAAE,SAAS,EACvC,OAAO,CAAC,KAAK,YACpB,KAAK,gBAAgB,SAAS,GAAG,IAAI,IAAI,UAAU,GAC3D;EACH;;;;EAKA,iBAAiB,aAAqB,WAAkB;AACtD,UAAM,gBAAgB,aAAa,KAAK,iBAAgB;AAExD,QAAI,KAAK,gBAAgB,aAAa,aAAa,KAAK;AACtD,aAAO,CAAA;AAGT,UAAM,OAAoB,CAAA;AAC1B,QAAI,UAAU;AAGd,UAAM,mBAAmB,CAAC,GAAG,KAAK,UAAU,EAAE,KAAK,CAAC,GAAG,MACrD,KAAK,gBAAgB,EAAE,aAAa,EAAE,WAAW,CAAC;AAGpD,eAAW,aAAa;AAEtB,MACE,KAAK,gBAAgB,SAAS,UAAU,WAAW,KAAK,KACxD,KAAK,gBAAgB,SAAS,UAAU,SAAS,IAAI,KACrD,KAAK,gBAAgB,UAAU,WAAW,aAAa,KAAK,MAE5D,KAAK,KAAK,SAAS,GACnB,UAAU,UAAU;AAIxB,WAAO;EACT;;;;EAKA,kBAAkB,gBAAsB;AAEtC,WADa,KAAK,iBAAiB,cAAc,EACrC,SAAS;EACvB;;;;EAKA,MAAM,gBACJ,OACA,SAKC;AAED,UAAM,OAAO,KAAK,iBAAiB,QAAQ,aAAa,QAAQ,SAAS,GACnE,gBAAgB,QAAQ,aAAa,KAAK,iBAAgB;AAEhE,QAAI,KAAK,WAAW;AAClB,aAAO;QACL,SAAS,CAAA;QACT,gBAAgB,QAAQ;QACxB;QACA,cAAc;;AAIlB,UAAM,UAAiC;MACrC,SAAS,CAAA;MACT,gBAAgB,QAAQ;MACxB;MACA,cAAc;;AAGhB,eAAW,aAAa;AACtB,UAAK,QAAQ;AA6BX,gBAAQ,QAAQ,KAAK;UACnB,MAAM,UAAU;UAChB,IAAI,UAAU;UACd,aAAa,UAAU;UACvB,QAAQ;SACT;;AAjCD,YAAI;AACF,gBAAM,SAAS,MAAM,UAAU,MAAM,KAAK;AAY1C,cAVA,QAAQ,QAAQ,KAAK;YACnB,MAAM,UAAU;YAChB,IAAI,UAAU;YACd,aAAa,UAAU;YACvB,SAAS;WACV,GAED,QAAQ,gBAAgB,OAAO,mBAG3B,CAAC,OAAO;AACV,kBAAM,SAAS,cAAc,UAAU;;AAGvC,kBAAM,IAAI,MAAM,OAAO,KAAK;QAEhC,SAAS,OAAO;AACd,gBAAM,IAAI,MACR,aAAa,UAAU,WAAW,WAAM,UAAU,SAAS,YACzD,gBAAgB,KAAK,CACvB,EAAE;QAEN;AAYJ,WAAO;EACT;;;;EAKA,oBACE,aACA,WAAkB;AAYlB,UAAM,OAAO,KAAK,iBAAiB,aAAa,SAAS,GACnD,gBAAgB,aAAa,KAAK,iBAAgB;AAExD,WAAO;MACL,gBAAgB;MAChB;MACA,kBAAkB,KAAK;MACvB,YAAY,KAAK,IAAI,CAAC,OAAO;QAC3B,MAAM,EAAE;QACR,IAAI,EAAE;QACN,aAAa,EAAE;QACf,GAAI,EAAE,kBAAkB,EAAE,iBAAiB,GAAI,IAAK,CAAA;QACpD;;EAEN;;;;;EAMQ,gBAAgB,GAAW,GAAS;AAC1C,UAAM,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,GAChC,SAAS,EAAE,MAAM,GAAG,EAAE,IAAI,MAAM,GAEhC,YAAY,KAAK,IAAI,OAAO,QAAQ,OAAO,MAAM;AAEvD,aAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,YAAM,QAAQ,OAAO,CAAC,KAAK,GACrB,QAAQ,OAAO,CAAC,KAAK;AAE3B,UAAI,QAAQ;AAAO,eAAO;AAC1B,UAAI,QAAQ;AAAO,eAAO;IAC5B;AAEA,WAAO;EACT;;AAyBF,eAAe,4BACb,OAAY;AAEZ,QAAM,SAAS,MAAM,iCAAiC,KAAK;AAC3D,2CAAkC,KAAK,GAChC;AACT;AAGA,SAAS,gBAAgB,UAAiB;AACxC,SAAI,CAAC,YAAY,OAAO,YAAa,WAC5B,OAEF,SACJ,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAI,CAAE,EACzB,OAAO,CAAC,SAAS,KAAK,SAAS,CAAC;AACrC;AAEA,SAAS,kCAAkC,OAAY;AAErD,QAAM,QAAQ,MAAM,OAAO,IAAI,YAAY;AAC3C,MAAK;AAGL,eAAW,WAAW,MAAM,aAAY,GAAI;AAC1C,UAAI,QAAQ,iBAAiB;AAC3B;AAEF,YAAM,WAAW,gBAAgB,QAAQ,WAAW,QAAQ;AAC5D,MAAI,aACF,QAAQ,aAAa,EAAE,GAAG,QAAQ,YAAY,SAAQ,GACtD,MAAM,cAAc,OAAO;IAE/B;AACF;AAEA,eAAe,iCACb,OAAY;AAEZ,QAAM,KAAK,MAAM,OAAO,aAAa,GAC/B,aAAa,MAAM,OAAO,MAAM,GAChC,OAAO,MAAM,OAAO,MAAM,GAE1B,eAAe,WAAW,KAAK,MAAM,UAAU,4CAA4C;AACjG,MAAI,gBAAgB;AAEpB,MAAI;AACF,UAAM,GAAG,OAAO,YAAY;EAC9B,SAAS,OAAO;AAEd,QAAK,MAA4B,SAAS;AACxC,aAAO,EAAE,eAAe,GAAG,QAAQ,CAAA,EAAE;AAEvC,UAAM;EACR;AAEA,QAAM,QAAQ,MAAM,GAAG,QAAQ,YAAY,GACrC,SAAmB,CAAA;AAEzB,aAAW,QAAQ,OAAO;AACxB,QAAI,CAAC,KAAK,SAAS,OAAO,KAAK,CAAC,KAAK,SAAS,MAAM;AAClD;AAGF,UAAM,WAAW,WAAW,KAAK,cAAc,IAAI;AACnD,QAAI;AACF,YAAM,UAAU,MAAM,GAAG,SAAS,UAAU,OAAO;AACnD,UAAI,WAAW;AACf,YAAM,MAAM,KAAK,MAAM,OAAO;AAE9B,UAAI,CAAC,OAAO,OAAO,OAAQ;AACzB;AAGF,iBAAW,CAAC,EAAE,OAAO,KAAK,OAAO,QAAQ,GAAG,GAAG;AAC7C,YACE,CAAC,WACD,OAAO,WAAY,YAClB,QAAoC,iBAAiB;AAEtD;AAGF,cAAM,aAAc,QAAoC;AAIxD,YAAI,cAAc,OAAO,cAAe,UAAU;AAChD,gBAAM,WAAW,gBAAgB,WAAW,QAAQ;AACpD,UAAI,aACF,WAAW,WAAW,UACtB,WAAW;QAEf;MACF;AAEA,MAAI,aACF,MAAM,GAAG,UAAU,UAAU,KAAK,UAAU,GAAG,GAAG,OAAO,GACzD;IAEJ,SAAS,OAAO;AACd,aAAO,KAAK,qBAAqB,IAAI,KAAK,gBAAgB,KAAK,CAAC,EAAE;IACpE;EACF;AAEA,SAAO,EAAE,eAAe,OAAM;AAChC;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|