@shidesheng0218/dsh-plugin-lint 0.1.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.
Files changed (66) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +515 -81
  3. package/bin/dsh-plugin-analyze.mjs +378 -0
  4. package/bin/dsh-plugin-lint.mjs +315 -27
  5. package/bin/dsh-plugin-matrix.mjs +78 -0
  6. package/bin/dsh-plugin-upgrade-plan.mjs +180 -0
  7. package/package.json +12 -4
  8. package/schema/dsh-manifest.schema.json +134 -0
  9. package/scripts/lib/probe.mjs +28 -385
  10. package/scripts/probe-boot.mjs +103 -37
  11. package/src/composition.js +1 -0
  12. package/src/core/baseline.js +98 -0
  13. package/src/core/changed.js +41 -0
  14. package/src/core/load.js +145 -0
  15. package/src/core/mask.js +207 -0
  16. package/src/core/pack.js +95 -0
  17. package/src/core/packlist.js +186 -0
  18. package/src/core/peer.js +39 -0
  19. package/src/core/profile-scan.js +121 -0
  20. package/src/core/report.js +174 -0
  21. package/src/core/rules.js +581 -0
  22. package/src/core/semver.js +192 -0
  23. package/src/core/tarball.js +90 -0
  24. package/src/data/event-types.json +111 -9
  25. package/src/experimental/conflicts.js +315 -0
  26. package/src/experimental/index.js +4 -0
  27. package/src/experimental/performance.js +515 -0
  28. package/src/experimental/security.js +462 -0
  29. package/src/index.js +15 -17
  30. package/src/load.js +1 -116
  31. package/src/mask.js +1 -156
  32. package/src/matrix/cache.js +19 -0
  33. package/src/matrix/cell.js +79 -0
  34. package/src/matrix/cells.js +58 -0
  35. package/src/matrix/conflicts.js +210 -0
  36. package/src/matrix/export.js +155 -0
  37. package/src/matrix/index.js +71 -0
  38. package/src/matrix/manifest.js +17 -0
  39. package/src/matrix/migrate.js +41 -0
  40. package/src/matrix/run.js +241 -0
  41. package/src/matrix/sanitize.js +42 -0
  42. package/src/matrix/schema.js +178 -0
  43. package/src/matrix/site.js +61 -0
  44. package/src/matrix/sync.js +72 -0
  45. package/src/matrix/verify.js +152 -0
  46. package/src/performance.js +1 -0
  47. package/src/probe/artifact-resolver.js +161 -0
  48. package/src/probe/classify.js +8 -0
  49. package/src/probe/dsh-adapter.js +212 -0
  50. package/src/probe/environment/docker.js +270 -0
  51. package/src/probe/environment/host-unsafe.js +39 -0
  52. package/src/probe/environment/materialize.js +22 -0
  53. package/src/probe/environment.js +50 -0
  54. package/src/probe/installed-artifact.js +17 -0
  55. package/src/probe/probe.js +278 -0
  56. package/src/report.js +1 -79
  57. package/src/rules.js +1 -379
  58. package/src/security.js +1 -0
  59. package/src/shared/errors.js +7 -0
  60. package/src/shared/hash.js +31 -0
  61. package/src/shared/status.js +12 -0
  62. package/src/shared/validate.js +14 -0
  63. package/src/shared/version.js +4 -0
  64. package/src/upgrade/compatibility.js +14 -0
  65. package/src/upgrade/planner.js +119 -0
  66. package/src/upgrade/profile.js +82 -0
package/README.md CHANGED
@@ -1,13 +1,14 @@
1
1
  # dsh-plugin-lint
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@shidesheng0218/dsh-plugin-lint)](https://www.npmjs.com/package/@shidesheng0218/dsh-plugin-lint)
3
4
  [![CI](https://github.com/shidesheng0218/dsh-plugin-lint/actions/workflows/ci.yml/badge.svg)](https://github.com/shidesheng0218/dsh-plugin-lint/actions/workflows/ci.yml)
4
5
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](LICENSE)
5
6
  [![Node](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](package.json)
6
7
  [![Dependencies](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](package.json)
7
8
 
8
- **Conformance checks and a compatibility matrix for DeepSeek Harness plugins — factual, evidence-backed, zero-config.**
9
+ **Conformance checks and a compatibility matrix for DeepSeek Harness plugins: what the code declares, what it publishes, and what the host does with it.**
9
10
 
10
- [Rules](#the-rules) · [Probe](#the-probe) · [Matrix](#compatibility-matrix) · [Panel](#the-panel) · [Conflicts](#composition-conflicts) · [中文说明](#中文说明)
11
+ [Rules](#the-rules) · [Probe](#the-probe) · [Matrix](#the-compatibility-matrix) · [Panel](#the-panel) · [v0.3 analysis (experimental)](#v03-analysis-experimental-unpublished) · [Federation](#federation) · [中文](#中文)
11
12
 
12
13
  ![The compatibility panel inside the DSH settings page](docs/assets/panel.png)
13
14
 
@@ -15,9 +16,27 @@
15
16
 
16
17
  ---
17
18
 
19
+ ## Why this exists
20
+
21
+ The ecosystem catalog used for the measurements below lists 4,508 entries (snapshot 2026-10-11). Upgrade breakage is a recurring problem, and the code that plugins publish is not checked against the documented practices.
22
+
23
+ Measured on 2026-10-13 against the Top 100 published community plugins (static analysis of the published tarballs; no plugin code was executed):
24
+
25
+ | Observation | Result |
26
+ | --- | --- |
27
+ | plugins that load at least one Harness Client package | **62 / 100 (62%)** |
28
+ | plugins declaring `dsh.compatibility.dshReleases` | **13 / 100 (13%)** |
29
+ | plugins declaring no compatibility range | **87 / 100 (87%)** |
30
+ | plugins declaring an `icon` | 31 / 100 (31%) |
31
+ | plugins triggering at least one error-level rule | 1 / 100 (1%) |
32
+
33
+ A coupling to `@deepseek-ai/dsh-client-*` breaks when the host updates those packages without notice. A missing compatibility declaration means upgrade day is untested. A `files[]` gap passes local testing and only fails in the published tarball.
34
+
35
+ The raw artifacts behind these numbers (`reports/`, `matrix/cells/`) are generated locally and gitignored; they are not part of a clean clone. See [docs/09](docs/09-ecosystem-measurements.md) for the aggregate record and the [data policy](#data-policy).
36
+
18
37
  ## What this is
19
38
 
20
- Four tools that answer four different questions about a DeepSeek Harness plugin. None of them needs a build step, a config file, or a network connection to run.
39
+ Four tools that answer four different questions about a DeepSeek Harness plugin.
21
40
 
22
41
  ```mermaid
23
42
  flowchart TB
@@ -26,7 +45,7 @@ flowchart TB
26
45
  pkg --> lint["<b>dsh-plugin-lint</b><br/>static rules<br/>is it written the way the reference says?"]
27
46
  pkg --> probe["<b>probe-boot</b><br/>one plugin, throwaway profile<br/>does it mount, and what does the model get?"]
28
47
 
29
- probe --> matrix["<b>matrix runner</b><br/>plugin x DSH version x composition<br/>which version breaks it?"]
48
+ probe --> matrix["<b>matrix runner</b><br/>plugin × DSH version × composition<br/>which version breaks it?"]
30
49
  probe --> conflicts["<b>conflicts</b><br/>several plugins at once<br/>do they collide?"]
31
50
 
32
51
  matrix --> panel["<b>settings panel</b><br/>your installed plugins<br/>what about <i>my</i> setup?"]
@@ -36,7 +55,9 @@ flowchart TB
36
55
  class lint,probe,matrix,conflicts,panel tool
37
56
  ```
38
57
 
39
- Every finding carries a file, a line, an evidence excerpt, and the upstream document the rule encodes.
58
+ The linter and the static analysis commands need no build step, no config file and no network access. The probe and the matrix runner install packages from npm, so they need network access; the upgrade planner reads a local matrix and makes no network request.
59
+
60
+ Every lint finding carries a file, a line, an evidence excerpt, and the upstream document the rule encodes.
40
61
 
41
62
  ## What this is not
42
63
 
@@ -52,41 +73,46 @@ Every finding carries a file, a line, an evidence excerpt, and the upstream docu
52
73
  | **dsh-plugin-lint** | **is it written the way the reference says, will it survive publishing, and will it survive an upgrade?** | — |
53
74
 
54
75
  ```console
55
- $ npx @shidesheng0218/dsh-plugin-lint .
56
- demo-plugin@1.0.0 11 checks
76
+ $ node bin/dsh-plugin-lint.mjs .
77
+ demo-plugin@1.0.0 13 checks
57
78
 
58
- E2 error client bundle factory id does not match the package name
79
+ E2 error client bundle factory id matches nothing the loader can ask for
59
80
  lib/client.js:2
60
- factory id "other-name" does not equal the package name "demo-plugin".
81
+ factory id "other-name" is neither the package name "demo-plugin" nor a row name in the bundle patch.
61
82
  evidence: __ModuleLoader__.load({ id: "other-name" })
62
- why: the browser module table keys the factory by package name; a mismatch
63
- leaves the registration unreachable at runtime.
83
+ why: the browser module table keys the factory by the id it asks for and the loader
84
+ throws when a bundle registers another one, leaving the client half unreachable.
64
85
  doc: dsh plugin-development / references/ui-plugin.md
65
86
 
66
- W1 warn loads a Harness Client package as a module
87
+ W1 warn couples the plugin to a Harness Client package
67
88
  lib/client.js:12
68
- loads "@deepseek-ai/dsh-client-ui-primitives" from the browser module table.
69
- why: the reference advises copying the primitive instead — these packages
70
- change without notice and a throwing component blanks the slot. Official
71
- first-party bundles do the same, so treat this as a coupling risk to
72
- watch at upgrade time, not as a defect.
89
+ loads 1 Harness Client package from the browser module table: @deepseek-ai/dsh-client-ui-primitives.
90
+ why: the reference advises copying the primitive instead — these packages change without
91
+ notice and a throwing component blanks the slot. Official first-party bundles do
92
+ the same, so treat this as a coupling risk to watch at upgrade time, not a defect.
73
93
 
74
94
  1 error · 1 warn · 0 info
75
95
  ```
76
96
 
97
+ The header count is read from the rule set (`RULES.length`); it is 12 in this checkout.
98
+
77
99
  ## The rules
78
100
 
79
101
  Levels are assigned by **how reliably a rule can decide**, not by how bad the pattern sounds. A rule that cannot decide reliably does not get to be an error.
80
102
 
103
+ Rule IDs are stable identifiers, not severity labels: `W3` reports at info level and `I3` at warn level. The tables below are grouped by level.
104
+
81
105
  **error** — mechanically decidable, never observed in the official packages
82
106
 
83
107
  | Rule | What it catches |
84
108
  | --- | --- |
85
- | E1 | `session.append("type", …)` with a type outside the Harness event catalog (45 types, harvested from the official packages) |
86
- | E2 | client bundle factory id ≠ package name (the registration silently never resolves) |
109
+ | E1 | `session.append("type", …)` with a type outside the Harness event catalog (380 types harvested from the official packages; 379 observed) |
110
+ | E2 | client bundle factory id ≠ anything the loader can ask for (the registration silently never resolves) |
87
111
  | E3 | `main` / `exports["./client"]` / `dsh.bundle.patch` / `icon` points at a missing file |
88
112
  | E4 | `dsh.client` declared with no resolvable `./client` export |
89
113
  | E5 | `files[]` omits an artifact the manifest depends on — the "works locally, broken after publish" bug |
114
+ | E6 | a `@deepseek-ai/dsh*` peer range does not accept the target runtime (`--dsh-version`); the host's real admission gate |
115
+ | E7 | a published subpath export or bin target points at a missing file; icon format/size and stale `files` entries are warns |
90
116
 
91
117
  **warn** — documented risk patterns, evidence required
92
118
 
@@ -102,10 +128,28 @@ Levels are assigned by **how reliably a rule can decide**, not by how bad the pa
102
128
  | --- | --- |
103
129
  | I1 | no `dsh.compatibility.dshReleases` (or it omits the running DSH version, with `--dsh-version`) |
104
130
  | I2 | no `icon`, or no `meta` title/description in `locale/<lang>.json` |
105
- | W3 | a timer that looks unowned (brace depth is approximate on bundled output) |
131
+ | W3 | a timer that looks unowned (brace depth is approximate on bundled output, which is why this is a hint and not a warning) |
106
132
 
107
133
  Rules the first draft had, and why they are gone: "a waterfall listener must call `next()`" (real plugins reject deliberately, and re-enter `ctx.llm.stream()` on purpose), "never listen to `system-prompt/assemble`" (the official packages do, four times), "never subscribe to `session/event`" (the reference forbids *rescanning*, not subscribing). See [docs/06](docs/06-detailed-design.md).
108
134
 
135
+ W1 encodes this passage from the reference, verbatim:
136
+
137
+ > Do not `require('@deepseek-ai/dsh-client-ui-primitives')` or load any other Harness Client package as a module … copy markup, CSS, and behavior from the primitive into the plugin.
138
+ > — `dsh plugin-development / references/practices.md`
139
+
140
+ Measured on 2026-10-13: 62 of the Top 100 published plugins load at least one of these packages. The ui-primitives package is referenced by 48 distinct plugins; 71 of the 86 W1 findings mention it (one plugin can produce more than one finding).
141
+
142
+ ### Measured with this tool
143
+
144
+ | Run | Result |
145
+ | --- | --- |
146
+ | 80 official `@deepseek-ai/*` packages (zero-false-positive baseline) | **0 error · 0 warn** · 241 info |
147
+ | Top 100 published community plugins | **1 error · 86 warn** · 240 info |
148
+ | — of which couple to a Harness Client package | **62%** |
149
+ | — of which declare no DSH compatibility range | **87%** |
150
+
151
+ The official-package run predates the Top 100 run (both 2026-10-13); the two info totals are no longer equal (241 = 80 I1 + 160 I2 + 1 W3 on the official set; 240 = 87 I1 + 136 I2 + 17 W3 on the Top 100). The error rules fired on 1 of the 100 published plugins (E1: an append type outside the catalog). The daily value is the coupling inventory (W1) and declaration completeness (I1/I2), which nearly every plugin is missing something on. See [docs/07](docs/07-m1-results.md) and [docs/09](docs/09-ecosystem-measurements.md).
152
+
109
153
  ## The probe
110
154
 
111
155
  Static analysis reads code. The probe reads **behaviour**: it boots a real DSH in a throwaway home with one plugin installed and records what the host did with it.
@@ -128,39 +172,20 @@ Two details make it work, and both were learned the hard way:
128
172
  - **The exit code is never the signal.** A mount failure is a warning and the process still exits 0.
129
173
 
130
174
  ```bash
131
- node scripts/probe-boot.mjs dsh-mnemon --tools
175
+ node scripts/probe-boot.mjs dsh-mnemon --tools --dsh 0.2.0-rc.2
132
176
  # active (3732 ms)
133
177
  # tools 24 -> 41 +17: mnemon_remember, mnemon_recall, mnemon_forget, ...
134
178
  # system prompt 2640 -> 7153 chars
135
179
  ```
136
180
 
137
- See [docs/11](docs/11-m4-capability-probe.md).
138
-
139
- ## Why this exists
140
-
141
- Measured on 2026-10-13 against the top 40 published community plugins and an installed DSH 0.2.0-rc.2:
181
+ That is example output from the 2026-10-13 run. The probe runs the target in a hardened Docker container (read-only root, no capabilities, no-new-privileges, non-root, network only during install) and refuses to run when Docker is unavailable. `--unsafe-host` is the explicit escape hatch: it runs the desktop DSH and the plugin in the current user's environment without isolation, and its results stay self-reported. The Docker backend needs an explicit `--dsh <version>`; the desktop DSH can only be probed with `--unsafe-host`. Run the probe only on plugins you are willing to execute.
142
182
 
143
- | Observation | Result |
144
- | --- | --- |
145
- | plugins whose `peerDependencies` couple them to a Harness Client package | **16 / 40 (40%)** |
146
- | plugins declaring `dsh.compatibility.dshReleases` | **5 / 40 (13%)** |
147
- | plugins declaring an `icon` | 16 / 40 (40%) |
148
-
149
- The ecosystem has 4,508 catalogued plugins and adds roughly a thousand a month. Upgrade breakage is a product category of its own (three tools sell the *recovery*), but nothing was checking the code against the documented practices.
150
-
151
- ### Measured with this tool
152
-
153
- | Run | Result |
154
- | --- | --- |
155
- | 80 official `@deepseek-ai/*` packages (zero-false-positive baseline) | **0 error · 0 warn** · 239 info |
156
- | Top 100 published community plugins | **1 error · 86 warn** · 223 info |
157
- | — of which couple to a Harness Client package | **62%** |
158
- | — of which declare no DSH compatibility range | **87%** |
159
-
160
- The error rules did not fire on any published plugin. They are insurance against rare, fatal mistakes — the daily value is the coupling inventory (W1) and declaration completeness (I1/I2), which nearly every plugin is missing something on. See [docs/07](docs/07-m1-results.md).
183
+ See [docs/11](docs/11-m4-capability-probe.md).
161
184
 
162
185
  ## Usage
163
186
 
187
+ The npm release is **0.1.0** (published 2026-10-14). This checkout is the **0.4.0 release candidate, unpublished**; the analysis commands described below exist only in the checkout.
188
+
164
189
  ```bash
165
190
  npx @shidesheng0218/dsh-plugin-lint . # lint the package in the current directory
166
191
  npx @shidesheng0218/dsh-plugin-lint ./plugins/a ./plugins/b
@@ -172,42 +197,211 @@ npx @shidesheng0218/dsh-plugin-lint . --exit-code # exit 1 when an error-level
172
197
 
173
198
  Exit codes: `0` ok · `1` errors found with `--exit-code` · `2` the run failed.
174
199
 
175
- ## Compatibility matrix
200
+ The 0.4.0 author workflow adds the CI-facing surface:
176
201
 
177
- The probe also runs as a grid: one cell per plugin version × DSH version × composition.
202
+ ```bash
203
+ node bin/dsh-plugin-lint.mjs . --phase author # pre-build TypeScript tree: unbuilt outputs are hints, not errors
204
+ node bin/dsh-plugin-lint.mjs . --fail-on warn # gate on errors+warnings; --max-warnings N also works
205
+ node bin/dsh-plugin-lint.mjs . --format github # GitHub Actions annotations
206
+ node bin/dsh-plugin-lint.mjs . --format junit -o report.xml
207
+ node bin/dsh-plugin-lint.mjs . --format json-envelope # { schemaVersion, reports, baseline }
208
+ node bin/dsh-plugin-lint.mjs . --baseline .dsh-plugin-lint-baseline.json
209
+ node bin/dsh-plugin-lint.mjs . --write-baseline # record the current findings
210
+ node bin/dsh-plugin-lint.mjs --init # write a workflow, baseline, and agent skill
211
+ node bin/dsh-plugin-lint.mjs . --dsh-version 0.2.2-rc.1 # E6 checks the host's peer admission
212
+ node bin/dsh-plugin-lint.mjs . --changed HEAD # only packages touched since HEAD (or --staged)
213
+ node bin/dsh-plugin-lint.mjs . --pack # lint the tarball npm pack would publish
214
+ node bin/dsh-plugin-lint.mjs dist/plugin-1.2.3.tgz # or lint an existing tarball
215
+ node bin/dsh-plugin-lint.mjs --from-npm @scope/name@1.2.3 # lint a published artifact
216
+ node bin/dsh-plugin-lint.mjs --profile ~/.dsh/profiles/default # host-truth scan of an installed profile
217
+ node bin/dsh-plugin-lint.mjs --rules --explain E6
218
+ ```
219
+
220
+ `--changed` and `--staged` map a git diff back to the nearest `package.json`, so a monorepo lints only the packages a PR touches. `--pack`, `.tgz` and `--from-npm` force the publish phase and lint the real artifact: missing subpath exports, bins or icons are reported against what npm will actually ship, and tarballs containing path traversal or symlinks are rejected before extraction. `--profile` reads the runtime version and installed bundles from a DSH profile, reports which plugins the host will load or reject, and prints the exact `dsh plugin allow-version … --accept-risk` command for each rejection; with `--exit-code` it fails on any `will-reject`.
221
+
222
+ The composite action is the same surface in CI:
223
+
224
+ ```yaml
225
+ jobs:
226
+ lint:
227
+ runs-on: ubuntu-latest
228
+ permissions:
229
+ contents: read
230
+ pull-requests: write # needed only when comment: true
231
+ steps:
232
+ - uses: actions/checkout@v4
233
+ - uses: shidesheng0218/dsh-plugin-lint@main # or the release tag once v0.4.0 is tagged
234
+ with:
235
+ path: .
236
+ fail-on: error
237
+ baseline: .dsh-plugin-lint-baseline.json
238
+ comment: true # one sticky PR comment, created once and updated in place
239
+ ```
240
+
241
+ Set `sarif: dsh-plugin-lint.sarif` to write SARIF, then upload it in a separate step (the job also needs `security-events: write`):
242
+
243
+ ```yaml
244
+ - uses: shidesheng0218/dsh-plugin-lint@main
245
+ with:
246
+ path: .
247
+ sarif: dsh-plugin-lint.sarif
248
+ - uses: github/codeql-action/upload-sarif@v3 # use the current major from GitHub's docs
249
+ with:
250
+ sarif_file: dsh-plugin-lint.sarif
251
+ ```
252
+
253
+ The package ships `schema/dsh-manifest.schema.json` (subpath `@shidesheng0218/dsh-plugin-lint/schema/dsh-manifest.schema.json`). Point `$schema` at it so the editor validates the `dsh` block before CI runs:
254
+
255
+ ```json
256
+ {
257
+ "$schema": "https://raw.githubusercontent.com/shidesheng0218/dsh-plugin-lint/main/schema/dsh-manifest.schema.json",
258
+ "name": "@you/my-plugin",
259
+ "version": "1.0.0",
260
+ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
261
+ }
262
+ ```
263
+
264
+ The baseline fingerprint is `sha256(rule + file + normalized evidence)` and deliberately excludes the line number, so reformatting or rebuilding does not revive a suppressed finding. Every report still prints how many findings the baseline suppressed.
265
+
266
+ To run the checkout instead of the npm release, use `node bin/dsh-plugin-lint.mjs <dir>`. Commands in this README that start with `node scripts/` also run from a **checkout** — the matrix runner, the probe, the upgrade preview, and the report builders read files the npm package does not ship.
267
+
268
+ ## v0.3 analysis (experimental, unpublished)
269
+
270
+ The workspace is at 0.4.0 (release candidate); the latest npm release is 0.1.0. The four capabilities below ship in the checkout only and are **experimental**: they are static heuristics over files on disk. None of them executes a plugin, measures runtime cost, or issues a security verdict. A clean run means no pattern matched.
271
+
272
+ | Command | The question it answers |
273
+ | --- | --- |
274
+ | `dsh-plugin-analyze conflicts <dir...>` | do two or more plugins claim the same slot, tool name, route or config key? |
275
+ | `dsh-plugin-analyze performance <dir>` | how much does the package ship, and which static patterns look expensive? |
276
+ | `dsh-plugin-analyze security <dir>` | which patterns would a reviewer want to look at first? |
277
+ | `dsh-plugin-analyze full <dir>` | lint + performance + security for one package |
278
+ | `dsh-plugin-upgrade-plan ...` | given a profile and a target DSH version, which plugins need action? |
279
+
280
+ All four `analyze` subcommands accept `--json` (stdout is pure JSON; diagnostics go to stderr) and `--exit-code`. `--help` exits 0; an unknown option exits 2; a missing package directory exits 2.
281
+
282
+ With `--exit-code`: `conflicts` exits 1 when any conflict is found, `security` exits 1 when a warning-level finding is present (info-level inventory entries do not gate), `full` exits 1 when lint reports an error or security reports a warning, and `performance` always exits 0 (a score is not a gate). Without the flag every successful run exits 0.
283
+
284
+ ### Composition conflicts
285
+
286
+ ```bash
287
+ node bin/dsh-plugin-analyze.mjs conflicts ./plugin-a ./plugin-b ./plugin-c
288
+ node bin/dsh-plugin-analyze.mjs conflicts ./plugin-a ./plugin-b --json
289
+ ```
290
+
291
+ The static scan compares literals at registration sites: UI slot names (`ctx.slots.inject` / `ctx.slots.register`), tool names (`ctx.tools.register(defineTool(...))` and declarative arrays), route paths (`connection.fetch.register`, older `ctx.router.*`), config keys, and waterfall listeners. A name claimed by two plugins is reported with the file, line and a resolution hint; two declarations by the same plugin are not reported as a conflict. Dynamic names are not resolved. The runtime authority is `scripts/matrix/conflicts.mjs`, which installs the plugins together and reads what the host registered — the static scan is a pre-flight hint, and a miss is not proof that two plugins compose.
292
+
293
+ ### Performance analysis
294
+
295
+ ```bash
296
+ node bin/dsh-plugin-analyze.mjs performance ./my-plugin
297
+ ```
298
+
299
+ Static counts, not measurements: shipped and JavaScript bytes, largest files, lines/functions/nesting depth (long functions are >100 lines), DOM access patterns, literal network call sites, and heavy dependency patterns. Comments are excluded because the analysis reads the masked source. The 0–100 score is a heuristic index assembled from fixed penalty thresholds; it is useful for comparing two packages or tracking a package over time, not as a runtime claim. A package with no JavaScript to analyse scores 0 with rating `insufficient-data` — the absence of evidence is not a clean bill.
300
+
301
+ ### Security deep analysis
302
+
303
+ ```bash
304
+ node bin/dsh-plugin-analyze.mjs security ./my-plugin
305
+ ```
306
+
307
+ Ten pattern rules (S1–S10) look for session data sent to external domains, credentials written to browser storage, `eval`/`Function`, non-static `innerHTML` in the client entry, literal external domains, shell commands built by interpolation, absolute-path file access, crypto use, `process.env` access, and insecure HTTP. Every rule reports at warn or info level: these patterns are not mechanically decidable, so none is an error. Rules read the masked source, so a URL or `eval` inside a comment is not a finding. S5 is an inventory of literal external domains — the report does not compare them against a declared capability, because the DSH manifest has no such field. `riskLevel` summarises the warning count (minimal · low · medium · high, no "critical") and is not a verdict; read the findings, not the label.
308
+
309
+ ### Upgrade plan
310
+
311
+ ```bash
312
+ node bin/dsh-plugin-upgrade-plan.mjs --from 0.2.0-rc.2 --to 0.2.1-alpha.2 --installed ~/.dsh/profiles/default
313
+ node bin/dsh-plugin-upgrade-plan.mjs --from 0.2.0-rc.2 --to 0.2.1-alpha.2 --installed <profile> --matrix matrix/cells --json
314
+ node bin/dsh-plugin-upgrade-plan.mjs --check-compatibility ./my-plugin --target 0.2.1-alpha.2
315
+ ```
316
+
317
+ It reads the profile's installed bundles, joins them against the local matrix in `matrix/cells` (override with `--matrix <dir>`), and reports what changes on the target version. It makes no network request and does not query npm for newer versions: a recommended version is one the matrix observed active on the target DSH version. `--json` emits pure JSON in both modes.
318
+
319
+ Exit codes for the analysis mode: `0` no action needed · `1` at least one plugin needs an update or has no compatible version · `2` the run failed, including when no matrix data is available. `--check-compatibility`: `0` compatible · `1` incompatible · `2` unknown or no data.
320
+
321
+ The matrix is not shipped in the npm package and `matrix/cells/` is gitignored, so this command only works where a matrix snapshot exists. Its coverage is the 50 plugins in that snapshot, not the whole ecosystem.
322
+
323
+ ### Programmatic API
324
+
325
+ The analysis functions live behind the `./experimental` subpath; the package entry point keeps only the ten stable core exports. In the unpublished checkout:
326
+
327
+ ```javascript
328
+ import { loadPackage } from './src/index.js';
329
+ import {
330
+ detectConflicts,
331
+ analyzePerformance,
332
+ analyzeSecurityDeep
333
+ } from './src/experimental/index.js';
334
+
335
+ const packages = ['./plugin-a', './plugin-b'].map((dir) => {
336
+ const pkg = loadPackage(dir);
337
+ return { name: pkg.manifest.name, manifest: pkg.manifest, jsFiles: pkg.jsFiles };
338
+ });
339
+
340
+ const conflicts = detectConflicts(packages); // { slots, toolNames, routes, configKeys, eventListeners, summary, experimental, limits }
341
+ const perf = analyzePerformance(loadPackage('./plugin-a')); // { score, rating, recommendations, experimental, limits, … }
342
+ const security = analyzeSecurityDeep(loadPackage('./plugin-a')); // { findings, summary, riskLevel, experimental, limits, … }
343
+ ```
344
+
345
+ The published 0.1.0 package predates these exports. See [docs/19](docs/19-analysis-tools.md) for field-level documentation and [docs/21](docs/21-release-0.4.0.md) for the 0.4.0 release notes (draft).
346
+
347
+ ## The compatibility matrix
348
+
349
+ The probe also runs as a grid: one cell per plugin × DSH version × composition.
178
350
 
179
351
  ```bash
180
352
  node scripts/matrix/run.mjs --targets matrix/targets-top50.json --concurrency 4
181
353
  node scripts/matrix/site.mjs
182
354
  ```
183
355
 
184
- Measured on 2026-10-13 over **228 cells** (57 plugins, two DSH versions, headless and web):
356
+ The current tracked snapshot was collected on **2026-10-16** (`generatedAt 2026-10-16T02:45:50Z`): **50 plugins × 2 DSH versions × 2 compositions = 200 cells**. The counts are in [`panel/data/aggregate.json`](panel/data/aggregate.json):
185
357
 
186
- | Status | Cells | Meaning |
358
+ | Status | 0.2.0-rc.2 | 0.2.1-alpha.2 |
187
359
  | --- | --- | --- |
188
- | active | 136 | the host half mounted |
189
- | awaiting-service | 31 | this composition does not provide a service it waits for |
190
- | rejected-by-host | 30 | **the host itself refused the install** |
191
- | install-failed | 3 | network (native package download) |
360
+ | active | 80 | 72 |
361
+ | awaiting-service | 16 | 20 |
362
+ | rejected-by-host | 2 | 8 |
363
+ | install-failed | 2 | 0 |
192
364
 
193
- **17 of 57 plugins get a worse verdict on the newer DSH version and none get better**; 14 are the host refusing the install outright, and the largest system-prompt growth observed was 10,862 characters. Aggregate numbers: [docs/09](docs/09-ecosystem-measurements.md).
365
+ Each version column sums to 100 cells (50 plugins × headless/web). The snapshot covers only the two pinned DSH versions; newer releases are not tested.
194
366
 
195
- Plugin authors can run the same probe from their own CI — [docs/15](docs/15-federation.md):
367
+ Three data-quality caveats apply to the matrix and to anything derived from it:
196
368
 
197
- ```yaml
198
- - uses: shidesheng0218/dsh-plugin-lint/.github/actions/probe@v1
199
- with:
200
- target: .
201
- dsh-versions: 0.2.0-rc.2,0.2.1-alpha.2
369
+ - **Recorded version ≠ loaded version.** A cell records `plugin.version` from the registry at collection time, but the probe installs by bare package name. In **23 of the 200 cells** the recorded version differs from the version the cell notes was actually loaded (e.g. `dsh-context` records 0.72.0 while 0.71.0 was installed). The `reproduce` command in a cell does not pin a version, so rerunning it today can test a different release.
370
+ - **The per-plugin data is not in the repository.** `matrix/cells/` and `matrix/index/` are gitignored. A clean clone contains the target list and the aggregate counts, not the cells; rebuilding them needs network access, npm and the probe.
371
+ - **Older totals are not cited.** Earlier drafts reported a larger plugin run and a plugin-level count of verdicts that got worse. That run cannot be reconstructed from this repository, so it is not cited here; [docs/09](docs/09-ecosystem-measurements.md) records why.
372
+
373
+ ### Upgrade preview
374
+
375
+ The matrix answers "which version breaks this plugin"; the preview answers the question a user actually has: **should this profile upgrade?** It reads the profile's installed bundles, joins them against the matrix, and reports what changes on the target version:
376
+
377
+ ```bash
378
+ node scripts/upgrade-preview.mjs ~/.dsh/profiles/default
202
379
  ```
203
380
 
204
- Self-reported cells are never presented as ours: they are sampled, re-run, and either promoted to `verified` or marked `disputed` with both verdicts kept.
381
+ ```text
382
+ Upgrade preview: 0.2.0-rc.2 -> 0.2.1-alpha.2
383
+ Profile ~/.dsh/profiles/default · 8 installed (6 community, 2 first-party)
384
+
385
+ 2 worse · 0 better · 3 unchanged · 1 not covered by the matrix
386
+
387
+ ✗ @michengai/dsh-archive-manager
388
+ 0.2.0-rc.2: active -> 0.2.1-alpha.2: rejected-by-host
389
+ ```
390
+
391
+ The output above is an illustrative sample. A plugin the matrix does not cover is reported as unknown, not as a verdict. First-party bundles are counted but not previewed — the matrix says nothing about them.
205
392
 
206
393
  ## Composition conflicts
207
394
 
208
- A plugin that mounts alone can still collide with another. `conflicts.mjs` reads the tool names already recorded in the matrix cells, groups them by owner, and then verifies a candidate by installing the pair into **one** profile.
395
+ A plugin that mounts alone can still collide with another. `conflicts.mjs` scans two dimensions: the tool names recorded in the matrix cells (what the host registered), and the slot names each plugin's client bundle declares (what the code claims). A name claimed by two plugins in either dimension is a candidate, verified by installing the pair into **one** profile.
396
+
397
+ Measured on 2026-10-13 over the sampled ecosystem (16 plugins):
209
398
 
210
- The sampled ecosystem had no collisions (16 plugins, 118 tool names), so two fixtures claim the same tool name on purpose. Measured result:
399
+ | Dimension | Collisions found |
400
+ | --- | --- |
401
+ | tool names (118 distinct) | **0** |
402
+ | slot names (37 distinct) | **10** — `settings.section` alone is claimed by three plugins |
403
+
404
+ The tool-name result is a clean bill; the slot result is not. Two fixtures claim the same tool name on purpose to show what a collision looks like:
211
405
 
212
406
  | Stage | Result |
213
407
  | --- | --- |
@@ -219,31 +413,68 @@ tool-beta: Error: tool "collide_probe" is already registered
219
413
  (for a per-agent variant, register through that agent's `agent.ctx` instead)
220
414
  ```
221
415
 
222
- It is not "the last one wins" — the later plugin is discarded whole. And because a failed mount is only a warning with exit code 0, a user can run two clashing plugins with one of them silently doing nothing. See [docs/18](docs/18-composition-conflicts.md).
416
+ It is not "the last one wins" — the later plugin is discarded whole. And because a failed mount is only a warning with exit code 0, a user can run two clashing plugins with one of them silently doing nothing. This measurement is historical: it depends on matrix cells that are gitignored, and the installed-pair verification is a manual step, not part of the CLI. See [docs/18](docs/18-composition-conflicts.md).
223
417
 
224
418
  ## The panel
225
419
 
226
- [`panel/`](panel/) is a real DSH plugin: a settings section listing what this profile has installed and how each plugin fares across DSH versions, read from the matrix snapshot. The screenshot at the top of this file is it, captured by [`scripts/screenshot-panel.mjs`](scripts/screenshot-panel.mjs) in a throwaway web profile.
420
+ [`panel/`](panel/) is a real DSH plugin: a settings section — titled **Plugin Health Center** — listing what this profile has installed, how each plugin fares across DSH versions, and, below the table, aggregate ecosystem counts per version (never a package name). The screenshot at the top of this file was captured by [`scripts/screenshot-panel.mjs`](scripts/screenshot-panel.mjs) in a throwaway web profile.
421
+
422
+ The panel reads two data files: the full matrix snapshot when one exists locally (`data/matrix-index.json`, overridable with `DSH_PLUGIN_LINT_INDEX`), and the aggregate that ships in the npm package (`data/aggregate.json`, counts only). **An npm-installed panel therefore shows the ecosystem counts and marks installed plugins as uncovered; per-plugin rows need a local matrix snapshot.** The npm release is panel 0.1.5; the 0.2.0 in this checkout is a release candidate, unpublished. The published 0.1.5 predates the server-side action guards in this checkout (it has no content-type, confirmation, protected-package, or installed-only check and hardcodes `--profile desktop`), so do not install it into a profile that runs untrusted plugins until a fixed release ships.
423
+
424
+ With no per-plugin data, the page still shows the matrix overview: 50 plugins across 2 DSH versions, with 80 of 100 cells active on 0.2.0-rc.2 and 72 of 100 on 0.2.1-alpha.2.
227
425
 
228
426
  ```bash
229
- node scripts/matrix/bundle-panel-data.mjs # snapshot the matrix into the package
230
- dsh plugin --profile <profile> add <absolute path to panel/>
427
+ node scripts/matrix/bundle-panel-data.mjs # snapshot the local matrix into the package
428
+ dsh plugin --profile <profile> add <path to panel/> # from a checkout
231
429
  ```
232
430
 
233
431
  It mounts on both pinned DSH versions in both compositions, and it passes this project's own linter with zero findings — only `react` comes from the browser module table, no Harness Client package is imported, and colours use `--dsw-alias-*` tokens with fallbacks.
234
432
 
235
433
  Visual verification caught two bugs a code review had not: the page said "Running DSH unknown" because `import.meta.resolve` cannot see the harness from inside a profile, and it called first-party bundles "not installed" although the dsh installation provides them. Both are fixed, and [docs/16](docs/16-m5-panel.md) records what was checked.
236
434
 
435
+ ## Federation
436
+
437
+ Plugin authors can run the same probe from their own CI — [docs/15](docs/15-federation.md). The composite action in `.github/actions/probe/` is **not usable yet** (its `github.action_path` points at the action directory, which does not contain the scripts it calls), so today the working path is a checkout plus:
438
+
439
+ ```bash
440
+ node scripts/probe-boot.mjs <plugin-dir> --tools --dsh 0.2.0-rc.2 --mode both
441
+ ```
442
+
443
+ Self-reported cells are never presented as ours: they are sampled, re-run, and either promoted to `verified` or marked `disputed` with both verdicts kept. A disagreement is never hidden — that is the property the rest of the matrix depends on.
444
+
445
+ | Trust level | Source | Meaning |
446
+ | --- | --- | --- |
447
+ | `verified` ✅ | our runs, or a sampled re-run that matched | we stand behind "this is what we observed" |
448
+ | `self-reported` 🟡 | author's CI, not yet re-run | represents only what the author reported |
449
+ | `disputed` ⚠️ | re-run disagreed | both verdicts stay in the file; the difference is public |
450
+ | `stale` ⏳ | older than 30 days, or a newer DSH exists | the verdict may have expired |
451
+
452
+ The self-reported path is not complete: the cells and the verification report it depends on are gitignored, so a self-reported cell has no tracked route into the matrix yet. Treat the mechanism as designed-but-not-operational.
453
+
454
+ ## Methodology
455
+
456
+ Every round of wider sampling exposed a systematic misjudgement in the measuring, not the measured. Each correction is now load-bearing. The runs below are historical; their cells are gitignored and cannot be reconstructed from this repository:
457
+
458
+ | Round | Scale | What it exposed | The fix |
459
+ | --- | --- | --- | --- |
460
+ | smoke | 22 cells | "waiting for a service" was scored as failure | renamed `awaiting-service` + added the web composition |
461
+ | Top 50 | 200 cells | exit codes were trusted as install results (23 of 26 "failures" had in fact installed) | verify the artifact: did the profile's package.json change? |
462
+ | rule design | — | 3 of the first 12 rules would have flagged the official packages | rules re-graded by decidability; the official packages are the zero-false-positive baseline |
463
+
464
+ A small sample cannot find systematic bias, so widening the sample is itself a verification step. No rule and no verdict criterion ships until it has run on real data. See [docs/09](docs/09-ecosystem-measurements.md).
465
+
237
466
  ## Data policy
238
467
 
239
- This repository ships the **tool**, not the results. Any output that names a package — `reports/`, `notify/`, `matrix/cells/`, `matrix/index/`, `site/`, and the panel's matrix snapshot — is generated locally and untracked.
468
+ This repository ships the **tool**, not the results. Any output that names a package — `reports/`, `notify/`, `matrix/cells/`, `matrix/index/`, `site/`, and the panel's per-plugin matrix snapshot — is generated locally and untracked.
240
469
 
241
470
  Two reasons, and they point the same way:
242
471
 
243
472
  1. **Naming a package is a claim about someone's work.** Doing it well means contacting that maintainer first, and that is a decision, not a default.
244
473
  2. **Verified data is only meaningful for the environment that produced it.** Whoever runs the probe gets their own answer; a snapshot from our machine is not evidence about theirs.
245
474
 
246
- The aggregate numbers are published, because they describe the ecosystem rather than any one maintainer: see [docs/09](docs/09-ecosystem-measurements.md).
475
+ The aggregate counts (`panel/data/aggregate.json`) are tracked, because they describe the ecosystem rather than any one maintainer. See [docs/09](docs/09-ecosystem-measurements.md).
476
+
477
+ `matrix/public/` — the runtime source the panel tries to sync — is also gitignored. Publishing it is a deliberate, manual decision (`matrix refresh` with `publish_named_data=true`), not a side effect of the weekly schedule; until that decision is made, the panel falls back to the bundled aggregate.
247
478
 
248
479
  ## Reports
249
480
 
@@ -258,15 +489,19 @@ Every report names packages, so the project follows one rule (ADR D-001): **name
258
489
  ## Development
259
490
 
260
491
  ```bash
261
- npm test # unit tests for every rule (node:test, no dependencies)
262
- npm run e2e:official # zero-false-positive baseline against official packages
492
+ npm test # node --test test/*.test.js: 430 cases across 49 files
493
+ npm run test:coverage # lines >=85%, branches >=80%, functions >=80%
494
+ npm run e2e:official # zero-false-positive baseline against official packages (needs npm access)
495
+ npm run package:smoke # pack both tarballs, install them, run every bin and entry point
263
496
  ```
264
497
 
265
- `npm run e2e:official` is the acceptance test that matters: it lints the official `@deepseek-ai/*` packages, which are the reference implementation of the documented practices. **Any error-level finding on them is a bug in a rule, not in the package.** Extract the fixtures first:
498
+ At this revision the suite has **430 node:test cases across 49 files**, coverage is gated at lines ≥85%, branches ≥80%, functions ≥80%, and the measured run is **95.3% lines / 86.2% branches / 96.1% functions**. `npm test` passes from a plain shell and from inside a DSH shell — the panel tests isolate ambient `DSH_*` variables, and the CLI tests build fixtures in temporary directories. The count is measured, not aspirational; re-run the command if you need the current number.
499
+
500
+ `npm run e2e:official` is the acceptance test that matters: it lints the official `@deepseek-ai/*` packages, which are the reference implementation of the documented practices. **Any error-level finding on them is a bug in a rule, not in the package.** The CI job fetches the packages from npm first:
266
501
 
267
502
  ```bash
268
- node scripts/extract-official-fixtures.mjs # reads an installed DSH (asar)
269
- node scripts/extract-official-fixtures.mjs --limit 80
503
+ node scripts/fetch-official-from-npm.mjs --limit 80
504
+ node scripts/e2e-official.mjs
270
505
  ```
271
506
 
272
507
  ## License
@@ -275,11 +510,11 @@ Apache-2.0. See [NOTICE](NOTICE) for the data derived from DeepSeek Harness (MIT
275
510
 
276
511
  ---
277
512
 
278
- ## 中文说明
513
+ ## 中文
279
514
 
280
515
  **给 DeepSeek Harness 插件做合规体检与兼容性实测的工具链:只陈述检测到的事实,每条结论都带文件、行号、证据和官方出处。**
281
516
 
282
- **四件事,四个问题**:
517
+ 四个工具,各回答一个问题:
283
518
 
284
519
  | 工具 | 回答的问题 |
285
520
  | --- | --- |
@@ -288,14 +523,213 @@ Apache-2.0. See [NOTICE](NOTICE) for the data derived from DeepSeek Harness (MIT
288
523
  | 矩阵跑批 | 哪个版本会让它坏掉 |
289
524
  | `conflicts` | 几个插件装在一起会不会互相顶掉 |
290
525
 
291
- **它不是什么**:不是安全背书(干净≠安全);也不替代旁边那几个工具——doctor 管包结构、testkit 管真实宿主生命周期、trust-check 管能力披露、my-guardian 管崩了之后怎么办。
526
+ 它不是什么:不是安全背书(干净的运行只代表没有规则命中);也不替代旁边几个工具——doctor 管包结构、testkit 管真实宿主生命周期、trust-check 管能力披露、my-guardian 管崩了之后怎么办。
527
+
528
+ 静态检查与静态分析命令不需要构建、不需要配置文件、不需要网络;探针与矩阵跑批会从 npm 安装包,因此需要网络;升级规划只读本地矩阵,不发起网络请求。
529
+
530
+ ### 为什么需要它
531
+
532
+ 用于测量的生态目录快照收录 4,508 个条目(2026-10-11)。2026-10-13 对 Top 100 已发布社区插件做静态检查(分析已发布 tarball,不执行插件代码):
533
+
534
+ | 观测 | 结果 |
535
+ | --- | --- |
536
+ | 加载了官方 Harness Client 包 | **62 / 100(62%)** |
537
+ | 声明了 `dsh.compatibility.dshReleases` | **13 / 100(13%)** |
538
+ | 没有声明任何 DSH 兼容范围 | **87 / 100(87%)** |
539
+ | 声明了展示图标 | 31 / 100(31%) |
540
+ | 触发至少一条 error 级规则 | 1 / 100(1%) |
541
+
542
+ 耦合了 `@deepseek-ai/dsh-client-*` 的插件,在宿主无通知更新这些包时会坏;没声明兼容范围的插件,升级那天就是未测试状态;`files[]` 漏掉产物的问题本地测试发现不了,发布后才出现。
543
+
544
+ 用本工具实测(2026-10-13):
545
+
546
+ | 跑批 | 结果 |
547
+ | --- | --- |
548
+ | 80 个官方 `@deepseek-ai/*` 包(零误报基线) | **0 error · 0 warn** · 241 info |
549
+ | Top 100 已发布社区插件 | **1 error · 86 warn** · 240 info |
550
+ | — 其中耦合官方 Client 包 | **62%** |
551
+ | — 其中没有声明兼容范围 | **87%** |
552
+
553
+ 这些数字背后的原始产物(`reports/`、`matrix/cells/`)在本地生成、被 gitignore,干净 clone 里没有。聚合记录见 [生态测量](docs/09-ecosystem-measurements.md),数据政策见下文。
554
+
555
+ ### 规则分级
556
+
557
+ 按"能不能可靠判定"分级,而不是按听起来多严重:error 级必须机器可判、且在官方包里从不出现;warn 级是官方文档点名的风险写法,必须附证据;info 级是提示与元数据完整性。判不准的规则不配对插件下结论——判不准就降级,或者删掉。规则编号是稳定标识,不是严重度缩写:`W3` 实际报 info,`I3` 实际报 warn。初版 12 条规则里有 3 条按字面实现会误报官方包,已按这个原则处理,详见 [详细设计](docs/06-detailed-design.md)。
558
+
559
+ 验收基线是 80 个官方 `@deepseek-ai/*` 包 **0 error · 0 warn**(241 条 info):官方包是文档规范的参考实现,error 级规则命中官方包,先假定是规则的 bug。
560
+
561
+ E1 的事件白名单是 380 个官方 session 事件类型(采集自官方包,其中 379 个被实际观测到)。
562
+
563
+ W1 规则对应的官方文档原文:
564
+
565
+ > Do not `require('@deepseek-ai/dsh-client-ui-primitives')` or load any other Harness Client package as a module … copy markup, CSS, and behavior from the primitive into the plugin.
566
+ > — `dsh plugin-development / references/practices.md`
567
+
568
+ Top 100 里 62 个插件加载了至少一个官方 Client 包;ui-primitives 被 48 个插件引用(71 是提及它的 W1 finding 条数)。
569
+
570
+ ### 探针
571
+
572
+ 静态分析读代码,探针读**行为**:在一次性的 DSH_HOME 里物化官方 profile,装入一个插件,用本地桩 provider 接住宿真实发出的请求,从请求体里读出注册的工具清单和系统提示词长度。不需要 API key。
573
+
574
+ 两个只有实测才能发现的坑:
575
+
576
+ - **退出码永远不是信号。** 挂载失败只是 warning,进程退出码仍为 0——所以探针解析 stderr。同理,安装结果也不能信退出码:历史跑批里 26 个"安装失败"有 23 个其实装成功了,判据改为校验产物(profile 的 package.json 是否变化)。
577
+ - **成功的哨兵是一个缺失的凭据。** 一次性环境里没有 API key,所以挂载成功的插件会以 `MISSING_CREDENTIAL` 结束。这就是信号。
578
+
579
+ 探针默认在加固的 Docker 容器里运行目标插件(只读根文件系统、无 capability、非 root、仅在安装阶段联网),Docker 不可用时拒绝执行,而不是静默退回本机。`--unsafe-host` 是显式逃生口:它用桌面版 DSH 在当前用户环境里无隔离运行,结果只能算 self-reported。Docker 后端必须显式给 `--dsh <版本>`;桌面 DSH 只能用 `--unsafe-host`。只对愿意执行的插件运行探针。
580
+
581
+ ### 兼容性矩阵
582
+
583
+ 探针按网格跑:一格 = 一个插件 × 一个 DSH 版本 × 一种组合。当前进版本库的快照采集于 **2026-10-16**:**50 个插件 × 2 个 DSH 版本 × 2 种组合 = 200 格**,计数在 [`panel/data/aggregate.json`](panel/data/aggregate.json):
584
+
585
+ | 状态 | 0.2.0-rc.2 | 0.2.1-alpha.2 |
586
+ | --- | --- | --- |
587
+ | active | 80 | 72 |
588
+ | awaiting-service | 16 | 20 |
589
+ | rejected-by-host | 2 | 8 |
590
+ | install-failed | 2 | 0 |
591
+
592
+ 每个版本列合计 100 格(50 个插件 × headless/web)。快照只覆盖这两个固定 DSH 版本,更新的版本没有测过。
593
+
594
+ 矩阵及由其派生的结论有三条数据质量限制:
595
+
596
+ - **记录版本 ≠ 实际加载版本。** cell 里的 `plugin.version` 是采集时从 registry 抓到的版本,而探针安装时用的是裸包名。**200 格里 23 格**记录的版本与 cell 自己记录的"实际装入版本"不一致(例如 `dsh-context` 记录 0.72.0、实际装的是 0.71.0)。cell 里的 `reproduce` 命令不带版本,今天重跑可能测到另一个发布版本。
597
+ - **逐插件数据不在仓库里。** `matrix/cells/` 与 `matrix/index/` 被 gitignore;干净 clone 只有目标清单和聚合计数,重建需要网络、npm 与探针。
598
+ - **旧跑批数字不引用。** 早期草稿引用的更大跑批与"变差插件数"无法从本仓库重建,本 README 不引用;原因见 [生态测量](docs/09-ecosystem-measurements.md)。
599
+
600
+ ### 升级预览
601
+
602
+ 矩阵回答"哪个版本会坏",升级预览回答用户真正的问题:**这个 profile 该不该升**。读当前 profile 装了什么,与矩阵对照,报告目标版本上每个插件的变化:
603
+
604
+ ```bash
605
+ node scripts/upgrade-preview.mjs ~/.dsh/profiles/default
606
+ ```
607
+
608
+ 输出为示例。矩阵没覆盖的插件报 "not covered / no data",不是裁决;官方 bundle 计数但不预览——矩阵对它们无话可说。
609
+
610
+ ### 构成冲突
611
+
612
+ 一个插件单独能挂载,不代表它和别的插件能共存。`conflicts.mjs` 扫两个维度:矩阵格子里记录的工具名(宿主注册了什么),和每个插件 client bundle 里声明的 slot 名(代码声称要挂到哪)。任一维度撞名即为候选,再装进**同一个** profile 验证。
613
+
614
+ 实测(2026-10-13,16 个插件):工具名 **0** 撞名(118 个不同名字),**slot 名 10 个撞名**——`settings.section` 一个位置就有 3 个插件认领。工具名干净,slot 不然。
615
+
616
+ 两个夹具插件故意撞名演示后果:各自单独装都正常(各 +1 个工具),**装在一起后,后注册的那个整个插件没有挂载**:
617
+
618
+ ```text
619
+ tool-beta: Error: tool "collide_probe" is already registered
620
+ ```
621
+
622
+ 不是"后装的覆盖先装的",是后装的整个被丢弃。又因为挂载失败只是 warning、退出码 0,用户可以在一个插件悄悄不干活的状态下一直用下去。这组测量依赖被 gitignore 的矩阵 cell,且"装进同一 profile 验证"是手工步骤,不属于 v0.3 CLI。详见 [构成冲突](docs/18-composition-conflicts.md)。
623
+
624
+ ### 面板
625
+
626
+ [`panel/`](panel/) 是一个真实的 DSH 插件:设置页里一个 **Plugin Health Center** 区块,列出当前 profile 装了哪些插件、每个插件在各 DSH 版本上的兼容性结论,下方是各版本 active / awaiting-service / rejected-by-host / install-failed 的聚合计数(只有数字,没有包名)。本文顶部的截图由 [`scripts/screenshot-panel.mjs`](scripts/screenshot-panel.mjs) 在一次性 web profile 里截取。
627
+
628
+ 面板读两个数据文件:本地存在全量矩阵快照时读 `data/matrix-index.json`(可用 `DSH_PLUGIN_LINT_INDEX` 覆盖),否则读 npm 包里随附的 `data/aggregate.json`(只有计数)。**因此从 npm 安装的面板只显示聚合数据,逐插件矩阵列需要本地快照。** npm 上的面板版本是 0.1.5;本工作区的 0.2.0 是未发布的 release candidate。已发布的 0.1.5 早于本仓库的服务端动作校验(没有 content-type、确认、受保护包或已安装校验,且硬编码 `--profile desktop`),在运行不受信插件的 profile 里先不要安装,直到修复版发布。
629
+
630
+ 没有逐插件数据时,页面仍展示矩阵全景:50 个插件 × 2 个版本,0.2.0-rc.2 上 100 格里 80 格 active,0.2.1-alpha.2 上 72 格 active。
631
+
632
+ 它用这个项目的 linter 检查自己是 0 发现(13 checks):只有 react 来自浏览器模块表,没有引入任何 Harness Client 包,颜色只用 `--dsw-alias-*` 令牌(带回退)。视觉验证抓到两个代码评审没发现的 bug,见 [docs/16](docs/16-m5-panel.md)。
633
+
634
+ ### v0.3 分析能力(experimental,未发布)
635
+
636
+ 工作区版本 0.4.0(release candidate);npm 最新发布是 0.1.0。下面四个能力(`full` 是组合命令)只在源码 checkout 里存在,且都是 **experimental**:对磁盘文件做静态启发式检查,不执行插件、不做运行时测量、不做安全裁决。干净运行只代表没有模式命中。
637
+
638
+ | 命令 | 回答的问题 |
639
+ | --- | --- |
640
+ | `dsh-plugin-analyze conflicts <dir...>` | 两个以上插件是否抢同一个 slot / 工具名 / 路由 / 配置键 |
641
+ | `dsh-plugin-analyze performance <dir>` | 包体积与静态"看起来重"的模式有多少 |
642
+ | `dsh-plugin-analyze security <dir>` | 哪些模式值得人工先看一眼 |
643
+ | `dsh-plugin-analyze full <dir>` | 单包的 lint + performance + security |
644
+ | `dsh-plugin-upgrade-plan ...` | 给定 profile 与目标 DSH 版本,哪些插件需要处理 |
645
+
646
+ 四个 `analyze` 子命令都支持 `--json`(stdout 纯 JSON,诊断信息写 stderr)与 `--exit-code`;`--help` 退出 0,未知选项退出 2,目录缺 package.json 退出 2。带 `--exit-code` 时:`conflicts` 有冲突退 1,`security` 有 warn 级 finding 退 1(info 级清单不触发门禁),`full` 在 lint 有 error 或 security 有 warn 级 finding 时退 1,`performance` 始终退 0(评分不是门禁)。
647
+
648
+ 安全分析 S1–S10 全部是 warn/info,没有 error:这些模式不是机器可判的。性能评分 0–100 是按固定阈值扣分得到的启发式指标,静态计数而非运行时测量;没有可分析的 JS 时 score 为 0、rating 为 insufficient-data。冲突检测是静态模式匹配,只认得解析器覆盖的注册写法,漏报可能出现;运行时权威是装进同一 profile 的 `scripts/matrix/conflicts.mjs`。
649
+
650
+ `dsh-plugin-upgrade-plan` 读 profile 已装 bundle 与本地矩阵(默认 `matrix/cells`,可用 `--matrix <dir>` 指定),不访问网络、不查 npm 新版本;分析模式退出码 0 无需动作 / 1 需更新或无兼容版本 / 2 运行失败(含无矩阵数据);`--check-compatibility` 0 兼容 / 1 不兼容 / 2 未知或无数据。矩阵不在 npm 包内且 `matrix/cells/` 被 gitignore,覆盖范围是快照里的 50 个插件。
651
+
652
+ API 见 [docs/19](docs/19-analysis-tools.md),未发布版本的 release notes 见 [docs/21](docs/21-release-0.4.0.md)。
653
+
654
+ ### 联邦机制
655
+
656
+ 插件作者可以在自己的 CI 里跑同一个探针(见 [联邦机制](docs/15-federation.md))。注意 `.github/actions/probe/` 里的 composite action 目前不可用(`github.action_path` 指向 action 目录,目录里没有它要调用的脚本),当前可行路径是 checkout 本仓库后直接跑 `node scripts/probe-boot.mjs`。自报的格子不会被当成我们的结论:会被抽样复跑,一致就升级为 verified,不一致就标 disputed 且**两个结论都保留**。分歧不会被掩盖——这是矩阵其余部分值得信任的前提。
657
+
658
+ | 信任级别 | 来源 | 含义 |
659
+ | --- | --- | --- |
660
+ | verified ✅ | 本项目跑批,或抽检复跑一致 | 我们背书"我们跑出来是这样" |
661
+ | self-reported 🟡 | 作者 CI 上传,未经复验 | 只代表作者所述 |
662
+ | disputed ⚠️ | 复跑结果与自报不一致 | 两个结论都留在文件里,差异公开 |
663
+ | stale ⏳ | 超过 30 天,或 DSH 已发新版 | 结论可能过期 |
664
+
665
+ 自报路径目前不完整:它依赖的 cell 与复验报告都被 gitignore,自报结论还没有进版本库的通道。这一机制算"设计存在、尚未跑通"。
666
+
667
+ ### 数据政策
668
+
669
+ 仓库只发工具,不发点名结果——凡是会点名具体包的数据(`reports/`、`matrix/cells/`、`notify/`、`site/`、面板的逐插件矩阵快照)都在本地生成、不进版本库。原因有两条:
670
+
671
+ 1. 点名一个包是对他人工作的公开断言——做对的做法是先通知维护者,这是一个决定,不是默认值;
672
+ 2. 验证数据只对产生它的环境有意义——谁跑探针,数据就是谁的答案;我们机器上的快照不是关于别人环境的证据。
673
+
674
+ 聚合计数(`panel/data/aggregate.json`)进版本库,因为它描述的是生态而不是某个维护者,见 [生态测量](docs/09-ecosystem-measurements.md)。
675
+
676
+ 面板运行时会尝试同步的 `matrix/public/` 同样被 gitignore。发布它是显式的人工决定(`matrix refresh` 勾选 `publish_named_data=true`),不是每周调度的默认副作用;在这个决定做出之前,面板回退到随包附带的聚合计数。
677
+
678
+ ### 用法
679
+
680
+ npm 发布版本是 **0.1.0**(2026-10-14 发布)。本工作区是 **0.4.0 release candidate,未发布**;下文的分析命令只在源码 checkout 里存在。
681
+
682
+ ```bash
683
+ npx @shidesheng0218/dsh-plugin-lint . # 体检当前目录的插件(npm 0.1.0)
684
+ npx @shidesheng0218/dsh-plugin-lint . --json # 机器可读报告
685
+ npx @shidesheng0218/dsh-plugin-lint . --exit-code # 有 error 级发现时退出码为 1
686
+ node scripts/probe-boot.mjs <插件名> --tools --dsh 0.2.0-rc.2 # Docker 探针(或加 --unsafe-host 用桌面版)
687
+ node scripts/matrix/run.mjs --targets matrix/targets-top50.json # 重跑兼容矩阵
688
+ node bin/dsh-plugin-analyze.mjs full panel --json # v0.3 分析(源码 checkout)
689
+ ```
690
+
691
+ 退出码:`0` 正常 · `1` 有 error 级发现(配合 `--exit-code`)· `2` 本次运行失败。
292
692
 
293
- **规则分级按"能不能可靠判定"来定,而不是按听起来多严重**:error 级必须机器可判且官方包里从不出现;warn 级是官方文档点名的风险写法,必须附证据;info 级是提示与元数据完整性。
693
+ 0.4.0 的作者工作流新增:
294
694
 
295
- **实测数据(2026-10-13)**:Top 40 社区插件里,**40% 依赖了官方的 Client 包**、**只有 13% 声明了兼容的 DSH 版本**;228 格矩阵里 **57 个插件中有 17 个在新版上结论变差,没有一个变好**。
695
+ ```bash
696
+ node bin/dsh-plugin-lint.mjs . --phase author # 构建前的 TS 源码树:未生成的产物是提示,不是 error
697
+ node bin/dsh-plugin-lint.mjs . --fail-on warn # 按 error/warn 门禁;--max-warnings N 也可用
698
+ node bin/dsh-plugin-lint.mjs . --format github # GitHub Actions 注解
699
+ node bin/dsh-plugin-lint.mjs . --format junit -o report.xml
700
+ node bin/dsh-plugin-lint.mjs . --baseline baseline.json # 只拦新增问题
701
+ node bin/dsh-plugin-lint.mjs . --write-baseline
702
+ node bin/dsh-plugin-lint.mjs --init # 生成 workflow、空 baseline 和 agent skill
703
+ node bin/dsh-plugin-lint.mjs . --dsh-version 0.2.2-rc.1 # E6 按宿主真实 peer 门禁预检
704
+ node bin/dsh-plugin-lint.mjs . --changed HEAD # 只体检 HEAD 以来改动的包(--staged 同理)
705
+ node bin/dsh-plugin-lint.mjs . --pack # 体检 npm pack 真正会发布的 tarball
706
+ node bin/dsh-plugin-lint.mjs dist/plugin-1.2.3.tgz # 或直接体检已有 tarball
707
+ node bin/dsh-plugin-lint.mjs --from-npm @scope/name@1.2.3 # 体检 registry 上的产物
708
+ node bin/dsh-plugin-lint.mjs --profile ~/.dsh/profiles/default # 对已安装 profile 做宿主真值扫描
709
+ ```
710
+
711
+ `--changed` / `--staged` 把 git diff 映射到最近的 `package.json`,monorepo 里只体检 PR 碰过的包。`--pack`、`.tgz` 与 `--from-npm` 强制进入 publish 阶段并体检真实产物:缺失的子路径 export、bin、icon 都按 npm 实际会发布的内容报告,含路径穿越或符号链接的 tarball 会在解包前被拒绝。`--profile` 从 DSH profile 读取运行时版本与已安装 bundle,列出宿主会加载/拒绝哪些插件,并为每个拒绝打印可直接执行的 `dsh plugin allow-version … --accept-risk` 命令;配合 `--exit-code` 时,任何 `will-reject` 都会让命令失败。
712
+
713
+ baseline 的指纹是 `sha256(rule + file + 归一化 evidence)`,不含行号:格式化或重新构建不会让已抑制的问题复活;报告仍会显示被 baseline 抑制的数量。
296
714
 
297
- **两个只有实测才能知道的坑**:宿主拒绝安装时只是 warning、退出码仍为 0(所以退出码永远不是信号);两个插件抢同一个工具名时,**后注册的那个整个插件报废**,而不是覆盖。
715
+ composite action 把同一套能力带进 CI。`comment: true` 需要 job 的 `pull-requests: write`,只会创建一条评论并原地更新;`sarif: dsh-plugin-lint.sarif` 会写出 SARIF,再由 `codeql-action/upload-sarif` 上传(该 job 另需 `security-events: write`,upload 的 major 版本以 GitHub 文档为准)。npm 包同时带 `schema/dsh-manifest.schema.json`(子路径 `@shidesheng0218/dsh-plugin-lint/schema/dsh-manifest.schema.json`);把 `$schema` 指向它,编辑器就能在 CI 之前校验 `dsh` 块。
298
716
 
299
- **数据政策**:仓库只发工具,不发点名结果——凡是会点名具体包的数据都在本地生成、不进版本库。生态聚合数字照常发布。
717
+ 本 README 里以 `node scripts/` 开头的命令需要**克隆本仓库**运行——矩阵跑批、升级预览、探针与报告生成都依赖 npm 包里不包含的文件。
300
718
 
301
- 初版规则里有三条被实测否掉(官方包自己就在监听 `system-prompt/assemble`;真实插件会故意不调 `next()`),详见 [详细设计](docs/06-detailed-design.md)。
719
+ ### 测试
720
+
721
+ ```bash
722
+ npm test # node --test test/*.test.js:430 个用例,49 个文件
723
+ npm run test:coverage # 行 ≥85%、分支 ≥80%、函数 ≥80%
724
+ npm run package:smoke # 打包两个 tarball、安装、运行全部 bin 与入口
725
+ ```
726
+
727
+ 当前 revision 的实测:430 个 `node:test` 用例、49 个测试文件,覆盖率门禁为行 ≥85%、分支 ≥80%、函数 ≥80%,实测行 95.3% / 分支 86.2% / 函数 96.1%,普通 shell 与 DSH shell 里均通过(panel 测试隔离 ambient `DSH_*` 变量;CLI 测试在临时目录造 fixture)。需要当前数字时直接跑 `npm test`。
728
+
729
+ ### 文档
730
+
731
+ [生态调研](docs/01-ecosystem-map.md) · [决策记录](docs/05-decision-log.md) · [详细设计](docs/06-detailed-design.md) · [M1 结果](docs/07-m1-results.md) · [作者通知](docs/08-author-notification.md) · [生态测量](docs/09-ecosystem-measurements.md) · [探针验证](docs/10-m4-verification.md) · [能力探针](docs/11-m4-capability-probe.md) · [联邦机制](docs/15-federation.md) · [面板](docs/16-m5-panel.md) · [发布记录](docs/17-release.md) · [构成冲突](docs/18-composition-conflicts.md) · [分析工具](docs/19-analysis-tools.md) · [v0.4.0 release notes(draft)](docs/21-release-0.4.0.md) · [v0.3.0 历史草稿](docs/20-release-0.3.0.md)
732
+
733
+ ## License / 许可证
734
+
735
+ Apache-2.0. See [NOTICE](NOTICE) for the data derived from DeepSeek Harness (MIT).