dsh-plugin-guide 0.4.3 → 0.4.4
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/docs/boost.md +59 -29
- package/docs/entry.md +195 -0
- package/docs/flow.md +149 -9
- package/docs/levels.md +23 -3
- package/docs/recipes.md +251 -0
- package/docs/rules.md +99 -0
- package/package.json +1 -1
package/docs/boost.md
CHANGED
|
@@ -21,35 +21,63 @@ The canonical text is
|
|
|
21
21
|
|
|
22
22
|
## Evidence, not a badge
|
|
23
23
|
|
|
24
|
+
<a id="thirteen-fields"></a>
|
|
24
25
|
An evidence card is per practice, per plugin, with a baseline and an after
|
|
25
|
-
measurement.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
26
|
+
measurement. The audit is strict about the field names, so use exactly these
|
|
27
|
+
thirteen:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"LEV-1": {
|
|
32
|
+
"status": "measured",
|
|
33
|
+
"evidenceCard": {
|
|
34
|
+
"subject": "@acme/dsh-hello@0.1.0",
|
|
35
|
+
"host": "dsh 0.2.2-alpha.2 / cordis 4.0.6-alpha.1 / node 24.9.0 / linux-x64",
|
|
36
|
+
"method": "count timers and listeners before and after dispose; 20 runs",
|
|
37
|
+
"scope": "LEV-1 reversible effects — timer and watcher in apply()",
|
|
38
|
+
"command": "node bench/effects.mjs",
|
|
39
|
+
"result": "effects 3 → 0 after dispose; effect count 3 → 3 after restart",
|
|
40
|
+
"negativeControl": "the same bench with the plugin disabled",
|
|
41
|
+
"generatedAt": "2026-11-06",
|
|
42
|
+
"expiresAt": "2027-05-06",
|
|
43
|
+
"recordHash": "sha256:<64 lowercase hex over this card>",
|
|
44
|
+
"claimLevel": "self-assessed",
|
|
45
|
+
"issuer": "author",
|
|
46
|
+
"selfAssessed": true
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
49
50
|
```
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
| Field | Validator | What to put there |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| `subject` | non-empty string | what was measured, `name@version` — not the repo name |
|
|
55
|
+
| `host` | non-empty string | DSH / Cordis / Node versions and the platform |
|
|
56
|
+
| `method` | non-empty string | how it was measured, including the run count or comparator |
|
|
57
|
+
| `scope` | non-empty string | which LEV practice and which part of the plugin |
|
|
58
|
+
| `command` | non-empty string | the exact command that produced the result |
|
|
59
|
+
| `result` | non-empty string | numbers, `before → after`; no adjectives |
|
|
60
|
+
| `negativeControl` | non-empty string | the same measurement with the plugin disabled |
|
|
61
|
+
| `generatedAt` | `YYYY-MM-DD` | the day it was run |
|
|
62
|
+
| `expiresAt` | `YYYY-MM-DD`, not before `generatedAt` | when the measurement stops being current (180 days is a reasonable cap) |
|
|
63
|
+
| `recordHash` | `sha256:` + 64 lowercase hex | sha256 over the card JSON without this field; the validator checks the shape, a reviewer recomputes it |
|
|
64
|
+
| `claimLevel` | `self-reported` \| `self-assessed` \| `host-reviewed` \| `independent` \| `reference` | be honest; anything stronger is not self-declared |
|
|
65
|
+
| `issuer` | non-empty string | who ran it (`author`, an org, a CI job) |
|
|
66
|
+
| `selfAssessed` | boolean | `true` only when the author ran and judged it |
|
|
67
|
+
|
|
68
|
+
How the audit treats a card:
|
|
69
|
+
|
|
70
|
+
- Missing or malformed field → the practice is `not-measured`; a `status` word
|
|
71
|
+
alone is not evidence.
|
|
72
|
+
- `expiresAt` before `generatedAt` → invalid card, `not-measured`.
|
|
73
|
+
- `expiresAt` in the past (compared to today) → `stale`, and the whole BOOST
|
|
74
|
+
result is `stale`; refresh the card.
|
|
75
|
+
- A `claimLevel` outside the five values → invalid card.
|
|
76
|
+
- All eight practices need a valid card for `measured`; one card is
|
|
77
|
+
`1/8 practices have valid measured evidence cards`, not a pass.
|
|
78
|
+
- `status: "failed"` is `failed` whether or not a card exists.
|
|
79
|
+
- The card may also sit directly under `evidence` as an object, but
|
|
80
|
+
`evidenceCard` is unambiguous; a string `evidence` is only a note.
|
|
53
81
|
|
|
54
82
|
If an effect cannot be measured (developer hours, maintenance cost, “fewer
|
|
55
83
|
pitfalls”), record it as a WARN or a note. Do not invent a number.
|
|
@@ -69,9 +97,11 @@ npx -p dsh-standards dsh-levels ./my-plugin \
|
|
|
69
97
|
--boost-evidence boost.json --format json
|
|
70
98
|
```
|
|
71
99
|
|
|
72
|
-
Without evidence, the result is `not-measured`, not a pass.
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
Without evidence, the result is `not-measured`, not a pass. The card is
|
|
101
|
+
validated by
|
|
102
|
+
[`40-checker/scripts/level-audit.mjs`](https://github.com/PerryLink/dsh-standards/blob/main/40-checker/scripts/level-audit.mjs);
|
|
103
|
+
keep the raw logs next to the card so a reviewer can re-run `command` and
|
|
104
|
+
recompute `recordHash`.
|
|
75
105
|
|
|
76
106
|
## Author checklist
|
|
77
107
|
|
package/docs/entry.md
CHANGED
|
@@ -16,6 +16,33 @@ market, index, or user hits.
|
|
|
16
16
|
The exact rule IDs and evidence rules are in
|
|
17
17
|
[`31-DSH-ENTRY`](https://github.com/PerryLink/dsh-standards/blob/main/31-DSH-ENTRY%E5%85%A5%E9%97%A8%E8%A7%84%E8%8C%83.md)
|
|
18
18
|
and [`40-checker`](https://github.com/PerryLink/dsh-standards/tree/main/40-checker).
|
|
19
|
+
Every ID is also listed in [rules.md](rules.md); the copy-paste shapes are in
|
|
20
|
+
[recipes.md](recipes.md).
|
|
21
|
+
|
|
22
|
+
<a id="package-shape"></a>
|
|
23
|
+
## Package shape (DSC-01, DSC-02, DSC-03)
|
|
24
|
+
|
|
25
|
+
`DSC-01` is one report line but six subitems; the detail lines say which one
|
|
26
|
+
failed. `DSC-03` runs only in the listing profile — in the package profile it is
|
|
27
|
+
`NA` with `install spec not supplied (package profile)`, which is allowed.
|
|
28
|
+
|
|
29
|
+
| Detail | What the report says | Fix |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `C-1.1` | `name must be a valid npm package name` / `version must be an exact SemVer version` | use a lowercase npm name and an exact `1.2.3` (or `1.2.3-beta.1`); a tag or range is not a version |
|
|
32
|
+
| `C-1.2` | `no resolvable runtime entry` / `entry escapes the package` | point `exports["."]` or `main` at a file that exists inside the package; no `../` target |
|
|
33
|
+
| `C-1.2` | `exports["."] points to a missing file` vs `entry not present in checkout; source/build hint exists` (WARN) | shipped packages need the built file; if the checker sees `src/index.ts` or a `build`/`prepare` script it downgrades to WARN, do not rely on that |
|
|
34
|
+
| `C-1.2` | `patch-only bundle; no runtime entry by design` (`NA`) | fine: a package that only ships `dsh.bundle.patch` has no runtime entry and is not charged for it |
|
|
35
|
+
| `C-1.3` | `no apply/default-class shape found by static scan` (WARN) | export `apply` (function plugin) or a default class (service plugin), not both; L1 is authoritative |
|
|
36
|
+
| `C-1.4` | `patch path escapes the package` / `patch file is missing` | keep the patch inside the package and reference it relatively, e.g. `"./cordis.patch.yml"` |
|
|
37
|
+
| `C-1.5` | `license is missing` (WARN) | add `"license": "MIT"` or another SPDX id |
|
|
38
|
+
| `C-1.6` | `private:true; not publishable` (`NA`) | expected for a local profile package, not for a marketplace entry |
|
|
39
|
+
| `G4` | `dsh.client exists but exports["./client"] is missing; a package-root row cannot resolve it` | add `exports["./client"]` and the package-root row — see [recipes.md](recipes.md#client-entry-and-root-row) |
|
|
40
|
+
| `C-2.1` | `neither engines.dsh nor a @deepseek-ai/dsh* peer range is declared` | declare `"engines": { "dsh": ">=0.2.2-0 <0.3.0" }` |
|
|
41
|
+
| `C-2.2` | `engines.dsh <range> does not satisfy runtime <v>` | widen the range to the host you target, or run the checker with `--runtime-version <v>` to judge against the right one |
|
|
42
|
+
| `C-2.3` | `engines.dsh must not be a workspace: range` | publish a real SemVer range; `workspace:` must not reach a tarball |
|
|
43
|
+
| `C-2.4` | `peer <name> range <range> conflicts with engines.dsh` (WARN) | the host decides on `engines.dsh` alone; align the peer anyway |
|
|
44
|
+
| `C-3.1`/`C-3.2`/`C-3.5` | `install spec` / `source is pinned` / market entry | listing only: exact version or verifiable tag, `#<40-hex>` for git, and a `format:1` index entry with pinned versions |
|
|
45
|
+
| `C-3.3`/`C-3.4` | `non-registry dependency` / `install-time build script present` (WARN) | pin git/url/file dependencies and declare what install scripts build |
|
|
19
46
|
|
|
20
47
|
## The two claims
|
|
21
48
|
|
|
@@ -28,6 +55,79 @@ and [`40-checker`](https://github.com/PerryLink/dsh-standards/tree/main/40-check
|
|
|
28
55
|
entry; it needs G0–G3. A client-only package is build-only unless a browser
|
|
29
56
|
probe exists.
|
|
30
57
|
|
|
58
|
+
<a id="runtime-surface"></a>
|
|
59
|
+
## Runtime surface (DSC-04 to DSC-09, DSC-14)
|
|
60
|
+
|
|
61
|
+
These are the L1–L3 checks: load, provide, effects, unload, restart, and
|
|
62
|
+
declared-vs-measured. They apply to `kind=plugin`; a `bundle` is verified by
|
|
63
|
+
`--scope entry` G0–G3 instead (in `--scope ds-c` it is `NA`), and a client,
|
|
64
|
+
preset, skill, theme, or MCP config is `NA` and must not claim runtime. `DSC-14`
|
|
65
|
+
is the ENTRY run gate: for a plugin it mirrors L1, for a bundle it is G0–G3 and
|
|
66
|
+
needs `--host`, and under `--entry-claim static` it is `NA`
|
|
67
|
+
(`entry static claim: no runtime evidence`).
|
|
68
|
+
|
|
69
|
+
| Detail | What the report says | Fix |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `C-4.1`/`C-4.2` | `entry import failed: …` / `load failed: …` / `fiber state <n> is not ACTIVE (2)` | the entry must import and `apply()` without throwing; move timers, watchers, I/O, and subprocesses into `apply()` |
|
|
72
|
+
| `C-4.3` | `plugin load timed out` / `killed after <n>ms; plugin blocked the event loop` | do not block on module-scope work or a top-level `await`; the L1 child is hard-killed |
|
|
73
|
+
| — | `blocked by missing host dependency` (ERROR, `not-run`) | environment, not code: the plugin imports a bare package the fixture does not have. Install it or point `--runtime` at a complete host |
|
|
74
|
+
| `C-5.1`/`C-5.2` | `no provide declaration` (`NA`) / `declared provide <x> was not registered` / `owned by another fiber` | register the service from this plugin's own fiber: `ctx.provide(name, impl)` or a `Service` |
|
|
75
|
+
| `C-6.1` | `direct write to a Cordis private registration surface` | never assign into `ctx.events._hooks`, `ctx.reflect.store`, `ctx.registry._internal`, or `fiber._disposables` |
|
|
76
|
+
| `C-6.2` | `no registered effects observed` / `all effects are anonymous` (WARN) | give every effect a label: `ctx.effect(fn, 'my-plugin.timer')` |
|
|
77
|
+
| `C-7.1`–`C-7.3` | `getEffects().length=<n>` / `service store returned to baseline` / `registry.size <a> -> <b>` | every effect returns a disposer; `dispose()` must reach zero effects and restore the store and registry |
|
|
78
|
+
| `C-7.5` | `dispose failed: …` | await quiescence; do not fire-and-forget cleanup |
|
|
79
|
+
| `C-8.1`–`C-8.3` | `state after restart=<n>` / `effect count <a> -> <b>` / `revision … -> …` | `restart()` must re-enter `ACTIVE`, keep the effect count, advance revision/epoch, and work from a fresh `Context` |
|
|
80
|
+
| `C-9.1`–`C-9.3` | `declared provide <x> is missing from measured services` / `measured service <x> is not declared` / `inject declares <x> but no static ctx.<x> read was found` | declarations must equal reality: provide ⊆ measured, measured ⊆ declared, and each `inject` is read |
|
|
81
|
+
|
|
82
|
+
The runner stubs the services you declare in `inject` before it calls
|
|
83
|
+
`ctx.plugin()`, so a plugin that only needs declared services can reach `ACTIVE`.
|
|
84
|
+
Anything it cannot stub is reported as `ERROR`/`not-run`; that is not your
|
|
85
|
+
defect and must not be written as a pass.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
export const name = 'hello'
|
|
89
|
+
export const inject = ['tools'] // only what you read
|
|
90
|
+
export const provide = ['hello'] // only what you register
|
|
91
|
+
|
|
92
|
+
export function apply(ctx) {
|
|
93
|
+
ctx.provide('hello', { greet: () => 'hello' }) // C-5
|
|
94
|
+
ctx.effect(() => { // C-6, C-7
|
|
95
|
+
const timer = setInterval(poll, 60_000)
|
|
96
|
+
return () => clearInterval(timer) // dispose → 0 effects
|
|
97
|
+
}, 'hello.timer')
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A bundle is verified by G0–G3, not by L1: G0 parses the patch, G1 composes the
|
|
102
|
+
rows, G2 runs the host compatibility gate, G3 boots the target rows. A `partial`
|
|
103
|
+
G3 is `ERROR`/`not-run`; only `G0-G3 verified; rows=<n>/<n>` with
|
|
104
|
+
`DSC-14 PASS` is a runtime claim. G4 is a composition property: with
|
|
105
|
+
`dsh.client`, the composed layer must contain a row whose `name` is the package
|
|
106
|
+
name and `exports["./client"]` must resolve.
|
|
107
|
+
|
|
108
|
+
<a id="client-files"></a>
|
|
109
|
+
## Client files (G4)
|
|
110
|
+
|
|
111
|
+
A client package is dual-face: the server half is loaded as a row, the browser
|
|
112
|
+
half is a prebuilt file at `exports["./client"]`.
|
|
113
|
+
|
|
114
|
+
- `dsh.client` must be a non-empty object; the host's Web consumer selects it
|
|
115
|
+
with `platform: "web"`.
|
|
116
|
+
- `exports["./client"]` must be present. The host accepts a string, or an object
|
|
117
|
+
with a string `default`; the checker only fails when it is missing.
|
|
118
|
+
- The composed config needs a **package-root row**: a patch `insert` whose
|
|
119
|
+
`name` equals `manifest.name`. Without an active row the client scanner never
|
|
120
|
+
sees the package, even though the export exists.
|
|
121
|
+
- The client file must be a built artifact and must be included in `files`, or
|
|
122
|
+
the tarball ships a manifest that points at nothing.
|
|
123
|
+
- G4 is static. It does not prove the browser bundle renders; that is a
|
|
124
|
+
build/browser probe and belongs in `limitations`, not in a runtime claim.
|
|
125
|
+
|
|
126
|
+
The copy-paste client manifest and patch row are in
|
|
127
|
+
[recipes.md](recipes.md#client-entry-and-root-row). A missing export or row is
|
|
128
|
+
reported as `G4: FAIL` under `DSC-01` (or as `DSC-14` with `--host`), never as a
|
|
129
|
+
pass.
|
|
130
|
+
|
|
31
131
|
## Run it
|
|
32
132
|
|
|
33
133
|
```sh
|
|
@@ -64,6 +164,7 @@ pass.
|
|
|
64
164
|
|
|
65
165
|
## Declaration fields
|
|
66
166
|
|
|
167
|
+
<a id="declaration-fields"></a>
|
|
67
168
|
Two optional fields become checks `DSC-11` and `DSC-13` once they exist:
|
|
68
169
|
|
|
69
170
|
```json
|
|
@@ -75,6 +176,11 @@ Two optional fields become checks `DSC-11` and `DSC-13` once they exist:
|
|
|
75
176
|
}
|
|
76
177
|
```
|
|
77
178
|
|
|
179
|
+
Both are **standard extensions**: this host does not read them yet. They are
|
|
180
|
+
declarations plus UI disclosure, not a sandbox and not enforcement. Declaring
|
|
181
|
+
`[]` and importing `node:fs` directly still passes `DSC-11`; the check only
|
|
182
|
+
compares declarations with static triggers.
|
|
183
|
+
|
|
78
184
|
Allowed `capabilities`: `fs:read`, `fs:write`, `shell`, `subprocess`,
|
|
79
185
|
`network:egress`, `network:ingress`, `credentials:read`, `credentials:list`,
|
|
80
186
|
`credentials:provider`, `llm:adapter`, `sessions:read`, `system-prompt:write`,
|
|
@@ -83,6 +189,95 @@ Allowed `capabilities`: `fs:read`, `fs:write`, `shell`, `subprocess`,
|
|
|
83
189
|
Allowed `labels`: `prompt-modification`, `red-team`,
|
|
84
190
|
`third-party-model-provider`, `remote-access`, `mirror`.
|
|
85
191
|
|
|
192
|
+
<a id="capability-triggers"></a>
|
|
193
|
+
## Capability triggers (DSC-11)
|
|
194
|
+
|
|
195
|
+
`C-11.2` is the check authors hit most: if `dsh.capabilities` exists, every
|
|
196
|
+
static trigger below must have its capability in the array, or the check FAILs
|
|
197
|
+
with `static trigger <cap> is not declared in dsh.capabilities`.
|
|
198
|
+
|
|
199
|
+
| Static trigger (source shape) | Declare |
|
|
200
|
+
|---|---|
|
|
201
|
+
| `ctx.webServer` / `ctx.webhookRuntime` | `network:ingress` |
|
|
202
|
+
| `installBundle(` / `removeBundle(` | `plugin:install` |
|
|
203
|
+
| `registerAdapter(` / `ctx.llm.register` | `llm:adapter` |
|
|
204
|
+
| `ctx.systemPrompt.section(` / `.context(` / `.variable(` | `system-prompt:write` |
|
|
205
|
+
| `ctx.tools.register(` | `tools:register` |
|
|
206
|
+
| `@Remote(` / `@RemoteScope(` | `remote:expose` |
|
|
207
|
+
| `.listRecords(` | `credentials:list` |
|
|
208
|
+
| `ctx.sessions` / `ctx.sessionQuery` | `sessions:read` |
|
|
209
|
+
| `ctx.shell` / `ctx.bash` | `shell` |
|
|
210
|
+
| `ctx.subprocess` | `subprocess` |
|
|
211
|
+
| `ctx.fs` | `fs:read` (a declared `fs:write` satisfies it) |
|
|
212
|
+
| `ctx.web` / a bare `fetch(` | `network:egress` |
|
|
213
|
+
|
|
214
|
+
Reading the other subitems:
|
|
215
|
+
|
|
216
|
+
- `C-11.1`: an unknown capability id is a FAIL; an empty array is a WARN. A
|
|
217
|
+
missing field is a transitional WARN, and `C-11.2` is then **not evaluated** —
|
|
218
|
+
so an undeclared array hides the very failure you want to see.
|
|
219
|
+
- `C-11.3`: a bundle patch that enables `@deepseek-ai/dsh-plugin-manager/tools`
|
|
220
|
+
without `disabled: true` is a FAIL.
|
|
221
|
+
- `C-11.4`: a string literal that is exactly `danger-full-access` is a WARN; a
|
|
222
|
+
mention in a comment or a `console.log` description is not.
|
|
223
|
+
- `C-11.5`: a declared capability with no matching trigger is a WARN (over-
|
|
224
|
+
declaration). Keep declarations to what the scan can see; a dynamic call may
|
|
225
|
+
not match.
|
|
226
|
+
|
|
227
|
+
The scan covers `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, and `.jsx` files in the
|
|
228
|
+
package, and skips `tests`, `docs`, `scripts`, `examples`, `fixtures`, `bench`,
|
|
229
|
+
`tools`, and dot-directories, plus the other build and asset directories the
|
|
230
|
+
walker ignores. A `fetch(` in `tests/` does not require `network:egress`; the
|
|
231
|
+
same call in `lib/client.js` does.
|
|
232
|
+
|
|
233
|
+
<a id="credentials-and-seams"></a>
|
|
234
|
+
## Credentials and seams (DSC-12)
|
|
235
|
+
|
|
236
|
+
`DSC-12` is a declaration-consistency check, not a security audit. It looks for
|
|
237
|
+
direct access to the credential backing file and for global seam registration.
|
|
238
|
+
|
|
239
|
+
| Detail | What the report says | Fix |
|
|
240
|
+
|---|---|---|
|
|
241
|
+
| `C-12.1` | `credential backing-file access near a file-read API without credentials:provider` (a path in text or UI is PASS) | read through `ctx.credentials`; if you really are a provider, declare `credentials:provider` |
|
|
242
|
+
| `C-12.2` | `listRecords() used without credentials:list` | declare `credentials:list`, or stop enumerating records |
|
|
243
|
+
| `C-12.3` | `global seam provider registration detected` (WARN) | do not provide `shell`/`credentials`/`sandboxPolicy`/`sandbox`/`deepSeekAccount` unless that is the plugin's purpose |
|
|
244
|
+
| `C-12.4` | `danger-full-access/--add-dir literal` (WARN) | do not hard-code a sandbox bypass in a command or policy value |
|
|
245
|
+
| `C-12.6` | `dsh.commitments` missing (`NA`) or `missing commitment: <id>` (WARN) | optional: a boolean for each of `no-session-upload`, `no-third-party-proxy`, `no-credential-enumeration`, `no-silent-prompt-rewrite`, `no-adapter-bypass`, `no-webhook-auto-session`, `no-security-seam-replacement`, `third-party-endpoint-disclosure`, `no-danger-full-access` |
|
|
246
|
+
| `C-12.7` | `dsh.disclosure` missing (`NA`) or a shape WARN | optional: `endpoints` (https URLs), `retention` (string), `systemPrompt` (string) |
|
|
247
|
+
|
|
248
|
+
```js
|
|
249
|
+
// prefer: go through the seam
|
|
250
|
+
export function apply(ctx) {
|
|
251
|
+
ctx.credentials.resolve('acme/api-key')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// avoid: the backing file is an implementation detail
|
|
255
|
+
// readFileSync(join(process.env.DSH_HOME, '.credentials.yaml'))
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
<a id="labels-and-mirror"></a>
|
|
259
|
+
## Labels and mirror (DSC-13)
|
|
260
|
+
|
|
261
|
+
A label is a declaration, not an endorsement. `C-13.2` maps a **declared
|
|
262
|
+
capability** to a required label:
|
|
263
|
+
|
|
264
|
+
| Declared capability | Required label |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `system-prompt:write` | `prompt-modification` |
|
|
267
|
+
| `llm:adapter` **and** `network:egress` | `third-party-model-provider` |
|
|
268
|
+
| `network:ingress` | `remote-access` |
|
|
269
|
+
| `dsh.upstream` declared | `mirror` |
|
|
270
|
+
|
|
271
|
+
- `C-13.1`: an unknown label is a FAIL; a missing `dsh.labels` is a transitional
|
|
272
|
+
WARN.
|
|
273
|
+
- `C-13.3`: `dsh.upstream` must be an `https://` URL and must not point at this
|
|
274
|
+
package's own `repository`; a mirror also needs the `mirror` label.
|
|
275
|
+
- `C-13.4`: `official`, `verified`, `certified`, `secure`, `audited` in
|
|
276
|
+
`dsh.labels` is a FAIL. The prose ban is separate: the checker does not scan
|
|
277
|
+
README or `description`, so keeping those words out is on you.
|
|
278
|
+
- `C-13.5`: the `red-team` label should come with `dsh.usageScope`; without it
|
|
279
|
+
the check only WARNs.
|
|
280
|
+
|
|
86
281
|
Do not label a plugin `official`, `verified`, `certified`, `secure`, or
|
|
87
282
|
`audited` — those words are treated as an endorsement claim and fail `DSC-13`.
|
|
88
283
|
|
package/docs/flow.md
CHANGED
|
@@ -19,6 +19,134 @@ the first run, configuration, upgrade, and removal are understandable.
|
|
|
19
19
|
The canonical text is
|
|
20
20
|
[`32-DSH-FLOW`](https://github.com/PerryLink/dsh-standards/blob/main/32-DSH-FLOW%E9%A1%BA%E7%95%85%E8%A7%84%E8%8C%83.md).
|
|
21
21
|
|
|
22
|
+
<a id="evidence-file"></a>
|
|
23
|
+
## The evidence file: `dsh-flow-evidence/2`
|
|
24
|
+
|
|
25
|
+
A journey row without a command, exit code, log hash, and timestamp is a claim,
|
|
26
|
+
not evidence. The shape below is the evidence-file contract this guide
|
|
27
|
+
recommends: one subject, one host, one entry per journey, and a chain pointer so
|
|
28
|
+
a rewrite is detectable.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"schema": "dsh-flow-evidence/2",
|
|
33
|
+
"evidenceId": "01JCM8Z6R4W1V2K3N4P5Q6R7S8",
|
|
34
|
+
"generatedAt": "2026-11-06T12:00:00Z",
|
|
35
|
+
"producer": { "kind": "local", "runner": "flow-collect 0.1.0", "runUrl": null, "actor": "author" },
|
|
36
|
+
"subject": {
|
|
37
|
+
"package": "@acme/dsh-hello",
|
|
38
|
+
"version": "0.1.0",
|
|
39
|
+
"source": "npm",
|
|
40
|
+
"integrity": "sha512-…",
|
|
41
|
+
"tarballSha256": "sha256:…",
|
|
42
|
+
"commit": "ce7073d51254c26cd63b22a60835b8f948deb447",
|
|
43
|
+
"treeSha256": "sha256:…"
|
|
44
|
+
},
|
|
45
|
+
"host": { "dsh": "0.2.2-alpha.2", "dshCommit": null, "cordis": "4.0.6-alpha.1", "node": "v24.11.0", "platform": "linux-x64" },
|
|
46
|
+
"journeys": {
|
|
47
|
+
"J1": {
|
|
48
|
+
"status": "pass",
|
|
49
|
+
"kind": "observed",
|
|
50
|
+
"checks": [
|
|
51
|
+
{
|
|
52
|
+
"id": "J1.install",
|
|
53
|
+
"status": "pass",
|
|
54
|
+
"command": ["dsh", "plugin", "add", "."],
|
|
55
|
+
"exitCode": 0,
|
|
56
|
+
"startedAt": "2026-11-06T11:00:01Z",
|
|
57
|
+
"endedAt": "2026-11-06T11:00:09Z",
|
|
58
|
+
"stdout": { "path": "logs/J1.install.out", "sha256": "sha256:…" },
|
|
59
|
+
"assertions": [{ "claim": "row visible in dump", "observed": true }]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"limitations": []
|
|
63
|
+
},
|
|
64
|
+
"J8": { "status": "na", "reason": "no dsh.client" }
|
|
65
|
+
},
|
|
66
|
+
"totals": { "pass": 7, "na": 1, "fail": 0 },
|
|
67
|
+
"conclusion": "flow-pass",
|
|
68
|
+
"claims": { "claimLevel": "self-attested", "notClaimed": ["certification", "security audit"] },
|
|
69
|
+
"chain": { "seq": 12, "prev": "sha256:…", "entrySha256": "sha256:…" },
|
|
70
|
+
"signature": null
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Field | Required | What belongs there |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `schema`, `evidenceId`, `generatedAt` | yes | the version, a unique id, an ISO timestamp |
|
|
77
|
+
| `producer` | yes | `kind` = `sandbox` / `ci` / `local`, the `runner`, a `runUrl` for CI, the `actor` |
|
|
78
|
+
| `subject` | yes | `name@version` plus `integrity`, `tarballSha256`, `commit`, `treeSha256` — binds evidence to one artifact, so a copied file for another version is `stale` |
|
|
79
|
+
| `host` | yes | `dsh`, `dshCommit`, `cordis`, `node`, `platform` — a pass on another host does not transfer |
|
|
80
|
+
| `journeys.Jn` | yes, per run journey | `status` (`pass` / `fail` / `not-run` / `na`), `kind` (`observed` / `static` / `manual`), `checks[]`, `limitations[]`, and `reason` for a skip |
|
|
81
|
+
| `journeys.Jn.checks[]` | yes, per check | `id` (`J1.install`), `command[]`, `exitCode`, `startedAt`, `endedAt`, `stdout.path` + `stdout.sha256`, `assertions[]` |
|
|
82
|
+
| `totals`, `conclusion` | yes | counts and `flow-pass` / `flow-fail` / `flow-blocked` / `flow-warn` |
|
|
83
|
+
| `claims` | yes | `claimLevel` (`self-attested`, `sandbox-attested`, …) and what is explicitly **not** claimed |
|
|
84
|
+
| `chain` | when chained | `seq`, `prev`, `entrySha256` from the append-only evidence chain |
|
|
85
|
+
| `signature` | optional | `null` unless you actually sign |
|
|
86
|
+
|
|
87
|
+
Honesty rules for the file itself:
|
|
88
|
+
|
|
89
|
+
- `kind: "static"` never counts as a FLOW pass. J5 and a client-only J8 are
|
|
90
|
+
static preflight; label them that way.
|
|
91
|
+
- `na` is a skip with a reason (`no dsh.client`), not a pass.
|
|
92
|
+
- A missing `command[]`, `exitCode`, or log hash means the row is not evidence.
|
|
93
|
+
Put the limitation in `limitations[]` instead of dropping the row.
|
|
94
|
+
|
|
95
|
+
<a id="what-the-checker-reads"></a>
|
|
96
|
+
### What reads this file today
|
|
97
|
+
|
|
98
|
+
`dsh-levels --flow-evidence` does **not** read the nested v2 shape yet. Its
|
|
99
|
+
reader wants a flat map and only reads `status`:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"J1": { "status": "pass", "evidence": "add → dump → remove; logs/J1.*.out" },
|
|
104
|
+
"J2": { "status": "pass", "evidence": "same row id disable/enable; two dumps" }
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- Accepted values are `pass`, `fail`, `not-run`, `manual`. Anything else,
|
|
109
|
+
including `na` and `warn`, is silently downgraded to `not-run`.
|
|
110
|
+
- FLOW becomes `runtime` only at **8/8 pass**. One `na` gives `7/8` and
|
|
111
|
+
`not-run` — which is the honest answer for a package with no client, but it
|
|
112
|
+
is not a pass.
|
|
113
|
+
- The flat map has no binding, no hash, and no host. A hand-written 8-pass file
|
|
114
|
+
passes. That is why the v2 file above exists: keep the flat map as the
|
|
115
|
+
projection, and keep the v2 file plus logs as the thing you can defend.
|
|
116
|
+
|
|
117
|
+
Project the v2 file to the flat map before running the audit:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
node -e '
|
|
121
|
+
const fs = require("node:fs");
|
|
122
|
+
const evidence = require("./flow-evidence.json");
|
|
123
|
+
const flat = {};
|
|
124
|
+
for (const [id, journey] of Object.entries(evidence.journeys)) {
|
|
125
|
+
flat[id] = { status: journey.status, evidence: journey.reason ?? journey.checks?.[0]?.id ?? null };
|
|
126
|
+
}
|
|
127
|
+
fs.writeFileSync("flow-flat.json", JSON.stringify(flat, null, 2));
|
|
128
|
+
'
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
<a id="minimum-evidence-per-journey"></a>
|
|
132
|
+
## Minimum evidence per journey
|
|
133
|
+
|
|
134
|
+
| Journey | Run and record | The row is a pass only when |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| **J1** install / remove | `dsh plugin add .` → config dump → `dsh plugin remove` → dump | the row appears and then disappears, with no hand-edited profile; record both dumps and their hashes |
|
|
137
|
+
| **J2** enable / disable | disable and enable the same row id, dumping after each | the change is visible in the composed config under the same stable id |
|
|
138
|
+
| **J3** configuration | boot with an empty config; boot with an invalid field; change a `volatile()` field | empty config boots; the invalid value fails loudly with a field path; the volatile change restarts 0 times |
|
|
139
|
+
| **J4** first success | follow the README quickstart on a clean profile; also start with no credential | a five-minute first success, and the error names the cause, the next command, and the doc anchor; a no-key start stops at a clear credential boundary |
|
|
140
|
+
| **J5** permissions | compare `capabilities`/`labels`/commitments/disclosure with the README | every declaration matches the README and no endorsement claim appears; static only, so `kind:"static"` and **not** counted as pass |
|
|
141
|
+
| **J6** upgrade / rollback / uninstall | install `--previous`, upgrade, roll back, uninstall | the installed version changes and comes back, data survives, and no files or rows are left behind |
|
|
142
|
+
| **J7** observability | boot normally, with a missing service, and with a port conflict | the startup line prints `name@version`, and each of the three cases is diagnosable from the log |
|
|
143
|
+
| **J8** client / server | headless run, web run, client-disabled run | each mode works or is explicitly rejected; with no `dsh.client` the row is `na` with that reason; browser rendering goes in `limitations[]` |
|
|
144
|
+
|
|
145
|
+
When a journey fails, quote the five things a reader needs: `Jn.<subcheck>`,
|
|
146
|
+
the original `command[]`, `exitCode`, the last 20 lines of stderr, and the
|
|
147
|
+
absolute log path. Then attach the clause anchor the journey maps to (for
|
|
148
|
+
example `J2.toggle → 32-DSH-FLOW #R2`).
|
|
149
|
+
|
|
22
150
|
## Status, not a score
|
|
23
151
|
|
|
24
152
|
| Status | Meaning |
|
|
@@ -27,26 +155,38 @@ The canonical text is
|
|
|
27
155
|
| `failed` | at least one journey has an attributable FAIL |
|
|
28
156
|
| `not-run` | no evidence, or fewer than eight journeys have pass evidence |
|
|
29
157
|
|
|
30
|
-
Each journey is recorded as `pass` / `fail` / `not-run` / `manual
|
|
31
|
-
`
|
|
158
|
+
Each journey is recorded as `pass` / `fail` / `not-run` / `manual`; the v2 file
|
|
159
|
+
adds `na` for a justified skip, and the flat reader downgrades `na` (and `warn`)
|
|
160
|
+
to `not-run`. There is no `flow-warn` **FLOW state** — a partial run is
|
|
161
|
+
`not-run`, not a soft pass. The journey harness may print `flow-warn` or
|
|
162
|
+
`flow-blocked` in its own `dsh-flow-runtime/1` conclusion; neither is `runtime`.
|
|
32
163
|
|
|
33
164
|
`not-run` is never a pass. Do not publish a pass rate or a grade from FLOW.
|
|
34
165
|
|
|
35
166
|
## Current limit
|
|
36
167
|
|
|
37
|
-
The
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
168
|
+
The harness is a draft, and this document is deliberately ahead of it:
|
|
169
|
+
|
|
170
|
+
- The v2 shape above is the contract to record against. The audit's
|
|
171
|
+
`--flow-evidence` reader still consumes the flat map, so project v2 → flat
|
|
172
|
+
before you run it (see above). A v2 file fed to the audit directly reads as
|
|
173
|
+
`0/8` and `not-run`.
|
|
174
|
+
- There is no shipped command that collects all eight journeys for you. J1 has
|
|
175
|
+
a runtime fixture (`journey-runtime.mjs`); the other journeys are recorded by
|
|
176
|
+
hand or by a fixture-level run. Browser rendering has no probe.
|
|
177
|
+
- `na`, `manual`, and `static` rows cannot become `runtime`. If one journey is
|
|
178
|
+
`na`, FLOW stays `not-run` — write that, not a pass.
|
|
41
179
|
|
|
42
180
|
The source harness lives in
|
|
43
181
|
[`40-checker/scripts/journey.mjs`](https://github.com/PerryLink/dsh-standards/blob/main/40-checker/scripts/journey.mjs)
|
|
44
182
|
and
|
|
45
|
-
[`journey-runtime.mjs`](https://github.com/PerryLink/dsh-standards/blob/main/40-checker/scripts/journey-runtime.mjs)
|
|
183
|
+
[`journey-runtime.mjs`](https://github.com/PerryLink/dsh-standards/blob/main/40-checker/scripts/journey-runtime.mjs);
|
|
184
|
+
the audit is
|
|
185
|
+
[`40-checker/scripts/level-audit.mjs`](https://github.com/PerryLink/dsh-standards/blob/main/40-checker/scripts/level-audit.mjs).
|
|
46
186
|
The three-layer audit reads FLOW evidence through `--flow-evidence`:
|
|
47
187
|
|
|
48
188
|
```sh
|
|
49
|
-
npx -p dsh-standards dsh-levels ./my-plugin --flow-evidence flow.json --format json
|
|
189
|
+
npx -p dsh-standards dsh-levels ./my-plugin --flow-evidence flow-flat.json --format json
|
|
50
190
|
```
|
|
51
191
|
|
|
52
192
|
## Author checklist
|
|
@@ -56,4 +196,4 @@ npx -p dsh-standards dsh-levels ./my-plugin --flow-evidence flow.json --format j
|
|
|
56
196
|
- Errors say what happened, what to run next, and where the doc is.
|
|
57
197
|
- Permissions say what the plugin can/cannot do and how to turn it off.
|
|
58
198
|
- Upgrade and uninstall are tested against real data, not only a clean install.
|
|
59
|
-
- No “certified”, “official”, or “
|
|
199
|
+
- No “certified”, “verified”, “audited”, “official”, “secure”, or “ranked” wording.
|
package/docs/levels.md
CHANGED
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
|
|
12
12
|
> **Current baseline (2026-11-04):** DSH `0.2.2-alpha.3` (npm `alpha`), Node `>=24.9.0` (since 0.2.2-alpha.2), Cordis `4.0.6-alpha.2`. `engines.dsh`, when declared, decides alone since 0.2.1-alpha.4; `ctx.menus` is gone in 0.2.2-alpha.3; Typert codecs use `create()` since 0.1.6-alpha.2; `@deepseek-ai/dsh-client-runtime` was removed in 0.1.2-alpha.1 and has no 0.2.x.
|
|
13
13
|
|
|
14
|
+
## Where to go next
|
|
15
|
+
|
|
16
|
+
| You have | Read |
|
|
17
|
+
|---|---|
|
|
18
|
+
| a report line with a `DSC-nn` or `C-n.m` ID | [rules.md](rules.md) — every check, whether it blocks, and the one-line fix |
|
|
19
|
+
| a FAIL to fix right now | [recipes.md](recipes.md) — copy-paste `package.json`, `cordis.patch.yml`, and client entry |
|
|
20
|
+
| an ENTRY question | [entry.md](entry.md) — package shape, runtime surface, declarations |
|
|
21
|
+
| a FLOW journey to record | [flow.md](flow.md) — the `dsh-flow-evidence/2` file and J1–J8 evidence |
|
|
22
|
+
| a BOOST practice to evidence | [boost.md](boost.md) — the thirteen-field evidence card |
|
|
23
|
+
|
|
24
|
+
The two reference docs exist because a report ID alone does not tell you what to
|
|
25
|
+
change: [rules.md](rules.md) maps IDs to fixes, and [recipes.md](recipes.md)
|
|
26
|
+
holds the shapes those fixes need.
|
|
27
|
+
|
|
14
28
|
## ENTRY
|
|
15
29
|
|
|
16
30
|
ENTRY is the compatibility floor. It checks the package shape, entry points,
|
|
@@ -24,6 +38,8 @@ made — an actual start.
|
|
|
24
38
|
|
|
25
39
|
Start here: [ENTRY guide](entry.md) and the canonical
|
|
26
40
|
[31-DSH-ENTRY](https://github.com/PerryLink/dsh-standards/blob/main/31-DSH-ENTRY%E5%85%A5%E9%97%A8%E8%A7%84%E8%8C%83.md).
|
|
41
|
+
Then fix by ID: [rules.md `DSC-01`–`DSC-14`](rules.md#table) and
|
|
42
|
+
[recipes.md](recipes.md).
|
|
27
43
|
|
|
28
44
|
## FLOW
|
|
29
45
|
|
|
@@ -33,11 +49,13 @@ modes.
|
|
|
33
49
|
|
|
34
50
|
- The current FLOW harness is a draft. Some J1–J8 checks have runtime evidence;
|
|
35
51
|
others are static preflight or `not-run`.
|
|
36
|
-
- `
|
|
37
|
-
|
|
52
|
+
- `runtime`, `failed`, and `not-run` are the FLOW statuses. A partial run is
|
|
53
|
+
`not-run`, never a soft pass.
|
|
38
54
|
|
|
39
55
|
Start here: [FLOW guide](flow.md) and the canonical
|
|
40
56
|
[32-DSH-FLOW](https://github.com/PerryLink/dsh-standards/blob/main/32-DSH-FLOW%E9%A1%BA%E7%95%85%E8%A7%84%E8%8C%83.md).
|
|
57
|
+
The evidence file shape and the J1–J8 minimum are in
|
|
58
|
+
[flow.md § the evidence file](flow.md#evidence-file).
|
|
41
59
|
|
|
42
60
|
## BOOST
|
|
43
61
|
|
|
@@ -52,6 +70,8 @@ parallel/jobs, and composition.
|
|
|
52
70
|
|
|
53
71
|
Start here: [BOOST guide](boost.md) and the canonical
|
|
54
72
|
[33-DSH-BOOST](https://github.com/PerryLink/dsh-standards/blob/main/33-DSH-BOOST%E5%8A%A0%E9%80%9F%E5%AE%9E%E8%B7%B5.md).
|
|
73
|
+
The card fields are in
|
|
74
|
+
[boost.md § evidence, not a badge](boost.md#thirteen-fields).
|
|
55
75
|
|
|
56
76
|
## Run the checks
|
|
57
77
|
|
|
@@ -75,4 +95,4 @@ The legacy build skill/CLI is [`dsh-primer`](https://github.com/PerryLink/dsh-pr
|
|
|
75
95
|
- `not-run` ≠ `pass`
|
|
76
96
|
- `boost-evidenced` ≠ plugin grade
|
|
77
97
|
- `A–D` grades (where they exist) are orthogonal to ENTRY/FLOW/BOOST
|
|
78
|
-
- No official, certified, secure, recommended, or ranked claim
|
|
98
|
+
- No official, certified, verified, audited, secure, recommended, or ranked claim
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Recipes: copy these, then re-run the checker
|
|
2
|
+
|
|
3
|
+
<a id="before-you-copy"></a>
|
|
4
|
+
## Before you copy
|
|
5
|
+
|
|
6
|
+
Every block below was run against `dsh-standards` `40-checker` (`0.1.0`,
|
|
7
|
+
anchor `ce7073d51254`, default runtime `0.2.2-alpha.2`). The package name
|
|
8
|
+
`@acme/dsh-hello` is a placeholder — replace it everywhere, including the patch
|
|
9
|
+
row. The numbers in the verify sections are what the checker printed here; if
|
|
10
|
+
your output differs, the diff line is your fix.
|
|
11
|
+
|
|
12
|
+
<a id="minimal-packagejson"></a>
|
|
13
|
+
## Minimal `package.json`
|
|
14
|
+
|
|
15
|
+
A plugin that exports `apply`, registers one tool, and can also be composed as
|
|
16
|
+
a bundle row.
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"name": "@acme/dsh-hello",
|
|
21
|
+
"version": "0.1.0",
|
|
22
|
+
"description": "Minimal DSH plugin that registers one tool.",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "./index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./index.js"
|
|
28
|
+
},
|
|
29
|
+
"files": ["index.js", "cordis.patch.yml"],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=24.9.0",
|
|
32
|
+
"dsh": ">=0.2.2-0 <0.3.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@deepseek-ai/dsh": ">=0.2.2-0 <0.3.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependenciesMeta": {
|
|
38
|
+
"@deepseek-ai/dsh": { "optional": true }
|
|
39
|
+
},
|
|
40
|
+
"dsh": {
|
|
41
|
+
"manifestVersion": 1,
|
|
42
|
+
"kind": "plugin",
|
|
43
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
44
|
+
"capabilities": ["tools:register"],
|
|
45
|
+
"labels": []
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Field | What it buys | Check |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `name` + exact `version` | a resolvable identity; a range or tag here is a FAIL | `C-1.1` |
|
|
53
|
+
| `exports["."]` (or `main`) | the entry must resolve to a file inside the package | `C-1.2` |
|
|
54
|
+
| `files` including the entry and patch | not checked, but `npm pack` can otherwise ship a manifest whose entry is absent | — |
|
|
55
|
+
| `engines.dsh` | the only declaration that decides compatibility alone; `workspace:` is a FAIL | `C-2.1`/`C-2.3` |
|
|
56
|
+
| `license` | avoids the `C-1.5` WARN; SPDX string | `C-1.5` |
|
|
57
|
+
| `dsh.bundle.patch` | the file a bundle layer composes; in-package relative path | `C-1.4` |
|
|
58
|
+
| `dsh.capabilities` | declares the host surfaces you touch; closed set of 15 | `C-11.1` |
|
|
59
|
+
| `dsh.labels` | declares high-risk categories; closed set of 5; `[]` is allowed | `C-13.1` |
|
|
60
|
+
| `dsh.kind` | `plugin` gets L1; `bundle` gets G0–G3; a client is static-only | `DSC-14` |
|
|
61
|
+
|
|
62
|
+
Notes that come from the checker, not taste:
|
|
63
|
+
|
|
64
|
+
- `dsh.capabilities` and `dsh.labels` are **standard extensions**: this host
|
|
65
|
+
does not read them. They are declarations, not enforcement. Declaring them
|
|
66
|
+
does not make the plugin safe, and omitting them is a WARN today.
|
|
67
|
+
- `dsh.manifestVersion`, `dsh.bundle`, `dsh.client` are host fields; keep
|
|
68
|
+
`manifestVersion: 1`.
|
|
69
|
+
- `engines.node` is **not** checked by the checker. Keep it for humans, but do
|
|
70
|
+
not expect a check to fail without it.
|
|
71
|
+
|
|
72
|
+
<a id="entry-module"></a>
|
|
73
|
+
## The entry module
|
|
74
|
+
|
|
75
|
+
`C-1.2` resolves the entry, so the file must exist. `DSC-10` and `DSC-06` both
|
|
76
|
+
look at what runs when and how it is cleaned up.
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
export const name = 'dsh-hello'
|
|
80
|
+
export const inject = []
|
|
81
|
+
|
|
82
|
+
export function apply(ctx) {
|
|
83
|
+
ctx.effect(() => {
|
|
84
|
+
const timer = setInterval(() => {}, 60_000)
|
|
85
|
+
return () => clearInterval(timer)
|
|
86
|
+
}, 'dsh-hello.timer')
|
|
87
|
+
|
|
88
|
+
ctx.tools.register({
|
|
89
|
+
name: 'hello',
|
|
90
|
+
description: 'Say hello.',
|
|
91
|
+
async run() {
|
|
92
|
+
return 'hello'
|
|
93
|
+
},
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `inject` lists only services you read; the runner stubs them for L1.
|
|
99
|
+
- `ctx.effect(fn, 'label')` gives the disposer a label, so `dispose()` can go
|
|
100
|
+
back to zero (`C-6.2`, `C-7.1`).
|
|
101
|
+
- `ctx.tools.register(...)` is the static trigger that requires
|
|
102
|
+
`capabilities: ["tools:register"]`.
|
|
103
|
+
|
|
104
|
+
<a id="cordispatchyml-insert"></a>
|
|
105
|
+
## `cordis.patch.yml`: the insert row
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
- insert:
|
|
109
|
+
- id: dsh-hello
|
|
110
|
+
name: '@acme/dsh-hello'
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Rules the checker and the host care about:
|
|
114
|
+
|
|
115
|
+
- `id` is the stable row identifier later layers use to override this row. Keep
|
|
116
|
+
it short and unique.
|
|
117
|
+
- `name` is the module the loader imports. For the **package-root row** it must
|
|
118
|
+
be the package name exactly (`@acme/dsh-hello`), because G4 looks for a
|
|
119
|
+
composed row whose `name` equals `manifest.name`. An `id` alone is not a row.
|
|
120
|
+
- An insert is required. An `override`-only patch adds nothing to the profile;
|
|
121
|
+
the plugin is never loaded (G1 reports no target rows, G3 reports no active
|
|
122
|
+
rows).
|
|
123
|
+
- The patch file stays inside the package and uses a relative path; `..`,
|
|
124
|
+
absolute paths, `~`, and `$VAR` are rejected (`C-1.4`).
|
|
125
|
+
- Do not enable `@deepseek-ai/dsh-plugin-manager/tools` in a patch unless you
|
|
126
|
+
mean to hand every session the ability to install code (`C-11.3`).
|
|
127
|
+
- G0 parses, G1 composes rows, G2 runs the compatibility gate, G3 boots the
|
|
128
|
+
target rows. Only G3 `verified` is a start; a `partial` result is
|
|
129
|
+
`ERROR`/`not-run`, not a pass.
|
|
130
|
+
|
|
131
|
+
<a id="client-entry-and-root-row"></a>
|
|
132
|
+
## Client entry and the package-root row
|
|
133
|
+
|
|
134
|
+
A client is a dual-face package: a server/`dsh` half that is loaded as a row,
|
|
135
|
+
and a prebuilt browser bundle at `exports["./client"]`.
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"name": "@acme/dsh-panel",
|
|
140
|
+
"version": "0.1.0",
|
|
141
|
+
"license": "MIT",
|
|
142
|
+
"type": "module",
|
|
143
|
+
"exports": {
|
|
144
|
+
".": "./index.js",
|
|
145
|
+
"./client": "./lib/client.js"
|
|
146
|
+
},
|
|
147
|
+
"files": ["index.js", "lib", "cordis.patch.yml"],
|
|
148
|
+
"engines": { "dsh": ">=0.2.2-0 <0.3.0" },
|
|
149
|
+
"dsh": {
|
|
150
|
+
"manifestVersion": 1,
|
|
151
|
+
"kind": "client",
|
|
152
|
+
"client": { "platform": "web" },
|
|
153
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
154
|
+
"capabilities": [],
|
|
155
|
+
"labels": []
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
# cordis.patch.yml — the same package name as the manifest
|
|
162
|
+
- insert:
|
|
163
|
+
- id: dsh-panel
|
|
164
|
+
name: '@acme/dsh-panel'
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The server half only has to load as a row; the browser half is the built file:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
// index.js — the server half; lib/client.js is the browser half
|
|
171
|
+
export const name = 'dsh-panel'
|
|
172
|
+
export const inject = []
|
|
173
|
+
|
|
174
|
+
export function apply() {}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
What each piece is for:
|
|
178
|
+
|
|
179
|
+
| Piece | Why it is required | Where it shows up |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `dsh.client` (non-empty) | marks the package as having a browser face | `G4`, `C-1` |
|
|
182
|
+
| `dsh.client.platform` | the host selects the Web consumer with `"web"`; the checker only requires `dsh.client` to be non-empty | host manifest type |
|
|
183
|
+
| `exports["./client"]` | a string path, or an object with a string `default`; must point at a **built** file (the checker only fails when it is absent) | `G4` → `DSC-01` FAIL if missing |
|
|
184
|
+
| root row (`name` = package name) | the client scanner reads the loader's rows; no active row means the bundle is never discovered | `G4` in the bundle smoke |
|
|
185
|
+
| `dsh.client.inject` | informational package names, not Cordis injection | host only |
|
|
186
|
+
| `dsh.client.external` | extra module-table requests; omit unless you need them | host build |
|
|
187
|
+
|
|
188
|
+
`"capabilities": []` says the server half touches no host surface. The checker
|
|
189
|
+
still WARNs (`C-11.1: dsh.capabilities is empty`) and the report's reason line
|
|
190
|
+
says `advisory: DSC-11`; in ENTRY that WARN is advisory, so the status stays
|
|
191
|
+
`pass`. If the server half does touch something, fill the array from the trigger
|
|
192
|
+
table in [entry.md](entry.md#capability-triggers) — an empty array is not a
|
|
193
|
+
substitute for the capabilities you actually use.
|
|
194
|
+
|
|
195
|
+
Two failure shapes you will see, and how to tell them apart:
|
|
196
|
+
|
|
197
|
+
```text
|
|
198
|
+
G4: FAIL dsh.client exists but exports["./client"] is missing; a package-root row cannot resolve it
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The message names both causes. Add `exports["./client"]` first; if the export
|
|
202
|
+
already exists, the bundle smoke is telling you the composed root row is
|
|
203
|
+
missing — check that the patch `name` equals `manifest.name` and that the row is
|
|
204
|
+
`insert`ed.
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
DSC-14 FAIL C-14 L1 G4 failed: dsh.client has no package-root row or missing client export
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
That is the same condition on a runtime claim. Without `--host`, a client is
|
|
211
|
+
`DSH-ENTRY recognized` at best, and `DSC-14` is `NA`: build-only is not runtime.
|
|
212
|
+
|
|
213
|
+
<a id="verify"></a>
|
|
214
|
+
## Verify what you copied
|
|
215
|
+
|
|
216
|
+
```sh
|
|
217
|
+
# recognition only (no runtime claim): expect status=pass, exit=0
|
|
218
|
+
npx dsh-standards . --scope entry --entry-claim static
|
|
219
|
+
|
|
220
|
+
# plugin runtime: expect DSC-14 PASS and level "DSH-ENTRY runtime"
|
|
221
|
+
npx dsh-standards . --scope entry \
|
|
222
|
+
--runtime /path/to/cordis/lib/index.js
|
|
223
|
+
|
|
224
|
+
# bundle: expect "G0-G3 verified; rows=1/1"
|
|
225
|
+
npx dsh-standards . --scope entry \
|
|
226
|
+
--host /path/to/@deepseek-ai/dsh-app-boot/lib/index.js
|
|
227
|
+
|
|
228
|
+
# machine-readable: read .conclusion.level and .g4
|
|
229
|
+
npx dsh-standards . --format json
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
For the plugin recipe above, the static run prints:
|
|
233
|
+
|
|
234
|
+
```text
|
|
235
|
+
DSC-01 PASS C-1 L0
|
|
236
|
+
DSC-02 PASS C-2 L0
|
|
237
|
+
DSC-11 PASS C-11 L0
|
|
238
|
+
DSC-12 PASS C-12 L0
|
|
239
|
+
DSC-13 PASS C-13 L0
|
|
240
|
+
level: DSH-ENTRY recognized
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
For the client recipe, the same run ends `status=pass`, prints
|
|
244
|
+
`level: DSH-ENTRY recognized`, and still shows
|
|
245
|
+
`C-11.1: dsh.capabilities is empty` with `reason: advisory: DSC-11`. That is the
|
|
246
|
+
transitional empty-array WARN, not a G4 failure. A `kind: client` package is
|
|
247
|
+
static-only: `DSC-14` is `NA`, so `--host` cannot turn it into
|
|
248
|
+
`DSH-ENTRY runtime`. Only a package installed as a bundle (`kind: bundle`)
|
|
249
|
+
reaches runtime through G0–G3, and its G4 needs both the client export and the
|
|
250
|
+
root row. If `G4` fails you get `exit=1` and no level — add the missing export
|
|
251
|
+
or row, do not reword the report.
|
package/docs/rules.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Rules: decode a check report in one table
|
|
2
|
+
|
|
3
|
+
<a id="how-to-read"></a>
|
|
4
|
+
## How to read one report line
|
|
5
|
+
|
|
6
|
+
A DS-C report has one line per check: `id`, `result`, `clause`, `layer`,
|
|
7
|
+
`reason`. The detail lines under it (`C-1.2`, `C-11.5`, `G4`) are what you
|
|
8
|
+
actually fix.
|
|
9
|
+
|
|
10
|
+
| Result | Meaning | What it does to the verdict |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| `PASS` | the check ran and found nothing | counts as pass |
|
|
13
|
+
| `WARN` | ran, found something worth fixing | does not fail the run; `status` becomes `warn` (advisory checks excepted) |
|
|
14
|
+
| `FAIL` | ran, found an attributable defect | `status=fail`, exit `1` |
|
|
15
|
+
| `NA` | the check does not apply (wrong `dsh.kind`, no field, package profile) | allowed; not a pass |
|
|
16
|
+
| `ERROR` | the check could not run (`coverage: not-run`, e.g. no `--runtime`/`--host`) | `status=incomplete`, exit `2` |
|
|
17
|
+
|
|
18
|
+
Exit codes: `0` completed (pass or warn), `1` attributable FAIL, `2`
|
|
19
|
+
incomplete/not-run, `3` usage error. `WARN` is not a clean pass and
|
|
20
|
+
`incomplete` is not a pass — both are printed as such on purpose.
|
|
21
|
+
|
|
22
|
+
The JSON report also carries a sibling `advice[]` array: one entry per live
|
|
23
|
+
detail, with `title`, `why`, `fix`, `docsUrl`, `severity`, and `evidence`. It is
|
|
24
|
+
additive and never changes the verdict (turn it off with `--no-advice`). The
|
|
25
|
+
`docsUrl` is built from a release-pinned base (`v0.4.3` in the current rule
|
|
26
|
+
table); if it points at an older page, this table is the current mapping.
|
|
27
|
+
|
|
28
|
+
<a id="scope-matrix"></a>
|
|
29
|
+
## Which checks run in which scope
|
|
30
|
+
|
|
31
|
+
| Scope / profile | Mandatory checks | Notes |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `--scope ds-c` (default), `package` | 01, 02, 04–09, 11, 12, 13 | 03 is `NA`, 10 is advisory, 14 is not run |
|
|
34
|
+
| `--scope ds-c`, `listing` | + 03 | install spec / market index are now judged |
|
|
35
|
+
| `--scope entry --entry-claim static` | 01, 02, 11, 12, 13 (+ 03 for listing) | 04–09 are attempted and printed (usually `ERROR`/`NA`), 14 is `NA`; a `DSC-11`/`DSC-12`/`DSC-13` FAIL is downgraded to `WARN` |
|
|
36
|
+
| `--scope entry --entry-claim runtime` | + 14 | 14 carries the runtime verdict, not 04–09 |
|
|
37
|
+
| `--scope mvp` | 01, 02, 04–09 | the MVP subset, labelled `MVP-13` |
|
|
38
|
+
|
|
39
|
+
<a id="table"></a>
|
|
40
|
+
## The check table
|
|
41
|
+
|
|
42
|
+
| Check | Clause | Blocking or advice | One-line fix | Guide |
|
|
43
|
+
|---|---|---|---|---|
|
|
44
|
+
| **DSC-01** | C-1 manifest + entry | Blocking (ds-c, entry). C-1.3/C-1.5/C-1.6 are WARN/NA. A `G4` detail is reported here. | Exact `name`/`version`, `exports["."]` or `main` resolving to a file inside the package, in-package `dsh.bundle.patch` paths, and a `license`. | [entry.md § package shape](entry.md#package-shape) · [recipes](recipes.md#minimal-packagejson) |
|
|
45
|
+
| **DSC-02** | C-2 version range | Blocking (ds-c). In ENTRY, a static claim downgrades the whole check to WARN; a runtime claim keeps `C-2.2`/`C-2.3` blocking. | Declare `engines.dsh` as a range that contains the host you target (`>=0.2.2-0 <0.3.0`); never publish `workspace:`. | [entry.md § package shape](entry.md#package-shape) |
|
|
46
|
+
| **DSC-03** | C-3 install source | Blocking (listing only); `NA` in package profile. | Pin the source: exact version or verifiable tag, `#<40-hex>` for git, an in-package path, and a `format:1` market entry with pinned versions. | [entry.md § package shape](entry.md#package-shape) |
|
|
47
|
+
| **DSC-04** | C-4 load to ACTIVE | Blocking (ds-c, when a runtime runs). | Make the entry import cleanly and reach `ACTIVE`: no throw at import, no module-scope work, declare `inject` or read `ctx.get()`, move work into `apply()`. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
48
|
+
| **DSC-05** | C-5 declared provide | Blocking (ds-c). | Every name in `export const provide` must be registered by this plugin's own fiber via `ctx.provide(name, impl)`. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
49
|
+
| **DSC-06** | C-6 effects | Blocking (ds-c); anonymous effects are WARN. | Wrap timers, watchers, sockets, and subprocesses in `ctx.effect(fn, 'label')`; never write `ctx.reflect.store` or `ctx.events._hooks`. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
50
|
+
| **DSC-07** | C-7 unload residue | Blocking (ds-c). | Return a disposer from every effect so `dispose()` leaves `getEffects()===0`, the service store at baseline, and `registry.size` unchanged. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
51
|
+
| **DSC-08** | C-8 restart idempotence | Blocking (ds-c). | Make `restart()` re-enter `ACTIVE`, keep the effect count, advance revision/epoch, and succeed from a fresh `Context`. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
52
|
+
| **DSC-09** | C-9 declared vs measured | Blocking (ds-c) for `C-9.1`; `C-9.2`/`C-9.3` are WARN. | Keep declarations equal to reality: provided ⊆ measured, measured ⊆ declared, and each declared `inject` is really read as `ctx.<service>`. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
53
|
+
| **DSC-10** | C-10 import-time work | Advice only; never changes the verdict. | Move module-scope `await`, timers, `process.on`, fs writes, and spawns into `apply()` and clean up with `ctx.effect()`. | [entry.md § import-time side effects](entry.md#import-time-side-effects) |
|
|
54
|
+
| **DSC-11** | C-11 capabilities | Blocking (ds-c); `WARN` in ENTRY, and a `WARN` here is advisory even in ds-c. | Declare `dsh.capabilities` from the closed set, add one entry per static trigger, and never enable `@deepseek-ai/dsh-plugin-manager/tools` in a patch. | [entry.md § capability triggers](entry.md#capability-triggers) |
|
|
55
|
+
| **DSC-12** | C-12 credentials + seams | Blocking (ds-c); `WARN` in ENTRY. | Read credentials through `ctx.credentials`, declare `credentials:list` for `listRecords()` and `credentials:provider` for a provider, and never read the backing file. | [entry.md § credentials and seams](entry.md#credentials-and-seams) |
|
|
56
|
+
| **DSC-13** | C-13 labels + mirror | Blocking (ds-c); `WARN` in ENTRY, advisory as a WARN. | Use only the five labels, add the label each capability requires, point `dsh.upstream` at an https URL that is not this package **and** add `mirror`, and drop endorsement words. | [entry.md § labels and mirror](entry.md#labels-and-mirror) |
|
|
57
|
+
| **DSC-14** | C-14 entry run | Blocking only for a runtime claim (`--scope entry`). | Supply the real host: `--host` for a bundle (G0–G3) or `--runtime` for a plugin (L1); a client or preset cannot claim runtime. | [entry.md § runtime surface](entry.md#runtime-surface) |
|
|
58
|
+
|
|
59
|
+
`DSC-14` has no `C-14` in the clause text; it is the ENTRY run-set gate
|
|
60
|
+
(`ENT-14` in 31-DSH-ENTRY). Treat it as "did the thing actually start".
|
|
61
|
+
|
|
62
|
+
<a id="warn"></a>
|
|
63
|
+
## Two things that look like passes but are not
|
|
64
|
+
|
|
65
|
+
**Advisory WARN.** `DSC-10` is always advisory. `DSC-11` and `DSC-13` become
|
|
66
|
+
advisory whenever the check itself returns `WARN`, because the host does not
|
|
67
|
+
read those fields yet. The effect is visible: this guide's own report prints
|
|
68
|
+
`DSC-11 WARN`, `DSC-13 WARN`, yet `coverage.warn=0` and
|
|
69
|
+
`reason: advisory: DSC-11, DSC-13`. Read the reason line, not only the coverage
|
|
70
|
+
counts. A `FAIL` that ENTRY downgrades to `WARN` is a plain WARN and does stay
|
|
71
|
+
in the counts.
|
|
72
|
+
|
|
73
|
+
**`incomplete`.** If a mandatory check has `coverage: not-run` (no runtime, no
|
|
74
|
+
host, a killed child, a partial AST parse), the status is `incomplete` and the
|
|
75
|
+
exit code is `2`. The report prints `NOT A DS-C PASS`. Do not paste a
|
|
76
|
+
`not-run` row into a README as a pass; write `not-run` or run the check.
|
|
77
|
+
|
|
78
|
+
<a id="not-checked"></a>
|
|
79
|
+
## What the checker does not check
|
|
80
|
+
|
|
81
|
+
These are good practice but **not** evidence of a pass, because no check reads
|
|
82
|
+
them:
|
|
83
|
+
|
|
84
|
+
- `engines.node` (the 24.9 line is a guide recommendation; `DSC-02` looks at
|
|
85
|
+
`engines.dsh` and `@deepseek-ai/dsh*` peers only)
|
|
86
|
+
- README install instructions, display metadata, keywords, `icon`
|
|
87
|
+
- endorsement words in prose, `description`, or README — `C-13.4` scans only
|
|
88
|
+
the `dsh.labels` array
|
|
89
|
+
- whether a `red-team` label was actually used for red-team work (`C-13.5`
|
|
90
|
+
only asks for `dsh.usageScope`)
|
|
91
|
+
- browser rendering of a client bundle (`G4` is a static shape check)
|
|
92
|
+
|
|
93
|
+
The checker's default `--runtime-version` is `0.2.2-alpha.2`. Pass
|
|
94
|
+
`--runtime-version <the host you target>` when that differs, or `DSC-02` will
|
|
95
|
+
judge your range against the wrong version.
|
|
96
|
+
|
|
97
|
+
If a report FAILs and you cannot find the ID here, open
|
|
98
|
+
[`docs/recipes.md`](recipes.md) for the copy-paste shape, then re-run the same
|
|
99
|
+
command and compare the detail lines.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-guide",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "A practical guide to DSH plugin quality: ENTRY checks, FLOW checks, and BOOST guidance. Community guide; not official, not a certification, not a ranking.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|