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.
@@ -1,5 +1,5 @@
1
1
  import { createHash, randomBytes } from 'node:crypto';
2
- import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { lstat, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
3
  import { basename, resolve } from 'node:path';
4
4
  import { gunzipSync } from 'node:zlib';
5
5
  function text(bytes) {
@@ -56,7 +56,23 @@ async function atomicWrite(path, bytes) {
56
56
  const temporaryPath = `${path}.${randomBytes(12).toString('hex')}.partial`;
57
57
  try {
58
58
  await writeFile(temporaryPath, bytes, { flag: 'wx' });
59
- await rename(temporaryPath, path);
59
+ try {
60
+ await rename(temporaryPath, path);
61
+ }
62
+ catch (error) {
63
+ // Windows can refuse a competing rename after another downloader has
64
+ // already published this exact artifact. Accept only identical regular
65
+ // files; never unlink an existing cache entry to work around the race.
66
+ const code = error.code;
67
+ if (code !== 'EPERM' && code !== 'EEXIST')
68
+ throw error;
69
+ const existing = await lstat(path).catch(() => undefined);
70
+ if (!existing?.isFile() || existing.isSymbolicLink())
71
+ throw error;
72
+ const published = await readFile(path).catch(() => undefined);
73
+ if (published === undefined || !published.equals(Buffer.from(bytes)))
74
+ throw error;
75
+ }
60
76
  }
61
77
  finally {
62
78
  await rm(temporaryPath, { force: true }).catch(() => undefined);
@@ -30,6 +30,8 @@ function satisfiesRange(version, range) {
30
30
  if (parseVersion(version) === undefined || range.trim().length === 0)
31
31
  return undefined;
32
32
  try {
33
+ if (['workspace:^', 'workspace:~', 'workspace:*'].includes(range))
34
+ return true;
33
35
  return semverSatisfies(version, range, { includePrerelease: true });
34
36
  }
35
37
  catch {
@@ -47,7 +49,8 @@ function peerDiagnostics(model) {
47
49
  if (range === undefined)
48
50
  continue;
49
51
  const actual = runtime[fact];
50
- const item = evidence(peer.source, `${peer.packageName}:${fact}`, `requires ${fact} ${range}; observed ${actual ?? 'unknown'}`, peer.evidenceKind, peer.packageName);
52
+ const label = fact === 'dsh' ? peer.dshPackage ?? fact : fact;
53
+ const item = evidence(peer.source, `${peer.packageName}:${fact}`, `requires ${label} ${range}; observed ${actual ?? 'unknown'}`, peer.evidenceKind, peer.packageName);
51
54
  if (actual === undefined) {
52
55
  diagnostics.push(diagnostic('peer-version-facts-unavailable', 'warning', 'Runtime version fact is unavailable', [item], `The ${fact} version needed to evaluate ${peer.packageName} was not supplied.`, 'Provide the selected runtime version or treat this static compatibility finding as unproven.'));
53
56
  continue;
@@ -86,12 +89,16 @@ function rowKeys(row) {
86
89
  function rowDiagnostics(rows) {
87
90
  const diagnostics = [];
88
91
  for (const [id, group] of groups(rows.filter((row) => row.id !== undefined), (row) => row.id)) {
92
+ const inserts = group.filter((row) => row.operation === 'insert' || row.operation === 'declare');
93
+ if (inserts.length > 1) {
94
+ diagnostics.push(diagnostic('duplicate-loader-declarations', 'warning', 'Multiple loader declarations introduce the same row id', inserts.map((row) => evidence(row.source, id, 'An independent declaration introduces this loader id.', row.evidenceKind, row.name)), 'Separate insertion declarations share a loader id; static evidence does not prove the final loader outcome.', 'Review the bundle and profile insertion paths; retain the intended declaration and configuration manually.'));
95
+ }
89
96
  const layers = groups(group, (row) => row.layer ?? row.source);
90
97
  if (layers.size === 1 && group.length > 1) {
91
98
  diagnostics.push(diagnostic('duplicate-row-in-same-layer', 'warning', 'Duplicate composition row id in one layer', group.map((row) => evidence(row.source, id, `row id ${id} is declared more than once in the same layer`, row.evidenceKind, row.name)), `The DSH public row semantics do not establish which declaration should win within one layer for ${id}.`, 'Give the row a unique id or verify the final resolved tree with a public dump provider.'));
92
99
  continue;
93
100
  }
94
- if (layers.size < 2)
101
+ if (layers.size < 2 || inserts.length === group.length)
95
102
  continue;
96
103
  const ordered = [...group].sort((left, right) => (left.layerOrder ?? Number.MAX_SAFE_INTEGER) - (right.layerOrder ?? Number.MAX_SAFE_INTEGER));
97
104
  const previous = ordered[ordered.length - 2];
@@ -53,7 +53,7 @@ function snapshotFromModel(model, input) {
53
53
  const hashes = Object.fromEntries((input?.files ?? []).filter((file) => ['package.json', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock'].includes(file.relativePath)).map((file) => [file.relativePath, file.sha256]));
54
54
  return {
55
55
  schemaVersion: 2,
56
- doctorVersion: '0.1.3',
56
+ doctorVersion: '0.4.0',
57
57
  runtime: { ...(model.runtime?.dsh === undefined ? {} : { dsh: model.runtime.dsh }), ...(model.runtime?.cordis === undefined ? {} : { cordis: model.runtime.cordis }), node: model.runtime?.node ?? process.versions.node, platform: model.runtime?.platform ?? process.platform },
58
58
  profile: { ...(profileName === undefined ? {} : { name: profileName }), ...(manifest === undefined ? {} : { manifestHash: manifest.sha256 }) },
59
59
  packages: sortJson(packages),
@@ -0,0 +1,23 @@
1
+ export function renderFailure(result, markdown = false) {
2
+ const lines = [markdown ? '# DSH Composition Doctor — Failure Explanation' : 'DSH Composition Doctor — Failure Explanation', '',
3
+ `Profile: ${result.profile}; runtime: ${result.runtime}`, ''];
4
+ if (result.explanations.length === 0)
5
+ lines.push(result.outcome === 'no-match'
6
+ ? '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.', '');
7
+ for (const item of result.explanations.slice(0, markdown ? 50 : 3)) {
8
+ lines.push(`${markdown ? '## ' : ''}${item.detectedFailure}`, `${item.origin}; evidence: ${item.evidenceLevel}`, '', `Cause: ${item.observableCause}`, '');
9
+ for (const [index, path] of item.provenancePaths.entries()) {
10
+ lines.push(`Path ${index + 1}: ${path.packageName === undefined ? '' : `bundle ${path.packageName} → `}${path.source}`, ` ${path.layer === undefined ? 'layer: unknown' : `layer: ${path.layer}`}${path.rowId === undefined ? '' : ` → row: ${path.rowId}`}`);
11
+ if (path.provenance.length > (markdown ? 0 : 1))
12
+ lines.push(` provenance: ${path.provenance.map((step) => `${step.relation}: ${step.source} (${step.basis})`).join(' → ')}`);
13
+ }
14
+ const unknown = markdown ? item.unknowns.join(' ') : `runtime not observed / not-run${item.evidenceLevel === 'static' ? '; final composition unknown' : ''}${item.causeStatus === 'unknown' ? '; cause unproven' : ''}.`;
15
+ lines.push('', `Unknown: ${unknown}`, `Next: ${item.manualRemediation}`, '');
16
+ }
17
+ if (!markdown && result.explanations.length > 3)
18
+ lines.push(`${result.explanations.length - 3} more explanation(s); use --format json or markdown.`, '');
19
+ if (markdown || result.explanations.length === 0 || result.input.log === 'not-provided')
20
+ lines.push(result.nextStep);
21
+ lines.push('Doctor did not modify your profile.', '');
22
+ return lines.join('\n');
23
+ }
@@ -1,3 +1,4 @@
1
+ import { renderFailure } from './failure.js';
1
2
  function formatEvidence(item) {
2
3
  const subject = item.subject === undefined ? '' : ` — ${item.subject}`;
3
4
  const packageLabel = item.packageName === undefined ? '' : ` (${item.packageName}${item.version === undefined ? '' : `@${item.version}`})`;
@@ -9,7 +10,8 @@ function formatDiagnostic(item) {
9
10
  }
10
11
  export function renderMarkdown(report) {
11
12
  const summary = ['error', 'warning', 'info'].map((severity) => `${severity}: ${report.diagnostics.filter((item) => item.severity === severity).length}`).join(', ');
12
- const body = report.diagnostics.length === 0 ? 'No diagnostics were produced.' : report.diagnostics.map(formatDiagnostic).join('\n\n');
13
+ const diagnosticBody = report.diagnostics.length === 0 ? 'No diagnostics were produced.' : report.diagnostics.map(formatDiagnostic).join('\n\n');
14
+ const body = report.failureExplanation === undefined ? diagnosticBody : `${renderFailure(report.failureExplanation, true)}\n${diagnosticBody}`;
13
15
  const coverage = report.metadataCoverage === undefined ? '' : `\nMetadata coverage: ${report.metadataCoverage.mode}; unscanned: ${report.metadataCoverage.unscannedSurfaces.join('; ')}\n`;
14
16
  return `# DSH Composition Doctor Report\n\nSchema: ${report.schemaVersion}; evidence schema: ${report.evidenceSchemaVersion}\n\nProfile: \`${report.profileDir}\`\n\nGenerated: ${report.generatedAt}\n\nEvidence mode: ${report.evidenceMode}; runtime observed: ${report.runtimeObserved ? 'yes' : 'no'}\n${coverage}\nSummary: ${summary}\n\n${body}\n`;
15
17
  }
@@ -12,6 +12,34 @@ 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
+ failureExplanation: {
16
+ type: 'object', required: ['schemaVersion', 'outcome', 'profile', 'runtime', 'input', 'explanations', 'unknowns', 'nextStep'],
17
+ properties: {
18
+ schemaVersion: { const: 1 }, outcome: { enum: ['explained', 'unknown', 'no-match', 'no-supported-issues'] },
19
+ profile: { const: '<PROFILE>' }, runtime: { const: 'not-observed' },
20
+ input: { type: 'object', required: ['log', 'recognizedSignatures', 'truncated'], properties: {
21
+ log: { enum: ['not-provided', 'reported-by-log'] }, recognizedSignatures: { type: 'integer', minimum: 0 }, truncated: { type: 'boolean' }
22
+ }, additionalProperties: false },
23
+ explanations: { type: 'array', items: {
24
+ type: 'object', required: ['pattern', 'detectedFailure', 'origin', 'causeStatus', 'observableCause', 'provenancePaths', 'evidenceLevel', 'unknowns', 'manualRemediation', 'relatedDiagnostics'],
25
+ properties: {
26
+ pattern: { enum: ['duplicate-loader-entry', 'bundle-unavailable', 'bundle-patch-unavailable', 'patch-target-missing', 'peer-incompatible', 'config-replacement'] },
27
+ detectedFailure: { type: 'string' }, origin: { enum: ['reported-by-log', 'observed-from-composition'] },
28
+ matchedEntity: { type: 'object', required: ['kind', 'id'], properties: { kind: { enum: ['row', 'package'] }, id: { type: 'string' } }, additionalProperties: false },
29
+ causeStatus: { enum: ['observable-condition', 'unknown'] }, observableCause: { type: 'string' },
30
+ provenancePaths: { type: 'array', items: { type: 'object', required: ['source', 'provenance', 'evidence'], properties: {
31
+ source: { type: 'string' }, layer: { type: 'string' }, packageName: { type: 'string' }, rowId: { type: 'string' }, rowKey: { type: 'string' },
32
+ 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 } },
33
+ evidence: { enum: ['static', 'composed', 'runtime-observed'] }
34
+ }, additionalProperties: false } },
35
+ evidenceLevel: { enum: ['static', 'composed', 'runtime-observed', 'unknown'] },
36
+ unknowns: { type: 'array', items: { type: 'string' } }, manualRemediation: { type: 'string' },
37
+ relatedDiagnostics: { type: 'array', items: { type: 'object' } }
38
+ }, additionalProperties: false
39
+ } },
40
+ unknowns: { type: 'array', items: { type: 'string' } }, nextStep: { type: 'string' }
41
+ }, additionalProperties: false
42
+ },
15
43
  compositionFacts: {
16
44
  type: 'object',
17
45
  required: ['schemaVersion', 'evidenceMode', 'nodes', 'edges', 'rows', 'unknownSurfaces'],
@@ -39,6 +67,7 @@ export const analysisReportJsonSchema = {
39
67
  type: 'object', required: ['entity', 'key', 'source', 'sourceBasis', 'provenance', 'replacement', 'configKeys', 'configKeysBasis', 'evidenceMode', 'relatedDiagnosticIds', 'unknown'],
40
68
  properties: {
41
69
  entity: { const: 'row' }, key: { type: 'string' }, id: { type: 'string' }, name: { type: 'string' },
70
+ operation: { enum: ['insert', 'update', 'declare'] },
42
71
  source: { type: 'string' }, sourceBasis: { const: 'observed' }, layer: { type: 'string' }, layerOrder: { type: 'number' },
43
72
  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
73
  replacement: { type: 'object', required: ['state', 'basis'], properties: { state: { enum: ['yes', 'no', 'unknown'] }, basis: { enum: ['observed', 'derived', 'unknown'] } }, additionalProperties: false },
package/package.json CHANGED
@@ -1,66 +1,67 @@
1
- {
2
- "name": "dsh-composition-doctor",
3
- "version": "0.3.0",
4
- "private": false,
5
- "description": "Read-only DSH and Cordis composition diagnostics, snapshots, diffs, and isolated preflight.",
6
- "license": "MIT",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/lemonxiny55/dsh-composition-doctor.git"
10
- },
11
- "main": "./dist/plugin/index.js",
12
- "bin": {
13
- "dsh-doctor": "./dist/cli/main.js"
14
- },
15
- "exports": {
16
- ".": "./dist/plugin/index.js",
17
- "./client": "./dist/client.js",
18
- "./cli": "./dist/cli/main.js",
19
- "./package.json": "./package.json"
20
- },
21
- "files": [
22
- "dist",
23
- "cordis.patch.yml",
24
- "README.md",
25
- "README.zh.md",
26
- "CHANGELOG.md",
27
- "LICENSE"
28
- ],
29
- "dsh": {
30
- "bundle": {
31
- "patch": "./cordis.patch.yml"
32
- },
33
- "client": {
34
- "platform": "web",
35
- "inject": [
36
- "@deepseek-ai/dsh-client-runtime",
37
- "@deepseek-ai/dsh-client-ui-settings",
38
- "@deepseek-ai/dsh-client-ui-settings-plugins",
39
- "@deepseek-ai/dsh-client-locale"
40
- ],
41
- "immediately": false
42
- }
43
- },
44
- "type": "module",
45
- "engines": {
46
- "node": ">=20"
47
- },
48
- "packageManager": "pnpm@10.17.1",
49
- "scripts": {
50
- "test": "pnpm build && vitest run",
51
- "typecheck": "tsc --noEmit",
52
- "build": "tsc && node scripts/build-client.mjs",
53
- "dsh-doctor": "node dist/cli/main.js"
54
- },
55
- "dependencies": {
56
- "@deepseek-ai/schemastery": "^3.18.1",
57
- "semver": "^7.7.2",
58
- "yaml": "^2.8.1"
59
- },
60
- "devDependencies": {
61
- "@types/node": "^24.3.0",
62
- "esbuild": "^0.28.2",
63
- "typescript": "^5.9.2",
64
- "vitest": "^3.2.4"
65
- }
66
- }
1
+ {
2
+ "name": "dsh-composition-doctor",
3
+ "version": "0.4.0",
4
+ "private": false,
5
+ "description": "Explain DSH plugin failures through observable composition paths. Read-only, offline by default, with snapshots and upgrade preflight.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lemonxiny55/dsh-composition-doctor.git"
10
+ },
11
+ "main": "./dist/plugin/index.js",
12
+ "bin": {
13
+ "dsh-doctor": "./dist/cli/main.js"
14
+ },
15
+ "exports": {
16
+ ".": "./dist/plugin/index.js",
17
+ "./client": "./dist/client.js",
18
+ "./cli": "./dist/cli/main.js",
19
+ "./package.json": "./package.json"
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "cordis.patch.yml",
24
+ "README.md",
25
+ "README.zh.md",
26
+ "CHANGELOG.md",
27
+ "LICENSE"
28
+ ],
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ },
33
+ "client": {
34
+ "platform": "web",
35
+ "inject": [
36
+ "@deepseek-ai/dsh-client-runtime",
37
+ "@deepseek-ai/dsh-client-ui-settings",
38
+ "@deepseek-ai/dsh-client-ui-settings-plugins",
39
+ "@deepseek-ai/dsh-client-locale"
40
+ ],
41
+ "immediately": false
42
+ }
43
+ },
44
+ "type": "module",
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "dependencies": {
49
+ "@deepseek-ai/schemastery": "^3.18.1",
50
+ "semver": "^7.7.2",
51
+ "yaml": "^2.8.1"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^24.3.0",
55
+ "esbuild": "^0.28.2",
56
+ "typescript": "^5.9.2",
57
+ "vitest": "^3.2.4"
58
+ },
59
+ "scripts": {
60
+ "test": "pnpm build && vitest run",
61
+ "typecheck": "tsc --noEmit",
62
+ "build": "tsc && node scripts/build-client.mjs",
63
+ "dsh-doctor": "node dist/cli/main.js",
64
+ "smoke:pack": "node scripts/pack-smoke.mjs",
65
+ "demo:failure": "node scripts/failure-demo.mjs"
66
+ }
67
+ }