dsh-composition-doctor 0.2.1 → 0.3.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/CHANGELOG.md +30 -19
- package/README.md +77 -71
- package/README.zh.md +77 -71
- package/dist/cli/main.js +90 -5
- package/dist/client/report-view.js +143 -1
- package/dist/client.js +131 -1
- package/dist/core/composition-provenance.js +206 -0
- package/dist/core/dsh-dump-parser.js +4 -1
- package/dist/core/rules.js +15 -2
- package/dist/reports/schema.js +38 -0
- package/package.json +66 -65
|
@@ -35,7 +35,55 @@ export function toReportViewModel(report) {
|
|
|
35
35
|
const counts = { info: 0, warning: 0, error: 0 };
|
|
36
36
|
for (const diagnostic of report.diagnostics)
|
|
37
37
|
counts[diagnostic.severity] += 1;
|
|
38
|
-
return {
|
|
38
|
+
return {
|
|
39
|
+
report, counts, graph: buildConflictGraph(report),
|
|
40
|
+
compositionGraph: { nodes: report.compositionFacts?.nodes ?? [], edges: report.compositionFacts?.edges ?? [] }
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function compositionNodeDetail(report, nodeId) {
|
|
44
|
+
const facts = report.compositionFacts;
|
|
45
|
+
const node = facts?.nodes.find((candidate) => candidate.id === nodeId);
|
|
46
|
+
if (facts === undefined || node === undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
const relatedRows = facts.rows.filter((candidate) => {
|
|
49
|
+
if (node.rowKey !== undefined)
|
|
50
|
+
return candidate.key === node.rowKey;
|
|
51
|
+
if (node.entity === 'bundle')
|
|
52
|
+
return candidate.packageName === node.packageName || candidate.provenance.some((step) => step.source === node.label);
|
|
53
|
+
if (node.entity === 'source')
|
|
54
|
+
return candidate.source === node.source || candidate.provenance.some((step) => step.source === node.source);
|
|
55
|
+
if (node.entity === 'layer')
|
|
56
|
+
return candidate.layer === node.layer && (node.layerOrder === undefined || candidate.layerOrder === node.layerOrder);
|
|
57
|
+
if (node.entity === 'diagnostic')
|
|
58
|
+
return node.diagnosticId !== undefined && candidate.relatedDiagnosticIds.includes(node.diagnosticId);
|
|
59
|
+
return false;
|
|
60
|
+
});
|
|
61
|
+
const fields = [
|
|
62
|
+
{ label: 'entity', value: node.entity },
|
|
63
|
+
{ label: 'source', value: node.source ?? node.label },
|
|
64
|
+
{ label: 'evidence', value: node.evidenceMode ?? 'unknown' }
|
|
65
|
+
];
|
|
66
|
+
if (node.layer !== undefined)
|
|
67
|
+
fields.push({ label: 'layer', value: node.layer });
|
|
68
|
+
if (node.layerOrder !== undefined)
|
|
69
|
+
fields.push({ label: 'layer order', value: String(node.layerOrder) });
|
|
70
|
+
if (node.packageName !== undefined)
|
|
71
|
+
fields.push({ label: 'package', value: `${node.packageName}${node.packageVersion === undefined ? '' : `@${node.packageVersion}`}` });
|
|
72
|
+
if (relatedRows.length > 0) {
|
|
73
|
+
fields.push({ label: 'related rows', value: relatedRows.map((row) => row.id ?? row.name ?? row.key).join(', ') });
|
|
74
|
+
fields.push({ label: 'config keys', value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.configKeys.length === 0 ? '(none observed)' : row.configKeys.join(', ')}`).join('\n') });
|
|
75
|
+
fields.push({ label: 'replacement', value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.replacement.state} (${row.replacement.basis})`).join('\n') });
|
|
76
|
+
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(' → ')}`).join('\n') });
|
|
77
|
+
}
|
|
78
|
+
const relatedDiagnosticIds = new Set(relatedRows.flatMap((row) => row.relatedDiagnosticIds));
|
|
79
|
+
const rowDiagnostics = report.diagnostics.filter((item) => relatedDiagnosticIds.has(item.id));
|
|
80
|
+
const nodeDiagnostic = node.diagnosticId === undefined ? [] : report.diagnostics.filter((item) => item.id === node.diagnosticId);
|
|
81
|
+
return {
|
|
82
|
+
title: node.label,
|
|
83
|
+
fields,
|
|
84
|
+
diagnostics: [...new Map([...rowDiagnostics, ...nodeDiagnostic].map((item) => [item.id, item])).values()],
|
|
85
|
+
unknown: [...new Set([...relatedRows.flatMap((row) => row.unknown), ...facts.unknownSurfaces])]
|
|
86
|
+
};
|
|
39
87
|
}
|
|
40
88
|
function renderConflictGraph(document, graph) {
|
|
41
89
|
const section = document.createElement('section');
|
|
@@ -59,6 +107,99 @@ function renderConflictGraph(document, graph) {
|
|
|
59
107
|
section.append(list);
|
|
60
108
|
return section;
|
|
61
109
|
}
|
|
110
|
+
export function createCompositionExplorer(document, report) {
|
|
111
|
+
const section = document.createElement('section');
|
|
112
|
+
section.setAttribute('aria-labelledby', 'dsh-composition-doctor-explorer');
|
|
113
|
+
const heading = document.createElement('h3');
|
|
114
|
+
heading.id = 'dsh-composition-doctor-explorer';
|
|
115
|
+
heading.textContent = 'Composition Explorer';
|
|
116
|
+
section.append(heading);
|
|
117
|
+
const facts = report.compositionFacts;
|
|
118
|
+
const graph = facts === undefined ? { nodes: [], edges: [] } : { nodes: facts.nodes, edges: facts.edges };
|
|
119
|
+
if (graph.nodes.length === 0) {
|
|
120
|
+
const message = document.createElement('p');
|
|
121
|
+
message.textContent = 'Composition provenance is unavailable in this legacy report.';
|
|
122
|
+
section.append(message);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const list = document.createElement('ul');
|
|
126
|
+
for (const node of graph.nodes.filter((value) => value.entity !== 'diagnostic')) {
|
|
127
|
+
const item = document.createElement('li');
|
|
128
|
+
const select = button(document, `${node.entity}: ${node.label}`, () => {
|
|
129
|
+
const detail = compositionNodeDetail(report, node.id);
|
|
130
|
+
if (detail === undefined)
|
|
131
|
+
return;
|
|
132
|
+
const panel = section.querySelector('[data-composition-detail]');
|
|
133
|
+
if (panel === null)
|
|
134
|
+
return;
|
|
135
|
+
panel.replaceChildren();
|
|
136
|
+
const title = document.createElement('h4');
|
|
137
|
+
title.textContent = detail.title;
|
|
138
|
+
panel.append(title);
|
|
139
|
+
const fields = document.createElement('dl');
|
|
140
|
+
for (const field of detail.fields) {
|
|
141
|
+
const term = document.createElement('dt');
|
|
142
|
+
term.textContent = field.label;
|
|
143
|
+
const value = document.createElement('dd');
|
|
144
|
+
value.textContent = field.value;
|
|
145
|
+
fields.append(term, value);
|
|
146
|
+
}
|
|
147
|
+
panel.append(fields);
|
|
148
|
+
const diagnostics = document.createElement('ul');
|
|
149
|
+
for (const diagnostic of detail.diagnostics) {
|
|
150
|
+
const entry = document.createElement('li');
|
|
151
|
+
entry.textContent = `[${diagnostic.severity}] ${diagnostic.title}: ${diagnostic.explanation}`;
|
|
152
|
+
diagnostics.append(entry);
|
|
153
|
+
}
|
|
154
|
+
if (detail.diagnostics.length > 0)
|
|
155
|
+
panel.append(diagnostics);
|
|
156
|
+
const unknown = document.createElement('ul');
|
|
157
|
+
for (const value of detail.unknown) {
|
|
158
|
+
const entry = document.createElement('li');
|
|
159
|
+
entry.textContent = value;
|
|
160
|
+
unknown.append(entry);
|
|
161
|
+
}
|
|
162
|
+
if (detail.unknown.length > 0) {
|
|
163
|
+
const unknownTitle = document.createElement('h4');
|
|
164
|
+
unknownTitle.textContent = 'Unknown / not modelled';
|
|
165
|
+
panel.append(unknownTitle, unknown);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
select.dataset.compositionNode = node.id;
|
|
169
|
+
item.append(select);
|
|
170
|
+
const outgoing = graph.edges.filter((edge) => edge.from === node.id);
|
|
171
|
+
if (outgoing.length > 0) {
|
|
172
|
+
const relationships = document.createElement('ul');
|
|
173
|
+
for (const edge of outgoing) {
|
|
174
|
+
const target = graph.nodes.find((candidate) => candidate.id === edge.to);
|
|
175
|
+
if (target === undefined || target.entity === 'diagnostic')
|
|
176
|
+
continue;
|
|
177
|
+
const relationship = document.createElement('li');
|
|
178
|
+
relationship.textContent = `${edge.relation} → ${target.label} (${edge.basis}; ${edge.evidenceMode})`;
|
|
179
|
+
relationships.append(relationship);
|
|
180
|
+
}
|
|
181
|
+
item.append(relationships);
|
|
182
|
+
}
|
|
183
|
+
list.append(item);
|
|
184
|
+
}
|
|
185
|
+
section.append(list);
|
|
186
|
+
const detail = document.createElement('section');
|
|
187
|
+
detail.dataset.compositionDetail = 'true';
|
|
188
|
+
detail.setAttribute('aria-live', 'polite');
|
|
189
|
+
detail.textContent = 'Select a bundle, source, layer, or row to inspect its evidence.';
|
|
190
|
+
section.append(detail);
|
|
191
|
+
const unknownList = document.createElement('ul');
|
|
192
|
+
for (const value of facts?.unknownSurfaces ?? []) {
|
|
193
|
+
const entry = document.createElement('li');
|
|
194
|
+
entry.textContent = value;
|
|
195
|
+
unknownList.append(entry);
|
|
196
|
+
}
|
|
197
|
+
const unknownTitle = document.createElement('h4');
|
|
198
|
+
unknownTitle.textContent = 'Other surfaces';
|
|
199
|
+
section.append(unknownTitle, unknownList);
|
|
200
|
+
}
|
|
201
|
+
return section;
|
|
202
|
+
}
|
|
62
203
|
function isReport(value) {
|
|
63
204
|
if (value === null || typeof value !== 'object')
|
|
64
205
|
return false;
|
|
@@ -145,6 +286,7 @@ export function createReportView(document = globalThis.document, translate = (ke
|
|
|
145
286
|
status.textContent = `Generated: ${report.generatedAt}. Profile: ${report.profileDir}. Diagnostics: ${model.counts.error} error, ${model.counts.warning} warning, ${model.counts.info} info.`;
|
|
146
287
|
status.append(document.createTextNode(' '), badge);
|
|
147
288
|
root.append(renderConflictGraph(document, model.graph));
|
|
289
|
+
root.append(createCompositionExplorer(document, report));
|
|
148
290
|
const list = document.createElement('ul');
|
|
149
291
|
for (const diagnostic of report.diagnostics) {
|
|
150
292
|
const item = document.createElement('li');
|
package/dist/client.js
CHANGED
|
@@ -106,7 +106,48 @@ window.__ModuleLoader__.load({
|
|
|
106
106
|
function toReportViewModel(report) {
|
|
107
107
|
const counts = { info: 0, warning: 0, error: 0 };
|
|
108
108
|
for (const diagnostic of report.diagnostics) counts[diagnostic.severity] += 1;
|
|
109
|
-
return {
|
|
109
|
+
return {
|
|
110
|
+
report,
|
|
111
|
+
counts,
|
|
112
|
+
graph: buildConflictGraph(report),
|
|
113
|
+
compositionGraph: { nodes: report.compositionFacts?.nodes ?? [], edges: report.compositionFacts?.edges ?? [] }
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function compositionNodeDetail(report, nodeId) {
|
|
117
|
+
const facts = report.compositionFacts;
|
|
118
|
+
const node = facts?.nodes.find((candidate) => candidate.id === nodeId);
|
|
119
|
+
if (facts === void 0 || node === void 0) return void 0;
|
|
120
|
+
const relatedRows = facts.rows.filter((candidate) => {
|
|
121
|
+
if (node.rowKey !== void 0) return candidate.key === node.rowKey;
|
|
122
|
+
if (node.entity === "bundle") return candidate.packageName === node.packageName || candidate.provenance.some((step) => step.source === node.label);
|
|
123
|
+
if (node.entity === "source") return candidate.source === node.source || candidate.provenance.some((step) => step.source === node.source);
|
|
124
|
+
if (node.entity === "layer") return candidate.layer === node.layer && (node.layerOrder === void 0 || candidate.layerOrder === node.layerOrder);
|
|
125
|
+
if (node.entity === "diagnostic") return node.diagnosticId !== void 0 && candidate.relatedDiagnosticIds.includes(node.diagnosticId);
|
|
126
|
+
return false;
|
|
127
|
+
});
|
|
128
|
+
const fields = [
|
|
129
|
+
{ label: "entity", value: node.entity },
|
|
130
|
+
{ label: "source", value: node.source ?? node.label },
|
|
131
|
+
{ label: "evidence", value: node.evidenceMode ?? "unknown" }
|
|
132
|
+
];
|
|
133
|
+
if (node.layer !== void 0) fields.push({ label: "layer", value: node.layer });
|
|
134
|
+
if (node.layerOrder !== void 0) fields.push({ label: "layer order", value: String(node.layerOrder) });
|
|
135
|
+
if (node.packageName !== void 0) fields.push({ label: "package", value: `${node.packageName}${node.packageVersion === void 0 ? "" : `@${node.packageVersion}`}` });
|
|
136
|
+
if (relatedRows.length > 0) {
|
|
137
|
+
fields.push({ label: "related rows", value: relatedRows.map((row) => row.id ?? row.name ?? row.key).join(", ") });
|
|
138
|
+
fields.push({ label: "config keys", value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.configKeys.length === 0 ? "(none observed)" : row.configKeys.join(", ")}`).join("\n") });
|
|
139
|
+
fields.push({ label: "replacement", value: relatedRows.map((row) => `${row.id ?? row.key}: ${row.replacement.state} (${row.replacement.basis})`).join("\n") });
|
|
140
|
+
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") });
|
|
141
|
+
}
|
|
142
|
+
const relatedDiagnosticIds = new Set(relatedRows.flatMap((row) => row.relatedDiagnosticIds));
|
|
143
|
+
const rowDiagnostics = report.diagnostics.filter((item) => relatedDiagnosticIds.has(item.id));
|
|
144
|
+
const nodeDiagnostic = node.diagnosticId === void 0 ? [] : report.diagnostics.filter((item) => item.id === node.diagnosticId);
|
|
145
|
+
return {
|
|
146
|
+
title: node.label,
|
|
147
|
+
fields,
|
|
148
|
+
diagnostics: [...new Map([...rowDiagnostics, ...nodeDiagnostic].map((item) => [item.id, item])).values()],
|
|
149
|
+
unknown: [.../* @__PURE__ */ new Set([...relatedRows.flatMap((row) => row.unknown), ...facts.unknownSurfaces])]
|
|
150
|
+
};
|
|
110
151
|
}
|
|
111
152
|
function renderConflictGraph(document, graph) {
|
|
112
153
|
const section = document.createElement("section");
|
|
@@ -130,6 +171,94 @@ window.__ModuleLoader__.load({
|
|
|
130
171
|
section.append(list);
|
|
131
172
|
return section;
|
|
132
173
|
}
|
|
174
|
+
function createCompositionExplorer(document, report) {
|
|
175
|
+
const section = document.createElement("section");
|
|
176
|
+
section.setAttribute("aria-labelledby", "dsh-composition-doctor-explorer");
|
|
177
|
+
const heading = document.createElement("h3");
|
|
178
|
+
heading.id = "dsh-composition-doctor-explorer";
|
|
179
|
+
heading.textContent = "Composition Explorer";
|
|
180
|
+
section.append(heading);
|
|
181
|
+
const facts = report.compositionFacts;
|
|
182
|
+
const graph = facts === void 0 ? { nodes: [], edges: [] } : { nodes: facts.nodes, edges: facts.edges };
|
|
183
|
+
if (graph.nodes.length === 0) {
|
|
184
|
+
const message = document.createElement("p");
|
|
185
|
+
message.textContent = "Composition provenance is unavailable in this legacy report.";
|
|
186
|
+
section.append(message);
|
|
187
|
+
} else {
|
|
188
|
+
const list = document.createElement("ul");
|
|
189
|
+
for (const node of graph.nodes.filter((value) => value.entity !== "diagnostic")) {
|
|
190
|
+
const item = document.createElement("li");
|
|
191
|
+
const select = button(document, `${node.entity}: ${node.label}`, () => {
|
|
192
|
+
const detail2 = compositionNodeDetail(report, node.id);
|
|
193
|
+
if (detail2 === void 0) return;
|
|
194
|
+
const panel = section.querySelector("[data-composition-detail]");
|
|
195
|
+
if (panel === null) return;
|
|
196
|
+
panel.replaceChildren();
|
|
197
|
+
const title = document.createElement("h4");
|
|
198
|
+
title.textContent = detail2.title;
|
|
199
|
+
panel.append(title);
|
|
200
|
+
const fields = document.createElement("dl");
|
|
201
|
+
for (const field of detail2.fields) {
|
|
202
|
+
const term = document.createElement("dt");
|
|
203
|
+
term.textContent = field.label;
|
|
204
|
+
const value = document.createElement("dd");
|
|
205
|
+
value.textContent = field.value;
|
|
206
|
+
fields.append(term, value);
|
|
207
|
+
}
|
|
208
|
+
panel.append(fields);
|
|
209
|
+
const diagnostics = document.createElement("ul");
|
|
210
|
+
for (const diagnostic of detail2.diagnostics) {
|
|
211
|
+
const entry = document.createElement("li");
|
|
212
|
+
entry.textContent = `[${diagnostic.severity}] ${diagnostic.title}: ${diagnostic.explanation}`;
|
|
213
|
+
diagnostics.append(entry);
|
|
214
|
+
}
|
|
215
|
+
if (detail2.diagnostics.length > 0) panel.append(diagnostics);
|
|
216
|
+
const unknown = document.createElement("ul");
|
|
217
|
+
for (const value of detail2.unknown) {
|
|
218
|
+
const entry = document.createElement("li");
|
|
219
|
+
entry.textContent = value;
|
|
220
|
+
unknown.append(entry);
|
|
221
|
+
}
|
|
222
|
+
if (detail2.unknown.length > 0) {
|
|
223
|
+
const unknownTitle2 = document.createElement("h4");
|
|
224
|
+
unknownTitle2.textContent = "Unknown / not modelled";
|
|
225
|
+
panel.append(unknownTitle2, unknown);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
select.dataset.compositionNode = node.id;
|
|
229
|
+
item.append(select);
|
|
230
|
+
const outgoing = graph.edges.filter((edge) => edge.from === node.id);
|
|
231
|
+
if (outgoing.length > 0) {
|
|
232
|
+
const relationships = document.createElement("ul");
|
|
233
|
+
for (const edge of outgoing) {
|
|
234
|
+
const target = graph.nodes.find((candidate) => candidate.id === edge.to);
|
|
235
|
+
if (target === void 0 || target.entity === "diagnostic") continue;
|
|
236
|
+
const relationship = document.createElement("li");
|
|
237
|
+
relationship.textContent = `${edge.relation} \u2192 ${target.label} (${edge.basis}; ${edge.evidenceMode})`;
|
|
238
|
+
relationships.append(relationship);
|
|
239
|
+
}
|
|
240
|
+
item.append(relationships);
|
|
241
|
+
}
|
|
242
|
+
list.append(item);
|
|
243
|
+
}
|
|
244
|
+
section.append(list);
|
|
245
|
+
const detail = document.createElement("section");
|
|
246
|
+
detail.dataset.compositionDetail = "true";
|
|
247
|
+
detail.setAttribute("aria-live", "polite");
|
|
248
|
+
detail.textContent = "Select a bundle, source, layer, or row to inspect its evidence.";
|
|
249
|
+
section.append(detail);
|
|
250
|
+
const unknownList = document.createElement("ul");
|
|
251
|
+
for (const value of facts?.unknownSurfaces ?? []) {
|
|
252
|
+
const entry = document.createElement("li");
|
|
253
|
+
entry.textContent = value;
|
|
254
|
+
unknownList.append(entry);
|
|
255
|
+
}
|
|
256
|
+
const unknownTitle = document.createElement("h4");
|
|
257
|
+
unknownTitle.textContent = "Other surfaces";
|
|
258
|
+
section.append(unknownTitle, unknownList);
|
|
259
|
+
}
|
|
260
|
+
return section;
|
|
261
|
+
}
|
|
133
262
|
function isReport(value) {
|
|
134
263
|
if (value === null || typeof value !== "object") return false;
|
|
135
264
|
const candidate = value;
|
|
@@ -220,6 +349,7 @@ window.__ModuleLoader__.load({
|
|
|
220
349
|
status.textContent = `Generated: ${report.generatedAt}. Profile: ${report.profileDir}. Diagnostics: ${model.counts.error} error, ${model.counts.warning} warning, ${model.counts.info} info.`;
|
|
221
350
|
status.append(document.createTextNode(" "), badge);
|
|
222
351
|
root.append(renderConflictGraph(document, model.graph));
|
|
352
|
+
root.append(createCompositionExplorer(document, report));
|
|
223
353
|
const list = document.createElement("ul");
|
|
224
354
|
for (const diagnostic of report.diagnostics) {
|
|
225
355
|
const item = document.createElement("li");
|
|
@@ -1,3 +1,209 @@
|
|
|
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 packages = model.installedPackages ?? [];
|
|
62
|
+
const reportMode = model.evidenceMode ?? 'static';
|
|
63
|
+
const addNode = (node) => nodes.set(node.id, node);
|
|
64
|
+
const addEdge = (edge) => edgeMap.set(`${edge.from}\0${edge.relation}\0${edge.to}`, edge);
|
|
65
|
+
for (const packageFact of packages) {
|
|
66
|
+
const bundleId = `bundle:${encodeURIComponent(packageFact.name)}`;
|
|
67
|
+
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' });
|
|
68
|
+
}
|
|
69
|
+
for (const [index, row] of model.rows.entries()) {
|
|
70
|
+
const rowIdentity = row.id ?? row.name ?? `${normalizeCompositionSource(row.source, model.profileDir)}:${index}`;
|
|
71
|
+
const rowKey = `row:${stableKey(`${rowIdentity}|${normalizeCompositionSource(row.source, model.profileDir)}|${row.layer ?? ''}|${row.layerOrder ?? index}`)}`;
|
|
72
|
+
const chain = row.provenance === undefined ? [] : [...row.provenance];
|
|
73
|
+
const chainSources = chain.map((source) => normalizeCompositionSource(source, model.profileDir));
|
|
74
|
+
const source = normalizeCompositionSource(row.source, model.profileDir);
|
|
75
|
+
const directPackages = matchingPackages(model, chain.length > 0 ? chain : [row.source]);
|
|
76
|
+
const associatedPackage = directPackages.length === 1 ? directPackages[0] : undefined;
|
|
77
|
+
const replacement = row.replacement === true && row.replacementBasis !== 'derived'
|
|
78
|
+
? { state: 'yes', basis: 'observed' }
|
|
79
|
+
: row.replacement === true
|
|
80
|
+
? { state: 'unknown', basis: 'unknown' }
|
|
81
|
+
: row.replacement === false
|
|
82
|
+
? { state: 'no', basis: 'observed' }
|
|
83
|
+
: { state: 'unknown', basis: 'unknown' };
|
|
84
|
+
const unknown = [];
|
|
85
|
+
if (chain.length === 0)
|
|
86
|
+
unknown.push('provenance chain is unavailable');
|
|
87
|
+
if (row.layer === undefined)
|
|
88
|
+
unknown.push('layer is unknown');
|
|
89
|
+
if (row.replacement === true)
|
|
90
|
+
unknown.push(row.replacementBasis !== 'derived'
|
|
91
|
+
? 'field-level owner and prior config values are unknown'
|
|
92
|
+
: 'source chain indicates patching, but whole-row/config replacement, removed keys, and field owners are unknown');
|
|
93
|
+
if (row.evidenceKind === 'static')
|
|
94
|
+
unknown.push('final composed row is unknown; this is a static declaration');
|
|
95
|
+
if (model.runtimeObservation?.observed !== true)
|
|
96
|
+
unknown.push('runtime behavior was not observed; runtime-dependent values remain unknown/not-run');
|
|
97
|
+
const provenance = chain.length === 0
|
|
98
|
+
? []
|
|
99
|
+
: chain.map((chainSource, chainIndex) => ({
|
|
100
|
+
source: chainSources[chainIndex],
|
|
101
|
+
sourceBasis: 'observed',
|
|
102
|
+
relation: (chainIndex === 0 ? 'introduced' : 'patched-by'),
|
|
103
|
+
basis: 'derived'
|
|
104
|
+
}));
|
|
105
|
+
const fact = {
|
|
106
|
+
entity: 'row', key: rowKey,
|
|
107
|
+
...(row.id === undefined ? {} : { id: row.id }),
|
|
108
|
+
...(row.name === undefined ? {} : { name: row.name }),
|
|
109
|
+
source,
|
|
110
|
+
sourceBasis: 'observed',
|
|
111
|
+
...(row.layer === undefined ? {} : { layer: row.layer }),
|
|
112
|
+
...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }),
|
|
113
|
+
provenance,
|
|
114
|
+
replacement,
|
|
115
|
+
configKeys: rowConfigKeys(row),
|
|
116
|
+
configKeysBasis: row.configKeys === undefined ? 'derived' : 'observed',
|
|
117
|
+
evidenceMode: row.evidenceKind,
|
|
118
|
+
...(associatedPackage === undefined ? {} : { packageName: associatedPackage.name, ...(associatedPackage.installedVersion === undefined ? {} : { packageVersion: associatedPackage.installedVersion }) }),
|
|
119
|
+
relatedDiagnosticIds: relatedDiagnostics(row, directPackages.map((item) => item.name), diagnostics, model.profileDir),
|
|
120
|
+
unknown: [...new Set(unknown)].sort()
|
|
121
|
+
};
|
|
122
|
+
rows.push(fact);
|
|
123
|
+
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 });
|
|
124
|
+
if (row.layer !== undefined) {
|
|
125
|
+
const layerId = `layer:${stableKey(`${row.layer}:${row.layerOrder ?? ''}`)}`;
|
|
126
|
+
addNode({ id: layerId, entity: 'layer', label: row.layer, layer: row.layer, ...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }), evidenceMode: row.evidenceKind });
|
|
127
|
+
addEdge({ from: layerId, to: rowKey, relation: 'contains', basis: 'observed', evidenceMode: row.evidenceKind });
|
|
128
|
+
}
|
|
129
|
+
const linkSources = chain.length === 0 ? [row.source] : chain;
|
|
130
|
+
linkSources.forEach((label, chainIndex) => {
|
|
131
|
+
const normalized = normalizeCompositionSource(label, model.profileDir);
|
|
132
|
+
const matched = packages.filter((pkg) => pkg.name === label);
|
|
133
|
+
const packageFact = matched.length === 1 ? matched[0] : undefined;
|
|
134
|
+
const sourceId = `source:${stableKey(normalized)}`;
|
|
135
|
+
addNode({ id: sourceId, entity: 'source', label: normalized, source: normalized, evidenceMode: row.evidenceKind });
|
|
136
|
+
if (packageFact !== undefined) {
|
|
137
|
+
const bundleId = `bundle:${encodeURIComponent(packageFact.name)}`;
|
|
138
|
+
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' });
|
|
139
|
+
addEdge({ from: bundleId, to: sourceId, relation: 'package-source', basis: 'derived', evidenceMode: row.evidenceKind });
|
|
140
|
+
}
|
|
141
|
+
addEdge({
|
|
142
|
+
from: sourceId,
|
|
143
|
+
to: rowKey,
|
|
144
|
+
relation: chain.length === 0 ? 'source-of' : chainIndex === 0 ? 'introduced' : 'patched-by',
|
|
145
|
+
basis: chain.length === 0 ? 'observed' : 'derived',
|
|
146
|
+
evidenceMode: row.evidenceKind
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
for (const diagnosticId of fact.relatedDiagnosticIds) {
|
|
150
|
+
const diagnostic = diagnostics.find((item) => item.id === diagnosticId);
|
|
151
|
+
if (diagnostic === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
const diagnosticKey = `diagnostic:${diagnosticId}`;
|
|
154
|
+
addNode({ id: diagnosticKey, entity: 'diagnostic', label: diagnostic.title, diagnosticId, evidenceMode: diagnostic.evidence[0]?.evidenceKind ?? row.evidenceKind });
|
|
155
|
+
addEdge({ from: rowKey, to: diagnosticKey, relation: 'diagnosed-by', basis: 'derived', evidenceMode: diagnostic.evidence[0]?.evidenceKind ?? row.evidenceKind });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
rows.sort((left, right) => left.key.localeCompare(right.key));
|
|
159
|
+
return {
|
|
160
|
+
schemaVersion: 1,
|
|
161
|
+
evidenceMode: reportMode,
|
|
162
|
+
nodes: [...nodes.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
163
|
+
edges: [...edgeMap.values()].sort((left, right) => `${left.from}:${left.relation}:${left.to}`.localeCompare(`${right.from}:${right.relation}:${right.to}`)),
|
|
164
|
+
rows,
|
|
165
|
+
unknownSurfaces
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function findCompositionRow(facts, id) {
|
|
169
|
+
return facts.rows.find((row) => row.id === id);
|
|
170
|
+
}
|
|
171
|
+
export function compositionBundleImpact(report, name) {
|
|
172
|
+
const facts = report.compositionFacts;
|
|
173
|
+
const rows = facts?.rows.filter((row) => row.packageName === name || row.provenance.some((item) => item.source === name)) ?? [];
|
|
174
|
+
const rowKeys = new Set(rows.map((row) => row.key));
|
|
175
|
+
const sourceLabels = new Set(rows.flatMap((row) => row.provenance.map((item) => item.source)));
|
|
176
|
+
const sourceIds = new Set(facts?.nodes.filter((node) => node.entity === 'source' && sourceLabels.has(node.label)).map((node) => node.id) ?? []);
|
|
177
|
+
const bundleId = `bundle:${encodeURIComponent(name)}`;
|
|
178
|
+
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) ?? [];
|
|
179
|
+
const diagnosticIds = new Set(rows.flatMap((row) => row.relatedDiagnosticIds));
|
|
180
|
+
const diagnostics = report.diagnostics.filter((diagnostic) => diagnosticIds.has(diagnostic.id) || diagnostic.evidence.some((item) => item.packageName === name));
|
|
181
|
+
return {
|
|
182
|
+
schemaVersion: 1,
|
|
183
|
+
bundle: name,
|
|
184
|
+
evidenceMode: facts?.evidenceMode ?? (report.evidenceMode === 'resolved' ? 'static' : report.evidenceMode),
|
|
185
|
+
rows,
|
|
186
|
+
edges,
|
|
187
|
+
diagnostics,
|
|
188
|
+
unknown: facts?.unknownSurfaces ?? [...unknownSurfaces]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/** Normalize paths in diagnostic evidence before adding them to a report. */
|
|
192
|
+
export function redactDiagnosticPaths(diagnostics, model) {
|
|
193
|
+
const knownPaths = [model.profileDir, ...(model.installedPackages ?? []).flatMap((fact) => [fact.packageJsonSource, ...(fact.bundlePatch === undefined ? [] : [fact.bundlePatch])])]
|
|
194
|
+
.filter((value) => value.length > 0);
|
|
195
|
+
return diagnostics.map((diagnostic) => ({
|
|
196
|
+
...diagnostic,
|
|
197
|
+
evidence: diagnostic.evidence.map((item) => {
|
|
198
|
+
let detail = item.detail;
|
|
199
|
+
for (const path of knownPaths) {
|
|
200
|
+
const normalized = path.replaceAll('\\', '/');
|
|
201
|
+
detail = detail.replaceAll(path, normalizeCompositionSource(path, model.profileDir)).replaceAll(normalized, normalizeCompositionSource(path, model.profileDir));
|
|
202
|
+
}
|
|
203
|
+
return { ...item, source: normalizeCompositionSource(item.source, model.profileDir), detail };
|
|
204
|
+
})
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
1
207
|
export function rowEvidence(row) {
|
|
2
208
|
return {
|
|
3
209
|
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
|
-
...(
|
|
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);
|
package/dist/core/rules.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { satisfies as semverSatisfies } from 'semver';
|
|
2
|
+
import { buildCompositionFacts, redactDiagnosticPaths } from './composition-provenance.js';
|
|
2
3
|
function evidence(source, subject, detail, evidenceKind, packageName, version) {
|
|
3
4
|
return { source, subject, detail, evidenceKind, ...(packageName === undefined ? {} : { packageName }), ...(version === undefined ? {} : { version }) };
|
|
4
5
|
}
|
|
@@ -157,6 +158,18 @@ export function analyseComposition(model) {
|
|
|
157
158
|
}
|
|
158
159
|
diagnostics.sort((left, right) => left.id.localeCompare(right.id) || left.title.localeCompare(right.title) || left.evidence[0]?.source.localeCompare(right.evidence[0]?.source ?? '') || 0);
|
|
159
160
|
const evidenceMode = runtimeObserved ? 'runtime-observed' : requestedMode === 'runtime-observed' ? 'composed' : requestedMode;
|
|
160
|
-
const
|
|
161
|
-
|
|
161
|
+
const redactedDiagnostics = redactDiagnosticPaths(diagnostics, model);
|
|
162
|
+
const unverifiedFindings = redactedDiagnostics.filter((item) => item.id === 'runtime-composition-unavailable' || item.evidence.some((entry) => entry.evidenceKind === 'static')).map((item) => item.id);
|
|
163
|
+
const reportBase = {
|
|
164
|
+
schemaVersion: 1,
|
|
165
|
+
evidenceSchemaVersion: 2,
|
|
166
|
+
generatedAt: new Date().toISOString(),
|
|
167
|
+
profileDir: '<PROFILE>',
|
|
168
|
+
evidenceMode,
|
|
169
|
+
runtimeObserved,
|
|
170
|
+
...(model.metadataCoverage === undefined ? {} : { metadataCoverage: model.metadataCoverage }),
|
|
171
|
+
unverifiedFindings,
|
|
172
|
+
diagnostics: redactedDiagnostics
|
|
173
|
+
};
|
|
174
|
+
return { ...reportBase, compositionFacts: buildCompositionFacts({ ...model, evidenceMode }, redactedDiagnostics) };
|
|
162
175
|
}
|
package/dist/reports/schema.js
CHANGED
|
@@ -12,6 +12,44 @@ export const analysisReportJsonSchema = {
|
|
|
12
12
|
evidenceMode: { enum: ['static', 'composed', 'runtime-observed', 'mixed'] },
|
|
13
13
|
runtimeObserved: { type: 'boolean' },
|
|
14
14
|
unverifiedFindings: { type: 'array', items: { type: 'string' } },
|
|
15
|
+
compositionFacts: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
required: ['schemaVersion', 'evidenceMode', 'nodes', 'edges', 'rows', 'unknownSurfaces'],
|
|
18
|
+
properties: {
|
|
19
|
+
schemaVersion: { const: 1 },
|
|
20
|
+
evidenceMode: { enum: ['static', 'composed', 'runtime-observed', 'mixed'] },
|
|
21
|
+
nodes: { type: 'array', items: {
|
|
22
|
+
type: 'object', required: ['id', 'entity', 'label'],
|
|
23
|
+
properties: {
|
|
24
|
+
id: { type: 'string' }, entity: { enum: ['bundle', 'source', 'layer', 'row', 'diagnostic'] }, label: { type: 'string' },
|
|
25
|
+
source: { type: 'string' }, layer: { type: 'string' }, layerOrder: { type: 'number' },
|
|
26
|
+
packageName: { type: 'string' }, packageVersion: { type: 'string' },
|
|
27
|
+
evidenceMode: { enum: ['static', 'composed', 'runtime-observed'] }, rowKey: { type: 'string' }, diagnosticId: { type: 'string' }
|
|
28
|
+
}, additionalProperties: false
|
|
29
|
+
} },
|
|
30
|
+
edges: { type: 'array', items: {
|
|
31
|
+
type: 'object', required: ['from', 'to', 'relation', 'basis', 'evidenceMode'],
|
|
32
|
+
properties: {
|
|
33
|
+
from: { type: 'string' }, to: { type: 'string' },
|
|
34
|
+
relation: { enum: ['introduced', 'patched-by', 'source-of', 'contains', 'diagnosed-by', 'package-source'] },
|
|
35
|
+
basis: { enum: ['observed', 'derived'] }, evidenceMode: { enum: ['static', 'composed', 'runtime-observed'] }
|
|
36
|
+
}, additionalProperties: false
|
|
37
|
+
} },
|
|
38
|
+
rows: { type: 'array', items: {
|
|
39
|
+
type: 'object', required: ['entity', 'key', 'source', 'sourceBasis', 'provenance', 'replacement', 'configKeys', 'configKeysBasis', 'evidenceMode', 'relatedDiagnosticIds', 'unknown'],
|
|
40
|
+
properties: {
|
|
41
|
+
entity: { const: 'row' }, key: { type: 'string' }, id: { type: 'string' }, name: { type: 'string' },
|
|
42
|
+
source: { type: 'string' }, sourceBasis: { const: 'observed' }, layer: { type: 'string' }, layerOrder: { type: 'number' },
|
|
43
|
+
provenance: { type: 'array', items: { type: 'object', required: ['source', 'sourceBasis', 'relation', 'basis'], properties: { source: { type: 'string' }, sourceBasis: { const: 'observed' }, relation: { enum: ['introduced', 'patched-by'] }, basis: { enum: ['observed', 'derived'] } }, additionalProperties: false } },
|
|
44
|
+
replacement: { type: 'object', required: ['state', 'basis'], properties: { state: { enum: ['yes', 'no', 'unknown'] }, basis: { enum: ['observed', 'derived', 'unknown'] } }, additionalProperties: false },
|
|
45
|
+
configKeys: { type: 'array', items: { type: 'string' } }, configKeysBasis: { enum: ['observed', 'derived'] }, evidenceMode: { enum: ['static', 'composed', 'runtime-observed'] },
|
|
46
|
+
packageName: { type: 'string' }, packageVersion: { type: 'string' }, relatedDiagnosticIds: { type: 'array', items: { type: 'string' } },
|
|
47
|
+
removedConfigKeys: { type: 'array', items: { type: 'string' } }, unknown: { type: 'array', items: { type: 'string' } }
|
|
48
|
+
}, additionalProperties: false
|
|
49
|
+
} },
|
|
50
|
+
unknownSurfaces: { type: 'array', items: { type: 'string' } }
|
|
51
|
+
}, additionalProperties: false
|
|
52
|
+
},
|
|
15
53
|
diagnostics: {
|
|
16
54
|
type: 'array',
|
|
17
55
|
items: {
|