@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
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { Changeset } from "./changeset.js";
|
|
2
|
+
import { ChangesetGraph } from "./changeset-graph.js";
|
|
3
|
+
import { tryReadBaseIndex } from "./changeset-base-index.js";
|
|
4
|
+
import { stagedSourceReference } from "./staged-source-reference.js";
|
|
5
|
+
import { loadExtractionCoverage } from "./extraction-observability.js";
|
|
6
|
+
import { currentGitHead, resolveAnalyzerName } from "../analyzers/active-analyzer.js";
|
|
7
|
+
import { decisionKey } from "../analyzers/extraction-plan.js";
|
|
8
|
+
import { readFile } from "node:fs/promises";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
const LIFECYCLE_FIELDS = ["attributes.status"], LIFECYCLE_STAGES = [
|
|
11
|
+
["proposed", "draft"],
|
|
12
|
+
["planned", "under-review"],
|
|
13
|
+
["approved", "in-progress", "developing", "active"],
|
|
14
|
+
["implemented", "delivered", "achieved", "completed"],
|
|
15
|
+
["automated"]
|
|
16
|
+
];
|
|
17
|
+
function lifecycleStage(value) {
|
|
18
|
+
if (typeof value != "string")
|
|
19
|
+
return -1;
|
|
20
|
+
const v = value.trim().toLowerCase();
|
|
21
|
+
return LIFECYCLE_STAGES.findIndex((stage) => stage.includes(v));
|
|
22
|
+
}
|
|
23
|
+
function isLifecycleMove(difference) {
|
|
24
|
+
if (!LIFECYCLE_FIELDS.includes(difference.field))
|
|
25
|
+
return !1;
|
|
26
|
+
const from = lifecycleStage(difference.planned), to = lifecycleStage(difference.built);
|
|
27
|
+
return from >= 0 && to > from;
|
|
28
|
+
}
|
|
29
|
+
function readList(value, keep) {
|
|
30
|
+
return Array.isArray(value) ? value.filter((e) => e && typeof e == "object" && keep(e)) : [];
|
|
31
|
+
}
|
|
32
|
+
async function readRecordFile(file) {
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(await readFile(file, "utf-8"));
|
|
35
|
+
return parsed && typeof parsed == "object" ? parsed : null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function readExtractionRecord(rootPath, changeset, gitHead) {
|
|
41
|
+
const dir = path.join(rootPath, ".dr", "analyzers", "extract");
|
|
42
|
+
let raw = await readRecordFile(path.join(dir, `${changeset.id.replace(/[^\w.-]/g, "_")}.json`)), source = "changeset";
|
|
43
|
+
if (raw && (raw.pending || raw.changesetId !== void 0 && raw.changesetId !== changeset.id) && (raw = null), !raw) {
|
|
44
|
+
const pending = await readRecordFile(path.join(dir, "pending.json"));
|
|
45
|
+
pending?.pending && pending.changesetName === changeset.name && (raw = pending, source = "pending");
|
|
46
|
+
}
|
|
47
|
+
if (!raw)
|
|
48
|
+
return null;
|
|
49
|
+
const symbol = (e) => typeof e.symbol == "string", readAt = source === "pending" && typeof raw.gitHead == "string" && raw.gitHead !== gitHead ? raw.gitHead : void 0;
|
|
50
|
+
return {
|
|
51
|
+
source,
|
|
52
|
+
...readAt ? { readAt } : {},
|
|
53
|
+
staged: readList(raw.staged, (e) => typeof e.id == "string"),
|
|
54
|
+
cards: readList(raw.cards, symbol),
|
|
55
|
+
earlierCards: readList(raw.earlierCards, symbol),
|
|
56
|
+
updated: readList(raw.updated, (e) => typeof e.id == "string"),
|
|
57
|
+
candidateDeletes: Array.isArray(raw.candidateDeletes) ? raw.candidateDeletes.filter((d) => typeof d == "string") : []
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function planFromChangeset(changeset, context = {}) {
|
|
61
|
+
const resolve = context.resolve ?? ((id) => id), notes = [], elements = /* @__PURE__ */ new Map(), relationships = /* @__PURE__ */ new Map(), changes = [...changeset.changes].map((change, index) => ({ change, index })).sort((a, b) => (a.change.sequenceNumber ?? a.index) - (b.change.sequenceNumber ?? b.index));
|
|
62
|
+
for (const { change, index } of changes) {
|
|
63
|
+
const where = `change #${change.sequenceNumber ?? index} (${change.type} ${change.elementId})`;
|
|
64
|
+
if (change.type === "relationship-add" || change.type === "relationship-delete") {
|
|
65
|
+
const rel = readRelationship(change, resolve);
|
|
66
|
+
if (!rel) {
|
|
67
|
+
notes.push(`Skipped ${where}: it does not name a source, predicate and target.`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const key = relKey(rel), previous2 = relationships.get(key);
|
|
71
|
+
change.type === "relationship-add" ? previous2?.op === "delete" ? relationships.delete(key) : relationships.set(key, { ...rel, op: "add" }) : previous2?.op === "add" ? relationships.delete(key) : relationships.set(key, { ...rel, op: "delete" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const id = resolve(change.elementId), previous = elements.get(id);
|
|
75
|
+
if (change.type === "add" || change.type === "update") {
|
|
76
|
+
if (!change.after) {
|
|
77
|
+
notes.push(`Skipped ${where}: it has no element data.`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const record = toVerifyElement(change.after, change.layerName, id);
|
|
81
|
+
if (change.type === "add" && previous?.op !== "delete")
|
|
82
|
+
elements.set(id, { ...record, op: "add" });
|
|
83
|
+
else if (previous?.op === "add")
|
|
84
|
+
elements.set(id, { ...record, op: "add" });
|
|
85
|
+
else {
|
|
86
|
+
const before = previous?.before ?? (change.before ? toVerifyElement(change.before, change.layerName, id) : void 0), fields = before ? changedFields(before, record) : void 0;
|
|
87
|
+
elements.set(id, {
|
|
88
|
+
...record,
|
|
89
|
+
op: "update",
|
|
90
|
+
...before && fields ? { before, changedFields: fields, baseValues: Object.fromEntries(fields.map((f) => [f, fieldValue(before, f)])) } : {}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (change.type === "delete") {
|
|
96
|
+
if (previous?.op === "add") {
|
|
97
|
+
elements.delete(id);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const known = context.lookup?.(id), before = previous?.before ?? (change.before ? toVerifyElement(change.before, change.layerName, id) : known ? fromGraphElement(known) : fromId(id, change.layerName));
|
|
101
|
+
elements.set(id, { ...before, id, op: "delete", before });
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
notes.push(`Skipped ${where}: unknown change type.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
label: changeset.name,
|
|
108
|
+
elements: [...elements.values()].map(({ before: _before, ...rest }) => rest),
|
|
109
|
+
relationships: [...relationships.values()],
|
|
110
|
+
notes
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function planFromModel(model, changeset) {
|
|
114
|
+
const graph = await ChangesetGraph.build(model, changeset);
|
|
115
|
+
return planFromChangeset(changeset, {
|
|
116
|
+
resolve: (id) => graph.resolve(id),
|
|
117
|
+
lookup: (id) => graph.getElement(id)
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function builtFromGraph(graph, label = graph.changeset.name) {
|
|
121
|
+
const elements = [], changedElements = [];
|
|
122
|
+
for (const element of graph.elements.values()) {
|
|
123
|
+
const staged = element.status === "added" || element.status === "updated" && !sameValue(element.sourceReference ?? null, (element.before && stagedSourceReference(element.before)) ?? null), current = fromGraphElement(element);
|
|
124
|
+
let updatedFrom;
|
|
125
|
+
if (element.status === "updated" && element.before) {
|
|
126
|
+
const before = toVerifyElement(element.before, element.layer, element.id);
|
|
127
|
+
updatedFrom = Object.fromEntries(changedFields(before, current).map((f) => [f, fieldValue(before, f)])), sameValue(current.sourceReference ?? null, before.sourceReference ?? null) || (updatedFrom.source_reference = before.sourceReference ?? null);
|
|
128
|
+
}
|
|
129
|
+
const built = {
|
|
130
|
+
...current,
|
|
131
|
+
...element.status !== "unchanged" ? { change: element.status } : {},
|
|
132
|
+
...staged ? { stagedProvenance: element.sourceReference?.provenance ?? "none" } : {},
|
|
133
|
+
...updatedFrom ? { updatedFrom } : {}
|
|
134
|
+
};
|
|
135
|
+
element.status !== "deleted" && elements.push(built), element.status !== "unchanged" && changedElements.push(built);
|
|
136
|
+
}
|
|
137
|
+
const relationships = [], changedRelationships = [];
|
|
138
|
+
for (const rel of graph.relationships()) {
|
|
139
|
+
const built = {
|
|
140
|
+
source: rel.source,
|
|
141
|
+
predicate: rel.predicate,
|
|
142
|
+
target: rel.target,
|
|
143
|
+
...rel.status !== "unchanged" ? { change: rel.status } : {}
|
|
144
|
+
};
|
|
145
|
+
rel.status !== "deleted" && relationships.push(built), rel.status !== "unchanged" && changedRelationships.push(built);
|
|
146
|
+
}
|
|
147
|
+
return { label, kind: "changeset", elements, relationships, changedElements, changedRelationships, notes: [] };
|
|
148
|
+
}
|
|
149
|
+
async function builtFromBase(model) {
|
|
150
|
+
const now = (/* @__PURE__ */ new Date()).toISOString(), empty = new Changeset({
|
|
151
|
+
id: "base",
|
|
152
|
+
name: "base model",
|
|
153
|
+
created: now,
|
|
154
|
+
modified: now,
|
|
155
|
+
status: "committed",
|
|
156
|
+
baseSnapshot: "",
|
|
157
|
+
changes: []
|
|
158
|
+
}), graph = await ChangesetGraph.build(model, empty), side = builtFromGraph(graph, "base model");
|
|
159
|
+
return {
|
|
160
|
+
label: "base model",
|
|
161
|
+
kind: "base",
|
|
162
|
+
elements: side.elements,
|
|
163
|
+
relationships: side.relationships,
|
|
164
|
+
notes: [
|
|
165
|
+
"Compared with the base model: what the build changed is not recorded, so unplanned changes are limited to relationships the build gave the elements this plan adds. Compare with an as-built changeset for a full list."
|
|
166
|
+
]
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function builtFromSnapshot(snapshot) {
|
|
170
|
+
const elements = snapshot.elements.filter((e) => e.change !== "deleted"), relationships = snapshot.relationships.filter((r) => r.change !== "deleted");
|
|
171
|
+
return {
|
|
172
|
+
label: snapshot.label,
|
|
173
|
+
kind: "snapshot",
|
|
174
|
+
elements,
|
|
175
|
+
relationships,
|
|
176
|
+
...snapshot.allChanged ? {
|
|
177
|
+
changedElements: snapshot.elements.map((e) => ({ ...e, change: e.change ?? "added" })),
|
|
178
|
+
changedRelationships: snapshot.relationships.map((r) => ({ ...r, change: r.change ?? "added" }))
|
|
179
|
+
} : {},
|
|
180
|
+
notes: snapshot.notes ?? []
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function verifyChangeset(plan, built, options = {}) {
|
|
184
|
+
const matchBySource = options.matchBySource ?? !0, notes = [...plan.notes, ...built.notes], builtById = new Map(built.elements.map((e) => [e.id, e])), changedIds = new Set((built.changedElements ?? []).map((e) => e.id)), claimed = /* @__PURE__ */ new Set(), elementMap = /* @__PURE__ */ new Map(), report = {
|
|
185
|
+
planned: { label: plan.label, elements: plan.elements.length, relationships: plan.relationships.length },
|
|
186
|
+
against: { label: built.label, kind: built.kind },
|
|
187
|
+
verdict: "match",
|
|
188
|
+
verdictText: "",
|
|
189
|
+
summary: { elements: emptyCounts(), relationships: emptyCounts() },
|
|
190
|
+
elements: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
|
|
191
|
+
relationships: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
|
|
192
|
+
notes
|
|
193
|
+
}, observability = options.observability, unverifiableIds = /* @__PURE__ */ new Map(), missingHints = /* @__PURE__ */ new Map(), markUnverifiable = (planned) => {
|
|
194
|
+
if (!observability)
|
|
195
|
+
return !1;
|
|
196
|
+
const verdict2 = observability.element(planned);
|
|
197
|
+
if (verdict2.observable)
|
|
198
|
+
return !1;
|
|
199
|
+
if (verdict2.unlocated) {
|
|
200
|
+
const others = (built.changedElements ?? []).filter((e) => e.change !== "deleted" && e.layer === planned.layer && sameType(e, planned) && !claimed.has(e.id));
|
|
201
|
+
if (others.length > 0)
|
|
202
|
+
return missingHints.set(planned.id, `not found in the build by id or name, and it has no method, path or source reference to find it by; the build has ${others.length} unplanned ${planned.layer}.${planned.type} element(s) (${others.slice(0, 5).map((e) => e.id).join(", ")}${others.length > 5 ? ", ..." : ""}). If one of them is this one, give the plan's element its http_method and http_path, or its sourceFile and sourceSymbol, and verify again`), !1;
|
|
203
|
+
}
|
|
204
|
+
const reason = verdict2.reason ?? "extraction can't produce it";
|
|
205
|
+
return unverifiableIds.set(planned.id, reason), report.elements.unverifiable.push({
|
|
206
|
+
planned: planned.id,
|
|
207
|
+
layer: planned.layer,
|
|
208
|
+
type: planned.type,
|
|
209
|
+
name: planned.name,
|
|
210
|
+
op: planned.op,
|
|
211
|
+
reason
|
|
212
|
+
}), !0;
|
|
213
|
+
}, live = plan.elements.filter((e) => e.op !== "delete"), deletions = plan.elements.filter((e) => e.op === "delete");
|
|
214
|
+
for (const planned of live)
|
|
215
|
+
builtById.has(planned.id) && (claimed.add(planned.id), elementMap.set(planned.id, { built: planned.id, match: "exact" }));
|
|
216
|
+
const pending = live.filter((e) => !elementMap.has(e.id)), ambiguous = /* @__PURE__ */ new Map(), tiers = [
|
|
217
|
+
{
|
|
218
|
+
test: (p, c) => p.layer === c.layer && sameType(p, c) && namesMatch(p, c),
|
|
219
|
+
reason: (p, c) => `same layer and type, name matches ignoring case and separators ("${p.name}" ~ "${c.name}")`
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
test: (p, c) => p.layer === c.layer && !sameType(p, c) && namesMatch(p, c),
|
|
223
|
+
reason: (p, c) => `same layer and name, but built as ${c.type} instead of ${p.type}`
|
|
224
|
+
}
|
|
225
|
+
];
|
|
226
|
+
matchBySource && tiers.push({
|
|
227
|
+
test: (p, c) => p.layer === c.layer && sameType(p, c) && sharesSourceFile(p, c),
|
|
228
|
+
reason: (p, c) => `same layer and type, shares source file ${sharedSourceFiles(p, c).join(", ")}`
|
|
229
|
+
});
|
|
230
|
+
const baseIds = plan.baseElementIds ? new Set(plan.baseElementIds) : void 0, builtNeighbors = /* @__PURE__ */ new Map();
|
|
231
|
+
for (const rel of built.relationships)
|
|
232
|
+
for (const [a, b] of [
|
|
233
|
+
[rel.source, rel.target],
|
|
234
|
+
[rel.target, rel.source]
|
|
235
|
+
]) {
|
|
236
|
+
const set = builtNeighbors.get(a) ?? /* @__PURE__ */ new Set();
|
|
237
|
+
set.add(b), builtNeighbors.set(a, set);
|
|
238
|
+
}
|
|
239
|
+
const planTouched = /* @__PURE__ */ new Set([
|
|
240
|
+
...plan.elements.map((e) => e.id),
|
|
241
|
+
...plan.relationships.flatMap((r) => [r.source, r.target])
|
|
242
|
+
]), neighborCache = /* @__PURE__ */ new Map(), plannedNeighbors = (id) => {
|
|
243
|
+
const cached = neighborCache.get(id);
|
|
244
|
+
if (cached)
|
|
245
|
+
return cached;
|
|
246
|
+
const own = plan.relationships.filter((r) => r.op === "add" && (r.source === id || r.target === id)).map((r) => r.source === id ? r.target : r.source), ids = own.length > 0 ? own : [...planTouched].filter((other) => other !== id), near = new Set(ids.flatMap((other) => [other, elementMap.get(other)?.built ?? other]));
|
|
247
|
+
return neighborCache.set(id, near), near;
|
|
248
|
+
};
|
|
249
|
+
let fellBack = !1;
|
|
250
|
+
const canBeBuildOf = (planned, candidate) => {
|
|
251
|
+
if (planned.op !== "add")
|
|
252
|
+
return !0;
|
|
253
|
+
if (baseIds)
|
|
254
|
+
return !baseIds.has(candidate.id);
|
|
255
|
+
if (changedIds.has(candidate.id))
|
|
256
|
+
return !0;
|
|
257
|
+
fellBack = !0;
|
|
258
|
+
const near = plannedNeighbors(planned.id);
|
|
259
|
+
for (const other of builtNeighbors.get(candidate.id) ?? [])
|
|
260
|
+
if (near.has(other))
|
|
261
|
+
return !0;
|
|
262
|
+
return !1;
|
|
263
|
+
};
|
|
264
|
+
for (const tier of tiers)
|
|
265
|
+
for (const planned of pending) {
|
|
266
|
+
if (elementMap.has(planned.id))
|
|
267
|
+
continue;
|
|
268
|
+
const candidates = built.elements.filter((c) => !claimed.has(c.id) && tier.test(planned, c) && canBeBuildOf(planned, c));
|
|
269
|
+
if (candidates.length === 0)
|
|
270
|
+
continue;
|
|
271
|
+
let chosen = candidates;
|
|
272
|
+
if (chosen.length > 1 && changedIds.size > 0) {
|
|
273
|
+
const changed = chosen.filter((c) => changedIds.has(c.id));
|
|
274
|
+
changed.length > 0 && (chosen = changed);
|
|
275
|
+
}
|
|
276
|
+
if (chosen.length === 1) {
|
|
277
|
+
const candidate = chosen[0];
|
|
278
|
+
claimed.add(candidate.id), elementMap.set(planned.id, { built: candidate.id, match: "probable", reason: tier.reason(planned, candidate) }), ambiguous.delete(planned.id);
|
|
279
|
+
} else ambiguous.has(planned.id) || ambiguous.set(planned.id, chosen.map((c) => c.id));
|
|
280
|
+
}
|
|
281
|
+
fellBack && notes.push(`No base index was recorded for ${plan.label}, so the elements that existed before it are not known exactly: unchanged elements with no relationship to the plan's elements were not considered as the built form of a planned add.`);
|
|
282
|
+
const lifecycleMoves = [];
|
|
283
|
+
for (const planned of live) {
|
|
284
|
+
const mapping = elementMap.get(planned.id);
|
|
285
|
+
if (!mapping) {
|
|
286
|
+
if (markUnverifiable(planned))
|
|
287
|
+
continue;
|
|
288
|
+
const candidates = ambiguous.get(planned.id);
|
|
289
|
+
report.elements.missing.push({
|
|
290
|
+
planned: planned.id,
|
|
291
|
+
layer: planned.layer,
|
|
292
|
+
type: planned.type,
|
|
293
|
+
name: planned.name,
|
|
294
|
+
op: planned.op,
|
|
295
|
+
reason: candidates ? `several built elements could be it (${candidates.length}); none matched uniquely` : planned.op === "update" ? "the element the plan updates does not exist in the build" : missingHints.get(planned.id) ?? "not found in the build by id, name or source file",
|
|
296
|
+
...candidates ? { candidates } : {}
|
|
297
|
+
});
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const builtElement = builtById.get(mapping.built);
|
|
301
|
+
let fields = compareElements(planned, builtElement, planned.op === "update" ? planned.changedFields : void 0);
|
|
302
|
+
const match = {
|
|
303
|
+
planned: planned.id,
|
|
304
|
+
built: mapping.built,
|
|
305
|
+
op: planned.op,
|
|
306
|
+
match: mapping.match,
|
|
307
|
+
...mapping.reason ? { reason: mapping.reason } : {}
|
|
308
|
+
}, unverifiableFields = observability?.field ? fields.filter((f) => !observability.field(planned, f.field) && buildLeft(planned, builtElement, f)) : [];
|
|
309
|
+
if (unverifiableFields.length > 0 && (match.unverifiableFields = unverifiableFields, fields = fields.filter((f) => !unverifiableFields.includes(f))), planned.op === "update" && planned.changedFields && planned.changedFields.length > 0) {
|
|
310
|
+
const checkable = planned.changedFields.filter((field) => !unverifiableFields.some((u) => u.field === field));
|
|
311
|
+
if (checkable.length === 0) {
|
|
312
|
+
const reason = `extraction doesn't write ${unverifiableFields.map((f) => f.field).join(", ")}`;
|
|
313
|
+
report.elements.unverifiable.push({
|
|
314
|
+
planned: planned.id,
|
|
315
|
+
layer: planned.layer,
|
|
316
|
+
type: planned.type,
|
|
317
|
+
name: planned.name,
|
|
318
|
+
op: planned.op,
|
|
319
|
+
reason,
|
|
320
|
+
fields: unverifiableFields
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const unmet = fields.filter((f) => checkable.includes(f.field)), touched = unmet.some((f) => buildTouched(planned, builtElement, f.field));
|
|
325
|
+
if (unmet.length === checkable.length && !touched) {
|
|
326
|
+
if (markUnverifiable(planned))
|
|
327
|
+
continue;
|
|
328
|
+
report.elements.missing.push({
|
|
329
|
+
planned: planned.id,
|
|
330
|
+
layer: planned.layer,
|
|
331
|
+
type: planned.type,
|
|
332
|
+
name: planned.name,
|
|
333
|
+
op: planned.op,
|
|
334
|
+
reason: "the element exists but the build does not carry the planned update",
|
|
335
|
+
fields: unmet
|
|
336
|
+
});
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
const lifecycle = fields.filter(isLifecycleMove), descriptionChecked = planned.op === "update" && (planned.changedFields ?? []).includes("description"), informational = fields.filter((f) => f.field === "description" && !descriptionChecked), differences = fields.filter((f) => !isLifecycleMove(f) && !informational.includes(f));
|
|
341
|
+
if (lifecycle.length > 0) {
|
|
342
|
+
match.lifecycle = lifecycle;
|
|
343
|
+
for (const f of lifecycle)
|
|
344
|
+
lifecycleMoves.push(`${planned.id} ${f.field} ${String(f.planned)} \u2192 ${String(f.built)}`);
|
|
345
|
+
}
|
|
346
|
+
informational.length > 0 && (match.notes = informational), differences.length > 0 ? report.elements.divergent.push({ ...match, fields: differences }) : report.elements.matched.push(match);
|
|
347
|
+
}
|
|
348
|
+
lifecycleMoves.length > 0 && notes.push(`Lifecycle progress, not counted as divergence (${LIFECYCLE_FIELDS.join(", ")} moving to a later stage of ${LIFECYCLE_STAGES.map((stage) => stage.join("/")).join(" \u2192 ")}): ${lifecycleMoves.join("; ")}.`);
|
|
349
|
+
const described = [...report.elements.matched, ...report.elements.divergent].filter((m) => m.notes);
|
|
350
|
+
described.length > 0 && notes.push(`${described.length} matched element${described.length === 1 ? " has a description that differs" : "s have descriptions that differ"} from the plan's (under notes on each match). Descriptions are not compared: no need to edit the plan to match the build's text.`);
|
|
351
|
+
const unwritten = [...report.elements.matched, ...report.elements.divergent].filter((m) => m.unverifiableFields);
|
|
352
|
+
unwritten.length > 0 && notes.push(`${unwritten.length} matched element${unwritten.length === 1 ? " has" : "s have"} planned fields extraction doesn't write (under unverifiableFields on each match): check them in the code or the review.`);
|
|
353
|
+
for (const planned of deletions) {
|
|
354
|
+
const still = builtById.get(planned.id);
|
|
355
|
+
if (still) {
|
|
356
|
+
if (claimed.add(still.id), markUnverifiable(planned))
|
|
357
|
+
continue;
|
|
358
|
+
report.elements.deletionsNotDone.push({ planned: planned.id, built: still.id });
|
|
359
|
+
} else
|
|
360
|
+
claimed.add(planned.id), report.elements.matched.push({ planned: planned.id, built: planned.id, op: "delete", match: "exact" });
|
|
361
|
+
}
|
|
362
|
+
if (built.changedElements)
|
|
363
|
+
for (const element of built.changedElements)
|
|
364
|
+
claimed.has(element.id) || report.elements.unplanned.push({
|
|
365
|
+
id: element.id,
|
|
366
|
+
layer: element.layer,
|
|
367
|
+
type: element.type,
|
|
368
|
+
name: element.name,
|
|
369
|
+
change: element.change ?? "added"
|
|
370
|
+
});
|
|
371
|
+
const builtRels = new Map(built.relationships.map((r) => [relKey(r), r])), claimedRels = /* @__PURE__ */ new Set(), mapId = (id) => elementMap.get(id)?.built ?? id, probableEndpoints = (rel) => [rel.source, rel.target].filter((id, i, all) => all.indexOf(id) === i).filter((id) => elementMap.get(id)?.match === "probable").map((id) => `${id} \u2192 ${elementMap.get(id).built}`), missingEndpoints = new Set(report.elements.missing.map((m) => m.planned)), plannedById = new Map(plan.elements.map((e) => [e.id, e])), elementOf = (id) => builtById.get(elementMap.get(id)?.built ?? "") ?? plannedById.get(id) ?? builtById.get(id) ?? fromId(id, ""), unverifiableReason = (rel) => {
|
|
372
|
+
if (!observability)
|
|
373
|
+
return;
|
|
374
|
+
for (const end of [rel.source, rel.target]) {
|
|
375
|
+
const known = unverifiableIds.get(end);
|
|
376
|
+
if (known !== void 0)
|
|
377
|
+
return `${end} is unverifiable (${known})`;
|
|
378
|
+
if (missingHints.has(end))
|
|
379
|
+
continue;
|
|
380
|
+
const verdict3 = observability.element(elementOf(end));
|
|
381
|
+
if (!verdict3.observable)
|
|
382
|
+
return `extraction can't see ${end}: ${verdict3.reason ?? "no rule produces it"}`;
|
|
383
|
+
}
|
|
384
|
+
const verdict2 = observability.relationship(elementOf(rel.source), rel.predicate, elementOf(rel.target));
|
|
385
|
+
return verdict2.observable ? void 0 : verdict2.reason ?? "no mapped code edge produces it";
|
|
386
|
+
}, pushUnverifiable = (planned) => {
|
|
387
|
+
const reason = unverifiableReason(planned);
|
|
388
|
+
return reason === void 0 ? !1 : (report.relationships.unverifiable.push({ planned: strip(planned), op: planned.op, reason }), !0);
|
|
389
|
+
}, mapRel = (planned) => ({
|
|
390
|
+
source: mapId(planned.source),
|
|
391
|
+
predicate: planned.predicate,
|
|
392
|
+
target: mapId(planned.target)
|
|
393
|
+
}), matchInfoFor = (planned) => {
|
|
394
|
+
const probable = probableEndpoints(planned);
|
|
395
|
+
return probable.length > 0 ? { match: "probable", reason: `through probable element match: ${probable.join("; ")}` } : { match: "exact" };
|
|
396
|
+
}, unresolved = [];
|
|
397
|
+
for (const planned of plan.relationships) {
|
|
398
|
+
const mapped = mapRel(planned), found = builtRels.get(relKey(mapped));
|
|
399
|
+
if (!found || claimedRels.has(relKey(found))) {
|
|
400
|
+
unresolved.push(planned);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
claimedRels.add(relKey(found));
|
|
404
|
+
const plannedRef = strip(planned);
|
|
405
|
+
if (planned.op === "delete") {
|
|
406
|
+
if (pushUnverifiable(planned))
|
|
407
|
+
continue;
|
|
408
|
+
report.relationships.deletionsNotDone.push({ planned: plannedRef, built: strip(found) });
|
|
409
|
+
} else
|
|
410
|
+
report.relationships.matched.push({ planned: plannedRef, built: strip(found), op: "add", ...matchInfoFor(planned) });
|
|
411
|
+
}
|
|
412
|
+
const addedBuiltIds = new Set(live.filter((e) => e.op === "add" && elementMap.has(e.id)).map((e) => elementMap.get(e.id).built));
|
|
413
|
+
for (const planned of unresolved) {
|
|
414
|
+
const mapped = mapRel(planned), plannedRef = strip(planned), matchInfo = matchInfoFor(planned);
|
|
415
|
+
if (planned.op === "delete") {
|
|
416
|
+
claimedRels.add(relKey(mapped)), report.relationships.matched.push({ planned: plannedRef, built: mapped, op: "delete", ...matchInfo });
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const isAlternative = (r) => !claimedRels.has(relKey(r)) && (r.source === mapped.source && r.target === mapped.target && r.predicate !== mapped.predicate || r.source === mapped.target && r.target === mapped.source && r.predicate === mapped.predicate), alternative = built.changedRelationships ? built.changedRelationships.filter((r) => r.change !== "deleted").find(isAlternative) : (
|
|
420
|
+
// What the build changed is not known: only a relationship touching an element the plan adds
|
|
421
|
+
// can be the build's version; one between pre-existing elements may simply predate the change.
|
|
422
|
+
built.relationships.find((r) => isAlternative(r) && (addedBuiltIds.has(r.source) || addedBuiltIds.has(r.target)))
|
|
423
|
+
);
|
|
424
|
+
if (alternative) {
|
|
425
|
+
claimedRels.add(relKey(alternative));
|
|
426
|
+
const fields = alternative.predicate !== mapped.predicate ? [{ field: "predicate", planned: mapped.predicate, built: alternative.predicate }] : [
|
|
427
|
+
{
|
|
428
|
+
field: "direction",
|
|
429
|
+
planned: `${mapped.source} \u2192 ${mapped.target}`,
|
|
430
|
+
built: `${alternative.source} \u2192 ${alternative.target}`
|
|
431
|
+
}
|
|
432
|
+
];
|
|
433
|
+
report.relationships.divergent.push({
|
|
434
|
+
planned: plannedRef,
|
|
435
|
+
built: strip(alternative),
|
|
436
|
+
op: "add",
|
|
437
|
+
...matchInfo,
|
|
438
|
+
fields
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
if (pushUnverifiable(planned))
|
|
443
|
+
continue;
|
|
444
|
+
const candidates = built.changedRelationships ? [] : built.relationships.filter(isAlternative).map(strip), absent = [planned.source, planned.target].filter((id) => missingEndpoints.has(id) || !elementMap.has(id) && !builtById.has(id));
|
|
445
|
+
report.relationships.missing.push({
|
|
446
|
+
planned: plannedRef,
|
|
447
|
+
reason: absent.length > 0 ? `not built; endpoint${absent.length > 1 ? "s" : ""} ${absent.join(", ")} not found in the build` : candidates.length > 0 ? "not found in the build; a relationship between the same elements exists, but it may predate the change" : "not found in the build",
|
|
448
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
if (built.changedRelationships)
|
|
452
|
+
for (const rel of built.changedRelationships)
|
|
453
|
+
claimedRels.has(relKey(rel)) || report.relationships.unplanned.push({ ...strip(rel), change: rel.change ?? "added" });
|
|
454
|
+
else
|
|
455
|
+
for (const rel of built.relationships)
|
|
456
|
+
claimedRels.has(relKey(rel)) || (addedBuiltIds.has(rel.source) || addedBuiltIds.has(rel.target)) && report.relationships.unplanned.push({ ...strip(rel), change: "present" });
|
|
457
|
+
report.summary.elements = countCategories(plan.elements.length, report.elements), report.summary.relationships = countCategories(plan.relationships.length, report.relationships);
|
|
458
|
+
const { verdict, text } = decideVerdict(report);
|
|
459
|
+
return report.verdict = verdict, report.verdictText = text, report;
|
|
460
|
+
}
|
|
461
|
+
async function verifyChangesetInModel(model, options) {
|
|
462
|
+
const { planned } = options, plan = await planFromModel(model, planned), notes = [], baseIndex = await tryReadBaseIndex(model.rootPath, planned.id);
|
|
463
|
+
baseIndex.index && (plan.baseElementIds = Object.keys(baseIndex.index.elements)), baseIndex.warning && notes.push(baseIndex.warning);
|
|
464
|
+
let built, extraction, observability, warning;
|
|
465
|
+
if ("changeset" in options.against) {
|
|
466
|
+
const asBuilt = options.against.changeset;
|
|
467
|
+
asBuilt.id === planned.id && notes.push("The planned and as-built changesets are the same changeset, so everything matches by definition."), built = builtFromGraph(await ChangesetGraph.build(model, asBuilt), `${asBuilt.name} (${asBuilt.id})`);
|
|
468
|
+
const record = await readExtractionRecord(model.rootPath, { id: asBuilt.id, name: asBuilt.name }, currentGitHead(model.codebaseRoot));
|
|
469
|
+
if (!record)
|
|
470
|
+
extraction = {
|
|
471
|
+
applied: !1,
|
|
472
|
+
reason: "codebase_extract has no record of the as-built changeset (.dr/analyzers/extract/), so it is not taken as extraction output: every planned change is checked, and its elements count as added by hand, whatever provenance they declare.",
|
|
473
|
+
asBuilt: asBuiltShare(built, plan, { record: null })
|
|
474
|
+
}, EXTRACTION_MARKER.test(asBuilt.description ?? "") && notes.push("The as-built changeset's description says codebase_extract staged it, but its record is gone. Run codebase_extract on it again (with the same since) to record what it stages, then verify again.");
|
|
475
|
+
else {
|
|
476
|
+
const codeFiles = [...new Set((built.changedElements ?? []).flatMap((e) => sourceFiles(e)))];
|
|
477
|
+
({ extraction, observability } = await extractionRule(model, options, codeFiles, "The as-built changeset came from codebase_extract"));
|
|
478
|
+
const share2 = asBuiltShare(built, plan, { record, ...observability ? { observe: (e) => observability.element(e) } : {} });
|
|
479
|
+
extraction.asBuilt = share2, record.readAt && notes.push(`The extraction record is from a run that staged nothing, at ${record.readAt.slice(0, 7)}; the code has moved since (a commit after the extract). Its cards still name what extraction offered then; run codebase_extract on this changeset again if the code changed since.`), share2.copiedFromPlan?.length && notes.push(`${share2.copiedFromPlan.length} as-built element(s) share an id with the plan's adds but did not come from extraction (${share2.copiedFromPlan.join(", ")}). If they were copied from the plan, their match says nothing about the code; extract again with codebase_extract and leave them out.`);
|
|
480
|
+
}
|
|
481
|
+
warning = asBuiltWarning(extraction.asBuilt, extraction.applied);
|
|
482
|
+
} else
|
|
483
|
+
built = await builtFromBase(model), planned.status === "staged" && notes.push("The planned changeset is still staged, so its own changes are not in the base model: anything reported missing is missing from what was committed to the base (e.g. an as-built changeset)."), options.against.extracted ? { extraction, observability } = await extractionRule(model, options, [], "Compared with the base model, which holds a landed extraction (extracted: true)") : extraction = {
|
|
484
|
+
applied: !1,
|
|
485
|
+
reason: "Compared with the base model: every planned change is checked. Pass extracted: true when the base holds a landed extraction of the build."
|
|
486
|
+
};
|
|
487
|
+
const report = verifyChangeset(plan, built, { matchBySource: options.matchBySource, observability });
|
|
488
|
+
report.extraction = extraction, report.notes.push(...notes);
|
|
489
|
+
const share = extraction.asBuilt;
|
|
490
|
+
if (share) {
|
|
491
|
+
const agentIds = new Set((share.agentAdded ?? []).map((a) => a.id)), edits = new Map((share.editedAfterExtraction ?? []).map((e) => [e.id, e.edits])), matches = [...report.elements.matched, ...report.elements.divergent], onEdited = matches.filter((m) => edits.has(m.built));
|
|
492
|
+
for (const m of onEdited)
|
|
493
|
+
m.editedAfterExtraction = edits.get(m.built);
|
|
494
|
+
const onAgent = matches.filter((m) => agentIds.has(m.built) || edits.has(m.built));
|
|
495
|
+
for (const m of onAgent)
|
|
496
|
+
m.builtByAgent = !0;
|
|
497
|
+
onEdited.length > 0 && report.notes.push(`${onEdited.length} match(es) are against extracted elements changed after extraction staged them (editedAfterExtraction on each): the match shows the edit, not the code.`), warning && (report.warning = warning, report.verdictText += ` But ${warning.charAt(0).toLowerCase()}${warning.slice(1)}`), onAgent.length > 0 && share.record !== "not-found" && (report.verdict === "match" && (report.verdict = "probable-match"), report.verdictText += ` ${onAgent.length} of the matches ${onAgent.length === 1 ? "rests" : "rest"} on elements extraction didn't stage` + (onEdited.length > 0 ? ` or that were edited after it (${onEdited.length})` : "") + " (builtByAgent): confirm them in the code.");
|
|
498
|
+
}
|
|
499
|
+
return { plannedChangeset: { id: planned.id, name: planned.name }, ...report };
|
|
500
|
+
}
|
|
501
|
+
async function extractionRule(model, options, codeFiles, why) {
|
|
502
|
+
const analyzer = options.analyzer ?? await resolveAnalyzerName(model.rootPath).catch(() => "cbm"), coverage = options.coverage !== void 0 ? options.coverage : await loadExtractionCoverage(analyzer);
|
|
503
|
+
return coverage ? {
|
|
504
|
+
observability: {
|
|
505
|
+
element: (element) => coverage.element(element, codeFiles),
|
|
506
|
+
relationship: (source, predicate, target) => coverage.relationship(source, predicate, target),
|
|
507
|
+
field: (element, field) => coverage.field(element, field)
|
|
508
|
+
},
|
|
509
|
+
extraction: {
|
|
510
|
+
applied: !0,
|
|
511
|
+
reason: `${why}: planned changes extraction can't produce (intent, types or files no rule covers, relationships no code edge maps to, fields it doesn't write) are unverifiable, not missing.`,
|
|
512
|
+
analyzer: coverage.analyzer,
|
|
513
|
+
observableTypes: coverage.observableTypes()
|
|
514
|
+
}
|
|
515
|
+
} : {
|
|
516
|
+
extraction: {
|
|
517
|
+
applied: !1,
|
|
518
|
+
reason: `${why}, but the ${analyzer} mapping could not be read, so extraction's blind spots are not known: every planned change is checked.`,
|
|
519
|
+
analyzer
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const EXTRACTION_MARKER = /Staged by codebase_extract/;
|
|
524
|
+
function asBuiltShare(built, plan, context = {}) {
|
|
525
|
+
const record = context.record ?? null, changed = built.changedElements ?? [], planAdds = (plan?.elements ?? []).filter((e) => e.op === "add"), plannedAdds = new Set(planAdds.map((e) => e.id)), plannedNames = new Set(planAdds.flatMap((e) => [...nameKeys(e)])), stagedById = new Map((record?.staged ?? []).map((e) => [e.id, e])), updatedById = new Map((record?.updated ?? []).map((u) => [u.id, u])), candidateDeletes = new Set(record?.candidateDeletes ?? []), offered = /* @__PURE__ */ new Map();
|
|
526
|
+
for (const entry of [...record?.staged ?? [], ...record?.cards ?? [], ...record?.earlierCards ?? []]) {
|
|
527
|
+
const key = decisionKey(entry.file, entry.symbol);
|
|
528
|
+
offered.set(key, [...offered.get(key) ?? [], entry]);
|
|
529
|
+
}
|
|
530
|
+
const notFromExtraction = (e) => {
|
|
531
|
+
if (!record)
|
|
532
|
+
return "no codebase_extract record of this changeset confirms it";
|
|
533
|
+
if (e.change === "deleted")
|
|
534
|
+
return candidateDeletes.has(e.id) ? void 0 : "a delete that is not one of extraction's candidate deletes";
|
|
535
|
+
if (e.change === "updated")
|
|
536
|
+
return updatedById.has(e.id) ? void 0 : "an update extraction did not stage";
|
|
537
|
+
const type = `${e.layer}.${e.type}`, looksPlanned = plannedAdds.has(e.id) || [...nameKeys(e)].some((k) => plannedNames.has(k)), staged = stagedById.get(e.id), located = (e.sourceReference?.locations ?? []).filter((l) => typeof l?.symbol == "string" && l.symbol.length > 0), cards = located.flatMap((l) => offered.get(decisionKey(normalizeFile(l.file), l.symbol)) ?? []);
|
|
538
|
+
if (!staged && cards.length === 0) {
|
|
539
|
+
const verdict = context.observe?.(e);
|
|
540
|
+
return verdict && !verdict.observable ? `extraction can't produce it: ${verdict.reason ?? type}` : located.length === 0 ? "extraction did not stage it, and it names no source symbol" : "extraction did not stage it, and no card of its runs has its file and symbol";
|
|
541
|
+
}
|
|
542
|
+
const producible = context.observe ? context.observe(e) : { observable: !0 }, allowed = (c) => c.candidates?.length ? c.candidates.some((t) => t === type || t === `${e.layer}.*`) : producible.observable, fitting = cards.filter(allowed), withCandidates = cards.filter((c) => c.candidates?.length);
|
|
543
|
+
if (withCandidates.length > 0 && !withCandidates.some(allowed))
|
|
544
|
+
return `its card allows ${[...new Set(withCandidates.flatMap((c) => c.candidates))].join(", ")}, not ${type}`;
|
|
545
|
+
if (!producible.observable && withCandidates.length === 0)
|
|
546
|
+
return `extraction can't produce it ${staged ? "there" : "from its card"}: ${producible.reason ?? type}`;
|
|
547
|
+
if (looksPlanned && !(staged && namedAfterCard(e, staged) || fitting.some((c) => namedAfterCard(e, c))))
|
|
548
|
+
return staged ? "named after the plan's element, not after the symbol extraction staged it from" : "named after the plan's element, not after its card";
|
|
549
|
+
}, byProvenance = {}, extracted = /* @__PURE__ */ new Set(), agentAdded = [], edited = [];
|
|
550
|
+
for (const e of changed) {
|
|
551
|
+
const declared = e.change === "deleted" ? "deleted" : e.stagedProvenance ?? (e.change === "updated" ? "sourceUnchanged" : e.sourceReference?.provenance ?? "none");
|
|
552
|
+
byProvenance[declared] = (byProvenance[declared] ?? 0) + 1;
|
|
553
|
+
const reason = notFromExtraction(e);
|
|
554
|
+
if (reason !== void 0) {
|
|
555
|
+
agentAdded.push({ id: e.id, declared, reason });
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
extracted.add(e.id);
|
|
559
|
+
const edits = editsAfterExtraction(e, stagedById.get(e.id), updatedById.get(e.id));
|
|
560
|
+
edits.length > 0 && edited.push({ id: e.id, edits });
|
|
561
|
+
}
|
|
562
|
+
const relationships = (built.changedRelationships ?? []).filter((r) => r.change !== "deleted"), betweenExtracted = relationships.filter((r) => extracted.has(r.source) && extracted.has(r.target)).length, addedIds = new Set(changed.filter((e) => e.change === "added").map((e) => e.id)), copied = agentAdded.filter((a) => plannedAdds.has(a.id) && addedIds.has(a.id)).map((a) => a.id);
|
|
563
|
+
return {
|
|
564
|
+
elements: { total: changed.length, byProvenance, fromExtraction: extracted.size, agentAdded: agentAdded.length },
|
|
565
|
+
relationships: { total: relationships.length, betweenExtracted, other: relationships.length - betweenExtracted },
|
|
566
|
+
record: record ? record.source === "pending" ? "pending" : "found" : "not-found",
|
|
567
|
+
...agentAdded.length > 0 ? { agentAdded } : {},
|
|
568
|
+
...edited.length > 0 ? { editedAfterExtraction: edited } : {},
|
|
569
|
+
...copied.length > 0 ? { copiedFromPlan: copied } : {}
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function recordFieldValue(element, field) {
|
|
573
|
+
return field === "source_reference" ? element.sourceReference ?? null : fieldValue(element, field);
|
|
574
|
+
}
|
|
575
|
+
function sameRecordValue(field, a, b) {
|
|
576
|
+
if (field !== "source_reference")
|
|
577
|
+
return sameFieldValue(field, a, b);
|
|
578
|
+
const where = (v) => {
|
|
579
|
+
const locations = v?.locations;
|
|
580
|
+
return Array.isArray(locations) ? locations.map((l) => `${String(l?.file ?? "")}#${String(l?.symbol ?? "")}`).sort() : [];
|
|
581
|
+
};
|
|
582
|
+
return sameValue(where(a), where(b));
|
|
583
|
+
}
|
|
584
|
+
function editsAfterExtraction(e, staged, own) {
|
|
585
|
+
const edits = [], values = own?.values && typeof own.values == "object" ? own.values : void 0, byValue = (field) => {
|
|
586
|
+
if (!values || !Object.hasOwn(values, field))
|
|
587
|
+
return;
|
|
588
|
+
const now = recordFieldValue(e, field);
|
|
589
|
+
return sameRecordValue(field, values[field], now) ? void 0 : { field, extracted: values[field] ?? null, now: now ?? null };
|
|
590
|
+
};
|
|
591
|
+
if (e.change === "updated") {
|
|
592
|
+
if (!own)
|
|
593
|
+
return edits;
|
|
594
|
+
for (const [field, was] of Object.entries(e.updatedFrom ?? {}))
|
|
595
|
+
own.fields && !own.fields.includes(field) && edits.push({ field, extracted: was ?? null, now: recordFieldValue(e, field) ?? null });
|
|
596
|
+
for (const field of own.fields ?? []) {
|
|
597
|
+
const edit = byValue(field);
|
|
598
|
+
edit && edits.push(edit);
|
|
599
|
+
}
|
|
600
|
+
return edits;
|
|
601
|
+
}
|
|
602
|
+
if (e.change !== "added")
|
|
603
|
+
return edits;
|
|
604
|
+
const folded = new Set(own?.fields ?? []);
|
|
605
|
+
staged && edits.push(...editsSinceExtraction(e, staged).filter((d) => !folded.has(d.field) && !(own && !own.fields)));
|
|
606
|
+
for (const field of folded) {
|
|
607
|
+
const edit = byValue(field);
|
|
608
|
+
edit && edits.push(edit);
|
|
609
|
+
}
|
|
610
|
+
return edits;
|
|
611
|
+
}
|
|
612
|
+
function editsSinceExtraction(element, staged) {
|
|
613
|
+
const edits = [];
|
|
614
|
+
if (typeof staged.name != "string")
|
|
615
|
+
return edits;
|
|
616
|
+
staged.name !== element.name && edits.push({ field: "name", extracted: staged.name, now: element.name });
|
|
617
|
+
const stagedAttributes = staged.attributes && typeof staged.attributes == "object" ? staged.attributes : {};
|
|
618
|
+
{
|
|
619
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(stagedAttributes), ...Object.keys(element.attributes ?? {})]);
|
|
620
|
+
for (const key of [...keys].sort()) {
|
|
621
|
+
const was = stagedAttributes[key], now = element.attributes?.[key];
|
|
622
|
+
sameValue(was, now) || edits.push({ field: `attributes.${key}`, extracted: was ?? null, now: now ?? null });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return edits;
|
|
626
|
+
}
|
|
627
|
+
function namedAfterCard(element, card) {
|
|
628
|
+
if (card.proposed?.id && card.proposed.id === element.id)
|
|
629
|
+
return !0;
|
|
630
|
+
const route = routeKey(card.symbol) ?? (card.name ? routeKey(card.name) : void 0);
|
|
631
|
+
if (route && routeKey(element.name) === route)
|
|
632
|
+
return !0;
|
|
633
|
+
const keys = nameKeys(element), parts = card.symbol.split(/[.#/\s]+/).filter(Boolean), file = card.file ?? "";
|
|
634
|
+
return [card.symbol, card.name, card.proposed?.name, parts[parts.length - 1], file, file.replace(/\.[^./]+$/, "")].some((n) => typeof n == "string" && n.length > 0 && keys.has(normalizeName(n)));
|
|
635
|
+
}
|
|
636
|
+
function normalizeFile(file) {
|
|
637
|
+
return typeof file == "string" && file.length > 0 ? file.replace(/\\/g, "/").replace(/^\.\//, "") : null;
|
|
638
|
+
}
|
|
639
|
+
function asBuiltWarning(share, blindSpotsApplied = !0) {
|
|
640
|
+
const { total, agentAdded } = share.elements, edited = share.editedAfterExtraction?.length ?? 0;
|
|
641
|
+
if (total === 0 || (agentAdded + edited) * 2 <= total)
|
|
642
|
+
return;
|
|
643
|
+
const copied = share.copiedFromPlan?.length ?? 0;
|
|
644
|
+
return `${agentAdded + edited} of the ${total} as-built elements were not staged by extraction` + (edited > 0 ? ` or were edited after it (${edited})` : "") + (copied > 0 ? ` (${copied} share an id with the plan's adds)` : "") + ", so this comparison says little about the code." + (blindSpotsApplied ? " Extraction's blind spots were still applied." : share.record === "not-found" ? " No extraction record covers this changeset, so extraction's blind spots were not applied." : " Extraction's blind spots were not applied.") + " To verify the code, discard this changeset and stage the build with codebase_extract: from its cards, named after them (see extraction.asBuilt.agentAdded for why each one doesn't count).";
|
|
645
|
+
}
|
|
646
|
+
function normalizeName(name) {
|
|
647
|
+
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
648
|
+
}
|
|
649
|
+
function nameKeys(element) {
|
|
650
|
+
const keys = /* @__PURE__ */ new Set();
|
|
651
|
+
element.name && keys.add(normalizeName(element.name));
|
|
652
|
+
const segment = idNameSegment(element.id);
|
|
653
|
+
return segment && keys.add(normalizeName(segment)), keys.delete(""), keys;
|
|
654
|
+
}
|
|
655
|
+
function namesMatch(a, b) {
|
|
656
|
+
const keys = nameKeys(b);
|
|
657
|
+
for (const key of nameKeys(a))
|
|
658
|
+
if (keys.has(key))
|
|
659
|
+
return !0;
|
|
660
|
+
return !1;
|
|
661
|
+
}
|
|
662
|
+
function sameType(a, b) {
|
|
663
|
+
return normalizeName(a.type) === normalizeName(b.type);
|
|
664
|
+
}
|
|
665
|
+
function sourceFiles(element) {
|
|
666
|
+
return (element.sourceReference?.locations ?? []).map((l) => typeof l?.file == "string" ? l.file.replace(/^\.\//, "").replace(/\\/g, "/") : "").filter((f) => f.length > 0);
|
|
667
|
+
}
|
|
668
|
+
function sharedSourceFiles(a, b) {
|
|
669
|
+
const files = new Set(sourceFiles(b));
|
|
670
|
+
return sourceFiles(a).filter((f) => files.has(f));
|
|
671
|
+
}
|
|
672
|
+
function sharesSourceFile(a, b) {
|
|
673
|
+
return sharedSourceFiles(a, b).length > 0;
|
|
674
|
+
}
|
|
675
|
+
function compareElements(planned, built, removedBy) {
|
|
676
|
+
const fields = [];
|
|
677
|
+
planned.type && !sameType(planned, built) && fields.push({ field: "type", planned: planned.type, built: built.type }), planned.name && normalizeName(planned.name) !== normalizeName(built.name) && fields.push({ field: "name", planned: planned.name, built: built.name }), planned.description !== void 0 && planned.description.trim() !== (built.description ?? "").trim() && fields.push({ field: "description", planned: planned.description, built: built.description });
|
|
678
|
+
for (const [key, value] of Object.entries(planned.attributes ?? {})) {
|
|
679
|
+
const builtValue = built.attributes?.[key];
|
|
680
|
+
sameAttribute(key, value, builtValue) || fields.push({ field: `attributes.${key}`, planned: value, built: builtValue });
|
|
681
|
+
}
|
|
682
|
+
for (const field of removedBy ?? [])
|
|
683
|
+
if (field === "description")
|
|
684
|
+
planned.description === void 0 && (built.description ?? "").trim() !== "" && fields.push({ field, planned: void 0, built: built.description });
|
|
685
|
+
else if (field.startsWith("attributes.")) {
|
|
686
|
+
const key = field.slice(11);
|
|
687
|
+
!(planned.attributes !== void 0 && Object.hasOwn(planned.attributes, key)) && built.attributes?.[key] !== void 0 && fields.push({ field, planned: void 0, built: built.attributes[key] });
|
|
688
|
+
}
|
|
689
|
+
return fields.map((f) => ({ field: f.field, planned: f.planned ?? null, built: f.built ?? null }));
|
|
690
|
+
}
|
|
691
|
+
function fieldValue(element, field) {
|
|
692
|
+
if (field === "type")
|
|
693
|
+
return element.type;
|
|
694
|
+
if (field === "name")
|
|
695
|
+
return element.name;
|
|
696
|
+
if (field === "description")
|
|
697
|
+
return element.description;
|
|
698
|
+
if (field.startsWith("attributes."))
|
|
699
|
+
return element.attributes?.[field.slice(11)];
|
|
700
|
+
}
|
|
701
|
+
function sameFieldValue(field, a, b) {
|
|
702
|
+
return field === "type" || field === "name" ? normalizeName(String(a ?? "")) === normalizeName(String(b ?? "")) : field === "description" ? String(a ?? "").trim() === String(b ?? "").trim() : field.startsWith("attributes.") ? sameAttribute(field.slice(11), a, b) : sameValue(a, b);
|
|
703
|
+
}
|
|
704
|
+
function normalizeHttpPath(p) {
|
|
705
|
+
return p.replace(/\{[^}]*\}|:[A-Za-z_]\w*/g, "{}").replace(/\/+$/, "") || "/";
|
|
706
|
+
}
|
|
707
|
+
function routeKey(text) {
|
|
708
|
+
const m = /^__route__([A-Z]+)__(\/\S*)$/.exec(text.trim()) ?? /^([A-Z]+)\s+(\/\S*)$/.exec(text.trim());
|
|
709
|
+
return m ? `${m[1]} ${normalizeHttpPath(m[2])}` : void 0;
|
|
710
|
+
}
|
|
711
|
+
function sameAttribute(key, a, b) {
|
|
712
|
+
if (typeof a == "string" && typeof b == "string") {
|
|
713
|
+
if (key === "http_path")
|
|
714
|
+
return normalizeHttpPath(a) === normalizeHttpPath(b);
|
|
715
|
+
if (key === "http_method")
|
|
716
|
+
return a.toUpperCase() === b.toUpperCase();
|
|
717
|
+
}
|
|
718
|
+
return sameValue(a, b);
|
|
719
|
+
}
|
|
720
|
+
function buildLeft(planned, built, difference) {
|
|
721
|
+
return difference.built === null ? !0 : planned.op !== "update" ? !1 : planned.baseValues && Object.hasOwn(planned.baseValues, difference.field) ? sameFieldValue(difference.field, fieldValue(built, difference.field), planned.baseValues[difference.field]) : built.change !== "updated" && built.change !== "added";
|
|
722
|
+
}
|
|
723
|
+
function buildTouched(planned, built, field) {
|
|
724
|
+
return planned.baseValues && Object.hasOwn(planned.baseValues, field) ? !sameFieldValue(field, fieldValue(built, field), planned.baseValues[field]) : built.change === "updated" || built.change === "added";
|
|
725
|
+
}
|
|
726
|
+
function changedFields(before, after) {
|
|
727
|
+
const fields = [];
|
|
728
|
+
before.type !== after.type && fields.push("type"), before.name !== after.name && fields.push("name"), (before.description ?? "") !== (after.description ?? "") && fields.push("description");
|
|
729
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before.attributes ?? {}), ...Object.keys(after.attributes ?? {})]);
|
|
730
|
+
for (const key of keys)
|
|
731
|
+
sameValue(before.attributes?.[key], after.attributes?.[key]) || fields.push(`attributes.${key}`);
|
|
732
|
+
return fields;
|
|
733
|
+
}
|
|
734
|
+
function sameValue(a, b) {
|
|
735
|
+
return stableStringify(a) === stableStringify(b);
|
|
736
|
+
}
|
|
737
|
+
function stableStringify(value) {
|
|
738
|
+
return value === void 0 ? "undefined" : value === null || typeof value != "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(stableStringify).join(",")}]` : `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
|
|
739
|
+
}
|
|
740
|
+
function relKey(rel) {
|
|
741
|
+
return `${rel.source}::${rel.predicate}::${rel.target}`;
|
|
742
|
+
}
|
|
743
|
+
function strip(rel) {
|
|
744
|
+
return { source: rel.source, predicate: rel.predicate, target: rel.target };
|
|
745
|
+
}
|
|
746
|
+
function readRelationship(change, resolve) {
|
|
747
|
+
const data = change.type === "relationship-delete" ? change.before ?? change.after : change.after, [source, predicate, target] = data?.source && data.predicate && data.target ? [data.source, data.predicate, data.target] : change.elementId.split("::");
|
|
748
|
+
return !source || !predicate || !target ? null : { source: resolve(source), predicate, target: resolve(target) };
|
|
749
|
+
}
|
|
750
|
+
function idNameSegment(id) {
|
|
751
|
+
const parts = id.split(".");
|
|
752
|
+
return parts.length >= 3 ? parts.slice(2).join(".") : void 0;
|
|
753
|
+
}
|
|
754
|
+
function fromId(id, layerName) {
|
|
755
|
+
const parts = id.split(".");
|
|
756
|
+
return {
|
|
757
|
+
id,
|
|
758
|
+
layer: parts.length >= 3 ? parts[0] : layerName,
|
|
759
|
+
type: parts.length >= 3 ? parts[1] : "",
|
|
760
|
+
name: idNameSegment(id) ?? id,
|
|
761
|
+
attributes: {}
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function fromGraphElement(element) {
|
|
765
|
+
return {
|
|
766
|
+
id: element.id,
|
|
767
|
+
layer: element.layer,
|
|
768
|
+
type: element.type,
|
|
769
|
+
name: element.name,
|
|
770
|
+
...element.description !== void 0 ? { description: element.description } : {},
|
|
771
|
+
attributes: element.attributes ?? {},
|
|
772
|
+
...element.sourceReference ? { sourceReference: element.sourceReference } : {}
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
function toVerifyElement(record, layerName, id) {
|
|
776
|
+
const str = (value) => typeof value == "string" && value.length > 0 ? value : void 0, fallback = fromId(id, layerName);
|
|
777
|
+
return {
|
|
778
|
+
id,
|
|
779
|
+
layer: str(record.layer_id) ?? str(record.layer) ?? fallback.layer,
|
|
780
|
+
type: str(record.type) ?? fallback.type,
|
|
781
|
+
name: str(record.name) ?? fallback.name,
|
|
782
|
+
...str(record.description) !== void 0 ? { description: str(record.description) } : {},
|
|
783
|
+
attributes: record.attributes && typeof record.attributes == "object" ? record.attributes : {},
|
|
784
|
+
...stagedSourceReference(record) ? { sourceReference: stagedSourceReference(record) } : {}
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
function emptyCounts() {
|
|
788
|
+
return { planned: 0, matched: 0, probable: 0, missing: 0, divergent: 0, unplanned: 0, deletionsNotDone: 0, unverifiable: 0 };
|
|
789
|
+
}
|
|
790
|
+
function countCategories(planned, section) {
|
|
791
|
+
return {
|
|
792
|
+
planned,
|
|
793
|
+
matched: section.matched.length + section.divergent.length,
|
|
794
|
+
probable: [...section.matched, ...section.divergent].filter((m) => m.match === "probable").length,
|
|
795
|
+
missing: section.missing.length,
|
|
796
|
+
divergent: section.divergent.length,
|
|
797
|
+
unplanned: section.unplanned.length,
|
|
798
|
+
deletionsNotDone: section.deletionsNotDone.length,
|
|
799
|
+
unverifiable: section.unverifiable.length
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function decideVerdict(report) {
|
|
803
|
+
const e = report.summary.elements, r = report.summary.relationships;
|
|
804
|
+
if (e.planned === 0 && r.planned === 0)
|
|
805
|
+
return { verdict: "empty-plan", text: "The plan has no changes to verify." };
|
|
806
|
+
const parts = [], say = (n, what) => {
|
|
807
|
+
n > 0 && parts.push(`${n} ${what}`);
|
|
808
|
+
};
|
|
809
|
+
say(e.missing, plural(e.missing, "planned element", "planned elements") + " missing"), say(r.missing, plural(r.missing, "planned relationship", "planned relationships") + " missing"), say(e.deletionsNotDone + r.deletionsNotDone, "planned deletion(s) not done"), say(e.divergent, plural(e.divergent, "element differs", "elements differ")), say(r.divergent, plural(r.divergent, "relationship differs", "relationships differ")), say(e.unplanned, plural(e.unplanned, "unplanned element change", "unplanned element changes")), say(r.unplanned, plural(r.unplanned, "unplanned relationship", "unplanned relationships"));
|
|
810
|
+
const probable = e.probable + r.probable, probableText = probable > 0 ? ` ${probable} match(es) are probable, not exact; confirm them.` : "", unverifiable = e.unverifiable + r.unverifiable, unverifiableText = unverifiable > 0 ? ` ${[
|
|
811
|
+
e.unverifiable > 0 ? `${e.unverifiable} planned ${plural(e.unverifiable, "element", "elements")}` : "",
|
|
812
|
+
r.unverifiable > 0 ? `${r.unverifiable} planned ${plural(r.unverifiable, "relationship", "relationships")}` : ""
|
|
813
|
+
].filter(Boolean).join(" and ")} can't be seen by extraction (unverifiable, not counted): check them in the code or the review.` : "";
|
|
814
|
+
if (e.planned + r.planned === unverifiable && e.unplanned + r.unplanned === 0)
|
|
815
|
+
return {
|
|
816
|
+
verdict: "unverifiable",
|
|
817
|
+
text: `Nothing in the plan can be checked against extracted code.${unverifiableText}`
|
|
818
|
+
};
|
|
819
|
+
if (e.missing + r.missing + e.deletionsNotDone + r.deletionsNotDone > 0)
|
|
820
|
+
return { verdict: "incomplete", text: `The build is incomplete: ${parts.join(", ")}.${probableText}${unverifiableText}` };
|
|
821
|
+
if (e.divergent + r.divergent + e.unplanned + r.unplanned > 0)
|
|
822
|
+
return { verdict: "diverges", text: `The build diverges from the plan: ${parts.join(", ")}.${probableText}${unverifiableText}` };
|
|
823
|
+
const scope = unverifiable > 0 ? "Everything extraction can see was found." : "Everything planned was found.";
|
|
824
|
+
return probable > 0 ? { verdict: "probable-match", text: `${scope}${probableText}${unverifiableText}` } : {
|
|
825
|
+
verdict: "match",
|
|
826
|
+
text: unverifiable > 0 ? `The build matches what extraction can see of the plan.${unverifiableText}` : "The build matches the plan."
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function plural(n, one, many) {
|
|
830
|
+
return n === 1 ? one : many;
|
|
831
|
+
}
|
|
832
|
+
export {
|
|
833
|
+
LIFECYCLE_FIELDS,
|
|
834
|
+
LIFECYCLE_STAGES,
|
|
835
|
+
asBuiltShare,
|
|
836
|
+
asBuiltWarning,
|
|
837
|
+
builtFromBase,
|
|
838
|
+
builtFromGraph,
|
|
839
|
+
builtFromSnapshot,
|
|
840
|
+
isLifecycleMove,
|
|
841
|
+
normalizeName,
|
|
842
|
+
planFromChangeset,
|
|
843
|
+
planFromModel,
|
|
844
|
+
readExtractionRecord,
|
|
845
|
+
verifyChangeset,
|
|
846
|
+
verifyChangesetInModel
|
|
847
|
+
};
|
|
848
|
+
//# sourceMappingURL=changeset-verify.js.map
|