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 CHANGED
@@ -1,23 +1,34 @@
1
1
  # Changelog
2
2
 
3
- ## 0.2.1
3
+ ## 0.3.0
4
4
 
5
- - Fixed clean-checkout CI coverage by tracking the report schema and committed fixture packages that were previously hidden by broad ignore rules.
6
- - Fixed cross-platform Snapshot v2 path normalization for Windows-style paths on Linux CI.
7
- - Added the dsh-plugin.org listing badge to the bilingual README files.
5
+ - Add read-only `why row` and bounded `impact bundle` explanations using shared, versioned composition facts.
6
+ - Extend reports and Web Settings with a redacted bundle/source/layer/row provenance graph; older reports remain readable without the graph.
7
+ - Keep field ownership, removed config keys, route/slot ownership, runtime hook ownership, arbitrary dependencies, and possible dependents unknown unless directly observed.
8
8
 
9
- ## 0.2.0
10
-
11
- - Release the verified 0.2.0 package without changing the established diagnostic architecture or evidence boundaries.
12
-
13
- ## 0.1.3
14
-
15
- - Hardened the release package contract with the `dsh-doctor` binary entry.
16
- - Added packed-package verification coverage and documented scan exit-code thresholds.
17
- - Hardened npm artifact cache writes with integrity metadata and atomic temporary-file publication.
18
- - Reject unsafe tar paths and link entries before reading package metadata.
19
- - Kept DSH compatibility verified only for `0.1.5-rc.1` and `0.1.5-rc.2`; `0.1.6-alpha.1` remains expected-compatible only.
20
- - Runtime smoke remains `not-run` unless a trusted OS-level isolation backend is available.
21
- - Two Windows symlink regressions may remain `skipped` when the test process cannot create file symlinks; this is not a compatibility PASS.
22
- - `0.1.6-alpha.1` is experimental/expected-compatible and is not part of the verified release range.
23
- - A temporary directory is not a security sandbox; no runtime safety claim is made from temporary-directory execution.
9
+ ## 0.2.2
10
+
11
+ - Fixed the packaged `dsh-doctor` CLI entrypoint when pnpm's Windows shim supplies a non-canonical path.
12
+ - Added a built-entrypoint regression test covering `--version` and `--help`.
13
+
14
+ ## 0.2.1
15
+
16
+ - Fixed clean-checkout CI coverage by tracking the report schema and committed fixture packages that were previously hidden by broad ignore rules.
17
+ - Fixed cross-platform Snapshot v2 path normalization for Windows-style paths on Linux CI.
18
+ - Added the dsh-plugin.org listing badge to the bilingual README files.
19
+
20
+ ## 0.2.0
21
+
22
+ - Release the verified 0.2.0 package without changing the established diagnostic architecture or evidence boundaries.
23
+
24
+ ## 0.1.3
25
+
26
+ - Hardened the release package contract with the `dsh-doctor` binary entry.
27
+ - Added packed-package verification coverage and documented scan exit-code thresholds.
28
+ - Hardened npm artifact cache writes with integrity metadata and atomic temporary-file publication.
29
+ - Reject unsafe tar paths and link entries before reading package metadata.
30
+ - Kept DSH compatibility verified only for `0.1.5-rc.1` and `0.1.5-rc.2`; `0.1.6-alpha.1` remains expected-compatible only.
31
+ - Runtime smoke remains `not-run` unless a trusted OS-level isolation backend is available.
32
+ - Two Windows symlink regressions may remain `skipped` when the test process cannot create file symlinks; this is not a compatibility PASS.
33
+ - `0.1.6-alpha.1` is experimental/expected-compatible and is not part of the verified release range.
34
+ - A temporary directory is not a security sandbox; no runtime safety claim is made from temporary-directory execution.
package/README.md CHANGED
@@ -1,74 +1,80 @@
1
- # dsh-composition-doctor
2
-
3
- [![npm version](https://img.shields.io/npm/v/dsh-composition-doctor)](https://www.npmjs.com/package/dsh-composition-doctor)
4
- [![CI](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml)
5
- [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/lemonxiny55/dsh-composition-doctor)
6
-
7
- English | [中文](README.zh.md)
8
-
9
- Composition and upgrade preflight doctor for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`). It reads an explicitly selected profile and explains observable Cordis/plugin composition risks with concrete evidence. It never edits a real profile or silently changes permissions.
10
-
11
- Current package release: `0.2.1`.
12
-
13
- ## What the model gets
14
-
15
- | Command | Purpose |
16
- |---|---|
17
- | `dsh-doctor scan` | Detect duplicate Cordis rows, hook-order risks, UI slot/route ownership conflicts, bundle overrides, peer/platform mismatches, and profile drift. |
18
- | `dsh-doctor snapshot` | Create a redacted, comparable profile snapshot with lockfile hashes. |
19
- | `dsh-doctor diff` | Summarize added/removed/upgraded plugins, rows, hooks, UI claims, peers, and platforms. |
1
+ # dsh-composition-doctor
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-composition-doctor)](https://www.npmjs.com/package/dsh-composition-doctor)
4
+ [![CI](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml)
5
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/lemonxiny55/dsh-composition-doctor)
6
+
7
+ English | [中文](README.zh.md)
8
+
9
+ Composition and upgrade preflight doctor for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (`dsh`). It reads an explicitly selected profile and explains observable Cordis/plugin composition risks with concrete evidence. It never edits a real profile or silently changes permissions.
10
+
11
+ Current package release: `0.3.0`.
12
+
13
+ ## What the model gets
14
+
15
+ | Command | Purpose |
16
+ |---|---|
17
+ | `dsh-doctor scan` | Detect duplicate Cordis rows, hook-order risks, UI slot/route ownership conflicts, bundle overrides, peer/platform mismatches, and profile drift. |
18
+ | `dsh-doctor snapshot` | Create a redacted, comparable profile snapshot with lockfile hashes. |
19
+ | `dsh-doctor diff` | Summarize added/removed/upgraded plugins, rows, hooks, UI claims, peers, and platforms. |
20
20
  | `dsh-doctor preflight` | Rehearse a target DSH upgrade in an isolated temporary profile. |
21
-
22
- The Web Settings page is display/export only: it reads the latest local report, shows a conflict graph, and exports JSON/Markdown. It has no repair, install, or uninstall action.
23
-
24
- ## Reports and evidence boundaries
25
-
26
- `scan --output <dir>` writes only to the requested directory. To make the same report visible to the read-only Settings page, opt in explicitly: `--publish` copies it to the default plugin directory `.dsh-composition-doctor/reports`, while `--report-dir <dir>` publishes to a configured plugin directory. Use `--format both` so the Web route has `report.json` and Markdown remains exportable. Do not use a profile directory, `.env` location, or any directory containing keys, tokens, or other secrets as a report directory.
27
-
28
- Reports declare `evidenceMode`: `static` means allow-listed manifest/patch/package metadata only; `composed` means a public `dsh --profile <name> --dump-config` command returned a composition result; `runtime-observed` is reserved for a successful isolated runtime observation backend; and `mixed` is reserved for an adapter that supplies both. `dump-config` never proves runtime execution, including when a `!!js` or other runtime-dependent value is present. Static findings and bounded metadata coverage are not proof of the final runtime composition. If no compatible public DSH CLI is available, scan falls back to static and records a warning. Reports written before evidence schema 2 may still be read as legacy `resolved`, but new reports never emit that label.
29
-
30
- `preflight --candidate package@version` records a validated exact reference in an isolated temporary `package.json`. Target artifact discovery is local-first (`--dsh-bin`, package directory, tarball, installed `dsh`, `--package-manager-cache`, then Doctor-owned cache); registry access is allowed only with explicit `--online`. Online resolution downloads exact registry tarballs into Doctor-owned cache, records source/version/integrity/hash, and never installs or runs lifecycle scripts. `--allow-build` does not grant permission to execute third-party runtime code.
31
-
32
- `scan --fail-on never|info|warning|error` controls the scan exit code. The default is `never`: warnings and errors remain in the report without changing the exit code. `info` fails on any diagnostic, `warning` fails on warnings or errors, and `error` fails only on errors. Malformed arguments return exit code 2; operational failures return 1.
33
-
34
- ## Install
35
-
36
- ```powershell
37
- npm install -g dsh-composition-doctor
38
- npx @deepseek-ai/dsh plugin --profile web add dsh-composition-doctor
39
- dsh-doctor --version
40
- ```
41
-
42
- Restart the Web UI (`npx @deepseek-ai/dsh web`) after changing a profile.
43
-
44
- ## Example
45
-
46
- ```powershell
47
- dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\profile --publish
48
- dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\archive --report-dir C:\safe\doctor-reports
49
- dsh-doctor snapshot --profile C:\path\to\profile --output .\reports\before.json
50
- dsh-doctor diff --before .\reports\before.json --after .\reports\after.json --format both
21
+ | `dsh-doctor why row <id>` | Explain one row from an explicitly selected profile using observed source/layer facts. |
22
+ | `dsh-doctor impact bundle <name>` | List rows and diagnostics directly associated with an observed bundle source. |
23
+
24
+ The `why` and `impact` commands require `--profile <dir>`. Reports include an optional, versioned, redacted `compositionFacts` section shared by these commands and the Web Composition Explorer. The Web Settings page remains display/export only: it reads the latest local report, shows diagnostic and composition graphs, and exports JSON/Markdown. It has no repair, install, or uninstall action.
25
+
26
+ Composition facts describe the structure returned by the public `--dump-config` command or static declarations when that is unavailable. `introduced` and `patched-by` edges describe the source chain recorded by DSH; they do not prove per-field ownership. If earlier config keys are not available, removed keys and field owners remain unknown. Route/slot ownership, runtime hook ownership, arbitrary dependencies, and possible dependents are reported as not observed/not modelled.
27
+
28
+ ## Reports and evidence boundaries
29
+
30
+ `scan --output <dir>` writes only to the requested directory. To make the same report visible to the read-only Settings page, opt in explicitly: `--publish` copies it to the default plugin directory `.dsh-composition-doctor/reports`, while `--report-dir <dir>` publishes to a configured plugin directory. Use `--format both` so the Web route has `report.json` and Markdown remains exportable. Do not use a profile directory, `.env` location, or any directory containing keys, tokens, or other secrets as a report directory.
31
+
32
+ Reports declare `evidenceMode`: `static` means allow-listed manifest/patch/package metadata only; `composed` means a public `dsh --profile <name> --dump-config` command returned a composition result; `runtime-observed` is reserved for a successful isolated runtime observation backend; and `mixed` is reserved for an adapter that supplies both. `dump-config` never proves runtime execution, including when a `!!js` or other runtime-dependent value is present. Static findings and bounded metadata coverage are not proof of the final runtime composition. If no compatible public DSH CLI is available, scan falls back to static and records a warning. Reports written before evidence schema 2 may still be read as legacy `resolved`, but new reports never emit that label.
33
+
34
+ `preflight --candidate package@version` records a validated exact reference in an isolated temporary `package.json`. Target artifact discovery is local-first (`--dsh-bin`, package directory, tarball, installed `dsh`, `--package-manager-cache`, then Doctor-owned cache); registry access is allowed only with explicit `--online`. Online resolution downloads exact registry tarballs into Doctor-owned cache, records source/version/integrity/hash, and never installs or runs lifecycle scripts. `--allow-build` does not grant permission to execute third-party runtime code.
35
+
36
+ `scan --fail-on never|info|warning|error` controls the scan exit code. The default is `never`: warnings and errors remain in the report without changing the exit code. `info` fails on any diagnostic, `warning` fails on warnings or errors, and `error` fails only on errors. Malformed arguments return exit code 2; operational failures return 1.
37
+
38
+ ## Install
39
+
40
+ ```powershell
41
+ npm install -g dsh-composition-doctor
42
+ npx @deepseek-ai/dsh plugin --profile web add dsh-composition-doctor
43
+ dsh-doctor --version
44
+ ```
45
+
46
+ Restart the Web UI (`npx @deepseek-ai/dsh web`) after changing a profile.
47
+
48
+ ## Example
49
+
50
+ ```powershell
51
+ dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\profile --publish
52
+ dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\archive --report-dir C:\safe\doctor-reports
53
+ dsh-doctor snapshot --profile C:\path\to\profile --output .\reports\before.json
54
+ dsh-doctor diff --before .\reports\before.json --after .\reports\after.json --format both
51
55
  dsh-doctor preflight --profile C:\path\to\profile --target-dsh 0.1.5-rc.2
56
+ dsh-doctor why row tool-bash --profile C:\path\to\profile
57
+ dsh-doctor impact bundle @example/dsh-bundle --profile C:\path\to\profile
52
58
  ```
53
-
54
- Each finding is `info`, `warning`, or `error` and includes evidence, explanation, and the smallest remediation. `runtimeSmoke.status=not-run` is not a runtime PASS; `artifact-unavailable` is not `incompatible`; and a warning is not a confirmed failure.
55
-
56
- ## Safety and privacy
57
-
58
- Default operations are read-only or isolated under the OS temporary directory. The plugin does not modify profiles, install/remove plugins, migrate configuration, escalate permissions, or perform network I/O by default. It never reads `.env`, profile secrets, session bodies, or workspace file contents, and does not collect or persist arbitrary environment-variable values. Public CLI execution receives only the minimal process-routing variables required by the host platform.
59
-
60
- ## Support and limitations
61
-
62
- The real public CLI harness verifies `@deepseek-ai/dsh@0.1.5-rc.1` and `@deepseek-ai/dsh@0.1.5-rc.2`. `0.1.6-alpha.1` remains expected-compatible/experimental only; `0.1.0-rc.6` is historical context, not a current verified target. Missing artifacts are reported as unavailable, never as PASS. Verified development runtime: Node.js 24 on Windows; CI covers Node.js 20 on Ubuntu. Runtime hook/UI ownership is reported as unverified unless public metadata supplies it, and a temp profile is not a security sandbox.
63
-
64
- ## Development
65
-
66
- ```powershell
67
- pnpm test
68
- pnpm typecheck
69
- pnpm build
70
- ```
71
-
72
- For checkout-only development, run the CLI with `node dist/cli/main.js` after building.
73
-
74
- See [`README.zh.md`](README.zh.md), [`docs/compatibility.md`](docs/compatibility.md), and [`docs/examples/scan-report.md`](docs/examples/scan-report.md). MIT licensed.
59
+
60
+ Each finding is `info`, `warning`, or `error` and includes evidence, explanation, and the smallest remediation. `runtimeSmoke.status=not-run` is not a runtime PASS; `artifact-unavailable` is not `incompatible`; and a warning is not a confirmed failure.
61
+
62
+ ## Safety and privacy
63
+
64
+ Default operations are read-only or isolated under the OS temporary directory. The plugin does not modify profiles, install/remove plugins, migrate configuration, escalate permissions, or perform network I/O by default. It never reads `.env`, profile secrets, session bodies, or workspace file contents, and does not collect or persist arbitrary environment-variable values. Public CLI execution receives only the minimal process-routing variables required by the host platform.
65
+
66
+ ## Support and limitations
67
+
68
+ The real public CLI harness verifies `@deepseek-ai/dsh@0.1.5-rc.1` and `@deepseek-ai/dsh@0.1.5-rc.2`. `0.1.6-alpha.1` remains expected-compatible/experimental only; `0.1.0-rc.6` is historical context, not a current verified target. Missing artifacts are reported as unavailable, never as PASS. Verified development runtime: Node.js 24 on Windows; CI covers Node.js 20 on Ubuntu. Runtime hook/UI ownership is reported as unverified unless public metadata supplies it, and a temp profile is not a security sandbox.
69
+
70
+ ## Development
71
+
72
+ ```powershell
73
+ pnpm test
74
+ pnpm typecheck
75
+ pnpm build
76
+ ```
77
+
78
+ For checkout-only development, run the CLI with `node dist/cli/main.js` after building.
79
+
80
+ See [`README.zh.md`](README.zh.md), [`docs/compatibility.md`](docs/compatibility.md), and [`docs/examples/scan-report.md`](docs/examples/scan-report.md). MIT licensed.
package/README.zh.md CHANGED
@@ -1,74 +1,80 @@
1
- # dsh-composition-doctor(中文)
2
-
3
- [![npm version](https://img.shields.io/npm/v/dsh-composition-doctor)](https://www.npmjs.com/package/dsh-composition-doctor)
4
- [![CI](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml)
5
- [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/lemonxiny55/dsh-composition-doctor)
6
-
7
- [English](README.md) | 中文
8
-
9
- 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的组合与升级预检工具。它读取明确指定的 profile,用可追溯证据说明可观察到的 Cordis/plugin 组合风险;不会编辑真实 profile,也不会静默扩大权限。
10
-
11
- 当前 package release:`0.2.1`。
12
-
13
- ## 模型可用能力
14
-
15
- | 命令 | 作用 |
16
- |---|---|
17
- | `dsh-doctor scan` | 检测重复 Cordis row、hook 顺序风险、UI slot/route 所有权冲突、bundle 覆盖、peer/platform 不匹配和 profile 漂移。 |
18
- | `dsh-doctor snapshot` | 生成脱敏、可比较的 profile 快照及 lockfile 哈希。 |
19
- | `dsh-doctor diff` | 汇总新增、删除或升级的插件,以及 rows、hooks、UI 声明、peer 和平台变化。 |
1
+ # dsh-composition-doctor(中文)
2
+
3
+ [![npm version](https://img.shields.io/npm/v/dsh-composition-doctor)](https://www.npmjs.com/package/dsh-composition-doctor)
4
+ [![CI](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/lemonxiny55/dsh-composition-doctor/actions/workflows/ci.yml)
5
+ [![Listed on dsh-plugin.org](https://dsh-plugin.org/badges/listed.svg)](https://dsh-plugin.org/plugins/lemonxiny55/dsh-composition-doctor)
6
+
7
+ [English](README.md) | 中文
8
+
9
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(`dsh`)的组合与升级预检工具。它读取明确指定的 profile,用可追溯证据说明可观察到的 Cordis/plugin 组合风险;不会编辑真实 profile,也不会静默扩大权限。
10
+
11
+ 当前 package release:`0.3.0`。
12
+
13
+ ## 模型可用能力
14
+
15
+ | 命令 | 作用 |
16
+ |---|---|
17
+ | `dsh-doctor scan` | 检测重复 Cordis row、hook 顺序风险、UI slot/route 所有权冲突、bundle 覆盖、peer/platform 不匹配和 profile 漂移。 |
18
+ | `dsh-doctor snapshot` | 生成脱敏、可比较的 profile 快照及 lockfile 哈希。 |
19
+ | `dsh-doctor diff` | 汇总新增、删除或升级的插件,以及 rows、hooks、UI 声明、peer 和平台变化。 |
20
20
  | `dsh-doctor preflight` | 在独立临时 profile 中演练目标 DSH 升级。 |
21
-
22
- Web Settings 页面仅供显示与导出:它读取最新本地报告、展示冲突图,并导出 JSON/Markdown;不提供修复、安装或卸载操作。
23
-
24
- ## 报告与证据边界
25
-
26
- `scan --output <目录>` 只写入显式指定的目录。要让只读 Settings 页面看到同一份报告,必须显式选择 `--publish`,它会复制到默认插件目录 `.dsh-composition-doctor/reports`;也可以使用 `--report-dir <目录>` 发布到配置的插件报告目录。请使用 `--format both`,使 Web route 能读取 `report.json`,同时保留 Markdown 导出。报告目录不能是 profile、`.env` 所在位置,或任何含密钥、token 或其他敏感信息的目录。
27
-
28
- 报告中的 `evidenceMode` 标明证据来源:`static` 仅表示 allow-list 中的 manifest、patch、package metadata 等静态证据;`composed` 表示公开 `dsh --profile <name> --dump-config` 返回了 composition 结果;`runtime-observed` 只允许真正启动隔离 runtime 并观察到注册/行为的后端使用;`mixed` 预留给同时提供多类来源的适配器。`dump-config` 即使成功,也不代表 runtime 已验证;存在 `!!js` 等运行时依赖时尤其如此。找不到兼容的公开 DSH CLI 时会回退到 static,并明确报告 warning。旧的 evidence schema 2 之前报告仍可按 legacy `resolved` 读取,但新报告不会再输出该标签。
29
-
30
- `preflight --candidate package@version` 只会把通过校验的精确引用记录到隔离临时 `package.json`。目标 artifact 默认按本地优先顺序查找:`--dsh-bin`、本地 package directory、本地 tarball、已安装 `dsh`、`--package-manager-cache`、Doctor 自己的 cache;只有显式 `--online` 才允许访问 registry。在线解析会把精确 tarball 写入 Doctor 自己的 cache,记录来源、版本、integrity 和 hash,并且不会安装或执行 lifecycle script。`--allow-build` 不等于允许执行第三方 runtime code。
31
-
32
- `scan --fail-on never|info|warning|error` 控制 scan 的退出码。默认是 `never`:warning 和 error 会写入报告,但不改变退出码;`info` 遇到任意诊断时退出 1,`warning` 遇到 warning 或 error 时退出 1,`error` 只在 error 时退出 1。参数错误退出 2,执行失败退出 1。
33
-
34
- ## 安装
35
-
36
- ```powershell
37
- npm install -g dsh-composition-doctor
38
- npx @deepseek-ai/dsh plugin --profile web add dsh-composition-doctor
39
- dsh-doctor --version
40
- ```
41
-
42
- 更改 profile 后重启 Web UI:`npx @deepseek-ai/dsh web`。
43
-
44
- ## 示例
45
-
46
- ```powershell
47
- dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\profile --publish
48
- dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\archive --report-dir C:\safe\doctor-reports
49
- dsh-doctor snapshot --profile C:\path\to\profile --output .\reports\before.json
50
- dsh-doctor diff --before .\reports\before.json --after .\reports\after.json --format both
21
+ | `dsh-doctor why row <id>` | 基于明确指定的 profile 中可观察的来源和 layer 信息解释一个 row。 |
22
+ | `dsh-doctor impact bundle <name>` | 列出有直接来源关联证据的 bundle rows 和 diagnostics。 |
23
+
24
+ `why` 和 `impact` 都需要 `--profile <目录>`。报告新增可选、版本化且脱敏的 `compositionFacts`,由这两个命令和 Web Composition Explorer 共用。Web Settings 仍然只读:它读取最新本地报告,展示诊断图和 composition 图,并导出 JSON/Markdown;不提供修复、安装或卸载操作。
25
+
26
+ Composition facts 描述公开 `--dump-config` 返回的结构;无法取得时仅描述静态声明。`introduced` 和 `patched-by` 表示 DSH 输出中记录的来源链,不证明逐字段 owner。若未观察到前层 config key,移除的键和字段 owner 必须是 unknown。route/slot ownership、runtime hook ownership、任意依赖和可能的 dependents 会标记为未观察/未建模。
27
+
28
+ ## 报告与证据边界
29
+
30
+ `scan --output <目录>` 只写入显式指定的目录。要让只读 Settings 页面看到同一份报告,必须显式选择 `--publish`,它会复制到默认插件目录 `.dsh-composition-doctor/reports`;也可以使用 `--report-dir <目录>` 发布到配置的插件报告目录。请使用 `--format both`,使 Web route 能读取 `report.json`,同时保留 Markdown 导出。报告目录不能是 profile、`.env` 所在位置,或任何含密钥、token 或其他敏感信息的目录。
31
+
32
+ 报告中的 `evidenceMode` 标明证据来源:`static` 仅表示 allow-list 中的 manifest、patch、package metadata 等静态证据;`composed` 表示公开 `dsh --profile <name> --dump-config` 返回了 composition 结果;`runtime-observed` 只允许真正启动隔离 runtime 并观察到注册/行为的后端使用;`mixed` 预留给同时提供多类来源的适配器。`dump-config` 即使成功,也不代表 runtime 已验证;存在 `!!js` 等运行时依赖时尤其如此。找不到兼容的公开 DSH CLI 时会回退到 static,并明确报告 warning。旧的 evidence schema 2 之前报告仍可按 legacy `resolved` 读取,但新报告不会再输出该标签。
33
+
34
+ `preflight --candidate package@version` 只会把通过校验的精确引用记录到隔离临时 `package.json`。目标 artifact 默认按本地优先顺序查找:`--dsh-bin`、本地 package directory、本地 tarball、已安装 `dsh`、`--package-manager-cache`、Doctor 自己的 cache;只有显式 `--online` 才允许访问 registry。在线解析会把精确 tarball 写入 Doctor 自己的 cache,记录来源、版本、integrity 和 hash,并且不会安装或执行 lifecycle script。`--allow-build` 不等于允许执行第三方 runtime code。
35
+
36
+ `scan --fail-on never|info|warning|error` 控制 scan 的退出码。默认是 `never`:warning 和 error 会写入报告,但不改变退出码;`info` 遇到任意诊断时退出 1,`warning` 遇到 warning 或 error 时退出 1,`error` 只在 error 时退出 1。参数错误退出 2,执行失败退出 1。
37
+
38
+ ## 安装
39
+
40
+ ```powershell
41
+ npm install -g dsh-composition-doctor
42
+ npx @deepseek-ai/dsh plugin --profile web add dsh-composition-doctor
43
+ dsh-doctor --version
44
+ ```
45
+
46
+ 更改 profile 后重启 Web UI:`npx @deepseek-ai/dsh web`。
47
+
48
+ ## 示例
49
+
50
+ ```powershell
51
+ dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\profile --publish
52
+ dsh-doctor scan --profile C:\path\to\profile --format both --output .\reports\archive --report-dir C:\safe\doctor-reports
53
+ dsh-doctor snapshot --profile C:\path\to\profile --output .\reports\before.json
54
+ dsh-doctor diff --before .\reports\before.json --after .\reports\after.json --format both
51
55
  dsh-doctor preflight --profile C:\path\to\profile --target-dsh 0.1.5-rc.2
56
+ dsh-doctor why row tool-bash --profile C:\path\to\profile
57
+ dsh-doctor impact bundle @example/dsh-bundle --profile C:\path\to\profile
52
58
  ```
53
-
54
- 每条诊断均为 `info`、`warning` 或 `error`,并附带 evidence、explanation 和最小 remediation。`runtimeSmoke.status=not-run` 不等于 runtime PASS;`artifact-unavailable` 不等于 `incompatible`;warning 也不等于已确认失败。
55
-
56
- ## 安全与隐私
57
-
58
- 默认操作只读,或仅在操作系统临时目录中隔离运行。插件不会修改 profile、安装或删除插件、迁移配置、扩大权限,默认也不会执行网络 I/O。它不会读取 `.env`、profile 密钥、会话正文或工作区源文件内容,也不会收集或持久化任意环境变量值;启动公开 CLI 时只传递平台所需的最小进程路由变量。
59
-
60
- ## 支持范围与限制
61
-
62
- real-release harness 已使用真实公开 CLI 验证 `@deepseek-ai/dsh@0.1.5-rc.1` 和 `@deepseek-ai/dsh@0.1.5-rc.2`。`0.1.6-alpha.1` 仍只是 expected-compatible/experimental;`0.1.0-rc.6` 仅作历史兼容背景,不是当前 verified target。artifact 不可用时会明确报告 unavailable,绝不记为 PASS。已验证开发环境为 Windows + Node.js 24;CI 覆盖 Ubuntu + Node.js 20。只有公开 metadata 能提供 hook/UI ownership 时才会确认,否则标为 unverified;临时目录不是安全 sandbox。
63
-
64
- ## 开发
65
-
66
- ```powershell
67
- pnpm test
68
- pnpm typecheck
69
- pnpm build
70
- ```
71
-
72
- 仅在 checkout 开发时,构建完成后使用 `node dist/cli/main.js` 运行 CLI。
73
-
74
- 参阅 [`README.md`](README.md)、[`docs/compatibility.md`](docs/compatibility.md) 和 [`docs/examples/scan-report.md`](docs/examples/scan-report.md)。MIT 许可。
59
+
60
+ 每条诊断均为 `info`、`warning` 或 `error`,并附带 evidence、explanation 和最小 remediation。`runtimeSmoke.status=not-run` 不等于 runtime PASS;`artifact-unavailable` 不等于 `incompatible`;warning 也不等于已确认失败。
61
+
62
+ ## 安全与隐私
63
+
64
+ 默认操作只读,或仅在操作系统临时目录中隔离运行。插件不会修改 profile、安装或删除插件、迁移配置、扩大权限,默认也不会执行网络 I/O。它不会读取 `.env`、profile 密钥、会话正文或工作区源文件内容,也不会收集或持久化任意环境变量值;启动公开 CLI 时只传递平台所需的最小进程路由变量。
65
+
66
+ ## 支持范围与限制
67
+
68
+ real-release harness 已使用真实公开 CLI 验证 `@deepseek-ai/dsh@0.1.5-rc.1` 和 `@deepseek-ai/dsh@0.1.5-rc.2`。`0.1.6-alpha.1` 仍只是 expected-compatible/experimental;`0.1.0-rc.6` 仅作历史兼容背景,不是当前 verified target。artifact 不可用时会明确报告 unavailable,绝不记为 PASS。已验证开发环境为 Windows + Node.js 24;CI 覆盖 Ubuntu + Node.js 20。只有公开 metadata 能提供 hook/UI ownership 时才会确认,否则标为 unverified;临时目录不是安全 sandbox。
69
+
70
+ ## 开发
71
+
72
+ ```powershell
73
+ pnpm test
74
+ pnpm typecheck
75
+ pnpm build
76
+ ```
77
+
78
+ 仅在 checkout 开发时,构建完成后使用 `node dist/cli/main.js` 运行 CLI。
79
+
80
+ 参阅 [`README.md`](README.md)、[`docs/compatibility.md`](docs/compatibility.md) 和 [`docs/examples/scan-report.md`](docs/examples/scan-report.md)。MIT 许可。
package/dist/cli/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { fileURLToPath } from 'node:url';
2
- import { readFileSync } from 'node:fs';
2
+ import { readFileSync, realpathSync } from 'node:fs';
3
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
4
  import { dirname, resolve } from 'node:path';
5
5
  import { resolveComposition } from '../core/composition-adapter.js';
@@ -7,12 +7,13 @@ import { diffSnapshots, renderSnapshotDiffMarkdown } from '../core/diff.js';
7
7
  import { readProfile } from '../core/profile-reader.js';
8
8
  import { createSnapshot, isSnapshotV1, isSnapshotV2 } from '../core/snapshot.js';
9
9
  import { analyseComposition } from '../core/rules.js';
10
+ import { compositionBundleImpact, findCompositionRow } from '../core/composition-provenance.js';
10
11
  import { renderJson } from '../reports/json.js';
11
12
  import { renderMarkdown } from '../reports/markdown.js';
12
13
  import { resolveReportDirectory } from '../reports/location.js';
13
14
  import { runPreflight } from '../core/preflight.js';
14
- const usage = 'Usage: dsh-doctor <scan | snapshot | diff | preflight>';
15
- const commands = new Set(['scan', 'snapshot', 'diff', 'preflight']);
15
+ const usage = 'Usage: dsh-doctor <scan | snapshot | diff | preflight | why | impact>';
16
+ const commands = new Set(['scan', 'snapshot', 'diff', 'preflight', 'why', 'impact']);
16
17
  function packageVersion() {
17
18
  try {
18
19
  const manifest = JSON.parse(readFileSync(resolve(dirname(fileURLToPath(import.meta.url)), '../../package.json'), 'utf8'));
@@ -146,6 +147,68 @@ async function preflight(argv, io) {
146
147
  }
147
148
  return result.outcome === 'fail' ? 1 : 0;
148
149
  }
150
+ async function why(argv, io) {
151
+ if (argv[0] !== 'row' || argv[1] === undefined || argv[1].startsWith('--'))
152
+ return invalid(io, 'why requires: why row <id> --profile <dir>');
153
+ const profile = option(argv, '--profile');
154
+ if (profile === undefined)
155
+ return invalid(io, 'why row requires --profile');
156
+ const model = await resolveComposition(await readProfile({ profileDir: resolve(profile) }));
157
+ const report = analyseComposition(model);
158
+ const row = findCompositionRow(report.compositionFacts, argv[1]);
159
+ if (row === undefined) {
160
+ io.write(`${JSON.stringify({ schemaVersion: 1, query: { entity: 'row', id: argv[1] }, outcome: 'not-found', evidenceMode: report.evidenceMode, unknown: ['No row with this id was present in the selected profile evidence.'] }, null, 2)}\n`);
161
+ return 0;
162
+ }
163
+ const relatedDiagnostics = report.diagnostics.filter((item) => row.relatedDiagnosticIds.includes(item.id));
164
+ const unknown = [...row.unknown];
165
+ if (row.replacement.state === 'yes' && row.removedConfigKeys === undefined)
166
+ unknown.push('removed config keys are unknown; prior layer config was not fully observed');
167
+ io.write(`${JSON.stringify({
168
+ schemaVersion: 1,
169
+ query: { entity: 'row', id: argv[1] },
170
+ outcome: 'found',
171
+ evidenceMode: report.evidenceMode,
172
+ row: {
173
+ id: row.id,
174
+ ...(row.name === undefined ? {} : { name: row.name }),
175
+ effectiveObservedStructure: { configKeys: row.configKeys, values: 'not included; values may contain sensitive or runtime-dependent data' },
176
+ source: row.source,
177
+ sourceBasis: row.sourceBasis,
178
+ ...(row.layer === undefined ? {} : { layer: row.layer }),
179
+ ...(row.layerOrder === undefined ? {} : { layerOrder: row.layerOrder }),
180
+ provenance: row.provenance,
181
+ replacement: row.replacement,
182
+ configKeysBasis: row.configKeysBasis,
183
+ ...(row.removedConfigKeys === undefined ? {} : { removedConfigKeys: row.removedConfigKeys }),
184
+ ...(row.packageName === undefined ? {} : { package: { name: row.packageName, ...(row.packageVersion === undefined ? {} : { version: row.packageVersion }) } }),
185
+ evidenceMode: row.evidenceMode
186
+ },
187
+ relatedDiagnostics,
188
+ unknown: [...new Set(unknown)].sort()
189
+ }, null, 2)}\n`);
190
+ return 0;
191
+ }
192
+ async function impact(argv, io) {
193
+ if (argv[0] !== 'bundle' || argv[1] === undefined || argv[1].startsWith('--'))
194
+ return invalid(io, 'impact requires: impact bundle <name> --profile <dir>');
195
+ const profile = option(argv, '--profile');
196
+ if (profile === undefined)
197
+ return invalid(io, 'impact bundle requires --profile');
198
+ const model = await resolveComposition(await readProfile({ profileDir: resolve(profile) }));
199
+ const report = analyseComposition(model);
200
+ const result = compositionBundleImpact(report, argv[1]);
201
+ const known = report.compositionFacts?.nodes.some((node) => node.entity === 'bundle' && node.label === argv[1]) === true;
202
+ io.write(`${JSON.stringify({
203
+ ...result,
204
+ outcome: known ? (result.rows.length > 0 ? 'observed-contributions' : 'no-observable-rows') : 'bundle-not-observed',
205
+ observed: known,
206
+ patchContributions: result.rows.filter((row) => row.provenance.some((item) => item.source === argv[1] && item.relation === 'patched-by')),
207
+ overrides: result.rows.filter((row) => row.replacement.state === 'yes' && row.provenance.some((item) => item.source === argv[1] && item.relation === 'patched-by')),
208
+ unknown: [...new Set(result.unknown)]
209
+ }, null, 2)}\n`);
210
+ return 0;
211
+ }
149
212
  export async function runCli(argv, io = { write: (line) => console.log(line) }) {
150
213
  const command = argv[0];
151
214
  if (argv.length === 0 || command === '--help') {
@@ -167,14 +230,36 @@ export async function runCli(argv, io = { write: (line) => console.log(line) })
167
230
  return await snapshot(argv.slice(1), io);
168
231
  if (command === 'diff')
169
232
  return await diff(argv.slice(1), io);
170
- return await preflight(argv.slice(1), io);
233
+ if (command === 'preflight')
234
+ return await preflight(argv.slice(1), io);
235
+ if (command === 'why')
236
+ return await why(argv.slice(1), io);
237
+ return await impact(argv.slice(1), io);
171
238
  }
172
239
  catch (error) {
173
240
  io.write(error instanceof Error ? error.message : String(error));
174
241
  return 1;
175
242
  }
176
243
  }
177
- if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
244
+ function canonicalPath(value) {
245
+ try {
246
+ return realpathSync(value);
247
+ }
248
+ catch {
249
+ return resolve(value);
250
+ }
251
+ }
252
+ function isMainModule() {
253
+ const entrypoint = process.argv[1];
254
+ if (entrypoint === undefined)
255
+ return false;
256
+ const modulePath = canonicalPath(fileURLToPath(import.meta.url));
257
+ const entrypointPath = canonicalPath(entrypoint);
258
+ return process.platform === 'win32'
259
+ ? modulePath.toLowerCase() === entrypointPath.toLowerCase()
260
+ : modulePath === entrypointPath;
261
+ }
262
+ if (isMainModule()) {
178
263
  runCli(process.argv.slice(2)).then((exitCode) => {
179
264
  process.exitCode = exitCode;
180
265
  });