@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,3 +1,9 @@
|
|
|
1
|
+
function elementIdentifiers(element, ...extra) {
|
|
2
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3
|
+
for (const candidate of [element.id, element.path, ...extra])
|
|
4
|
+
typeof candidate == "string" && candidate.length > 0 && ids.add(candidate);
|
|
5
|
+
return [...ids];
|
|
6
|
+
}
|
|
1
7
|
class Relationships {
|
|
2
8
|
relationships = [];
|
|
3
9
|
dirty = !1;
|
|
@@ -20,14 +26,29 @@ class Relationships {
|
|
|
20
26
|
return deletedCount > 0 && (this.dirty = !0), deletedCount;
|
|
21
27
|
}
|
|
22
28
|
/**
|
|
23
|
-
* Delete all relationships involving an element (as source or target)
|
|
29
|
+
* Delete all relationships involving an element (as source or target).
|
|
30
|
+
*
|
|
31
|
+
* A row may name an element by its UUID `id` or by its dotted `path`, so callers
|
|
32
|
+
* should pass every identifier the element is known by (see
|
|
33
|
+
* `elementIdentifiers()`); any row whose source or target matches one of them
|
|
34
|
+
* is removed.
|
|
24
35
|
*/
|
|
25
36
|
deleteForElement(elementId) {
|
|
26
|
-
const initialLength = this.relationships.length;
|
|
27
|
-
this.relationships = this.relationships.filter((rel) => rel.source
|
|
37
|
+
const ids = new Set((Array.isArray(elementId) ? elementId : [elementId]).filter((id) => !!id)), initialLength = this.relationships.length;
|
|
38
|
+
this.relationships = this.relationships.filter((rel) => !ids.has(rel.source) && !ids.has(rel.target));
|
|
28
39
|
const deletedCount = initialLength - this.relationships.length;
|
|
29
40
|
return this.dirty = !0, deletedCount;
|
|
30
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Keep only the relationships for which `keep` returns true, preserving order.
|
|
44
|
+
* Returns the number of relationships removed.
|
|
45
|
+
*/
|
|
46
|
+
retain(keep) {
|
|
47
|
+
const initialLength = this.relationships.length;
|
|
48
|
+
this.relationships = this.relationships.filter(keep);
|
|
49
|
+
const removedCount = initialLength - this.relationships.length;
|
|
50
|
+
return removedCount > 0 && (this.dirty = !0), removedCount;
|
|
51
|
+
}
|
|
31
52
|
/**
|
|
32
53
|
* Get all relationships for an element
|
|
33
54
|
*/
|
|
@@ -73,6 +94,7 @@ class Relationships {
|
|
|
73
94
|
}
|
|
74
95
|
}
|
|
75
96
|
export {
|
|
76
|
-
Relationships
|
|
97
|
+
Relationships,
|
|
98
|
+
elementIdentifiers
|
|
77
99
|
};
|
|
78
100
|
//# sourceMappingURL=relationships.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/relationships.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Relationships management for intra-layer and cross-layer relationships\n */\n\nexport interface Relationship {\n source: string;\n predicate: string;\n target: string;\n category?: \"structural\" | \"behavioral\";\n layer: string;\n targetLayer?: string;\n properties?: Record<string, unknown>;\n}\n\n/**\n * Relationships class for managing the central relationships.yaml file\n */\nexport class Relationships {\n private relationships: Relationship[] = [];\n private dirty: boolean = false;\n\n constructor(relationships: Relationship[] = []) {\n this.relationships = relationships;\n }\n\n /**\n * Add a relationship\n */\n add(relationship: Relationship): void {\n this.relationships.push(relationship);\n this.dirty = true;\n }\n\n /**\n * Delete relationships matching criteria\n */\n delete(source: string, target: string, predicate?: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) =>\n !(\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n )\n );\n const deletedCount = initialLength - this.relationships.length;\n if (deletedCount > 0) {\n this.dirty = true;\n }\n return deletedCount;\n }\n\n /**\n * Delete all relationships involving an element (as source or target)\n */\n deleteForElement(elementId: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) => rel.source
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["/**\n * Relationships management for intra-layer and cross-layer relationships\n */\n\nexport interface Relationship {\n source: string;\n predicate: string;\n target: string;\n category?: \"structural\" | \"behavioral\";\n layer: string;\n targetLayer?: string;\n properties?: Record<string, unknown>;\n}\n\n/**\n * Every identifier a relationship row may use to name an element: its UUID `id`\n * and its dotted `path`. Both forms are legal in relationships.yaml.\n */\nexport function elementIdentifiers(\n element: { id?: unknown; path?: unknown },\n ...extra: Array<string | undefined>\n): string[] {\n const ids = new Set<string>();\n for (const candidate of [element.id, element.path, ...extra]) {\n if (typeof candidate === \"string\" && candidate.length > 0) {\n ids.add(candidate);\n }\n }\n return [...ids];\n}\n\n/**\n * Relationships class for managing the central relationships.yaml file\n */\nexport class Relationships {\n private relationships: Relationship[] = [];\n private dirty: boolean = false;\n\n constructor(relationships: Relationship[] = []) {\n this.relationships = relationships;\n }\n\n /**\n * Add a relationship\n */\n add(relationship: Relationship): void {\n this.relationships.push(relationship);\n this.dirty = true;\n }\n\n /**\n * Delete relationships matching criteria\n */\n delete(source: string, target: string, predicate?: string): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) =>\n !(\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n )\n );\n const deletedCount = initialLength - this.relationships.length;\n if (deletedCount > 0) {\n this.dirty = true;\n }\n return deletedCount;\n }\n\n /**\n * Delete all relationships involving an element (as source or target).\n *\n * A row may name an element by its UUID `id` or by its dotted `path`, so callers\n * should pass every identifier the element is known by (see\n * `elementIdentifiers()`); any row whose source or target matches one of them\n * is removed.\n */\n deleteForElement(elementId: string | string[]): number {\n const ids = new Set(\n (Array.isArray(elementId) ? elementId : [elementId]).filter((id) => !!id)\n );\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(\n (rel) => !ids.has(rel.source) && !ids.has(rel.target)\n );\n const deletedCount = initialLength - this.relationships.length;\n // Always mark as dirty to ensure file is rewritten (Python CLI behavior)\n this.dirty = true;\n return deletedCount;\n }\n\n /**\n * Keep only the relationships for which `keep` returns true, preserving order.\n * Returns the number of relationships removed.\n */\n retain(keep: (rel: Relationship) => boolean): number {\n const initialLength = this.relationships.length;\n this.relationships = this.relationships.filter(keep);\n const removedCount = initialLength - this.relationships.length;\n if (removedCount > 0) {\n this.dirty = true;\n }\n return removedCount;\n }\n\n /**\n * Get all relationships for an element\n */\n getForElement(elementId: string): {\n outgoing: Relationship[];\n incoming: Relationship[];\n } {\n const outgoing = this.relationships.filter((rel) => rel.source === elementId);\n const incoming = this.relationships.filter((rel) => rel.target === elementId);\n return { outgoing, incoming };\n }\n\n /**\n * Get all relationships\n */\n getAll(): Relationship[] {\n return [...this.relationships];\n }\n\n /**\n * Find relationships between two elements\n */\n find(source: string, target: string, predicate?: string): Relationship[] {\n return this.relationships.filter(\n (rel) =>\n rel.source === source &&\n rel.target === target &&\n (!predicate || rel.predicate === predicate)\n );\n }\n\n /**\n * Check if relationships have unsaved changes\n */\n isDirty(): boolean {\n return this.dirty;\n }\n\n /**\n * Mark relationships as clean (no unsaved changes)\n */\n markClean(): void {\n this.dirty = false;\n }\n\n /**\n * Serialize to array for YAML\n */\n toArray(): Relationship[] {\n return this.relationships;\n }\n\n /**\n * Load from array (from YAML)\n */\n static fromArray(data: Relationship[]): Relationships {\n return new Relationships(data);\n }\n}\n"],
|
|
5
|
+
"mappings": "AAkBM,SAAU,mBACd,YACG,OAAgC;AAEnC,QAAM,MAAM,oBAAI,IAAG;AACnB,aAAW,aAAa,CAAC,QAAQ,IAAI,QAAQ,MAAM,GAAG,KAAK;AACzD,IAAI,OAAO,aAAc,YAAY,UAAU,SAAS,KACtD,IAAI,IAAI,SAAS;AAGrB,SAAO,CAAC,GAAG,GAAG;AAChB;AAKM,MAAO,cAAa;EAChB,gBAAgC,CAAA;EAChC,QAAiB;EAEzB,YAAY,gBAAgC,CAAA,GAAE;AAC5C,SAAK,gBAAgB;EACvB;;;;EAKA,IAAI,cAA0B;AAC5B,SAAK,cAAc,KAAK,YAAY,GACpC,KAAK,QAAQ;EACf;;;;EAKA,OAAO,QAAgB,QAAgB,WAAkB;AACvD,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QACC,EACE,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,WAClC;AAEL,UAAM,eAAe,gBAAgB,KAAK,cAAc;AACxD,WAAI,eAAe,MACjB,KAAK,QAAQ,KAER;EACT;;;;;;;;;EAUA,iBAAiB,WAA4B;AAC3C,UAAM,MAAM,IAAI,KACb,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC,EAAE,CAAC,GAErE,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OACtC,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,MAAM,KAAK,CAAC,IAAI,IAAI,IAAI,MAAM,CAAC;AAEvD,UAAM,eAAe,gBAAgB,KAAK,cAAc;AAExD,gBAAK,QAAQ,IACN;EACT;;;;;EAMA,OAAO,MAAoC;AACzC,UAAM,gBAAgB,KAAK,cAAc;AACzC,SAAK,gBAAgB,KAAK,cAAc,OAAO,IAAI;AACnD,UAAM,eAAe,gBAAgB,KAAK,cAAc;AACxD,WAAI,eAAe,MACjB,KAAK,QAAQ,KAER;EACT;;;;EAKA,cAAc,WAAiB;AAI7B,UAAM,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS,GACtE,WAAW,KAAK,cAAc,OAAO,CAAC,QAAQ,IAAI,WAAW,SAAS;AAC5E,WAAO,EAAE,UAAU,SAAQ;EAC7B;;;;EAKA,SAAM;AACJ,WAAO,CAAC,GAAG,KAAK,aAAa;EAC/B;;;;EAKA,KAAK,QAAgB,QAAgB,WAAkB;AACrD,WAAO,KAAK,cAAc,OACxB,CAAC,QACC,IAAI,WAAW,UACf,IAAI,WAAW,WACd,CAAC,aAAa,IAAI,cAAc,UAAU;EAEjD;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,YAAS;AACP,SAAK,QAAQ;EACf;;;;EAKA,UAAO;AACL,WAAO,KAAK;EACd;;;;EAKA,OAAO,UAAU,MAAoB;AACnC,WAAO,IAAI,cAAc,IAAI;EAC/B;;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -248,22 +248,16 @@ class ReportDataModel {
|
|
|
248
248
|
return Math.round(specCompliant / analysis.totalRelationships * 100);
|
|
249
249
|
}
|
|
250
250
|
/**
|
|
251
|
-
* Calculate layer compliance score
|
|
252
|
-
*
|
|
251
|
+
* Calculate layer compliance score: the share of cross-layer relationships between known
|
|
252
|
+
* layers whose predicate is in the catalog. There is no layer-direction rule; direction is a
|
|
253
|
+
* property of the predicate (spec/DESIGN_CHARTER.md).
|
|
253
254
|
*/
|
|
254
255
|
calculateLayerComplianceScore(analysis) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
return sourceOrder > 0 && targetOrder > 0;
|
|
258
|
-
});
|
|
259
|
-
if (validRelationships.length === 0)
|
|
256
|
+
const crossLayer = analysis.classified.filter((rel) => rel.isCrossLayer && getLayerOrder(rel.sourceLayer) > 0 && getLayerOrder(rel.targetLayer) > 0);
|
|
257
|
+
if (crossLayer.length === 0)
|
|
260
258
|
return 100;
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const sourceOrder = getLayerOrder(rel.sourceLayer), targetOrder = getLayerOrder(rel.targetLayer);
|
|
264
|
-
sourceOrder >= targetOrder && compliantCount++;
|
|
265
|
-
}
|
|
266
|
-
return Math.round(compliantCount / validRelationships.length * 100);
|
|
259
|
+
const compliantCount = crossLayer.filter((rel) => rel.isSpecCompliant).length;
|
|
260
|
+
return Math.round(compliantCount / crossLayer.length * 100);
|
|
267
261
|
}
|
|
268
262
|
/**
|
|
269
263
|
* Clear all cached data
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/core/report-data-model.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of relationships following higher\u2192lower layer rule\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: proportion of relationships going higher -> lower\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score (directional compliance)\n * Measures adherence to the higher -> lower layer reference rule\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n // Only count relationships with known (valid) layer names\n const validRelationships = analysis.classified.filter((rel) => {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n return sourceOrder > 0 && targetOrder > 0;\n });\n\n if (validRelationships.length === 0) return 100;\n\n let compliantCount = 0;\n\n for (const rel of validRelationships) {\n const sourceOrder = getLayerOrder(rel.sourceLayer);\n const targetOrder = getLayerOrder(rel.targetLayer);\n\n // Compliant if source >= target (higher layer -> lower/same layer)\n if (sourceOrder >= targetOrder) {\n compliantCount++;\n }\n }\n\n return Math.round((compliantCount / validRelationships.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
|
|
5
|
-
"mappings": "AAQA,SAAS,sBAAkC;AAC3C,SAAS,2BAAkE;AAC3E,SAAS,qBAAqB;
|
|
4
|
+
"sourcesContent": ["/**\n * ReportDataModel - Unified reporting interface for architecture models\n *\n * Combines statistics collection, relationship classification, and data model analysis\n * to provide comprehensive insights into architecture model structure, quality, and completeness.\n */\n\nimport { Model } from \"./model.js\";\nimport { StatsCollector, ModelStats } from \"./stats-collector.js\";\nimport { RelationshipCatalog, RelationshipType, type Directionality } from \"./relationship-catalog.js\";\nimport { getLayerOrder } from \"./layers.js\";\nimport type { Relationship } from \"../types/index.js\";\n\n/**\n * Relationship with semantic classification metadata\n */\nexport interface ClassifiedRelationship {\n id: string;\n source: string; // Element ID\n target: string; // Element ID\n predicate: string;\n\n // Classification\n category: string; // e.g., \"structural\", \"behavioral\", \"dependency\"\n archimateAlignment: string | null;\n\n // Semantic properties\n directionality: Directionality;\n transitivity: boolean;\n symmetry: boolean;\n reflexivity?: boolean;\n\n // Analysis\n sourceLayer: string;\n targetLayer: string;\n isCrossLayer: boolean;\n isSpecCompliant: boolean;\n}\n\n/**\n * Data model entity (Layer 7 specific)\n */\nexport interface DataModelEntity {\n id: string;\n name: string;\n layer: string;\n attributes: string[];\n relatedEntities: string[];\n isReferenced: boolean; // Referenced from other layers\n}\n\n/**\n * Data model layer specific insights\n *\n * Coverage metrics are percentages (0-100) representing data model completeness:\n * - entityCoverage: Percentage of entities referenced from higher layers\n * - attributeCoverage: Percentage of attributes populated across entities\n */\nexport interface DataModelInsights {\n entityCount: number;\n attributeCount: number;\n relationshipCount: number;\n entities: DataModelEntity[];\n\n // Coverage metrics\n entityCoverage: number;\n attributeCoverage: number;\n\n // Complexity\n avgAttributesPerEntity: number;\n maxAttributesPerEntity: number;\n avgRelationshipsPerEntity: number;\n\n // Quality\n referencedEntities: number; // Entities referenced from higher layers\n orphanedEntities: string[];\n}\n\n/**\n * Relationship analysis results\n */\nexport interface RelationshipAnalysis {\n totalRelationships: number;\n classified: ClassifiedRelationship[];\n byCategory: Record<string, number>;\n byPredicate: Record<string, number>;\n crossLayerCount: number;\n intraLayerCount: number;\n circularDependencies: CircularPath[];\n}\n\n/**\n * Circular dependency detection result\n */\nexport interface CircularPath {\n elements: string[];\n predicates: string[];\n pathLength: number;\n}\n\n/**\n * Quality metrics for the model\n *\n * Percentages (0-100) represent quality metrics:\n * - Coverage metrics (elementCoverage, relationshipCoverage, documentationCoverage, archimateCompliance, specCompliance, semanticConsistency):\n * Express completeness or compliance as percentages\n * - Composition metrics (crossLayerReferenceHealth):\n * Descriptive ratio of cross-layer to total relationships (not a quality indicator)\n * - Structural metrics (orphanedElements, circularDependencies):\n * Count of problematic elements\n * - Compliance metrics (layerComplianceScore):\n * Percentage of cross-layer relationships whose predicate is in the catalog (direction is\n * defined by the predicate, not by layer order; see spec/DESIGN_CHARTER.md)\n */\nexport interface QualityMetrics {\n // Coverage\n elementCoverage: number;\n relationshipCoverage: number;\n documentationCoverage: number;\n layerCoverage: number;\n\n // Structural quality\n orphanedElements: number;\n circularDependencies: number;\n\n // Semantic quality\n archimateCompliance: number;\n specCompliance: number;\n semanticConsistency: number;\n\n // Layering quality\n crossLayerReferenceHealth: number;\n layerComplianceScore: number;\n}\n\n/**\n * Complete report containing statistics, relationships, data model insights, and quality metrics\n */\nexport interface ReportData {\n timestamp: string;\n statistics: ModelStats;\n relationships: RelationshipAnalysis;\n dataModel: DataModelInsights;\n quality: QualityMetrics;\n}\n\n/**\n * Factory function to create a ClassifiedRelationship with enforced invariants\n *\n * Ensures that `isCrossLayer` correctly reflects whether sourceLayer !== targetLayer.\n * This prevents accidental bugs where the invariant could be violated.\n */\nexport function createClassifiedRelationship(\n rel: Omit<ClassifiedRelationship, \"isCrossLayer\" | \"sourceLayer\" | \"targetLayer\">,\n sourceLayer: string,\n targetLayer: string\n): ClassifiedRelationship {\n return {\n ...rel,\n sourceLayer,\n targetLayer,\n isCrossLayer: sourceLayer !== targetLayer,\n };\n}\n\n/**\n * ReportDataModel class - Unified reporting interface\n */\nexport class ReportDataModel {\n private model: Model;\n private statsCollector: StatsCollector;\n private relationshipCatalog: RelationshipCatalog;\n\n private cachedStats: ModelStats | null = null;\n private cachedRelationships: RelationshipAnalysis | null = null;\n private cachedDataModel: DataModelInsights | null = null;\n private cachedQuality: QualityMetrics | null = null;\n private cachedReport: ReportData | null = null;\n\n private relationshipTypeMap: Map<string, RelationshipType> = new Map();\n private catalogLoaded = false;\n\n constructor(model: Model) {\n this.model = model;\n this.statsCollector = new StatsCollector(model);\n this.relationshipCatalog = new RelationshipCatalog();\n }\n\n /**\n * Load the relationship catalog and build relationship type map\n */\n async loadCatalog(): Promise<void> {\n if (this.catalogLoaded) return;\n\n await this.relationshipCatalog.load();\n const types = this.relationshipCatalog.getAllTypes();\n\n for (const type of types) {\n this.relationshipTypeMap.set(type.predicate, type);\n }\n\n this.catalogLoaded = true;\n }\n\n /**\n * Collect all report data (statistics, relationships, data model, quality metrics)\n */\n async collect(): Promise<ReportData> {\n if (this.cachedReport) {\n return this.cachedReport;\n }\n\n try {\n await this.loadCatalog();\n\n const statistics = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n const dataModel = await this.getDataModelInsights();\n const quality = await this.getQualityMetrics();\n\n this.cachedReport = {\n timestamp: new Date().toISOString(),\n statistics,\n relationships,\n dataModel,\n quality,\n };\n\n return this.cachedReport;\n } catch (error) {\n throw new Error(\n `Failed to collect report data: ${error instanceof Error ? error.message : String(error)}`,\n { cause: error }\n );\n }\n }\n\n /**\n * Get model statistics\n */\n async getStatistics(): Promise<ModelStats> {\n if (this.cachedStats) {\n return this.cachedStats;\n }\n\n this.cachedStats = await this.statsCollector.collect();\n return this.cachedStats;\n }\n\n /**\n * Get relationship analysis with classification\n */\n async getRelationshipAnalysis(): Promise<RelationshipAnalysis> {\n if (this.cachedRelationships) {\n return this.cachedRelationships;\n }\n\n await this.loadCatalog();\n\n const allRelationships = this.model.relationships.getAll();\n const classified: ClassifiedRelationship[] = [];\n const byCategory: Record<string, number> = {};\n const byPredicate: Record<string, number> = {};\n\n let crossLayerCount = 0;\n let intraLayerCount = 0;\n\n for (let i = 0; i < allRelationships.length; i++) {\n const rel = allRelationships[i];\n const relationshipId = `${rel.source}\u2192${rel.predicate}\u2192${rel.target}`;\n const classifiedRel = this.classifyRelationship(rel, relationshipId);\n classified.push(classifiedRel);\n\n // Count by category\n if (!byCategory[classifiedRel.category]) {\n byCategory[classifiedRel.category] = 0;\n }\n byCategory[classifiedRel.category]++;\n\n // Count by predicate\n if (!byPredicate[rel.predicate]) {\n byPredicate[rel.predicate] = 0;\n }\n byPredicate[rel.predicate]++;\n\n // Count cross vs intra-layer\n if (classifiedRel.isCrossLayer) {\n crossLayerCount++;\n } else {\n intraLayerCount++;\n }\n }\n\n const circularDependencies = this.detectCircularDependencies(classified);\n\n this.cachedRelationships = {\n totalRelationships: allRelationships.length,\n classified,\n byCategory,\n byPredicate,\n crossLayerCount,\n intraLayerCount,\n circularDependencies,\n };\n\n return this.cachedRelationships;\n }\n\n /**\n * Get relationships filtered by category\n */\n async getRelationshipsByCategory(category: string): Promise<ClassifiedRelationship[]> {\n const analysis = await this.getRelationshipAnalysis();\n return analysis.classified.filter((rel) => rel.category === category);\n }\n\n /**\n * Get data model layer (Layer 7) specific insights\n */\n async getDataModelInsights(): Promise<DataModelInsights> {\n if (this.cachedDataModel) {\n return this.cachedDataModel;\n }\n\n const dataModelLayer = await this.model.getLayer(\"data-model\");\n const entities: DataModelEntity[] = [];\n let attributeCount = 0;\n\n if (dataModelLayer) {\n const elements = dataModelLayer.listElements();\n\n // Get all references to data model entities from other layers\n const referencesFrom = new Set<string>();\n for (const layerName of this.model.getLayerNames()) {\n if (layerName === \"data-model\") continue;\n const layer = await this.model.getLayer(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.references) {\n for (const ref of element.references) {\n if (ref.target.startsWith(\"data-model.\")) {\n referencesFrom.add(ref.target);\n }\n }\n }\n }\n }\n\n for (const element of elements) {\n const attributes = element.attributes ? Object.keys(element.attributes) : [];\n attributeCount += attributes.length;\n\n entities.push({\n id: element.path || element.id || \"\",\n name: element.name,\n layer: \"data-model\",\n attributes,\n relatedEntities: element.relationships ? element.relationships.map((r) => r.target) : [],\n isReferenced: referencesFrom.has(element.path || element.id || \"\"),\n });\n }\n }\n\n const entityCount = entities.length;\n const referencedCount = entities.filter((e) => e.isReferenced).length;\n const orphaned = entities.filter((e) => !e.isReferenced && e.relatedEntities.length === 0);\n\n // Calculate actual attribute coverage: percentage of entities that have at least one attribute\n const entitiesWithAttributes = entities.filter((e) => e.attributes.length > 0).length;\n const attributeCoverage =\n entityCount > 0 ? (entitiesWithAttributes / entityCount) * 100 : 0;\n\n const avgAttrs = entityCount > 0 ? attributeCount / entityCount : 0;\n const maxAttrs = Math.max(0, ...entities.map((e) => e.attributes.length));\n const avgRels =\n entityCount > 0 ? entities.reduce((sum, e) => sum + e.relatedEntities.length, 0) / entityCount : 0;\n\n this.cachedDataModel = {\n entityCount,\n attributeCount,\n relationshipCount: entities.reduce((sum, e) => sum + e.relatedEntities.length, 0),\n entities,\n entityCoverage: entityCount > 0 ? (referencedCount / entityCount) * 100 : 0,\n attributeCoverage,\n avgAttributesPerEntity: Math.round(avgAttrs * 10) / 10,\n maxAttributesPerEntity: maxAttrs,\n avgRelationshipsPerEntity: Math.round(avgRels * 10) / 10,\n referencedEntities: referencedCount,\n orphanedEntities: orphaned.map((e) => e.id),\n };\n\n return this.cachedDataModel;\n }\n\n /**\n * Get quality metrics for the model\n */\n async getQualityMetrics(): Promise<QualityMetrics> {\n if (this.cachedQuality) {\n return this.cachedQuality;\n }\n\n const stats = await this.getStatistics();\n const relationships = await this.getRelationshipAnalysis();\n\n // Coverage metrics\n const elementCoverage = stats.statistics.totalElements > 0 ? 100 : 0;\n const relationshipCoverage =\n stats.statistics.totalElements > 0\n ? (stats.statistics.totalRelationships / stats.statistics.totalElements) * 100\n : 0;\n const documentationCoverage = this.calculateDocumentationCoverage(stats);\n const layerCoverage =\n stats.statistics.totalLayers > 0\n ? (stats.statistics.populatedLayers / stats.statistics.totalLayers) * 100\n : 0;\n\n // Structural quality\n const orphanedElements = stats.orphanedElements.length;\n const circularDependencies = relationships.circularDependencies.length;\n\n // Semantic quality - simplified calculation\n const archimateCompliance = this.calculateArchimateCompliance(relationships);\n const specCompliance = this.calculateSpecCompliance(relationships);\n const semanticConsistency = (archimateCompliance + specCompliance) / 2;\n\n // Layering quality\n const crossLayerReferenceHealth =\n relationships.totalRelationships > 0\n ? (relationships.crossLayerCount / relationships.totalRelationships) * 100\n : 0;\n\n // Layer compliance score: cross-layer relationships whose predicate is in the catalog\n const layerComplianceScore = this.calculateLayerComplianceScore(relationships);\n\n this.cachedQuality = {\n elementCoverage,\n relationshipCoverage: Math.min(100, relationshipCoverage),\n documentationCoverage: Math.min(100, documentationCoverage),\n layerCoverage: Math.min(100, layerCoverage),\n orphanedElements,\n circularDependencies,\n archimateCompliance,\n specCompliance,\n semanticConsistency,\n crossLayerReferenceHealth: Math.min(100, crossLayerReferenceHealth),\n layerComplianceScore,\n };\n\n return this.cachedQuality;\n }\n\n /**\n * Classify a single relationship with semantic metadata\n */\n private classifyRelationship(rel: Relationship, id: string): ClassifiedRelationship {\n // Extract layer name from element ID (format: layer.type.name)\n const sourceParts = rel.source.split(\".\");\n const targetParts = rel.target.split(\".\");\n\n // Validate element ID format: must have at least layer (part 0)\n const sourceLayer = sourceParts[0];\n const targetLayer = targetParts[0];\n\n if (!sourceLayer || !targetLayer) {\n // Log warning for malformed element IDs but continue with available data\n // This allows partial analysis without breaking the entire report\n console.warn(\n `[REPORT_001] Malformed element ID format in relationship: ` +\n `source=\"${rel.source}\" (${sourceParts.length} parts), ` +\n `target=\"${rel.target}\" (${targetParts.length} parts). ` +\n `Expected format: layer.type.name`\n );\n }\n\n const relationshipType = this.relationshipTypeMap.get(rel.predicate);\n\n // Determine spec compliance: unknown predicates are not compliant\n const isSpecCompliant = relationshipType !== undefined;\n\n return createClassifiedRelationship(\n {\n id,\n source: rel.source,\n target: rel.target,\n predicate: rel.predicate,\n category: relationshipType?.category || \"unknown\",\n archimateAlignment: relationshipType?.archimateAlignment || null,\n directionality: relationshipType?.semantics?.directionality || \"unidirectional\",\n transitivity: relationshipType?.semantics?.transitivity || false,\n symmetry: relationshipType?.semantics?.symmetry || false,\n reflexivity: relationshipType?.semantics?.reflexivity,\n isSpecCompliant,\n },\n sourceLayer,\n targetLayer\n );\n }\n\n /**\n * Detect circular dependencies in relationships\n * Max depth limit prevents stack overflow on complex graphs\n */\n private detectCircularDependencies(relationships: ClassifiedRelationship[]): CircularPath[] {\n const MAX_SEARCH_DEPTH = 100;\n const circularPaths: CircularPath[] = [];\n const visited = new Set<string>();\n const path: string[] = [];\n const predicatePath: string[] = [];\n\n for (const rel of relationships) {\n if (!visited.has(rel.source)) {\n this.findCircles(rel.source, relationships, visited, path, predicatePath, circularPaths, 0, MAX_SEARCH_DEPTH);\n }\n }\n\n return circularPaths;\n }\n\n /**\n * Helper to find circular paths using DFS with depth limit\n */\n private findCircles(\n node: string,\n relationships: ClassifiedRelationship[],\n visited: Set<string>,\n path: string[],\n predicatePath: string[],\n circularPaths: CircularPath[],\n depth: number,\n maxDepth: number\n ): void {\n // Prevent stack overflow on complex graphs\n if (depth > maxDepth) {\n return;\n }\n if (path.includes(node)) {\n const circleStart = path.indexOf(node);\n const circlePath = path.slice(circleStart);\n const circlePredicates = predicatePath.slice(circleStart);\n\n // Only add if not already found (use immutable sort for comparison)\n const circleKey = [...circlePath].sort().join(\"\u2192\");\n const isDuplicate = circularPaths.some(\n (c) => [...c.elements].sort().join(\"\u2192\") === circleKey\n );\n\n if (!isDuplicate && circlePath.length > 1) {\n circularPaths.push({\n elements: circlePath,\n predicates: circlePredicates,\n pathLength: circlePath.length,\n });\n }\n return;\n }\n\n if (visited.has(node)) {\n return;\n }\n\n path.push(node);\n visited.add(node);\n\n // Find all outgoing edges from this node\n for (const rel of relationships) {\n if (rel.source === node) {\n predicatePath.push(rel.predicate);\n this.findCircles(rel.target, relationships, visited, path, predicatePath, circularPaths, depth + 1, maxDepth);\n predicatePath.pop();\n }\n }\n\n path.pop();\n visited.delete(node); // Remove from visited during backtrack to detect cycles from alternate paths\n }\n\n /**\n * Calculate documentation coverage percentage\n * Checks what proportion of elements have meaningful descriptions\n */\n private calculateDocumentationCoverage(stats: ModelStats): number {\n if (stats.statistics.totalElements === 0) return 0;\n\n // Count elements with descriptions across all layers\n let documentedCount = 0;\n for (const layerName of this.model.getLayerNames()) {\n const layer = this.model.layers.get(layerName);\n if (!layer) continue;\n\n for (const element of layer.listElements()) {\n if (element.description && element.description.trim().length > 0) {\n documentedCount++;\n }\n }\n }\n\n return Math.round((documentedCount / stats.statistics.totalElements) * 100);\n }\n\n /**\n * Calculate ArchiMate alignment percentage\n */\n private calculateArchimateCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const archimateAligned = analysis.classified.filter((r) => r.archimateAlignment).length;\n return Math.round((archimateAligned / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate spec compliance percentage\n */\n private calculateSpecCompliance(analysis: RelationshipAnalysis): number {\n if (analysis.totalRelationships === 0) return 100;\n\n const specCompliant = analysis.classified.filter((r) => r.isSpecCompliant).length;\n return Math.round((specCompliant / analysis.totalRelationships) * 100);\n }\n\n /**\n * Calculate layer compliance score: the share of cross-layer relationships between known\n * layers whose predicate is in the catalog. There is no layer-direction rule; direction is a\n * property of the predicate (spec/DESIGN_CHARTER.md).\n */\n private calculateLayerComplianceScore(analysis: RelationshipAnalysis): number {\n const crossLayer = analysis.classified.filter(\n (rel) =>\n rel.isCrossLayer && getLayerOrder(rel.sourceLayer) > 0 && getLayerOrder(rel.targetLayer) > 0\n );\n\n if (crossLayer.length === 0) return 100;\n\n const compliantCount = crossLayer.filter((rel) => rel.isSpecCompliant).length;\n return Math.round((compliantCount / crossLayer.length) * 100);\n }\n\n /**\n * Clear all cached data\n */\n clearCache(): void {\n this.cachedStats = null;\n this.cachedRelationships = null;\n this.cachedDataModel = null;\n this.cachedQuality = null;\n this.cachedReport = null;\n }\n\n /**\n * Get the relationship catalog\n */\n getCatalog(): RelationshipCatalog {\n return this.relationshipCatalog;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,8 +4,25 @@ import { FileLock } from "../utils/file-lock.js";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import yaml from "yaml";
|
|
6
6
|
import ansis from "ansis";
|
|
7
|
-
import { Changeset } from "./changeset.js";
|
|
7
|
+
import { Changeset, compactChange, nextSequenceNumber, sequenceOf, withSequenceNumbers } from "./changeset.js";
|
|
8
8
|
import { getErrorMessage } from "../utils/errors.js";
|
|
9
|
+
const MAX_UNSTAGED_TOMBSTONES = 200;
|
|
10
|
+
function readTombstones(value, id) {
|
|
11
|
+
if (value == null)
|
|
12
|
+
return;
|
|
13
|
+
if (!Array.isArray(value)) {
|
|
14
|
+
console.warn(ansis.yellow(`\u26A0 Changeset '${id}': 'unstaged' in metadata.yaml is not a list; ignoring it`));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const valid = value.filter((t) => !!t && typeof t == "object" && typeof t.sequence == "number" && typeof t.removedAt == "number" && !!t.change && typeof t.change == "object" && typeof t.change.type == "string" && typeof t.change.elementId == "string");
|
|
18
|
+
return valid.length < value.length && console.warn(ansis.yellow(`\u26A0 Changeset '${id}': ignoring ${value.length - valid.length} malformed 'unstaged' entr(ies) in metadata.yaml`)), valid;
|
|
19
|
+
}
|
|
20
|
+
function addTombstones(changeset, tombstones) {
|
|
21
|
+
if (changeset.unstaged = [...changeset.unstaged, ...tombstones], changeset.unstaged.length > MAX_UNSTAGED_TOMBSTONES) {
|
|
22
|
+
const dropped = changeset.unstaged.slice(0, changeset.unstaged.length - MAX_UNSTAGED_TOMBSTONES);
|
|
23
|
+
changeset.unstaged = changeset.unstaged.slice(-MAX_UNSTAGED_TOMBSTONES), changeset.unstagedPrunedThrough = Math.max(changeset.unstagedPrunedThrough ?? -1, ...dropped.map((t) => t.removedAt));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
9
26
|
class StagedChangesetStorage {
|
|
10
27
|
changesetsDir;
|
|
11
28
|
constructor(rootPath) {
|
|
@@ -64,9 +81,11 @@ This may indicate disk failure or incomplete write. Check disk health and restor
|
|
|
64
81
|
status: metadata.status,
|
|
65
82
|
baseSnapshot: metadata.baseSnapshot,
|
|
66
83
|
changes,
|
|
67
|
-
stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 }
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
stats: metadata.stats || { additions: 0, modifications: 0, deletions: 0 },
|
|
85
|
+
...typeof metadata.lastSequence == "number" ? { lastSequence: metadata.lastSequence } : {},
|
|
86
|
+
...typeof metadata.unstagedPrunedThrough == "number" ? { unstagedPrunedThrough: metadata.unstagedPrunedThrough } : {}
|
|
87
|
+
}, unstaged = readTombstones(metadata.unstaged, id);
|
|
88
|
+
return unstaged && (data.unstaged = unstaged), Changeset.fromJSON(data);
|
|
70
89
|
} catch (error) {
|
|
71
90
|
throw new Error(`Failed to load changeset '${id}': YAML parsing failed.
|
|
72
91
|
Error: ${getErrorMessage(error)}
|
|
@@ -85,7 +104,7 @@ This indicates file corruption. Try:
|
|
|
85
104
|
throw new Error("Changeset must have an id to be saved in YAML format");
|
|
86
105
|
const sanitizedId = this.sanitizeId(changeset.id), changesetPath = path.join(this.changesetsDir, sanitizedId);
|
|
87
106
|
await ensureDir(changesetPath);
|
|
88
|
-
const metadata = {
|
|
107
|
+
const numbered = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset)), lastSequence = changeset.lastSequence !== void 0 ? Math.max(changeset.lastSequence, ...numbered.map((c) => c.sequenceNumber)) : void 0, metadata = {
|
|
89
108
|
id: changeset.id,
|
|
90
109
|
name: changeset.name,
|
|
91
110
|
description: changeset.description,
|
|
@@ -93,9 +112,13 @@ This indicates file corruption. Try:
|
|
|
93
112
|
modified: changeset.modified,
|
|
94
113
|
status: changeset.status,
|
|
95
114
|
baseSnapshot: changeset.baseSnapshot,
|
|
96
|
-
stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 }
|
|
115
|
+
stats: changeset.stats || { additions: 0, modifications: 0, deletions: 0 },
|
|
116
|
+
// Sequence bookkeeping (see nextSequenceNumber); absent on changesets never unstaged from.
|
|
117
|
+
...lastSequence !== void 0 ? { lastSequence } : {},
|
|
118
|
+
...changeset.unstagedPrunedThrough !== void 0 ? { unstagedPrunedThrough: changeset.unstagedPrunedThrough } : {},
|
|
119
|
+
...changeset.unstaged.length > 0 ? { unstaged: changeset.unstaged } : {}
|
|
97
120
|
};
|
|
98
|
-
await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(
|
|
121
|
+
await writeFile(path.join(changesetPath, "metadata.yaml"), yaml.stringify(metadata), "utf-8"), await writeFile(path.join(changesetPath, "changes.yaml"), yaml.stringify(numbered), "utf-8");
|
|
99
122
|
}
|
|
100
123
|
/**
|
|
101
124
|
* List all staged changesets
|
|
@@ -154,11 +177,11 @@ Run 'dr changeset list' to see all changesets.`);
|
|
|
154
177
|
const changeset = await this.load(id);
|
|
155
178
|
if (!changeset)
|
|
156
179
|
throw new Error(`Changeset '${id}' not found`);
|
|
157
|
-
const sequenceNumber = changeset
|
|
180
|
+
const sequenceNumber = nextSequenceNumber(changeset), stagedChange = {
|
|
158
181
|
...change,
|
|
159
182
|
sequenceNumber
|
|
160
183
|
};
|
|
161
|
-
return changeset.changes.push(stagedChange), changeset.updateModified(), await this.save(changeset), sequenceNumber;
|
|
184
|
+
return changeset.changes = withSequenceNumbers(changeset.changes), changeset.changes.push(stagedChange), changeset.lastSequence = sequenceNumber, changeset.updateModified(), await this.save(changeset), sequenceNumber;
|
|
162
185
|
});
|
|
163
186
|
} catch (error) {
|
|
164
187
|
const errorMessage = getErrorMessage(error);
|
|
@@ -177,6 +200,59 @@ To resolve:
|
|
|
177
200
|
4. Retry the operation`);
|
|
178
201
|
}
|
|
179
202
|
throw new Error(`Failed to add change to changeset '${id}': ${errorMessage}
|
|
203
|
+
Changeset path: ${changesetPath}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Add several changes, and rewrite some staged ones in place, in one write: one load and one save
|
|
208
|
+
* instead of one per change. Additions take the next sequence numbers in order; a rewrite keeps
|
|
209
|
+
* its change's number and leaves a tombstone, as {@link updateChange} does.
|
|
210
|
+
*
|
|
211
|
+
* @returns The sequence numbers assigned to the additions, in order
|
|
212
|
+
*/
|
|
213
|
+
async addChanges(id, changes, rewrites = []) {
|
|
214
|
+
if (changes.length === 0 && rewrites.length === 0)
|
|
215
|
+
return [];
|
|
216
|
+
const changesetPath = this.getChangesetPath(id), lock = new FileLock(changesetPath);
|
|
217
|
+
try {
|
|
218
|
+
return await lock.withLock(async () => {
|
|
219
|
+
const changeset = await this.load(id);
|
|
220
|
+
if (!changeset)
|
|
221
|
+
throw new Error(`Changeset '${id}' not found`);
|
|
222
|
+
changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));
|
|
223
|
+
for (const { elementId, change } of rewrites) {
|
|
224
|
+
const index = changeset.changes.findIndex((c) => c.elementId === elementId);
|
|
225
|
+
if (index === -1)
|
|
226
|
+
throw new Error(`No change for element '${elementId}' in changeset '${id}'`);
|
|
227
|
+
const existing = changeset.changes[index], rewrittenAt = nextSequenceNumber(changeset);
|
|
228
|
+
changeset.lastSequence = rewrittenAt, addTombstones(changeset, [
|
|
229
|
+
{ sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: !0 }
|
|
230
|
+
]), changeset.changes[index] = { ...existing, ...change, sequenceNumber: existing.sequenceNumber };
|
|
231
|
+
}
|
|
232
|
+
const numbers = [];
|
|
233
|
+
for (const change of changes) {
|
|
234
|
+
const sequenceNumber = nextSequenceNumber(changeset), staged = { ...change, sequenceNumber };
|
|
235
|
+
changeset.changes.push(staged), changeset.lastSequence = sequenceNumber, numbers.push(sequenceNumber);
|
|
236
|
+
}
|
|
237
|
+
return changeset.updateModified(), await this.save(changeset), numbers;
|
|
238
|
+
});
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const errorMessage = getErrorMessage(error);
|
|
241
|
+
if (errorMessage.includes("lock") || errorMessage.includes("timeout")) {
|
|
242
|
+
const lockFilePath = path.join(changesetPath, ".lock");
|
|
243
|
+
throw new Error(`Failed to add changes to changeset '${id}': Lock acquisition failed.
|
|
244
|
+
Error: ${errorMessage}
|
|
245
|
+
|
|
246
|
+
This usually means another dr process is modifying this changeset.
|
|
247
|
+
If no other processes are running, a stale lock file may exist.
|
|
248
|
+
|
|
249
|
+
To resolve:
|
|
250
|
+
1. Wait for other operations to complete
|
|
251
|
+
2. Check for running dr processes: ps aux | grep dr
|
|
252
|
+
3. If no processes found, remove stale lock: rm "${lockFilePath}"
|
|
253
|
+
4. Retry the operation`);
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`Failed to add changes to changeset '${id}': ${errorMessage}
|
|
180
256
|
Changeset path: ${changesetPath}`);
|
|
181
257
|
}
|
|
182
258
|
}
|
|
@@ -188,17 +264,33 @@ Changeset path: ${changesetPath}`);
|
|
|
188
264
|
* @throws {Error} If lock acquisition, changeset load, save, or lock release fails
|
|
189
265
|
*/
|
|
190
266
|
async removeChange(id, elementId) {
|
|
191
|
-
|
|
267
|
+
await this.removeChanges(id, [elementId]);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Remove every change to any of `elementIds` in one write. The removal is one revision: it takes
|
|
271
|
+
* one sequence number, and each removed change is kept as a tombstone.
|
|
272
|
+
*
|
|
273
|
+
* Uses file locking to prevent concurrent modification race conditions
|
|
274
|
+
*
|
|
275
|
+
* @throws {Error} If lock acquisition, changeset load, save, or lock release fails
|
|
276
|
+
*/
|
|
277
|
+
async removeChanges(id, elementIds) {
|
|
278
|
+
const keys = new Set(elementIds), changesetPath = this.getChangesetPath(id), lock = new FileLock(changesetPath);
|
|
192
279
|
try {
|
|
193
280
|
await lock.withLock(async () => {
|
|
194
281
|
const changeset = await this.load(id);
|
|
195
282
|
if (!changeset)
|
|
196
283
|
throw new Error(`Changeset '${id}' not found`);
|
|
197
|
-
const
|
|
198
|
-
changeset.changes =
|
|
199
|
-
...
|
|
200
|
-
|
|
201
|
-
|
|
284
|
+
const numbered = withSequenceNumbers(changeset.changes), removed = numbered.filter((c) => keys.has(c.elementId));
|
|
285
|
+
if (changeset.changes = numbered.filter((c) => !keys.has(c.elementId)), removed.length > 0) {
|
|
286
|
+
const removedAt = nextSequenceNumber({ ...changeset, changes: numbered });
|
|
287
|
+
changeset.lastSequence = removedAt, addTombstones(changeset, removed.map((c, index) => ({
|
|
288
|
+
sequence: sequenceOf(c, index),
|
|
289
|
+
removedAt,
|
|
290
|
+
change: compactChange(c)
|
|
291
|
+
})));
|
|
292
|
+
}
|
|
293
|
+
changeset.updateModified(), await this.save(changeset);
|
|
202
294
|
});
|
|
203
295
|
} catch (error) {
|
|
204
296
|
const errorMessage = getErrorMessage(error);
|
|
@@ -239,11 +331,14 @@ Changeset path: ${changesetPath}`);
|
|
|
239
331
|
const index = changeset.changes.findIndex((c) => c.elementId === elementId);
|
|
240
332
|
if (index === -1)
|
|
241
333
|
throw new Error(`No change for element '${elementId}' in changeset '${id}'`);
|
|
242
|
-
|
|
243
|
-
changeset.changes[index] =
|
|
334
|
+
changeset.changes = withSequenceNumbers(changeset.changes, nextSequenceNumber(changeset));
|
|
335
|
+
const existing = changeset.changes[index], rewrittenAt = nextSequenceNumber(changeset);
|
|
336
|
+
changeset.lastSequence = rewrittenAt, addTombstones(changeset, [
|
|
337
|
+
{ sequence: existing.sequenceNumber, removedAt: rewrittenAt, change: compactChange(existing), rewritten: !0 }
|
|
338
|
+
]), changeset.changes[index] = {
|
|
244
339
|
...existing,
|
|
245
340
|
...updated,
|
|
246
|
-
sequenceNumber: existing.sequenceNumber
|
|
341
|
+
sequenceNumber: existing.sequenceNumber
|
|
247
342
|
}, changeset.updateModified(), await this.save(changeset);
|
|
248
343
|
});
|
|
249
344
|
} catch (error) {
|
|
@@ -252,9 +347,40 @@ Changeset path: ${changesetPath}`);
|
|
|
252
347
|
Changeset path: ${changesetPath}`);
|
|
253
348
|
}
|
|
254
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Change a changeset's name and/or description in place, under the changeset's file lock so a
|
|
352
|
+
* concurrent stage cannot be lost. Staged changes are untouched.
|
|
353
|
+
*
|
|
354
|
+
* @returns The updated changeset
|
|
355
|
+
* @throws {Error} If the changeset is not found, or on lock or I/O failure
|
|
356
|
+
*/
|
|
357
|
+
async updateMetadata(id, patch) {
|
|
358
|
+
return new FileLock(this.getChangesetPath(id)).withLock(async () => {
|
|
359
|
+
const changeset = await this.load(id);
|
|
360
|
+
if (!changeset)
|
|
361
|
+
throw new Error(`Changeset '${id}' not found`);
|
|
362
|
+
return patch.name !== void 0 && (changeset.name = patch.name), patch.description !== void 0 && (changeset.description = patch.description || void 0), changeset.updateModified(), await this.save(changeset), changeset;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
255
365
|
/**
|
|
256
366
|
* Get the storage path for a changeset
|
|
257
367
|
*/
|
|
368
|
+
/**
|
|
369
|
+
* Remove every file in the changeset's directory except metadata.yaml and changes.yaml (and
|
|
370
|
+
* dotfiles such as a lock): e.g. the base index, once the changeset is discarded and nothing
|
|
371
|
+
* reads it again.
|
|
372
|
+
*
|
|
373
|
+
* @returns The names of the files removed
|
|
374
|
+
*/
|
|
375
|
+
async removeSideFiles(id) {
|
|
376
|
+
const changesetPath = this.getChangesetPath(id);
|
|
377
|
+
if (!await fileExists(changesetPath))
|
|
378
|
+
return [];
|
|
379
|
+
const keep = /* @__PURE__ */ new Set(["metadata.yaml", "changes.yaml"]), removed = [];
|
|
380
|
+
for (const entry of await readdir(changesetPath, { withFileTypes: !0 }))
|
|
381
|
+
keep.has(entry.name) || entry.name.startsWith(".") || !entry.isFile() || (await rm(path.join(changesetPath, entry.name), { force: !0 }), removed.push(entry.name));
|
|
382
|
+
return removed;
|
|
383
|
+
}
|
|
258
384
|
getChangesetPath(id) {
|
|
259
385
|
const sanitizedId = this.sanitizeId(id);
|
|
260
386
|
return path.join(this.changesetsDir, sanitizedId);
|
|
@@ -277,6 +403,7 @@ Changeset path: ${changesetPath}`);
|
|
|
277
403
|
}
|
|
278
404
|
}
|
|
279
405
|
export {
|
|
406
|
+
MAX_UNSTAGED_TOMBSTONES,
|
|
280
407
|
StagedChangesetStorage
|
|
281
408
|
};
|
|
282
409
|
//# sourceMappingURL=staged-changeset-storage.js.map
|