@documentation-robotics/cli 0.3.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 +10 -14
- package/dist/analyzers/active-analyzer.js +62 -0
- package/dist/analyzers/active-analyzer.js.map +7 -0
- package/dist/analyzers/cards/build.js +21 -1
- package/dist/analyzers/cards/build.js.map +2 -2
- package/dist/analyzers/cards/extract.js +138 -0
- package/dist/analyzers/cards/extract.js.map +7 -0
- package/dist/analyzers/cards/routes.js +96 -0
- package/dist/analyzers/cards/routes.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +76 -4
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +12 -6
- package/dist/analyzers/cbm-responses.js.map +2 -2
- package/dist/analyzers/code-changes.js +159 -0
- package/dist/analyzers/code-changes.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +6 -2
- package/dist/analyzers/edge-relationships.js.map +2 -2
- 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/cli.js +12 -10
- package/dist/cli.js.map +2 -2
- package/dist/coding-agents/drbot-mcp.js +120 -18
- package/dist/coding-agents/drbot-mcp.js.map +2 -2
- package/dist/commands/add.js +4 -2
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +35 -35
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/changeset-verify.js +16 -8
- package/dist/commands/changeset-verify.js.map +2 -2
- package/dist/commands/changeset.js +41 -11
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/init.js +9 -26
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +35 -31
- package/dist/commands/mcp.js.map +3 -3
- package/dist/commands/relationship.js +2 -12
- 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 +14 -13
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +28 -120
- package/dist/commands/upgrade.js.map +2 -2
- 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 +30 -12
- 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 +2 -1
- package/dist/core/catalog-migration.js.map +2 -2
- package/dist/core/changeset-base-index.js +7 -0
- package/dist/core/changeset-base-index.js.map +2 -2
- package/dist/core/changeset-brief-compact.js +256 -0
- package/dist/core/changeset-brief-compact.js.map +7 -0
- package/dist/core/changeset-verify.js +356 -26
- package/dist/core/changeset-verify.js.map +3 -3
- package/dist/core/extraction-observability.js +161 -0
- package/dist/core/extraction-observability.js.map +7 -0
- package/dist/core/migration-registry.js +13 -1
- package/dist/core/migration-registry.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-staging.js +94 -1
- package/dist/core/relationship-staging.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +83 -3
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staging-area.js +85 -8
- 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/generated/relationship-index.js +124 -15
- 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 +19 -5
- package/dist/integrations/claude_code/agents/dr-extractor.md +7 -1
- package/dist/integrations/claude_code/commands/dr-changeset.md +2 -1
- package/dist/integrations/claude_code/commands/dr-map.md +7 -1
- package/dist/integrations/claude_code/commands/dr-plan.md +42 -16
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +28 -31
- package/dist/integrations/claude_code/docs/README.md +20 -1
- 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_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +90 -31
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +5 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +5 -0
- package/dist/integrations/github_copilot/agents/dr-architect.md +2 -0
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +3 -0
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +88 -30
- package/dist/mcp/changeset-only.js +14 -3
- package/dist/mcp/changeset-only.js.map +2 -2
- 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 +39 -5
- package/dist/mcp/tool-registry.js.map +2 -2
- package/dist/mcp/tools/changeset-analysis-tools.js +25 -5
- package/dist/mcp/tools/changeset-analysis-tools.js.map +3 -3
- 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 +133 -57
- package/dist/mcp/tools/changeset-editing.js.map +3 -3
- package/dist/mcp/tools/changeset-lifecycle.js +1 -1
- package/dist/mcp/tools/changeset-lifecycle.js.map +2 -2
- package/dist/mcp/tools/changeset-verify.js +12 -6
- package/dist/mcp/tools/changeset-verify.js.map +2 -2
- 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 +15 -0
- package/dist/mcp/tools/index.js.map +2 -2
- package/dist/mcp/tools/model-add.js +189 -73
- package/dist/mcp/tools/model-add.js.map +2 -2
- package/dist/mcp/tools/model-update.js +16 -12
- 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 +89 -20
- package/dist/mcp/tools/relationship-tools.js.map +2 -2
- 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 +10 -2
- package/dist/mcp/tools/shared.js.map +2 -2
- 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 +22 -10
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +1 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +1 -1
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +3 -3
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +145 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +508 -237
- 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/telemetry/index.js +1 -1
- package/dist/utils/id-generator.js +5 -2
- package/dist/utils/id-generator.js.map +2 -2
- package/dist/utils/source-reference.js +58 -27
- 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/model-warnings.js +28 -0
- package/dist/validators/model-warnings.js.map +7 -0
- 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 +1 -1
- package/dist/viewer/assets/main-CxzcuU_o.js +0 -84
- package/dist/viewer/assets/main-j-7ULYpm.css +0 -1
|
@@ -2,6 +2,11 @@ import { Changeset } from "./changeset.js";
|
|
|
2
2
|
import { ChangesetGraph } from "./changeset-graph.js";
|
|
3
3
|
import { tryReadBaseIndex } from "./changeset-base-index.js";
|
|
4
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";
|
|
5
10
|
const LIFECYCLE_FIELDS = ["attributes.status"], LIFECYCLE_STAGES = [
|
|
6
11
|
["proposed", "draft"],
|
|
7
12
|
["planned", "under-review"],
|
|
@@ -21,6 +26,37 @@ function isLifecycleMove(difference) {
|
|
|
21
26
|
const from = lifecycleStage(difference.planned), to = lifecycleStage(difference.built);
|
|
22
27
|
return from >= 0 && to > from;
|
|
23
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
|
+
}
|
|
24
60
|
function planFromChangeset(changeset, context = {}) {
|
|
25
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));
|
|
26
62
|
for (const { change, index } of changes) {
|
|
@@ -84,9 +120,17 @@ async function planFromModel(model, changeset) {
|
|
|
84
120
|
function builtFromGraph(graph, label = graph.changeset.name) {
|
|
85
121
|
const elements = [], changedElements = [];
|
|
86
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
|
+
}
|
|
87
129
|
const built = {
|
|
88
|
-
...
|
|
89
|
-
...element.status !== "unchanged" ? { change: element.status } : {}
|
|
130
|
+
...current,
|
|
131
|
+
...element.status !== "unchanged" ? { change: element.status } : {},
|
|
132
|
+
...staged ? { stagedProvenance: element.sourceReference?.provenance ?? "none" } : {},
|
|
133
|
+
...updatedFrom ? { updatedFrom } : {}
|
|
90
134
|
};
|
|
91
135
|
element.status !== "deleted" && elements.push(built), element.status !== "unchanged" && changedElements.push(built);
|
|
92
136
|
}
|
|
@@ -143,9 +187,29 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
143
187
|
verdict: "match",
|
|
144
188
|
verdictText: "",
|
|
145
189
|
summary: { elements: emptyCounts(), relationships: emptyCounts() },
|
|
146
|
-
elements: { matched: [], divergent: [], missing: [], unplanned: [], deletionsNotDone: [] },
|
|
147
|
-
relationships: { matched: [], divergent: [], missing: [], unplanned: [], deletionsNotDone: [] },
|
|
190
|
+
elements: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
|
|
191
|
+
relationships: { matched: [], divergent: [], missing: [], unverifiable: [], unplanned: [], deletionsNotDone: [] },
|
|
148
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;
|
|
149
213
|
}, live = plan.elements.filter((e) => e.op !== "delete"), deletions = plan.elements.filter((e) => e.op === "delete");
|
|
150
214
|
for (const planned of live)
|
|
151
215
|
builtById.has(planned.id) && (claimed.add(planned.id), elementMap.set(planned.id, { built: planned.id, match: "exact" }));
|
|
@@ -219,6 +283,8 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
219
283
|
for (const planned of live) {
|
|
220
284
|
const mapping = elementMap.get(planned.id);
|
|
221
285
|
if (!mapping) {
|
|
286
|
+
if (markUnverifiable(planned))
|
|
287
|
+
continue;
|
|
222
288
|
const candidates = ambiguous.get(planned.id);
|
|
223
289
|
report.elements.missing.push({
|
|
224
290
|
planned: planned.id,
|
|
@@ -226,21 +292,39 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
226
292
|
type: planned.type,
|
|
227
293
|
name: planned.name,
|
|
228
294
|
op: planned.op,
|
|
229
|
-
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" : "not found in the build by id, name or source file",
|
|
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",
|
|
230
296
|
...candidates ? { candidates } : {}
|
|
231
297
|
});
|
|
232
298
|
continue;
|
|
233
299
|
}
|
|
234
|
-
const builtElement = builtById.get(mapping.built)
|
|
300
|
+
const builtElement = builtById.get(mapping.built);
|
|
301
|
+
let fields = compareElements(planned, builtElement, planned.op === "update" ? planned.changedFields : void 0);
|
|
302
|
+
const match = {
|
|
235
303
|
planned: planned.id,
|
|
236
304
|
built: mapping.built,
|
|
237
305
|
op: planned.op,
|
|
238
306
|
match: mapping.match,
|
|
239
307
|
...mapping.reason ? { reason: mapping.reason } : {}
|
|
240
|
-
};
|
|
241
|
-
if (planned.op === "update" && planned.changedFields && planned.changedFields.length > 0) {
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
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;
|
|
244
328
|
report.elements.missing.push({
|
|
245
329
|
planned: planned.id,
|
|
246
330
|
layer: planned.layer,
|
|
@@ -253,18 +337,27 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
253
337
|
continue;
|
|
254
338
|
}
|
|
255
339
|
}
|
|
256
|
-
const lifecycle = fields.filter(isLifecycleMove), differences = fields.filter((f) => !isLifecycleMove(f));
|
|
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));
|
|
257
341
|
if (lifecycle.length > 0) {
|
|
258
342
|
match.lifecycle = lifecycle;
|
|
259
343
|
for (const f of lifecycle)
|
|
260
344
|
lifecycleMoves.push(`${planned.id} ${f.field} ${String(f.planned)} \u2192 ${String(f.built)}`);
|
|
261
345
|
}
|
|
262
|
-
differences.length > 0 ? report.elements.divergent.push({ ...match, fields: differences }) : report.elements.matched.push(match);
|
|
346
|
+
informational.length > 0 && (match.notes = informational), differences.length > 0 ? report.elements.divergent.push({ ...match, fields: differences }) : report.elements.matched.push(match);
|
|
263
347
|
}
|
|
264
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.`);
|
|
265
353
|
for (const planned of deletions) {
|
|
266
354
|
const still = builtById.get(planned.id);
|
|
267
|
-
|
|
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" });
|
|
268
361
|
}
|
|
269
362
|
if (built.changedElements)
|
|
270
363
|
for (const element of built.changedElements)
|
|
@@ -275,7 +368,25 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
275
368
|
name: element.name,
|
|
276
369
|
change: element.change ?? "added"
|
|
277
370
|
});
|
|
278
|
-
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)),
|
|
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) => ({
|
|
279
390
|
source: mapId(planned.source),
|
|
280
391
|
predicate: planned.predicate,
|
|
281
392
|
target: mapId(planned.target)
|
|
@@ -291,7 +402,12 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
291
402
|
}
|
|
292
403
|
claimedRels.add(relKey(found));
|
|
293
404
|
const plannedRef = strip(planned);
|
|
294
|
-
planned.op === "delete"
|
|
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) });
|
|
295
411
|
}
|
|
296
412
|
const addedBuiltIds = new Set(live.filter((e) => e.op === "add" && elementMap.has(e.id)).map((e) => elementMap.get(e.id).built));
|
|
297
413
|
for (const planned of unresolved) {
|
|
@@ -323,6 +439,8 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
323
439
|
});
|
|
324
440
|
continue;
|
|
325
441
|
}
|
|
442
|
+
if (pushUnverifiable(planned))
|
|
443
|
+
continue;
|
|
326
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));
|
|
327
445
|
report.relationships.missing.push({
|
|
328
446
|
planned: plannedRef,
|
|
@@ -343,14 +461,187 @@ function verifyChangeset(plan, built, options = {}) {
|
|
|
343
461
|
async function verifyChangesetInModel(model, options) {
|
|
344
462
|
const { planned } = options, plan = await planFromModel(model, planned), notes = [], baseIndex = await tryReadBaseIndex(model.rootPath, planned.id);
|
|
345
463
|
baseIndex.index && (plan.baseElementIds = Object.keys(baseIndex.index.elements)), baseIndex.warning && notes.push(baseIndex.warning);
|
|
346
|
-
let built;
|
|
464
|
+
let built, extraction, observability, warning;
|
|
347
465
|
if ("changeset" in options.against) {
|
|
348
466
|
const asBuilt = options.against.changeset;
|
|
349
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);
|
|
350
482
|
} else
|
|
351
|
-
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).")
|
|
352
|
-
|
|
353
|
-
|
|
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).";
|
|
354
645
|
}
|
|
355
646
|
function normalizeName(name) {
|
|
356
647
|
return name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
@@ -386,7 +677,7 @@ function compareElements(planned, built, removedBy) {
|
|
|
386
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 });
|
|
387
678
|
for (const [key, value] of Object.entries(planned.attributes ?? {})) {
|
|
388
679
|
const builtValue = built.attributes?.[key];
|
|
389
|
-
|
|
680
|
+
sameAttribute(key, value, builtValue) || fields.push({ field: `attributes.${key}`, planned: value, built: builtValue });
|
|
390
681
|
}
|
|
391
682
|
for (const field of removedBy ?? [])
|
|
392
683
|
if (field === "description")
|
|
@@ -395,7 +686,7 @@ function compareElements(planned, built, removedBy) {
|
|
|
395
686
|
const key = field.slice(11);
|
|
396
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] });
|
|
397
688
|
}
|
|
398
|
-
return fields;
|
|
689
|
+
return fields.map((f) => ({ field: f.field, planned: f.planned ?? null, built: f.built ?? null }));
|
|
399
690
|
}
|
|
400
691
|
function fieldValue(element, field) {
|
|
401
692
|
if (field === "type")
|
|
@@ -408,7 +699,26 @@ function fieldValue(element, field) {
|
|
|
408
699
|
return element.attributes?.[field.slice(11)];
|
|
409
700
|
}
|
|
410
701
|
function sameFieldValue(field, a, b) {
|
|
411
|
-
return field === "type" || field === "name" ? normalizeName(String(a ?? "")) === normalizeName(String(b ?? "")) : field === "description" ? String(a ?? "").trim() === String(b ?? "").trim() : sameValue(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";
|
|
412
722
|
}
|
|
413
723
|
function buildTouched(planned, built, field) {
|
|
414
724
|
return planned.baseValues && Object.hasOwn(planned.baseValues, field) ? !sameFieldValue(field, fieldValue(built, field), planned.baseValues[field]) : built.change === "updated" || built.change === "added";
|
|
@@ -475,7 +785,7 @@ function toVerifyElement(record, layerName, id) {
|
|
|
475
785
|
};
|
|
476
786
|
}
|
|
477
787
|
function emptyCounts() {
|
|
478
|
-
return { planned: 0, matched: 0, probable: 0, missing: 0, divergent: 0, unplanned: 0, deletionsNotDone: 0 };
|
|
788
|
+
return { planned: 0, matched: 0, probable: 0, missing: 0, divergent: 0, unplanned: 0, deletionsNotDone: 0, unverifiable: 0 };
|
|
479
789
|
}
|
|
480
790
|
function countCategories(planned, section) {
|
|
481
791
|
return {
|
|
@@ -485,7 +795,8 @@ function countCategories(planned, section) {
|
|
|
485
795
|
missing: section.missing.length,
|
|
486
796
|
divergent: section.divergent.length,
|
|
487
797
|
unplanned: section.unplanned.length,
|
|
488
|
-
deletionsNotDone: section.deletionsNotDone.length
|
|
798
|
+
deletionsNotDone: section.deletionsNotDone.length,
|
|
799
|
+
unverifiable: section.unverifiable.length
|
|
489
800
|
};
|
|
490
801
|
}
|
|
491
802
|
function decideVerdict(report) {
|
|
@@ -496,8 +807,24 @@ function decideVerdict(report) {
|
|
|
496
807
|
n > 0 && parts.push(`${n} ${what}`);
|
|
497
808
|
};
|
|
498
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"));
|
|
499
|
-
const probable = e.probable + r.probable, probableText = probable > 0 ? ` ${probable} match(es) are probable, not exact; confirm them.` : ""
|
|
500
|
-
|
|
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
|
+
};
|
|
501
828
|
}
|
|
502
829
|
function plural(n, one, many) {
|
|
503
830
|
return n === 1 ? one : many;
|
|
@@ -505,6 +832,8 @@ function plural(n, one, many) {
|
|
|
505
832
|
export {
|
|
506
833
|
LIFECYCLE_FIELDS,
|
|
507
834
|
LIFECYCLE_STAGES,
|
|
835
|
+
asBuiltShare,
|
|
836
|
+
asBuiltWarning,
|
|
508
837
|
builtFromBase,
|
|
509
838
|
builtFromGraph,
|
|
510
839
|
builtFromSnapshot,
|
|
@@ -512,6 +841,7 @@ export {
|
|
|
512
841
|
normalizeName,
|
|
513
842
|
planFromChangeset,
|
|
514
843
|
planFromModel,
|
|
844
|
+
readExtractionRecord,
|
|
515
845
|
verifyChangeset,
|
|
516
846
|
verifyChangesetInModel
|
|
517
847
|
};
|