dsh-composition-doctor 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/CHANGELOG.md +22 -9
- package/README.md +125 -26
- package/README.zh.md +125 -26
- package/dist/cli/main.js +131 -4
- package/dist/client/report-view.js +43 -0
- package/dist/client.js +80 -1
- package/dist/core/bundle-inventory.js +9 -6
- package/dist/core/composition-provenance.js +6 -1
- package/dist/core/dsh-dump-parser.js +1 -1
- package/dist/core/failure-explainer.js +205 -0
- package/dist/core/failure-profile.js +110 -0
- package/dist/core/package-provenance.js +4 -0
- package/dist/core/preflight/npm-artifact.js +18 -2
- package/dist/core/rules.js +9 -2
- package/dist/core/snapshot.js +1 -1
- package/dist/reports/failure.js +23 -0
- package/dist/reports/markdown.js +3 -1
- package/dist/reports/schema.js +29 -0
- package/package.json +67 -66
|
@@ -107,6 +107,44 @@ function renderConflictGraph(document, graph) {
|
|
|
107
107
|
section.append(list);
|
|
108
108
|
return section;
|
|
109
109
|
}
|
|
110
|
+
export function createFailureExplanation(document, report) {
|
|
111
|
+
const section = document.createElement('section');
|
|
112
|
+
section.dataset.failureExplanation = 'true';
|
|
113
|
+
const heading = document.createElement('h3');
|
|
114
|
+
heading.textContent = 'Failure Explanation';
|
|
115
|
+
section.append(heading);
|
|
116
|
+
const result = report.failureExplanation;
|
|
117
|
+
if (result === undefined) {
|
|
118
|
+
const message = document.createElement('p');
|
|
119
|
+
message.textContent = 'No failure explanation in this report. Run dsh-doctor diagnose --profile <dir> --report-dir <doctor-reports> to create one.';
|
|
120
|
+
section.append(message);
|
|
121
|
+
return section;
|
|
122
|
+
}
|
|
123
|
+
const status = document.createElement('p');
|
|
124
|
+
status.textContent = `Outcome: ${result.outcome}; runtime: ${result.runtime}. Log signatures are reported claims, not composition evidence.`;
|
|
125
|
+
section.append(status);
|
|
126
|
+
for (const explanation of result.explanations) {
|
|
127
|
+
const details = document.createElement('details');
|
|
128
|
+
const summary = document.createElement('summary');
|
|
129
|
+
summary.textContent = `${explanation.detectedFailure} (${explanation.origin}; ${explanation.evidenceLevel})`;
|
|
130
|
+
const cause = document.createElement('p');
|
|
131
|
+
cause.textContent = explanation.observableCause;
|
|
132
|
+
const paths = document.createElement('pre');
|
|
133
|
+
paths.textContent = explanation.provenancePaths.map((path) => `${path.packageName ?? 'profile'} → ${path.source} → ${path.layer ?? 'unknown layer'} → ${path.rowId ?? 'unknown row'}\n${path.provenance.map((step) => `${step.relation}: ${step.source} (${step.basis})`).join(' → ')}`).join('\n\n');
|
|
134
|
+
const unknown = document.createElement('p');
|
|
135
|
+
unknown.textContent = `Unknown: ${explanation.unknowns.join(' ')}`;
|
|
136
|
+
const diagnostics = document.createElement('p');
|
|
137
|
+
diagnostics.textContent = `Related diagnostics: ${explanation.relatedDiagnostics.map((item) => item.id).join(', ') || 'none observed'}`;
|
|
138
|
+
const remediation = document.createElement('p');
|
|
139
|
+
remediation.textContent = `Manual next step: ${explanation.manualRemediation}`;
|
|
140
|
+
details.append(summary, cause, paths, diagnostics, unknown, remediation);
|
|
141
|
+
section.append(details);
|
|
142
|
+
}
|
|
143
|
+
const next = document.createElement('p');
|
|
144
|
+
next.textContent = `${result.nextStep} Doctor did not modify your profile.`;
|
|
145
|
+
section.append(next);
|
|
146
|
+
return section;
|
|
147
|
+
}
|
|
110
148
|
export function createCompositionExplorer(document, report) {
|
|
111
149
|
const section = document.createElement('section');
|
|
112
150
|
section.setAttribute('aria-labelledby', 'dsh-composition-doctor-explorer');
|
|
@@ -166,6 +204,10 @@ export function createCompositionExplorer(document, report) {
|
|
|
166
204
|
}
|
|
167
205
|
});
|
|
168
206
|
select.dataset.compositionNode = node.id;
|
|
207
|
+
if (report.failureExplanation?.explanations.some((item) => item.provenancePaths.some((path) => path.rowKey === node.rowKey && node.rowKey !== undefined || path.packageName === node.packageName && node.packageName !== undefined))) {
|
|
208
|
+
select.dataset.failureRelated = 'true';
|
|
209
|
+
select.textContent = `${node.entity}: ${node.label} [failure evidence]`;
|
|
210
|
+
}
|
|
169
211
|
item.append(select);
|
|
170
212
|
const outgoing = graph.edges.filter((edge) => edge.from === node.id);
|
|
171
213
|
if (outgoing.length > 0) {
|
|
@@ -286,6 +328,7 @@ export function createReportView(document = globalThis.document, translate = (ke
|
|
|
286
328
|
status.textContent = `Generated: ${report.generatedAt}. Profile: ${report.profileDir}. Diagnostics: ${model.counts.error} error, ${model.counts.warning} warning, ${model.counts.info} info.`;
|
|
287
329
|
status.append(document.createTextNode(' '), badge);
|
|
288
330
|
root.append(renderConflictGraph(document, model.graph));
|
|
331
|
+
root.append(createFailureExplanation(document, report));
|
|
289
332
|
root.append(createCompositionExplorer(document, report));
|
|
290
333
|
const list = document.createElement('ul');
|
|
291
334
|
for (const diagnostic of report.diagnostics) {
|
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
|
|
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
|
`;
|
|
@@ -171,6 +206,45 @@ window.__ModuleLoader__.load({
|
|
|
171
206
|
section.append(list);
|
|
172
207
|
return section;
|
|
173
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
|
+
}
|
|
174
248
|
function createCompositionExplorer(document, report) {
|
|
175
249
|
const section = document.createElement("section");
|
|
176
250
|
section.setAttribute("aria-labelledby", "dsh-composition-doctor-explorer");
|
|
@@ -226,6 +300,10 @@ window.__ModuleLoader__.load({
|
|
|
226
300
|
}
|
|
227
301
|
});
|
|
228
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
|
+
}
|
|
229
307
|
item.append(select);
|
|
230
308
|
const outgoing = graph.edges.filter((edge) => edge.from === node.id);
|
|
231
309
|
if (outgoing.length > 0) {
|
|
@@ -349,6 +427,7 @@ window.__ModuleLoader__.load({
|
|
|
349
427
|
status.textContent = `Generated: ${report.generatedAt}. Profile: ${report.profileDir}. Diagnostics: ${model.counts.error} error, ${model.counts.warning} warning, ${model.counts.info} info.`;
|
|
350
428
|
status.append(document.createTextNode(" "), badge);
|
|
351
429
|
root.append(renderConflictGraph(document, model.graph));
|
|
430
|
+
root.append(createFailureExplanation(document, report));
|
|
352
431
|
root.append(createCompositionExplorer(document, report));
|
|
353
432
|
const list = document.createElement("ul");
|
|
354
433
|
for (const diagnostic of report.diagnostics) {
|
|
@@ -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
|
-
|
|
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,
|
|
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,
|
|
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',
|
|
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 };
|
|
@@ -58,6 +58,7 @@ export function buildCompositionFacts(model, diagnostics) {
|
|
|
58
58
|
const nodes = new Map();
|
|
59
59
|
const edgeMap = new Map();
|
|
60
60
|
const rows = [];
|
|
61
|
+
const keyOccurrences = new Map();
|
|
61
62
|
const packages = model.installedPackages ?? [];
|
|
62
63
|
const reportMode = model.evidenceMode ?? 'static';
|
|
63
64
|
const addNode = (node) => nodes.set(node.id, node);
|
|
@@ -68,7 +69,10 @@ export function buildCompositionFacts(model, diagnostics) {
|
|
|
68
69
|
}
|
|
69
70
|
for (const [index, row] of model.rows.entries()) {
|
|
70
71
|
const rowIdentity = row.id ?? row.name ?? `${normalizeCompositionSource(row.source, model.profileDir)}:${index}`;
|
|
71
|
-
const
|
|
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}`;
|
|
72
76
|
const chain = row.provenance === undefined ? [] : [...row.provenance];
|
|
73
77
|
const chainSources = chain.map((source) => normalizeCompositionSource(source, model.profileDir));
|
|
74
78
|
const source = normalizeCompositionSource(row.source, model.profileDir);
|
|
@@ -104,6 +108,7 @@ export function buildCompositionFacts(model, diagnostics) {
|
|
|
104
108
|
}));
|
|
105
109
|
const fact = {
|
|
106
110
|
entity: 'row', key: rowKey,
|
|
111
|
+
...(row.operation === undefined ? {} : { operation: row.operation }),
|
|
107
112
|
...(row.id === undefined ? {} : { id: row.id }),
|
|
108
113
|
...(row.name === undefined ? {} : { name: row.name }),
|
|
109
114
|
source,
|
|
@@ -106,7 +106,7 @@ export function parseDshDump(stdout, stderr = '') {
|
|
|
106
106
|
for (const detail of unmatched)
|
|
107
107
|
diagnostics.push({
|
|
108
108
|
id: 'unmatched-patch-target', severity: 'warning', title: 'Resolved patch target did not match a row',
|
|
109
|
-
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] }) }],
|
|
110
110
|
explanation: 'The public DSH dump reported a patch target that did not match a resolved row.',
|
|
111
111
|
remediation: 'Verify the patch row id against the selected bundle/profile composition.'
|
|
112
112
|
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export const maxFailureLogBytes = 1024 * 1024;
|
|
2
|
+
const titles = {
|
|
3
|
+
'duplicate-loader-entry': 'Duplicate loader entry id', 'bundle-unavailable': 'Profile bundle cannot be resolved',
|
|
4
|
+
'bundle-patch-unavailable': 'Bundle patch is unavailable', 'patch-target-missing': 'Patch entry not found',
|
|
5
|
+
'peer-incompatible': 'Plugin peer/version incompatibility', 'config-replacement': 'Whole-config replacement / row override'
|
|
6
|
+
};
|
|
7
|
+
const ids = {
|
|
8
|
+
'duplicate-loader-entry': ['duplicate-loader-declarations', 'duplicate-row-in-same-layer', 'intentional-row-override'],
|
|
9
|
+
'bundle-unavailable': ['bundle-not-installed'],
|
|
10
|
+
'bundle-patch-unavailable': ['bundle-patch-missing', 'bundle-patch-unavailable', 'bundle-patch-invalid', 'bundle-patch-unsafe'],
|
|
11
|
+
'patch-target-missing': ['unmatched-patch-target'], 'peer-incompatible': ['peer-version-mismatch', 'peer-version-facts-unavailable'],
|
|
12
|
+
'config-replacement': ['row-config-replacement-risk', 'intentional-row-override']
|
|
13
|
+
};
|
|
14
|
+
/** No raw log lines, stack traces, URLs, paths or values are persisted. */
|
|
15
|
+
export function safeFailureEntity(value) {
|
|
16
|
+
return /^(?:@[a-z0-9_.-]+\/)?[a-z0-9_][a-z0-9_.:-]{0,159}$/i.test(value) &&
|
|
17
|
+
!/(?:^(?:sk-|gh[pousr]_|eyJ)|token|password|credential|secret|authorization)/i.test(value) ? value : undefined;
|
|
18
|
+
}
|
|
19
|
+
export function matchFailureSignatures(log) {
|
|
20
|
+
const signatures = new Map();
|
|
21
|
+
const entity = '(?:@[a-z0-9_.-]+/)?[a-z0-9_][a-z0-9_.:-]{0,159}';
|
|
22
|
+
const add = (pattern, value) => {
|
|
23
|
+
const candidate = value === undefined ? undefined : safeFailureEntity(value);
|
|
24
|
+
if (value !== undefined && candidate === undefined)
|
|
25
|
+
return;
|
|
26
|
+
const signature = { pattern, ...(candidate === undefined ? {} : { entity: candidate }) };
|
|
27
|
+
if (signatures.size < 32)
|
|
28
|
+
signatures.set(`${pattern}:${candidate ?? ''}`, signature);
|
|
29
|
+
};
|
|
30
|
+
// Bound the work before matching; strip terminal control sequences, never render them.
|
|
31
|
+
const text = log.slice(0, maxFailureLogBytes).replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
32
|
+
for (const line of text.split(/\r?\n/)) {
|
|
33
|
+
if (line.length > 4096)
|
|
34
|
+
continue;
|
|
35
|
+
let match = new RegExp(`duplicate loader entry id:\\s*["']?(${entity})(?=["'\\s]|$)`, 'i').exec(line);
|
|
36
|
+
if (match)
|
|
37
|
+
add('duplicate-loader-entry', match[1]);
|
|
38
|
+
match = new RegExp(`cannot resolve profile bundle ["'](${entity})["']`, 'i').exec(line);
|
|
39
|
+
if (match)
|
|
40
|
+
add('bundle-unavailable', match[1]);
|
|
41
|
+
match = new RegExp(`patch:\\s*entry ["'](${entity})["'] not found`, 'i').exec(line);
|
|
42
|
+
if (match)
|
|
43
|
+
add('patch-target-missing', match[1]);
|
|
44
|
+
if (/failed to read overlay\b/i.test(line)) {
|
|
45
|
+
match = new RegExp(`node_modules[\\\\/](${entity.replace('/', '[\\\\/]')})[\\\\/]cordis\\.patch\\.ya?ml(?=[\\s:]|$)`, 'i').exec(line);
|
|
46
|
+
add('bundle-patch-unavailable', match?.[1]?.replaceAll('\\', '/'));
|
|
47
|
+
}
|
|
48
|
+
match = new RegExp(`incompatible-version:\\s*(${entity})(?=[\\s,]|$)`, 'i').exec(line);
|
|
49
|
+
if (match)
|
|
50
|
+
add('peer-incompatible', match[1]);
|
|
51
|
+
match = new RegExp(`Plugin (${entity})@[0-9][a-z0-9.+-]* is incompatible with dsh [0-9][a-z0-9.+-]*: peerDependencies`, 'i').exec(line);
|
|
52
|
+
if (match)
|
|
53
|
+
add('peer-incompatible', match[1]);
|
|
54
|
+
else if (/\bERR_PNPM_PEER_DEP_ISSUES\b|\bplugin compatibility failure\b/.test(line))
|
|
55
|
+
add('peer-incompatible');
|
|
56
|
+
}
|
|
57
|
+
return [...signatures.values()];
|
|
58
|
+
}
|
|
59
|
+
function path(row) {
|
|
60
|
+
return { source: row.source, ...(row.layer === undefined ? {} : { layer: row.layer }),
|
|
61
|
+
...(row.packageName === undefined ? {} : { packageName: row.packageName }),
|
|
62
|
+
...(row.id === undefined ? {} : { rowId: row.id }), rowKey: row.key, provenance: row.provenance, evidence: row.evidenceMode };
|
|
63
|
+
}
|
|
64
|
+
function distinctRows(rows) {
|
|
65
|
+
// Preserve same-layer duplicates with their own row keys.
|
|
66
|
+
return [...new Map(rows.map((row) => [row.key, row])).values()];
|
|
67
|
+
}
|
|
68
|
+
function diagnosticEntities(diagnostic) {
|
|
69
|
+
return [...new Set(diagnostic.evidence.flatMap((item) => {
|
|
70
|
+
return [item.packageName, item.subject].filter((name) => name !== undefined && safeFailureEntity(name) !== undefined);
|
|
71
|
+
}))];
|
|
72
|
+
}
|
|
73
|
+
function explain(signature, report, reported) {
|
|
74
|
+
const { pattern, entity } = signature;
|
|
75
|
+
const allRows = report.compositionFacts?.rows ?? [];
|
|
76
|
+
const rowPattern = pattern === 'duplicate-loader-entry' || pattern === 'patch-target-missing' || pattern === 'config-replacement';
|
|
77
|
+
const rows = allRows.filter((row) => rowPattern ? row.id === entity :
|
|
78
|
+
row.packageName === entity || row.provenance.some((step) => step.source === entity));
|
|
79
|
+
const diagnostics = report.diagnostics.filter((item) => ids[pattern].includes(item.id) &&
|
|
80
|
+
(entity === undefined || diagnosticEntities(item).includes(entity)));
|
|
81
|
+
let paths = distinctRows(rows).map(path);
|
|
82
|
+
let cause = 'unknown: the reported signature has no supporting condition in the selected profile evidence.';
|
|
83
|
+
let observed = false;
|
|
84
|
+
let evidenceLevel = 'unknown';
|
|
85
|
+
const unknowns = ['Runtime failure and plugin behavior: not observed / not-run.'];
|
|
86
|
+
let remediation = 'Check the selected profile directory and capture the first DSH error. Compare it with the observed rows; do not remove a plugin based on a log string alone.';
|
|
87
|
+
if (pattern === 'duplicate-loader-entry') {
|
|
88
|
+
const inserted = distinctRows(rows.filter((row) => row.operation === 'insert' || row.operation === 'declare'));
|
|
89
|
+
const composed = distinctRows(rows.filter((row) => row.evidenceMode === 'composed'));
|
|
90
|
+
const candidates = composed.length > 1 ? composed : inserted;
|
|
91
|
+
paths = candidates.map(path);
|
|
92
|
+
if (candidates.length > 1) {
|
|
93
|
+
observed = true;
|
|
94
|
+
evidenceLevel = composed.length > 1 ? 'composed' : 'static';
|
|
95
|
+
cause = evidenceLevel === 'static'
|
|
96
|
+
? `${entity} is introduced by ${candidates.length} separate loader declarations. Both inserts use the same id; the final loader outcome is unknown until composition/runtime is observed.`
|
|
97
|
+
: `${entity} occurs more than once in the observed composed tree. These entries share a loader id; runtime rejection was not observed by Doctor.`;
|
|
98
|
+
}
|
|
99
|
+
else
|
|
100
|
+
unknowns.push('Two independent loader insert paths were not observed. An update or provenance patch chain is not a second insertion.');
|
|
101
|
+
remediation = 'Review the listed bundle declarations and profile patch. Keep one intended insertion for this id after checking its config; use why row and impact bundle for details.';
|
|
102
|
+
}
|
|
103
|
+
else if (pattern === 'config-replacement') {
|
|
104
|
+
const updates = rows.filter((row) => row.operation === 'update' && row.replacement.state !== 'no');
|
|
105
|
+
const replacements = rows.filter((row) => row.evidenceMode === 'composed' && row.replacement.state === 'yes');
|
|
106
|
+
const chain = rows.filter((row) => row.provenance.length > 1);
|
|
107
|
+
if (updates.length > 0 || replacements.length > 0 || chain.length > 0) {
|
|
108
|
+
observed = true;
|
|
109
|
+
evidenceLevel = replacements.length > 0 || chain.some((row) => row.evidenceMode === 'composed') ? 'composed' : 'static';
|
|
110
|
+
cause = replacements.length > 0 ? 'The composition provider explicitly reports a replacement for this row.' : updates.length > 0
|
|
111
|
+
? 'The profile declares a config update for this row. DSH patch semantics replace the whole config rather than deep-merge its keys.'
|
|
112
|
+
: 'The public composition source chain records this row being patched by a later source. Whole-config replacement is unknown without explicit replacement evidence.';
|
|
113
|
+
const previous = rows.filter((row) => row.operation === 'insert' || row.operation === 'declare');
|
|
114
|
+
if (previous.length > 0 && updates.length > 0) {
|
|
115
|
+
const dropped = previous.flatMap((row) => row.configKeys).filter((key) => !updates[updates.length - 1].configKeys.includes(key));
|
|
116
|
+
if (dropped.length > 0)
|
|
117
|
+
cause += ` The later declaration omits ${new Set(dropped).size} earlier config key(s); this is a static difference, not proof of runtime data loss.`;
|
|
118
|
+
}
|
|
119
|
+
unknowns.push('Prior effective values, field ownership and runtime effects are unknown. A source chain alone does not prove removed keys.');
|
|
120
|
+
}
|
|
121
|
+
remediation = 'Review the earlier row and the later patch together. Include required non-secret fields in the complete replacement config, then compare snapshots/diff or run preflight before upgrading.';
|
|
122
|
+
}
|
|
123
|
+
else if (pattern === 'patch-target-missing') {
|
|
124
|
+
const composedMissing = diagnostics.some((item) => item.evidence.some((entry) => entry.evidenceKind === 'composed' && entry.subject === entity));
|
|
125
|
+
const updates = rows.filter((row) => row.operation === 'update');
|
|
126
|
+
const introduction = rows.some((row) => row.operation === 'insert' || row.operation === 'declare');
|
|
127
|
+
if (composedMissing || (updates.length > 0 && !introduction && !rows.some((row) => row.evidenceMode === 'composed'))) {
|
|
128
|
+
observed = true;
|
|
129
|
+
evidenceLevel = composedMissing ? 'composed' : 'static';
|
|
130
|
+
cause = composedMissing ? 'The public dump provider reports that this patch target did not match a composed row.' :
|
|
131
|
+
'A patch targets this id, but no insertion is present in the permitted selected profile metadata. A built-in, home or CLI layer could still supply it; final absence is unknown.';
|
|
132
|
+
}
|
|
133
|
+
remediation = 'Compare the patch id with the intended bundle row. Check whether the bundle is declared and available; inspect home/CLI layers manually if they supply the row.';
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const supporting = diagnostics.filter((item) => pattern !== 'peer-incompatible' || item.id === 'peer-version-mismatch');
|
|
137
|
+
if (supporting.length > 0 && entity !== undefined) {
|
|
138
|
+
observed = true;
|
|
139
|
+
evidenceLevel = 'static';
|
|
140
|
+
cause = pattern === 'bundle-unavailable' ? 'This bundle is declared in dsh.profile.bundles, but the bounded resolver cannot read its package metadata inside the selected profile. An installation-level bundle or external link may still exist.' :
|
|
141
|
+
pattern === 'bundle-patch-unavailable' ? 'The declared bundle has missing, invalid or disallowed patch metadata. Its manifest declaration does not supply an observable loader contribution.' :
|
|
142
|
+
supporting.map((item) => item.evidence.map((entry) => entry.detail).join('; ')).join('; ');
|
|
143
|
+
}
|
|
144
|
+
paths = [...paths, ...diagnostics.flatMap((item) => item.evidence.map((entry) => ({ source: entry.source,
|
|
145
|
+
...(entry.packageName === undefined ? {} : { packageName: entry.packageName }), provenance: [], evidence: entry.evidenceKind })))];
|
|
146
|
+
remediation = pattern === 'peer-incompatible' ? 'Compare the installed plugin peer range with the selected DSH version. Choose a tested compatible pair or rehearse it with preflight; a peer warning alone does not prove a crash.' :
|
|
147
|
+
pattern === 'bundle-unavailable' ? 'Review package.json → dsh.profile.bundles and the intended installed package or link. Confirm installation-level resolution manually before reinstalling anything.' :
|
|
148
|
+
'Review the bundle manifest → dsh.bundle.patch and packaged YAML file. Check the publisher tarball contents; do not substitute or execute a file suggested by the log.';
|
|
149
|
+
if (pattern === 'peer-incompatible' && !observed)
|
|
150
|
+
unknowns.push('The selected runtime version or exact affected package is unknown. Log-reported versions are not trusted runtime facts.');
|
|
151
|
+
}
|
|
152
|
+
if (evidenceLevel === 'static')
|
|
153
|
+
unknowns.push('Final composition: unknown. Home/CLI overlays and installation-level bundles are not inspected by the default static reader.');
|
|
154
|
+
if (entity === undefined)
|
|
155
|
+
unknowns.push('Offending entity: unknown; multiple packages may fit. No package was selected by guessing.');
|
|
156
|
+
const entityObserved = entity !== undefined && (rows.length > 0 || diagnostics.some((item) => diagnosticEntities(item).includes(entity)) || report.compositionFacts?.nodes.some((node) => node.entity === 'bundle' && node.label === entity) === true);
|
|
157
|
+
if (entity !== undefined && !entityObserved)
|
|
158
|
+
unknowns.push('Reported entity: not observed in selected profile/composition metadata.');
|
|
159
|
+
if (paths.length === 0)
|
|
160
|
+
unknowns.push('Provenance: unavailable / not observed.');
|
|
161
|
+
return { pattern, detectedFailure: `${titles[pattern]}${entity === undefined ? '' : `: ${entity}`}`, origin: reported ? 'reported-by-log' : 'observed-from-composition',
|
|
162
|
+
...(!entityObserved || entity === undefined ? {} : { matchedEntity: { kind: rowPattern ? 'row' : 'package', id: entity } }),
|
|
163
|
+
causeStatus: observed ? 'observable-condition' : 'unknown', observableCause: cause,
|
|
164
|
+
provenancePaths: [...new Map(paths.map((entry) => [`${entry.rowKey ?? ''}|${entry.source}|${entry.layer ?? ''}|${entry.packageName ?? ''}`, entry])).values()], evidenceLevel, unknowns, manualRemediation: remediation, relatedDiagnostics: diagnostics };
|
|
165
|
+
}
|
|
166
|
+
/** Pure matching/explanation layer; log strings cannot create composition facts. */
|
|
167
|
+
export function explainFailures({ report, log }) {
|
|
168
|
+
const reported = log !== undefined;
|
|
169
|
+
const signatures = reported ? matchFailureSignatures(log) : [];
|
|
170
|
+
if (!reported) {
|
|
171
|
+
const add = (pattern, entity) => {
|
|
172
|
+
if (safeFailureEntity(entity) !== undefined && !signatures.some((item) => item.pattern === pattern && item.entity === entity))
|
|
173
|
+
signatures.push({ pattern, entity });
|
|
174
|
+
};
|
|
175
|
+
const rows = report.compositionFacts?.rows ?? [];
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
if (row.id === undefined)
|
|
178
|
+
continue;
|
|
179
|
+
if (rows.filter((other) => other.id === row.id && (other.operation === 'insert' || other.operation === 'declare')).length > 1 ||
|
|
180
|
+
rows.filter((other) => other.id === row.id && other.evidenceMode === 'composed').length > 1)
|
|
181
|
+
add('duplicate-loader-entry', row.id);
|
|
182
|
+
if (row.operation === 'update' && !rows.some((other) => other.id === row.id && (other.operation === 'insert' || other.operation === 'declare')))
|
|
183
|
+
add('patch-target-missing', row.id);
|
|
184
|
+
if ((row.operation === 'update' && row.replacement.state !== 'no') || row.provenance.length > 1 || row.replacement.state === 'yes')
|
|
185
|
+
add('config-replacement', row.id);
|
|
186
|
+
}
|
|
187
|
+
for (const diagnostic of report.diagnostics) {
|
|
188
|
+
for (const pattern of ['bundle-unavailable', 'bundle-patch-unavailable', 'peer-incompatible']) {
|
|
189
|
+
if (!ids[pattern].includes(diagnostic.id) || diagnostic.id === 'peer-version-facts-unavailable')
|
|
190
|
+
continue;
|
|
191
|
+
for (const name of [...new Set(diagnostic.evidence.map((entry) => entry.packageName ?? entry.subject).filter((value) => value !== undefined))])
|
|
192
|
+
add(pattern, name);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const explanations = signatures.slice(0, 50).map((signature) => explain(signature, report, reported));
|
|
197
|
+
const unreadable = report.diagnostics.filter((item) => ['profile-patch-invalid', 'bundle-inventory-manifest-invalid', 'runtime-composition-unavailable', 'composed-dump-parse-failed'].includes(item.id)).map((item) => item.id);
|
|
198
|
+
return { schemaVersion: 1, outcome: explanations.some((item) => item.causeStatus === 'observable-condition') ? 'explained' :
|
|
199
|
+
explanations.length > 0 || (!reported && unreadable.length > 0) ? 'unknown' : reported ? 'no-match' : 'no-supported-issues', profile: '<PROFILE>', runtime: 'not-observed',
|
|
200
|
+
input: { log: reported ? 'reported-by-log' : 'not-provided', recognizedSignatures: signatures.length,
|
|
201
|
+
truncated: signatures.length > 50 || (reported && signatures.length >= 32) || (log !== undefined && new TextEncoder().encode(log).length > maxFailureLogBytes) }, explanations,
|
|
202
|
+
unknowns: ['A recognized log signature is a report, not trusted composition or runtime evidence.', 'An absence of supported findings is not a healthy runtime verification.', ...unreadable.map((id) => `Metadata coverage incomplete: ${id}.`)],
|
|
203
|
+
nextStep: reported ? 'Review the named source paths. If the cause is unknown, verify the profile selection and compare a public dump, why row, snapshot/diff or preflight.' :
|
|
204
|
+
'To focus on a startup symptom: dsh-doctor diagnose --profile <dir> --log <file> (or pipe DSH stderr to diagnose).' };
|
|
205
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative } from 'node:path';
|
|
3
|
+
import { parse } from 'yaml';
|
|
4
|
+
import { valid, validRange } from 'semver';
|
|
5
|
+
import { resolveComposition } from './composition-adapter.js';
|
|
6
|
+
import { safeFailureEntity } from './failure-explainer.js';
|
|
7
|
+
function record(value) {
|
|
8
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function note(id, source, packageName, detail) {
|
|
11
|
+
return { id, severity: 'warning', title: detail,
|
|
12
|
+
evidence: [{ source, ...(packageName === undefined ? {} : { packageName, subject: packageName }), detail, evidenceKind: 'static' }],
|
|
13
|
+
explanation: detail, remediation: 'Review the declared bundle patch and selected profile. Doctor did not change either.' };
|
|
14
|
+
}
|
|
15
|
+
/** Parse declarations, never simulate a runtime tree or evaluate YAML tags. */
|
|
16
|
+
function declarations(text, source, layer, layerOrder, bundle) {
|
|
17
|
+
const parsed = parse(text, { maxAliasCount: 50, logLevel: 'silent', customTags: [
|
|
18
|
+
{ tag: 'tag:yaml.org,2002:js', resolve: () => '<runtime value: not-run>' },
|
|
19
|
+
{ tag: 'tag:yaml.org,2002:js/function', resolve: () => '<runtime value: not-run>' }
|
|
20
|
+
] });
|
|
21
|
+
const entries = Array.isArray(parsed) ? parsed : layer === 'cordis.yml' && record(parsed) ? parsed.plugins : undefined;
|
|
22
|
+
if (!Array.isArray(entries))
|
|
23
|
+
throw new Error('Patch must be a top-level array');
|
|
24
|
+
const rows = [];
|
|
25
|
+
const add = (entry, operation) => {
|
|
26
|
+
if (!record(entry))
|
|
27
|
+
return;
|
|
28
|
+
rows.push({ ...(typeof entry.id === 'string' && safeFailureEntity(entry.id) !== undefined ? { id: entry.id } : {}),
|
|
29
|
+
...(typeof entry.name === 'string' && safeFailureEntity(entry.name) !== undefined ? { name: entry.name } : {}),
|
|
30
|
+
source, layer, layerOrder, provenance: [bundle ?? source], operation, evidenceKind: 'static',
|
|
31
|
+
...(operation === 'update' && !('config' in entry) ? { replacement: false } : {}),
|
|
32
|
+
...('config' in entry ? { configKeys: record(entry.config) ? Object.keys(entry.config).filter((key) => /^[a-z_][\w.-]{0,127}$/i.test(key)).sort() : [],
|
|
33
|
+
...(operation === 'update' ? { replacement: true, replacementBasis: 'derived' } : {}) } : {}) });
|
|
34
|
+
};
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!record(entry))
|
|
37
|
+
continue;
|
|
38
|
+
if ('insert' in entry) {
|
|
39
|
+
if (!Array.isArray(entry.insert))
|
|
40
|
+
throw new Error('Insert must be an array');
|
|
41
|
+
for (const inserted of entry.insert)
|
|
42
|
+
add(inserted, 'insert');
|
|
43
|
+
}
|
|
44
|
+
else
|
|
45
|
+
add(entry, layer === 'cordis.yml' ? 'declare' : 'update');
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
/** Default diagnose reads only selected profile metadata and declared YAML patches.
|
|
50
|
+
* It does not look up executables, launch DSH, import packages or run lifecycles. */
|
|
51
|
+
export async function readFailureComposition(input, composed = false) {
|
|
52
|
+
const rows = [];
|
|
53
|
+
const diagnostics = [...input.inventoryDiagnostics];
|
|
54
|
+
const root = await realpath(input.profileDir);
|
|
55
|
+
for (const [index, pkg] of input.installedPackages.entries()) {
|
|
56
|
+
if (pkg.bundlePatch === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
try {
|
|
59
|
+
const path = await realpath(pkg.bundlePatch);
|
|
60
|
+
const rel = relative(root, path);
|
|
61
|
+
// A manifest is untrusted: a patch declaration is not permission to read secrets.
|
|
62
|
+
if (isAbsolute(rel) || rel.startsWith('..') || !/\.(?:yml|yaml)$/i.test(path) ||
|
|
63
|
+
/(?:^|[\\/])(?:\.env[^\\/]*|[^\\/]*(?:credential|token|secret|password)[^\\/]*|sessions?|chats?|workspace)(?:[\\/]|$)/i.test(rel)) {
|
|
64
|
+
diagnostics.push(note('bundle-patch-unsafe', pkg.packageJsonSource, pkg.name, 'Declared patch is outside the permitted YAML metadata surface.'));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const meta = await lstat(path);
|
|
68
|
+
if (!meta.isFile() || meta.size > 1024 * 1024)
|
|
69
|
+
throw new Error('Patch unavailable');
|
|
70
|
+
rows.push(...declarations(await readFile(path, 'utf8'), path, `dsh.profile.bundles[${index}]: ${pkg.name}`, index, pkg.name));
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// No parser snippets, values, or raw filesystem error text enter the report.
|
|
74
|
+
diagnostics.push(note('bundle-patch-invalid', pkg.packageJsonSource, pkg.name, 'Declared bundle patch is missing, unreadable, oversized or invalid YAML.'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const file of input.files) {
|
|
78
|
+
if (file.relativePath !== 'cordis.yml' && file.relativePath !== 'cordis.patch.yml')
|
|
79
|
+
continue;
|
|
80
|
+
try {
|
|
81
|
+
rows.push(...declarations(file.text, file.relativePath, file.relativePath, input.installedPackages.length + (file.relativePath === 'cordis.yml' ? 0 : 1)));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
diagnostics.push(note('profile-patch-invalid', file.relativePath, undefined, 'Selected profile YAML is invalid or not a supported top-level array.'));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const safeRange = (value) => value !== undefined && value.length <= 256 && validRange(value) !== null ? value : undefined;
|
|
88
|
+
const packages = input.installedPackages.filter((pkg) => safeFailureEntity(pkg.name) !== undefined).map((pkg) => ({
|
|
89
|
+
...pkg, installedVersion: pkg.installedVersion !== undefined && valid(pkg.installedVersion) !== null ? pkg.installedVersion : undefined
|
|
90
|
+
}));
|
|
91
|
+
const peers = packages.flatMap((pkg) => [
|
|
92
|
+
{ packageName: pkg.name, source: pkg.packageJsonSource, evidenceKind: 'static', cordis: safeRange(pkg.peerCordis), node: safeRange(pkg.engineNode) },
|
|
93
|
+
...Object.entries(pkg.dshPeers ?? (pkg.peerDsh === undefined ? {} : { '@deepseek-ai/dsh': pkg.peerDsh })).map(([dshPackage, range]) => ({
|
|
94
|
+
packageName: pkg.name, source: pkg.packageJsonSource, evidenceKind: 'static', dshPackage,
|
|
95
|
+
dsh: ['workspace:^', 'workspace:~', 'workspace:*'].includes(range) ? range : safeRange(range)
|
|
96
|
+
}))
|
|
97
|
+
]);
|
|
98
|
+
const base = { profileDir: root, rows, installedPackages: packages,
|
|
99
|
+
adapterDiagnostics: diagnostics, evidenceMode: 'static', runtimeObservation: { observed: false },
|
|
100
|
+
metadataCoverage: input.metadataCoverage, peerRequirements: peers,
|
|
101
|
+
runtime: { node: process.versions.node, platform: process.platform } };
|
|
102
|
+
if (!composed)
|
|
103
|
+
return base;
|
|
104
|
+
const observed = await resolveComposition(input);
|
|
105
|
+
// Preserve declarations separately via their evidence kind; a dump that dedupes
|
|
106
|
+
// rows cannot upgrade a static duplicate-path finding into a composed collision.
|
|
107
|
+
return { ...base, rows: [...rows, ...observed.rows.filter((row) => row.evidenceKind === 'composed')],
|
|
108
|
+
adapterDiagnostics: [...diagnostics, ...observed.adapterDiagnostics],
|
|
109
|
+
evidenceMode: observed.evidenceMode === 'composed' ? 'mixed' : 'static' };
|
|
110
|
+
}
|
|
@@ -36,6 +36,8 @@ export async function inspectInstalledPackage(profileDir, name, requestedSpec) {
|
|
|
36
36
|
const resolvedSource = await realpath(source);
|
|
37
37
|
if (!within(profileRoot, resolvedSource))
|
|
38
38
|
throw new Error(`Bundle package resolves outside the selected profile: ${name}`);
|
|
39
|
+
if (!/[\\/]package\.json$/i.test(resolvedSource) || /(?:^|[\\/])(?:\.env[^\\/]*|workspace|sessions?|chats?|[^\\/]*(?:credentials?|tokens?|secrets?|password)[^\\/]*)(?:[\\/]|$)/i.test(resolvedSource))
|
|
40
|
+
throw new Error('Bundle manifest is outside the permitted metadata surface');
|
|
39
41
|
const manifest = JSON.parse(await readFile(resolvedSource, 'utf8'));
|
|
40
42
|
const dsh = record(manifest.dsh);
|
|
41
43
|
const bundle = record(dsh?.bundle);
|
|
@@ -44,6 +46,7 @@ export async function inspectInstalledPackage(profileDir, name, requestedSpec) {
|
|
|
44
46
|
const engines = record(manifest.engines);
|
|
45
47
|
const scripts = record(manifest.scripts);
|
|
46
48
|
const peerDsh = string(peers?.['@deepseek-ai/dsh']);
|
|
49
|
+
const dshPeers = Object.fromEntries(Object.entries(peers ?? {}).filter(([key, value]) => /^@deepseek-ai\/dsh(?:-[a-z0-9_.-]+)?$/i.test(key) && typeof value === 'string'));
|
|
47
50
|
const peerCordis = string(peers?.['@deepseek-ai/cordis']) ?? string(peers?.cordis) ?? string(peers?.['@cordisjs/core']);
|
|
48
51
|
const engineNode = string(engines?.node);
|
|
49
52
|
const repository = repositoryValue(manifest.repository);
|
|
@@ -73,6 +76,7 @@ export async function inspectInstalledPackage(profileDir, name, requestedSpec) {
|
|
|
73
76
|
modifiedAt: fileStat.mtime.toISOString(),
|
|
74
77
|
...(Array.isArray(manifest.os) ? { platform: manifest.os.filter((item) => typeof item === 'string') } : {}),
|
|
75
78
|
...(peerDsh === undefined ? {} : { peerDsh }),
|
|
79
|
+
...(Object.keys(dshPeers).length === 0 ? {} : { dshPeers }),
|
|
76
80
|
...(peerCordis === undefined ? {} : { peerCordis }),
|
|
77
81
|
...(engineNode === undefined ? {} : { engineNode }),
|
|
78
82
|
...(scripts === undefined ? {} : { lifecycleScripts: ['preinstall', 'install', 'postinstall', 'prepare'].filter((key) => typeof scripts[key] === 'string') })
|