dsh-standards 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,6 +19,17 @@ node bin/dsh-plugin-verify.mjs /path/to/plugin --format json --out report.json
19
19
  node bin/dsh-plugin-verify.mjs /path/to/plugin --format sarif --out report.sarif
20
20
  ```
21
21
 
22
+ ### 作者入口:`dsh-standards check [dir]`
23
+
24
+ ```bash
25
+ dsh-standards check # 默认 cwd,输出 Markdown
26
+ dsh-standards check ./my-plugin --scope entry --entry-claim static --no-exec
27
+ dsh-standards check ./my-plugin --json # 原始 dsh-conformance/1
28
+ dsh-standards check ./my-plugin --html out.html # 单文件、内联 CSS、零外部资源、全转义
29
+ ```
30
+
31
+ Markdown = 一句结论(`conclusion.headline`)+ ≤3 条优先修复 + `buckets` 三栏全表 + advice + 免责声明/「没查什么」。exit code 与底层检查器一致(0/1/2/3),不在这里改写。`--json` 与 `--html` 互斥;`--format`/`--out` 不转发(由 `check` 自己决定输出)。其余 flag 原样转发(`--scope`、`--runtime-version`、`--market-index` 等)。advice 的公开 schema 是 `spec/schemas/advice.schema.json`(`dsh-advice/1`,13 个必填字段,`why`/`fix`/`docsUrl` 可 null,`evidence` ≤ 10,`gate`/`severity` ∈ {FAIL,WARN})。
32
+
22
33
  `buildReport()` 与 `--format json` 默认带**加性** `advice[]`(来自 `spec/remediation.json` 的 68 条规则;FAIL/WARN detail 优先,无 detail 的运行时 FAIL 用 `check.clause` 兜底)。每条 advice 的 `gate` 是该 check 的最终结果(PASS/WARN/FAIL),`severity` 跟随 `gate`:被降级为 WARN 的 check 即使 detail 仍是 FAIL,也只给 WARN,detail 原因保留在 `reason`。`--no-advice` 可关闭;它不改变任何 check 的结果或 exit code。
23
34
 
24
35
  ## 2. 检查项
@@ -142,6 +153,8 @@ node bin/dsh-flow-collect.mjs --package ./my-plugin --sandbox --out flow-evidenc
142
153
  - `--sandbox` 在临时 HOME/DSH_HOME 下跑 `dsh plugin add` + boot:先试 `unshare -n`,不行再试 `unshare -Urn`;用 `prlimit` 设 `nproc`/`as` 并从 `/proc/self/limits` 回读实际生效项;超时 SIGKILL 整个进程组(Windows `taskkill /T /F`)。硬断言 `GH_TOKEN`/`NPM_TOKEN`/`GITHUB_TOKEN` 不在子进程 env,发现即中止。
143
154
  - 只有**所有**隔离项都生效才写 `producer.kind=sandbox` / `claims.claimLevel=sandbox-attested`;任何一项缺失(`unshare-missing`、`prlimit-missing`、`env-token-present`、`home-not-isolated`)写 `sandbox-degraded` + `sandbox.degraded[]`,仍是 self-attested,`level-audit` 永不判 runtime。`--sandbox-no-network` / `--sandbox-no-limits` 用于强制验证降级路径。
144
155
  - 前置缺失(无 dsh、无 `--previous`、宿主兼容门禁拒绝)记 `blocked` + reason,出口码 0;真失败出口码 1,并打印五要素提示(子检查 / 命令 / exitCode / stderr 末 20 行 / 日志绝对路径 + 条款锚)。
156
+ - `level-audit --flow-evidence ... --dir <pkg>` 会把证据主体绑定到目录:`package.json` 的 name/version、重算的 `treeSha256`、`npm pack --ignore-scripts` 的 tarball sha256/integrity 必须与 `subject` 一致,否则 FLOW 直接 `invalid`,结果里输出 `subject.{declared,recomputed,mismatches}`。省略 `--dir` 时不做重绑定(旧行为)。
157
+ - **CI 归属不是证明**:runner 上存在 `GITHUB_RUN_ID`/`GITHUB_REPOSITORY`/`GITHUB_SHA` 时,collector 把它们写进 `producer.runUrl/repository/commit`,`level-audit` 输出 `attestation: ci | self-declared`。这些字段来自环境变量、是自我声明,既不升级 `claims.claimLevel`,也不证明命令真的在 CI 里跑过。
145
158
  - 本地证据默认 `self-attested`:结构自洽 + 绑定此包 + 未过期,**不证明真跑过**。即使 `--sandbox` 通过,它也只是 containment(无 seccomp/cgroup/OCI 镜像/签名),不是认证或安全审计。`level-audit` 只有见到 `claims.claimLevel=sandbox-attested` 或 `producer.kind=sandbox` 才判 `runtime`;self/ci/degraded 证据保留 `passed` 计数但记 `not-run`。
146
159
 
147
160
  ## 4. 运行 L1–L3
@@ -151,7 +164,7 @@ node bin/dsh-flow-collect.mjs --package ./my-plugin --sandbox --out flow-evidenc
151
164
  ```bash
152
165
  node bin/dsh-plugin-verify.mjs ./my-plugin \
153
166
  --runtime /path/to/deepseek-harness/vendor/cordis/lib/index.js \
154
- --runtime-version 0.2.2-alpha.2 \
167
+ --runtime-version 0.2.1-rc.5 \ # 省略时自动取 `dsh --version`(宿主实际版本)
155
168
  --cordis-version 4.0.6-alpha.1
156
169
  ```
157
170
 
@@ -179,6 +192,10 @@ node bin/dsh-plugin-verify.mjs ./my-plugin \
179
192
 
180
193
  **结论门**:`--scope ds-c` 只有在 profile 对应的强制项全部 `coverage: full`(允许显式 NA)时才给 `level`;否则 `status: incomplete`、exit 2。WARN 仍给 `level`,但写 `DS-C (profile, warnings)`,不冒充干净通过。
181
194
 
195
+ **覆盖度与分桶(加性)**:`coverage` 统计报告携带的**全部**检查(完整跑是 14 项)——`pass/warn/fail/na/notRun/error` 都按 `checks[]` 实算,`total` 是总数;`coverage.notRun` 含 ERROR/未跑与缺失的强制项,`na` 再单列。`metrics = {checked, total, declaredFields}` 中的 `declaredFields` 是 5 个可选声明面(capabilities/labels/commitments/disclosure/engines.dsh)中实际声明的个数,只作上下文、不进结论。`buckets.blocking`(FAIL)、`buckets.hygiene`(WARN,`DSC-11/12/13` 标 `declaration`)、`buckets.notChecked`(NA/not-run/ERROR)是 text 的渲染顺序。`entryBlocking` 增加 `checked`/`notChecked`:`pass` 仅在 6 个阻断前提(C-1.1/1.2/1.4、C-3.1/3.2/3.5;runtime claim 再加 C-2.2/2.3)**都**产出 detail 且无 FAIL 时为 true——没给 `--install-spec`/`--market-index` 时 C-3.x 未查,`pass` 必为 false。
196
+
197
+ **锚**:`anchor.dsh` 默认取宿主 `dsh --version`(`anchor.runtimeSource=host`);显式 `--runtime-version` 时 `runtimeSource=cli`,与宿主不一致则 `anchor.suspect=true`;找不到 dsh 才回退常量并标 `fallback`。
198
+
182
199
  ## 6. 已知限制
183
200
 
184
201
  1. **C-3 只在 listing 模式可判**;package 模式是 NA,仅附供应链 advisory 证据。
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * build-offline-kit — one .tgz for air-gapped / intranet users.
4
+ *
5
+ * The kit carries:
6
+ * - the dsh-standards npm tarball (zero runtime dependencies)
7
+ * - adoption/market (rev1 label schema, revocation, disclaimer, ...)
8
+ * - market-contract (rev2 proposal, validate.mjs, examples)
9
+ * - examples/ (the shipped example plugins)
10
+ * - README.md (offline instructions) + OFFLINE-KIT.json (content manifest)
11
+ *
12
+ * It does not download anything. `market-contract` is not part of this repo
13
+ * yet, so the source directory can be passed explicitly (--market-contract);
14
+ * otherwise the usual sibling locations are probed and a clear error is
15
+ * raised when none is found.
16
+ *
17
+ * Usage:
18
+ * node 40-checker/scripts/build-offline-kit.mjs \
19
+ * --out publish/dsh-standards-offline-1.2.2.tgz \
20
+ * --tarball /tmp/dsh-standards-1.2.2.tgz \
21
+ * --market-contract /path/to/00-live/market-contract
22
+ */
23
+ import fs from 'node:fs'
24
+ import os from 'node:os'
25
+ import path from 'node:path'
26
+ import crypto from 'node:crypto'
27
+ import { spawnSync } from 'node:child_process'
28
+ import { fileURLToPath } from 'node:url'
29
+
30
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
31
+ const ROOT = path.resolve(HERE, '..', '..')
32
+
33
+ function usage() {
34
+ console.log(`build-offline-kit — bundle a self-contained offline kit
35
+
36
+ Usage:
37
+ node 40-checker/scripts/build-offline-kit.mjs [options]
38
+
39
+ Options:
40
+ --out <file> output .tgz, default publish/dsh-standards-offline-<version>.tgz
41
+ --tarball <file> existing dsh-standards npm .tgz; default pack the current tree
42
+ --root <dir> source tree, default this repository
43
+ --market-contract <dir> market-contract source (rev2 proposal + validate.mjs + examples)
44
+ --version <version> kit/package version, default package.json version
45
+ --today <YYYY-MM-DD> fixed manifest/README date
46
+ --help show this help
47
+
48
+ The output is a gzip tar with README.md, OFFLINE-KIT.json, the npm tarball,
49
+ adoption/market, market-contract and examples at its root.`)
50
+ }
51
+
52
+ function readJson(file, label) {
53
+ try {
54
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
55
+ } catch (error) {
56
+ throw new Error(`${label} is not readable JSON (${file}): ${error.message}`)
57
+ }
58
+ }
59
+
60
+ function sha256File(file) {
61
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const options = {
66
+ out: null, tarball: null, root: ROOT, marketContract: null,
67
+ version: null, today: null,
68
+ }
69
+ for (let i = 0; i < argv.length; i += 1) {
70
+ const arg = argv[i]
71
+ const next = () => {
72
+ const value = argv[i + 1]
73
+ if (value === undefined) throw new Error(`${arg} needs a value`)
74
+ i += 1
75
+ return value
76
+ }
77
+ if (arg === '--out') options.out = next()
78
+ else if (arg === '--tarball') options.tarball = next()
79
+ else if (arg === '--root') options.root = next()
80
+ else if (arg === '--market-contract') options.marketContract = next()
81
+ else if (arg === '--version') options.version = next()
82
+ else if (arg === '--today') {
83
+ const day = next()
84
+ const parsed = new Date(`${day}T00:00:00.000Z`)
85
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(day) || Number.isNaN(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== day) {
86
+ throw new Error(`--today must be a real YYYY-MM-DD, got ${JSON.stringify(day)}`)
87
+ }
88
+ options.today = day
89
+ } else if (arg === '--help' || arg === '-h') {
90
+ usage()
91
+ process.exit(0)
92
+ } else {
93
+ throw new Error(`unknown option: ${arg}`)
94
+ }
95
+ }
96
+ options.root = path.resolve(options.root)
97
+ return options
98
+ }
99
+
100
+ function findMarketContract(root, explicit) {
101
+ const candidates = []
102
+ if (explicit) candidates.push(path.resolve(explicit))
103
+ if (process.env.DSH_MARKET_CONTRACT) candidates.push(path.resolve(process.env.DSH_MARKET_CONTRACT))
104
+ candidates.push(
105
+ path.join(root, 'market-contract'),
106
+ path.join(root, '..', 'deepseek-community-hub', '00-live', 'market-contract'),
107
+ path.join(root, '..', '..', 'deepseek-community-hub', '00-live', 'market-contract'),
108
+ path.join(root, '..', 'market-contract'),
109
+ path.join(root, '..', '..', '00-live', 'market-contract'),
110
+ path.join(root, '00-live', 'market-contract'),
111
+ )
112
+ for (const candidate of candidates) {
113
+ if (!fs.existsSync(path.join(candidate, 'validate.mjs'))) continue
114
+ if (!fs.existsSync(path.join(candidate, 'label.schema.rev2.json'))) continue
115
+ return candidate
116
+ }
117
+ throw new Error(
118
+ 'market-contract not found. Pass --market-contract <dir> (it must contain validate.mjs and label.schema.rev2.json). ' +
119
+ `Tried: ${candidates.join(', ')}`,
120
+ )
121
+ }
122
+
123
+ function findTarball(root, explicit, version) {
124
+ if (explicit) {
125
+ const file = path.resolve(explicit)
126
+ if (!fs.existsSync(file)) throw new Error(`--tarball not found: ${file}`)
127
+ return { file, temporary: false }
128
+ }
129
+ for (const name of [`dsh-standards-${version}.tgz`, `dsh-standards-${version}.tar.gz`]) {
130
+ const candidate = path.join(root, name)
131
+ if (fs.existsSync(candidate)) return { file: candidate, temporary: false }
132
+ }
133
+ const packDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-standards-pack-'))
134
+ const run = spawnSync('npm', ['pack', '--ignore-scripts', '--pack-destination', packDir], {
135
+ cwd: root,
136
+ encoding: 'utf8',
137
+ })
138
+ if (run.status !== 0) {
139
+ throw new Error(`npm pack failed (exit ${run.status}): ${(run.stderr || '').slice(-500)}`)
140
+ }
141
+ const produced = fs.readdirSync(packDir).filter(name => name.endsWith('.tgz'))
142
+ if (produced.length !== 1) throw new Error(`npm pack produced ${produced.length} tarballs, expected 1`)
143
+ return { file: path.join(packDir, produced[0]), temporary: true, packDir }
144
+ }
145
+
146
+ function copyDir(source, destination, label) {
147
+ if (!fs.existsSync(source)) throw new Error(`${label} not found: ${source}`)
148
+ if (!fs.statSync(source).isDirectory()) throw new Error(`${label} is not a directory: ${source}`)
149
+ fs.cpSync(source, destination, { recursive: true, force: true })
150
+ }
151
+
152
+ function listFiles(directory, prefix = '') {
153
+ const files = []
154
+ for (const name of fs.readdirSync(directory).sort()) {
155
+ const full = path.join(directory, name)
156
+ const relative = prefix ? `${prefix}/${name}` : name
157
+ if (fs.statSync(full).isDirectory()) files.push(...listFiles(full, relative))
158
+ else files.push(relative)
159
+ }
160
+ return files
161
+ }
162
+
163
+ function makeReadme({ version, day, tarballName }) {
164
+ return `# dsh-standards 离线包 ${version}
165
+
166
+ > 生成于 ${day}。**不需要联网**,不含遥测,不执行被检查插件的代码。
167
+
168
+ ## 内容
169
+
170
+ | 路径 | 用途 |
171
+ |---|---|
172
+ | \`${tarballName}\` | dsh-standards npm 包(零运行时依赖,含 \`dsh-standards\` / \`dsh-levels\`) |
173
+ | \`adoption/market/\` | 市场标签 rev1 schema、撤回说明、免责声明、渲染规则 |
174
+ | \`market-contract/\` | rev2 提案 schema + \`validate.mjs\` + 4 个可校验样张 |
175
+ | \`examples/\` | 仓库自带的示例插件 |
176
+ | \`OFFLINE-KIT.json\` | 本包文件清单、npm tarball 的 sha256 与大小 |
177
+
178
+ ## 快速开始(断网)
179
+
180
+ \`\`\`sh
181
+ tar -xzf dsh-standards-offline-${version}.tgz -C dsh-standards-offline
182
+ cd dsh-standards-offline
183
+ npm install --offline --no-audit --no-fund ./${tarballName}
184
+ node node_modules/dsh-standards/bin/dsh-levels.mjs <plugin-dir> --format json
185
+ \`\`\`
186
+
187
+ \`dsh-levels\` 默认只做静态 ENTRY 检查;FLOW 没有证据文件就是 \`not-run\`,
188
+ BOOST 没有证据卡就是 \`not-measured\`。静态 \`recognized\` **不等于** runtime。
189
+
190
+ ## 校验市场契约
191
+
192
+ \`\`\`sh
193
+ node market-contract/validate.mjs --examples # 期望 4/4 样张符合预期
194
+ \`\`\`
195
+
196
+ rev2 仍是提案(\`x-dsh-effective:false\`),现行市场记录仍按 \`adoption/market/label.schema.json\` 的 rev1 校验。
197
+ sidecar 是索引缓存,不是签发记录;本离线包不内置 \`publish/sidecar.json\`。
198
+
199
+ ## 边界
200
+
201
+ - 不是官方标准、不是认证、不是安全审计、不排序、不比较、不做担保。
202
+ - 离线包不联网、不下载、不安装被检查插件的依赖。
203
+ - 许可证见 npm tarball 内的 \`LICENSE\` / \`LICENSE-APACHE\` / \`LICENSE-CC-BY\`。
204
+ `
205
+ }
206
+
207
+ function makeTar(staging, outFile) {
208
+ const write = (flags) => spawnSync('tar', [...flags, '-czf', outFile, '-C', staging, '.'], {
209
+ encoding: 'utf8',
210
+ })
211
+ let run = write(['--sort=name', '--mtime=@0', '--owner=0', '--group=0', '--numeric-owner'])
212
+ if (run.status !== 0) {
213
+ fs.rmSync(outFile, { force: true })
214
+ run = write([])
215
+ }
216
+ if (run.status !== 0) {
217
+ throw new Error(`tar failed (exit ${run.status}): ${(run.stderr || '').slice(-500)}`)
218
+ }
219
+ }
220
+
221
+ function main() {
222
+ const options = parseArgs(process.argv.slice(2))
223
+ const rootPkg = readJson(path.join(options.root, 'package.json'), 'package.json')
224
+ const version = options.version ?? rootPkg.version
225
+ if (typeof version !== 'string' || version === '') throw new Error('cannot determine kit version')
226
+ const day = options.today ?? new Date().toISOString().slice(0, 10)
227
+ const outFile = path.resolve(options.out ?? path.join(options.root, 'publish', `dsh-standards-offline-${version}.tgz`))
228
+
229
+ const marketContract = findMarketContract(options.root, options.marketContract)
230
+ const tarball = findTarball(options.root, options.tarball, version)
231
+ const tarballName = `dsh-standards-${version}.tgz`
232
+ const staging = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-offline-kit-'))
233
+ try {
234
+ const kit = path.join(staging, 'kit')
235
+ fs.mkdirSync(kit, { recursive: true })
236
+ copyDir(path.join(options.root, 'adoption', 'market'), path.join(kit, 'adoption', 'market'), 'adoption/market')
237
+ copyDir(marketContract, path.join(kit, 'market-contract'), 'market-contract')
238
+ copyDir(path.join(options.root, 'examples'), path.join(kit, 'examples'), 'examples')
239
+ fs.copyFileSync(tarball.file, path.join(kit, tarballName))
240
+
241
+ const tarballSha256 = sha256File(tarball.file)
242
+ fs.writeFileSync(path.join(kit, 'README.md'), makeReadme({ version, day, tarballName }))
243
+
244
+ const contents = listFiles(kit).filter(file => file !== 'OFFLINE-KIT.json')
245
+ const manifest = {
246
+ schema: 'dsh-offline-kit/1',
247
+ name: 'dsh-standards-offline',
248
+ version,
249
+ generatedAt: options.today ? `${day}T00:00:00.000Z` : new Date().toISOString(),
250
+ package: {
251
+ name: rootPkg.name,
252
+ version,
253
+ tarball: tarballName,
254
+ sha256: tarballSha256,
255
+ bytes: fs.statSync(tarball.file).size,
256
+ },
257
+ marketContract: { source: 'market-contract', files: listFiles(path.join(kit, 'market-contract')).length },
258
+ contents,
259
+ }
260
+ fs.writeFileSync(path.join(kit, 'OFFLINE-KIT.json'), `${JSON.stringify(manifest, null, 2)}\n`)
261
+
262
+ fs.mkdirSync(path.dirname(outFile), { recursive: true })
263
+ makeTar(kit, outFile)
264
+ const bytes = fs.statSync(outFile).size
265
+ console.log(JSON.stringify({
266
+ out: outFile,
267
+ bytes,
268
+ sha256: sha256File(outFile),
269
+ kit: { version, package: tarballName, files: contents.length + 1 },
270
+ marketContract,
271
+ tarball: { file: tarball.file, sha256: tarballSha256, bytes: manifest.package.bytes },
272
+ }, null, 2))
273
+ } finally {
274
+ fs.rmSync(staging, { recursive: true, force: true })
275
+ if (tarball.packDir) fs.rmSync(tarball.packDir, { recursive: true, force: true })
276
+ }
277
+ }
278
+
279
+ try {
280
+ main()
281
+ } catch (error) {
282
+ console.error(`build-offline-kit: ${error.message}`)
283
+ process.exitCode = 3
284
+ }