dsh-composition-doctor 0.2.2 → 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/dist/client.js CHANGED
@@ -37,6 +37,39 @@ window.__ModuleLoader__.load({
37
37
  // src/client/report-view.ts
38
38
  var import_react = require("react");
39
39
 
40
+ // src/reports/failure.ts
41
+ function renderFailure(result, markdown = false) {
42
+ const lines = [
43
+ markdown ? "# DSH Composition Doctor \u2014 Failure Explanation" : "DSH Composition Doctor \u2014 Failure Explanation",
44
+ "",
45
+ `Profile: ${result.profile}; runtime: ${result.runtime}`,
46
+ ""
47
+ ];
48
+ if (result.explanations.length === 0) lines.push(result.outcome === "no-match" ? "No supported failure signature matched. Cause: unknown." : result.outcome === "unknown" ? "Selected metadata could not be fully inspected. Cause: unknown." : "No supported composition issue was observed. Runtime health: unknown.", "");
49
+ for (const item of result.explanations.slice(0, markdown ? 50 : 3)) {
50
+ lines.push(
51
+ `${markdown ? "## " : ""}${item.detectedFailure}`,
52
+ `${item.origin}; evidence: ${item.evidenceLevel}`,
53
+ "",
54
+ `Cause: ${item.observableCause}`,
55
+ ""
56
+ );
57
+ for (const [index, path] of item.provenancePaths.entries()) {
58
+ lines.push(
59
+ `Path ${index + 1}: ${path.packageName === void 0 ? "" : `bundle ${path.packageName} \u2192 `}${path.source}`,
60
+ ` ${path.layer === void 0 ? "layer: unknown" : `layer: ${path.layer}`}${path.rowId === void 0 ? "" : ` \u2192 row: ${path.rowId}`}`
61
+ );
62
+ if (path.provenance.length > (markdown ? 0 : 1)) lines.push(` provenance: ${path.provenance.map((step) => `${step.relation}: ${step.source} (${step.basis})`).join(" \u2192 ")}`);
63
+ }
64
+ const unknown = markdown ? item.unknowns.join(" ") : `runtime not observed / not-run${item.evidenceLevel === "static" ? "; final composition unknown" : ""}${item.causeStatus === "unknown" ? "; cause unproven" : ""}.`;
65
+ lines.push("", `Unknown: ${unknown}`, `Next: ${item.manualRemediation}`, "");
66
+ }
67
+ if (!markdown && result.explanations.length > 3) lines.push(`${result.explanations.length - 3} more explanation(s); use --format json or markdown.`, "");
68
+ if (markdown || result.explanations.length === 0 || result.input.log === "not-provided") lines.push(result.nextStep);
69
+ lines.push("Doctor did not modify your profile.", "");
70
+ return lines.join("\n");
71
+ }
72
+
40
73
  // src/reports/markdown.ts
41
74
  function formatEvidence(item) {
42
75
  const subject = item.subject === void 0 ? "" : ` \u2014 ${item.subject}`;
@@ -56,7 +89,9 @@ window.__ModuleLoader__.load({
56
89
  }
57
90
  function renderMarkdown(report) {
58
91
  const summary = ["error", "warning", "info"].map((severity) => `${severity}: ${report.diagnostics.filter((item) => item.severity === severity).length}`).join(", ");
59
- const body = report.diagnostics.length === 0 ? "No diagnostics were produced." : report.diagnostics.map(formatDiagnostic).join("\n\n");
92
+ const diagnosticBody = report.diagnostics.length === 0 ? "No diagnostics were produced." : report.diagnostics.map(formatDiagnostic).join("\n\n");
93
+ const body = report.failureExplanation === void 0 ? diagnosticBody : `${renderFailure(report.failureExplanation, true)}
94
+ ${diagnosticBody}`;
60
95
  const coverage = report.metadataCoverage === void 0 ? "" : `
61
96
  Metadata coverage: ${report.metadataCoverage.mode}; unscanned: ${report.metadataCoverage.unscannedSurfaces.join("; ")}
62
97
  `;
@@ -106,7 +141,48 @@ window.__ModuleLoader__.load({
106
141
  function toReportViewModel(report) {
107
142
  const counts = { info: 0, warning: 0, error: 0 };
108
143
  for (const diagnostic of report.diagnostics) counts[diagnostic.severity] += 1;
109
- return { report, counts, graph: buildConflictGraph(report) };
144
+ return {
145
+ report,
146
+ counts,
147
+ graph: buildConflictGraph(report),
148
+ compositionGraph: { nodes: report.compositionFacts?.nodes ?? [], edges: report.compositionFacts?.edges ?? [] }
149
+ };
150
+ }
151
+ function compositionNodeDetail(report, nodeId) {
152
+ const facts = report.compositionFacts;
153
+ const node = facts?.nodes.find((candidate) => candidate.id === nodeId);
154
+ if (facts === void 0 || node === void 0) return void 0;
155
+ const relatedRows = facts.rows.filter((candidate) => {
156
+ if (node.rowKey !== void 0) return candidate.key === node.rowKey;
157
+ if (node.entity === "bundle") return candidate.packageName === node.packageName || candidate.provenance.some((step) => step.source === node.label);
158
+ if (node.entity === "source") return candidate.source === node.source || candidate.provenance.some((step) => step.source === node.source);
159
+ if (node.entity === "layer") return candidate.layer === node.layer && (node.layerOrder === void 0 || candidate.layerOrder === node.layerOrder);
160
+ if (node.entity === "diagnostic") return node.diagnosticId !== void 0 && candidate.relatedDiagnosticIds.includes(node.diagnosticId);
161
+ return false;
162
+ });
163
+ const fields = [
164
+ { label: "entity", value: node.entity },
165
+ { label: "source", value: node.source ?? node.label },
166
+ { label: "evidence", value: node.evidenceMode ?? "unknown" }
167
+ ];
168
+ if (node.layer !== void 0) fields.push({ label: "layer", value: node.layer });
169
+ if (node.layerOrder !== void 0) fields.push({ label: "layer order", value: String(node.layerOrder) });
170
+ if (node.packageName !== void 0) fields.push({ label: "package", value: `${node.packageName}${node.packageVersion === void 0 ? "" : `@${node.packageVersion}`}` });
171
+ if (relatedRows.length > 0) {
172
+ fields.push({ label: "related rows", value: relatedRows.map((row) => row.id ?? row.name ?? row.key).join(", ") });
173
+ fields.push({ label: "config keys", value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.configKeys.length === 0 ? "(none observed)" : row.configKeys.join(", ")}`).join("\n") });
174
+ fields.push({ label: "replacement", value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.replacement.state} (${row.replacement.basis})`).join("\n") });
175
+ fields.push({ label: "provenance", value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.provenance.length === 0 ? "unknown" : row.provenance.map((item) => `${item.relation}: ${item.source} (${item.basis})`).join(" \u2192 ")}`).join("\n") });
176
+ }
177
+ const relatedDiagnosticIds = new Set(relatedRows.flatMap((row) => row.relatedDiagnosticIds));
178
+ const rowDiagnostics = report.diagnostics.filter((item) => relatedDiagnosticIds.has(item.id));
179
+ const nodeDiagnostic = node.diagnosticId === void 0 ? [] : report.diagnostics.filter((item) => item.id === node.diagnosticId);
180
+ return {
181
+ title: node.label,
182
+ fields,
183
+ diagnostics: [...new Map([...rowDiagnostics, ...nodeDiagnostic].map((item) => [item.id, item])).values()],
184
+ unknown: [.../* @__PURE__ */ new Set([...relatedRows.flatMap((row) => row.unknown), ...facts.unknownSurfaces])]
185
+ };
110
186
  }
111
187
  function renderConflictGraph(document, graph) {
112
188
  const section = document.createElement("section");
@@ -130,6 +206,137 @@ window.__ModuleLoader__.load({
130
206
  section.append(list);
131
207
  return section;
132
208
  }
209
+ function createFailureExplanation(document, report) {
210
+ const section = document.createElement("section");
211
+ section.dataset.failureExplanation = "true";
212
+ const heading = document.createElement("h3");
213
+ heading.textContent = "Failure Explanation";
214
+ section.append(heading);
215
+ const result = report.failureExplanation;
216
+ if (result === void 0) {
217
+ const message = document.createElement("p");
218
+ message.textContent = "No failure explanation in this report. Run dsh-doctor diagnose --profile <dir> --report-dir <doctor-reports> to create one.";
219
+ section.append(message);
220
+ return section;
221
+ }
222
+ const status = document.createElement("p");
223
+ status.textContent = `Outcome: ${result.outcome}; runtime: ${result.runtime}. Log signatures are reported claims, not composition evidence.`;
224
+ section.append(status);
225
+ for (const explanation of result.explanations) {
226
+ const details = document.createElement("details");
227
+ const summary = document.createElement("summary");
228
+ summary.textContent = `${explanation.detectedFailure} (${explanation.origin}; ${explanation.evidenceLevel})`;
229
+ const cause = document.createElement("p");
230
+ cause.textContent = explanation.observableCause;
231
+ const paths = document.createElement("pre");
232
+ paths.textContent = explanation.provenancePaths.map((path) => `${path.packageName ?? "profile"} \u2192 ${path.source} \u2192 ${path.layer ?? "unknown layer"} \u2192 ${path.rowId ?? "unknown row"}
233
+ ${path.provenance.map((step) => `${step.relation}: ${step.source} (${step.basis})`).join(" \u2192 ")}`).join("\n\n");
234
+ const unknown = document.createElement("p");
235
+ unknown.textContent = `Unknown: ${explanation.unknowns.join(" ")}`;
236
+ const diagnostics = document.createElement("p");
237
+ diagnostics.textContent = `Related diagnostics: ${explanation.relatedDiagnostics.map((item) => item.id).join(", ") || "none observed"}`;
238
+ const remediation = document.createElement("p");
239
+ remediation.textContent = `Manual next step: ${explanation.manualRemediation}`;
240
+ details.append(summary, cause, paths, diagnostics, unknown, remediation);
241
+ section.append(details);
242
+ }
243
+ const next = document.createElement("p");
244
+ next.textContent = `${result.nextStep} Doctor did not modify your profile.`;
245
+ section.append(next);
246
+ return section;
247
+ }
248
+ function createCompositionExplorer(document, report) {
249
+ const section = document.createElement("section");
250
+ section.setAttribute("aria-labelledby", "dsh-composition-doctor-explorer");
251
+ const heading = document.createElement("h3");
252
+ heading.id = "dsh-composition-doctor-explorer";
253
+ heading.textContent = "Composition Explorer";
254
+ section.append(heading);
255
+ const facts = report.compositionFacts;
256
+ const graph = facts === void 0 ? { nodes: [], edges: [] } : { nodes: facts.nodes, edges: facts.edges };
257
+ if (graph.nodes.length === 0) {
258
+ const message = document.createElement("p");
259
+ message.textContent = "Composition provenance is unavailable in this legacy report.";
260
+ section.append(message);
261
+ } else {
262
+ const list = document.createElement("ul");
263
+ for (const node of graph.nodes.filter((value) => value.entity !== "diagnostic")) {
264
+ const item = document.createElement("li");
265
+ const select = button(document, `${node.entity}: ${node.label}`, () => {
266
+ const detail2 = compositionNodeDetail(report, node.id);
267
+ if (detail2 === void 0) return;
268
+ const panel = section.querySelector("[data-composition-detail]");
269
+ if (panel === null) return;
270
+ panel.replaceChildren();
271
+ const title = document.createElement("h4");
272
+ title.textContent = detail2.title;
273
+ panel.append(title);
274
+ const fields = document.createElement("dl");
275
+ for (const field of detail2.fields) {
276
+ const term = document.createElement("dt");
277
+ term.textContent = field.label;
278
+ const value = document.createElement("dd");
279
+ value.textContent = field.value;
280
+ fields.append(term, value);
281
+ }
282
+ panel.append(fields);
283
+ const diagnostics = document.createElement("ul");
284
+ for (const diagnostic of detail2.diagnostics) {
285
+ const entry = document.createElement("li");
286
+ entry.textContent = `[${diagnostic.severity}] ${diagnostic.title}: ${diagnostic.explanation}`;
287
+ diagnostics.append(entry);
288
+ }
289
+ if (detail2.diagnostics.length > 0) panel.append(diagnostics);
290
+ const unknown = document.createElement("ul");
291
+ for (const value of detail2.unknown) {
292
+ const entry = document.createElement("li");
293
+ entry.textContent = value;
294
+ unknown.append(entry);
295
+ }
296
+ if (detail2.unknown.length > 0) {
297
+ const unknownTitle2 = document.createElement("h4");
298
+ unknownTitle2.textContent = "Unknown / not modelled";
299
+ panel.append(unknownTitle2, unknown);
300
+ }
301
+ });
302
+ select.dataset.compositionNode = node.id;
303
+ if (report.failureExplanation?.explanations.some((item2) => item2.provenancePaths.some((path) => path.rowKey === node.rowKey && node.rowKey !== void 0 || path.packageName === node.packageName && node.packageName !== void 0))) {
304
+ select.dataset.failureRelated = "true";
305
+ select.textContent = `${node.entity}: ${node.label} [failure evidence]`;
306
+ }
307
+ item.append(select);
308
+ const outgoing = graph.edges.filter((edge) => edge.from === node.id);
309
+ if (outgoing.length > 0) {
310
+ const relationships = document.createElement("ul");
311
+ for (const edge of outgoing) {
312
+ const target = graph.nodes.find((candidate) => candidate.id === edge.to);
313
+ if (target === void 0 || target.entity === "diagnostic") continue;
314
+ const relationship = document.createElement("li");
315
+ relationship.textContent = `${edge.relation} \u2192 ${target.label} (${edge.basis}; ${edge.evidenceMode})`;
316
+ relationships.append(relationship);
317
+ }
318
+ item.append(relationships);
319
+ }
320
+ list.append(item);
321
+ }
322
+ section.append(list);
323
+ const detail = document.createElement("section");
324
+ detail.dataset.compositionDetail = "true";
325
+ detail.setAttribute("aria-live", "polite");
326
+ detail.textContent = "Select a bundle, source, layer, or row to inspect its evidence.";
327
+ section.append(detail);
328
+ const unknownList = document.createElement("ul");
329
+ for (const value of facts?.unknownSurfaces ?? []) {
330
+ const entry = document.createElement("li");
331
+ entry.textContent = value;
332
+ unknownList.append(entry);
333
+ }
334
+ const unknownTitle = document.createElement("h4");
335
+ unknownTitle.textContent = "Other surfaces";
336
+ section.append(unknownTitle, unknownList);
337
+ }
338
+ return section;
339
+ }
133
340
  function isReport(value) {
134
341
  if (value === null || typeof value !== "object") return false;
135
342
  const candidate = value;
@@ -220,6 +427,8 @@ window.__ModuleLoader__.load({
220
427
  status.textContent = `Generated: ${report.generatedAt}. Profile: ${report.profileDir}. Diagnostics: ${model.counts.error} error, ${model.counts.warning} warning, ${model.counts.info} info.`;
221
428
  status.append(document.createTextNode(" "), badge);
222
429
  root.append(renderConflictGraph(document, model.graph));
430
+ root.append(createFailureExplanation(document, report));
431
+ root.append(createCompositionExplorer(document, report));
223
432
  const list = document.createElement("ul");
224
433
  for (const diagnostic of report.diagnostics) {
225
434
  const item = document.createElement("li");
@@ -3,12 +3,12 @@ import { inspectInstalledPackage } from './package-provenance.js';
3
3
  function record(value) {
4
4
  return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
5
5
  }
6
- function diagnostic(id, title, source, detail) {
6
+ function diagnostic(id, title, source, detail, packageName) {
7
7
  return {
8
8
  id,
9
9
  severity: 'warning',
10
10
  title,
11
- evidence: [{ source, detail, evidenceKind: 'static' }],
11
+ evidence: [{ source, detail, evidenceKind: 'static', ...(packageName === undefined ? {} : { packageName, subject: packageName }) }],
12
12
  explanation: detail,
13
13
  remediation: 'Install the declared bundle in the selected profile or provide the missing public manifest/patch metadata.'
14
14
  };
@@ -32,7 +32,10 @@ function bundleNames(manifest) {
32
32
  export async function inspectBundleInventory(profileDir, packageJsonText) {
33
33
  let manifest;
34
34
  try {
35
- manifest = JSON.parse(packageJsonText);
35
+ const parsed = record(JSON.parse(packageJsonText));
36
+ if (parsed === undefined)
37
+ throw new Error('Manifest must be an object');
38
+ manifest = parsed;
36
39
  }
37
40
  catch {
38
41
  return { packages: [], diagnostics: [diagnostic('bundle-inventory-manifest-invalid', 'Profile package manifest is invalid', 'package.json', 'The profile package.json could not be parsed, so dsh.profile.bundles cannot be inspected.')] };
@@ -48,18 +51,18 @@ export async function inspectBundleInventory(profileDir, packageJsonText) {
48
51
  await access(fact.bundlePatch);
49
52
  }
50
53
  catch {
51
- diagnostics.push(diagnostic('bundle-patch-missing', 'Bundle patch metadata is missing', fact.packageJsonSource, `${declared.name} declares dsh.bundle.patch at ${fact.bundlePatch}, but that file is not readable.`));
54
+ diagnostics.push(diagnostic('bundle-patch-missing', 'Bundle patch metadata is missing', fact.packageJsonSource, 'The declared dsh.bundle.patch file is not readable.', declared.name));
52
55
  }
53
56
  }
54
57
  else {
55
- diagnostics.push(diagnostic('bundle-patch-unavailable', 'Bundle patch metadata is unavailable', fact.packageJsonSource, `${declared.name} has no readable dsh.bundle.patch declaration.`));
58
+ diagnostics.push(diagnostic('bundle-patch-unavailable', 'Bundle patch metadata is unavailable', fact.packageJsonSource, 'The bundle has no readable dsh.bundle.patch declaration.', declared.name));
56
59
  }
57
60
  if (fact.integrity === undefined && fact.gitRef === undefined) {
58
61
  diagnostics.push(diagnostic('bundle-provenance-unavailable', 'Bundle immutable provenance is unavailable', fact.packageJsonSource, `${declared.name} has neither registry integrity nor a fixed Git reference in its readable manifest.`));
59
62
  }
60
63
  }
61
64
  catch (error) {
62
- diagnostics.push(diagnostic('bundle-not-installed', 'Declared bundle is not installed or readable', 'package.json', `${declared.name}${declared.requestedSpec === undefined ? '' : `@${declared.requestedSpec}`} could not be resolved from the selected profile: ${error instanceof Error ? error.message : String(error)}`));
65
+ diagnostics.push(diagnostic('bundle-not-installed', 'Declared bundle is not installed or readable', 'package.json', 'The declared bundle could not be resolved inside the selected profile.', /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_.-]+$/i.test(declared.name) ? declared.name : undefined));
63
66
  }
64
67
  }
65
68
  return { packages, diagnostics };
@@ -1,3 +1,214 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { basename, isAbsolute, relative, win32 } from 'node:path';
3
+ const unknownSurfaces = [
4
+ 'route ownership: not observed / not modelled',
5
+ 'slot ownership: not observed / not modelled',
6
+ 'runtime hook ownership: not observed / not modelled',
7
+ 'arbitrary dependencies: not modelled',
8
+ 'possible dependents: not modelled'
9
+ ];
10
+ function stableKey(value) {
11
+ return createHash('sha256').update(value).digest('hex').slice(0, 16);
12
+ }
13
+ /** Replace profile and external absolute paths before any composition fact is serialized. */
14
+ export function normalizeCompositionSource(source, profileDir) {
15
+ if (source.startsWith('<') && source.endsWith('>'))
16
+ return source;
17
+ const windowsAbsolute = /^[a-z]:[\\/]/i.test(source) || source.startsWith('\\\\');
18
+ const posixAbsolute = source.startsWith('/');
19
+ if (!windowsAbsolute && !posixAbsolute && !isAbsolute(source))
20
+ return source.replaceAll('\\', '/');
21
+ try {
22
+ if (windowsAbsolute || /^[a-z]:[\\/]/i.test(profileDir)) {
23
+ const relativePath = win32.relative(win32.resolve(profileDir), win32.resolve(source)).replaceAll('\\', '/');
24
+ if (relativePath === '')
25
+ return '<PROFILE>';
26
+ if (relativePath !== '..' && !relativePath.startsWith('../') && !/^[a-z]:/i.test(relativePath))
27
+ return `<PROFILE>/${relativePath}`;
28
+ }
29
+ else {
30
+ const relativePath = relative(profileDir, source).replaceAll('\\', '/');
31
+ if (relativePath === '')
32
+ return '<PROFILE>';
33
+ if (relativePath !== '..' && !relativePath.startsWith('../') && !isAbsolute(relativePath))
34
+ return `<PROFILE>/${relativePath}`;
35
+ }
36
+ }
37
+ catch { /* malformed paths are reduced to a basename below */ }
38
+ return `<EXTERNAL>/${basename(source.replaceAll('\\', '/'))}`;
39
+ }
40
+ function rowConfigKeys(row) {
41
+ if (row.configKeys !== undefined)
42
+ return [...new Set(row.configKeys)].sort();
43
+ if (row.config !== null && typeof row.config === 'object' && !Array.isArray(row.config))
44
+ return Object.keys(row.config).sort();
45
+ return [];
46
+ }
47
+ function matchingPackages(model, sources) {
48
+ const labels = new Set(sources);
49
+ return (model.installedPackages ?? []).filter((fact) => labels.has(fact.name));
50
+ }
51
+ function relatedDiagnostics(row, packageNames, diagnostics, profileDir) {
52
+ const sources = new Set([row.source, ...(row.provenance ?? [])].map((source) => normalizeCompositionSource(source, profileDir)));
53
+ return diagnostics.filter((diagnostic) => diagnostic.evidence.some((item) => (row.id !== undefined && item.subject === row.id) ||
54
+ packageNames.includes(item.packageName ?? '') ||
55
+ sources.has(item.source))).map((item) => item.id).sort();
56
+ }
57
+ export function buildCompositionFacts(model, diagnostics) {
58
+ const nodes = new Map();
59
+ const edgeMap = new Map();
60
+ const rows = [];
61
+ const keyOccurrences = new Map();
62
+ const packages = model.installedPackages ?? [];
63
+ const reportMode = model.evidenceMode ?? 'static';
64
+ const addNode = (node) => nodes.set(node.id, node);
65
+ const addEdge = (edge) => edgeMap.set(`${edge.from}\0${edge.relation}\0${edge.to}`, edge);
66
+ for (const packageFact of packages) {
67
+ const bundleId = `bundle:${encodeURIComponent(packageFact.name)}`;
68
+ addNode({ id: bundleId, entity: 'bundle', label: packageFact.name, source: normalizeCompositionSource(packageFact.packageJsonSource, model.profileDir), packageName: packageFact.name, ...(packageFact.installedVersion === undefined ? {} : { packageVersion: packageFact.installedVersion }), evidenceMode: 'static' });
69
+ }
70
+ for (const [index, row] of model.rows.entries()) {
71
+ const rowIdentity = row.id ?? row.name ?? `${normalizeCompositionSource(row.source, model.profileDir)}:${index}`;
72
+ const keyBase = `row:${stableKey(`${rowIdentity}|${normalizeCompositionSource(row.source, model.profileDir)}|${row.layer ?? ''}|${row.layerOrder ?? index}`)}`;
73
+ const occurrence = keyOccurrences.get(keyBase) ?? 0;
74
+ keyOccurrences.set(keyBase, occurrence + 1);
75
+ const rowKey = occurrence === 0 ? keyBase : `${keyBase}:${occurrence}`;
76
+ const chain = row.provenance === undefined ? [] : [...row.provenance];
77
+ const chainSources = chain.map((source) => normalizeCompositionSource(source, model.profileDir));
78
+ const source = normalizeCompositionSource(row.source, model.profileDir);
79
+ const directPackages = matchingPackages(model, chain.length > 0 ? chain : [row.source]);
80
+ const associatedPackage = directPackages.length === 1 ? directPackages[0] : undefined;
81
+ const replacement = row.replacement === true && row.replacementBasis !== 'derived'
82
+ ? { state: 'yes', basis: 'observed' }
83
+ : row.replacement === true
84
+ ? { state: 'unknown', basis: 'unknown' }
85
+ : row.replacement === false
86
+ ? { state: 'no', basis: 'observed' }
87
+ : { state: 'unknown', basis: 'unknown' };
88
+ const unknown = [];
89
+ if (chain.length === 0)
90
+ unknown.push('provenance chain is unavailable');
91
+ if (row.layer === undefined)
92
+ unknown.push('layer is unknown');
93
+ if (row.replacement === true)
94
+ unknown.push(row.replacementBasis !== 'derived'
95
+ ? 'field-level owner and prior config values are unknown'
96
+ : 'source chain indicates patching, but whole-row/config replacement, removed keys, and field owners are unknown');
97
+ if (row.evidenceKind === 'static')
98
+ unknown.push('final composed row is unknown; this is a static declaration');
99
+ if (model.runtimeObservation?.observed !== true)
100
+ unknown.push('runtime behavior was not observed; runtime-dependent values remain unknown/not-run');
101
+ const provenance = chain.length === 0
102
+ ? []
103
+ : chain.map((chainSource, chainIndex) => ({
104
+ source: chainSources[chainIndex],
105
+ sourceBasis: 'observed',
106
+ relation: (chainIndex === 0 ? 'introduced' : 'patched-by'),
107
+ basis: 'derived'
108
+ }));
109
+ const fact = {
110
+ entity: 'row', key: rowKey,
111
+ ...(row.operation === undefined ? {} : { operation: row.operation }),
112
+ ...(row.id === undefined ? {} : { id: row.id }),
113
+ ...(row.name === undefined ? {} : { name: row.name }),
114
+ source,
115
+ sourceBasis: 'observed',
116
+ ...(row.layer === undefined ? {} : { layer: row.layer }),
117
+ ...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }),
118
+ provenance,
119
+ replacement,
120
+ configKeys: rowConfigKeys(row),
121
+ configKeysBasis: row.configKeys === undefined ? 'derived' : 'observed',
122
+ evidenceMode: row.evidenceKind,
123
+ ...(associatedPackage === undefined ? {} : { packageName: associatedPackage.name, ...(associatedPackage.installedVersion === undefined ? {} : { packageVersion: associatedPackage.installedVersion }) }),
124
+ relatedDiagnosticIds: relatedDiagnostics(row, directPackages.map((item) => item.name), diagnostics, model.profileDir),
125
+ unknown: [...new Set(unknown)].sort()
126
+ };
127
+ rows.push(fact);
128
+ addNode({ id: rowKey, entity: 'row', label: row.id ?? row.name ?? 'anonymous row', source, ...(row.layer === undefined ? {} : { layer: row.layer }), ...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }), ...(fact.packageName === undefined ? {} : { packageName: fact.packageName }), ...(fact.packageVersion === undefined ? {} : { packageVersion: fact.packageVersion }), evidenceMode: row.evidenceKind, rowKey });
129
+ if (row.layer !== undefined) {
130
+ const layerId = `layer:${stableKey(`${row.layer}:${row.layerOrder ?? ''}`)}`;
131
+ addNode({ id: layerId, entity: 'layer', label: row.layer, layer: row.layer, ...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }), evidenceMode: row.evidenceKind });
132
+ addEdge({ from: layerId, to: rowKey, relation: 'contains', basis: 'observed', evidenceMode: row.evidenceKind });
133
+ }
134
+ const linkSources = chain.length === 0 ? [row.source] : chain;
135
+ linkSources.forEach((label, chainIndex) => {
136
+ const normalized = normalizeCompositionSource(label, model.profileDir);
137
+ const matched = packages.filter((pkg) => pkg.name === label);
138
+ const packageFact = matched.length === 1 ? matched[0] : undefined;
139
+ const sourceId = `source:${stableKey(normalized)}`;
140
+ addNode({ id: sourceId, entity: 'source', label: normalized, source: normalized, evidenceMode: row.evidenceKind });
141
+ if (packageFact !== undefined) {
142
+ const bundleId = `bundle:${encodeURIComponent(packageFact.name)}`;
143
+ addNode({ id: bundleId, entity: 'bundle', label: packageFact.name, source: normalizeCompositionSource(packageFact.packageJsonSource, model.profileDir), packageName: packageFact.name, ...(packageFact.installedVersion === undefined ? {} : { packageVersion: packageFact.installedVersion }), evidenceMode: 'static' });
144
+ addEdge({ from: bundleId, to: sourceId, relation: 'package-source', basis: 'derived', evidenceMode: row.evidenceKind });
145
+ }
146
+ addEdge({
147
+ from: sourceId,
148
+ to: rowKey,
149
+ relation: chain.length === 0 ? 'source-of' : chainIndex === 0 ? 'introduced' : 'patched-by',
150
+ basis: chain.length === 0 ? 'observed' : 'derived',
151
+ evidenceMode: row.evidenceKind
152
+ });
153
+ });
154
+ for (const diagnosticId of fact.relatedDiagnosticIds) {
155
+ const diagnostic = diagnostics.find((item) => item.id === diagnosticId);
156
+ if (diagnostic === undefined)
157
+ continue;
158
+ const diagnosticKey = `diagnostic:${diagnosticId}`;
159
+ addNode({ id: diagnosticKey, entity: 'diagnostic', label: diagnostic.title, diagnosticId, evidenceMode: diagnostic.evidence[0]?.evidenceKind ?? row.evidenceKind });
160
+ addEdge({ from: rowKey, to: diagnosticKey, relation: 'diagnosed-by', basis: 'derived', evidenceMode: diagnostic.evidence[0]?.evidenceKind ?? row.evidenceKind });
161
+ }
162
+ }
163
+ rows.sort((left, right) => left.key.localeCompare(right.key));
164
+ return {
165
+ schemaVersion: 1,
166
+ evidenceMode: reportMode,
167
+ nodes: [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
168
+ edges: [...edgeMap.values()].sort((left, right) => `${left.from}:${left.relation}:${left.to}`.localeCompare(`${right.from}:${right.relation}:${right.to}`)),
169
+ rows,
170
+ unknownSurfaces
171
+ };
172
+ }
173
+ export function findCompositionRow(facts, id) {
174
+ return facts.rows.find((row) => row.id === id);
175
+ }
176
+ export function compositionBundleImpact(report, name) {
177
+ const facts = report.compositionFacts;
178
+ const rows = facts?.rows.filter((row) => row.packageName === name || row.provenance.some((item) => item.source === name)) ?? [];
179
+ const rowKeys = new Set(rows.map((row) => row.key));
180
+ const sourceLabels = new Set(rows.flatMap((row) => row.provenance.map((item) => item.source)));
181
+ const sourceIds = new Set(facts?.nodes.filter((node) => node.entity === 'source' && sourceLabels.has(node.label)).map((node) => node.id) ?? []);
182
+ const bundleId = `bundle:${encodeURIComponent(name)}`;
183
+ const edges = facts?.edges.filter((edge) => rowKeys.has(edge.to) || rowKeys.has(edge.from) || sourceIds.has(edge.from) || sourceIds.has(edge.to) || edge.from === bundleId || edge.to === bundleId) ?? [];
184
+ const diagnosticIds = new Set(rows.flatMap((row) => row.relatedDiagnosticIds));
185
+ const diagnostics = report.diagnostics.filter((diagnostic) => diagnosticIds.has(diagnostic.id) || diagnostic.evidence.some((item) => item.packageName === name));
186
+ return {
187
+ schemaVersion: 1,
188
+ bundle: name,
189
+ evidenceMode: facts?.evidenceMode ?? (report.evidenceMode === 'resolved' ? 'static' : report.evidenceMode),
190
+ rows,
191
+ edges,
192
+ diagnostics,
193
+ unknown: facts?.unknownSurfaces ?? [...unknownSurfaces]
194
+ };
195
+ }
196
+ /** Normalize paths in diagnostic evidence before adding them to a report. */
197
+ export function redactDiagnosticPaths(diagnostics, model) {
198
+ const knownPaths = [model.profileDir, ...(model.installedPackages ?? []).flatMap((fact) => [fact.packageJsonSource, ...(fact.bundlePatch === undefined ? [] : [fact.bundlePatch])])]
199
+ .filter((value) => value.length > 0);
200
+ return diagnostics.map((diagnostic) => ({
201
+ ...diagnostic,
202
+ evidence: diagnostic.evidence.map((item) => {
203
+ let detail = item.detail;
204
+ for (const path of knownPaths) {
205
+ const normalized = path.replaceAll('\\', '/');
206
+ detail = detail.replaceAll(path, normalizeCompositionSource(path, model.profileDir)).replaceAll(normalized, normalizeCompositionSource(path, model.profileDir));
207
+ }
208
+ return { ...item, source: normalizeCompositionSource(item.source, model.profileDir), detail };
209
+ })
210
+ }));
211
+ }
1
212
  export function rowEvidence(row) {
2
213
  return {
3
214
  source: row.source,
@@ -83,6 +83,7 @@ export function parseDshDump(stdout, stderr = '') {
83
83
  const provenance = provenanceParts(typeof object.source === 'string' ? object.source : provenanceHints[index] ?? legacyProvenanceHints[index]);
84
84
  const source = typeof object.source === 'string' ? object.source : provenance.source ?? '<DSH dump>';
85
85
  const config = object.config;
86
+ const explicitReplacement = object.replacement === true || object.overridden === true;
86
87
  const row = {
87
88
  ...(typeof object.id === 'string' ? { id: object.id } : {}),
88
89
  ...(typeof object.name === 'string' ? { name: object.name } : typeof object.plugin === 'string' ? { name: object.plugin } : {}),
@@ -92,7 +93,9 @@ export function parseDshDump(stdout, stderr = '') {
92
93
  ...(typeof object.layer === 'string' ? { layer: object.layer } : {}),
93
94
  ...(typeof object.layerOrder === 'number' ? { layerOrder: object.layerOrder } : {}),
94
95
  ...(provenance.provenance === undefined ? {} : { provenance: provenance.provenance }),
95
- ...(object.replacement === true || object.overridden === true || (provenance.provenance !== undefined && provenance.provenance.length > 1) || (typeof object.id === 'string' && seenIds.has(object.id) && seenIds.get(object.id) !== source) ? { replacement: true } : {})
96
+ ...(explicitReplacement || (provenance.provenance !== undefined && provenance.provenance.length > 1) || (typeof object.id === 'string' && seenIds.has(object.id) && seenIds.get(object.id) !== source)
97
+ ? { replacement: true, replacementBasis: explicitReplacement ? 'observed' : 'derived' }
98
+ : {})
96
99
  };
97
100
  if (row.id !== undefined)
98
101
  seenIds.set(row.id, row.source);
@@ -103,7 +106,7 @@ export function parseDshDump(stdout, stderr = '') {
103
106
  for (const detail of unmatched)
104
107
  diagnostics.push({
105
108
  id: 'unmatched-patch-target', severity: 'warning', title: 'Resolved patch target did not match a row',
106
- evidence: [{ source: '<DSH stderr>', detail, evidenceKind: 'composed' }],
109
+ evidence: [{ source: '<DSH stderr>', detail, evidenceKind: 'composed', ...(detail.match(/entry ["']([a-z0-9_.:-]+)["'] not found/i)?.[1] === undefined ? {} : { subject: detail.match(/entry ["']([a-z0-9_.:-]+)["'] not found/i)[1] }) }],
107
110
  explanation: 'The public DSH dump reported a patch target that did not match a resolved row.',
108
111
  remediation: 'Verify the patch row id against the selected bundle/profile composition.'
109
112
  });