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.
@@ -1,11 +1,11 @@
1
1
  /** Programmatic API for dsh-plugin-verify. */
2
- export { main, parseArgs, usage, VERSION } from './cli.mjs'
2
+ export { main, parseArgs, usage, VERSION, detectHostRuntime, resolveRuntimeAnchor, declarationsOf } from './cli.mjs'
3
3
  export { runManifestChecks, checkMarketEntry, DEFAULT_RUNTIME_VERSION, inferKind, resolveEntry } from './manifest.mjs'
4
4
  export { checkCapabilities, checkCredentials, checkLabels, checkImportTimeAdvisory, CAPABILITIES, LABELS } from './declarations.mjs'
5
5
  export { runRuntimeChecks } from './runtime.mjs'
6
- export { buildReport, formatText, formatSarif, writeReport } from './report.mjs'
6
+ export { buildReport, formatText, formatSarif, writeReport, summarizeBlocking, DECLARATION_CHECKS } from './report.mjs'
7
7
  export { joinAdvice, DOCS_BASE } from './remediation.mjs'
8
8
  export { satisfies, validRange, parseVersion, compareVersions } from './semver.mjs'
9
9
  export { parseInstallSpec } from './install-spec.mjs'
10
- export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, isSandboxAttested, treeSha256, validateFlowEvidence } from './flow-evidence.mjs'
10
+ export { FLOW_IDS, FLOW_EVIDENCE_SCHEMA, evidenceHash, finalizeEvidence, journeyAggregate, isSandboxAttested, treeSha256, validateFlowEvidence, recomputeSubject, bindEvidenceSubject, ciAttribution } from './flow-evidence.mjs'
11
11
  export { sanitizedEnv, scrubProcessEnv, isSecretEnvName, isAllowedEnvName, envPolicy } from './sandbox-env.mjs'
@@ -31,6 +31,46 @@ function adjustEntryCheck(item, entryClaim) {
31
31
  return { ...item, result: 'WARN', reason: 'C-2.1 version declaration missing/invalid; entry treats it as WARN (C-2.2/C-2.3 remain blocking)' }
32
32
  }
33
33
 
34
+ /** Declaration-class checks: a WARN here is hygiene, not a loadability defect. */
35
+ export const DECLARATION_CHECKS = new Set(['DSC-11', 'DSC-12', 'DSC-13'])
36
+ const DETAIL_RANK = { FAIL: 5, ERROR: 4, WARN: 3, PASS: 2, NA: 1 }
37
+
38
+ function bucketRow(item, extra = {}) {
39
+ return {
40
+ id: item.id,
41
+ clause: item.clause ?? null,
42
+ result: item.result,
43
+ coverage: item.coverage ?? null,
44
+ reason: item.reason ?? '',
45
+ ...extra,
46
+ }
47
+ }
48
+
49
+ /**
50
+ * `entryBlocking` is only sound when every prerequisite was actually evaluated.
51
+ * C-1.x always produce details; C-3.1/3.2/3.5 only produce them when an install
52
+ * spec or market index was supplied. Missing details must therefore make
53
+ * `pass` false instead of silently claiming the gate held.
54
+ */
55
+ export function summarizeBlocking(checks, blocking) {
56
+ const seen = new Map()
57
+ for (const item of checks) {
58
+ for (const detail of item.details ?? []) {
59
+ if (!blocking.includes(detail.item)) continue
60
+ const rank = DETAIL_RANK[detail.result] ?? 0
61
+ if (!seen.has(detail.item) || rank > (DETAIL_RANK[seen.get(detail.item)] ?? 0)) seen.set(detail.item, detail.result)
62
+ }
63
+ }
64
+ const checked = blocking.filter(id => seen.has(id))
65
+ // C-2.3 (workspace: range) has no detail when C-2.2 was evaluated against a
66
+ // normal range; evaluating C-2.2 rules the workspace form out, so count it.
67
+ if (seen.has('C-2.2') && blocking.includes('C-2.3') && !checked.includes('C-2.3')) checked.push('C-2.3')
68
+ checked.sort((a, b) => blocking.indexOf(a) - blocking.indexOf(b))
69
+ const failed = blocking.filter(id => seen.get(id) === 'FAIL')
70
+ const notChecked = blocking.filter(id => !checked.includes(id))
71
+ return { required: blocking.length, checked, notChecked, failed, pass: failed.length === 0 && notChecked.length === 0 }
72
+ }
73
+
34
74
  function allowedNa(check, scope, profile, kind) {
35
75
  if (check.result !== 'NA') return false
36
76
  if (check.id === 'DSC-03' && scope !== 'mvp' && profile === 'package') return true
@@ -39,7 +79,7 @@ function allowedNa(check, scope, profile, kind) {
39
79
  return false
40
80
  }
41
81
 
42
- export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true }) {
82
+ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile, kind, staticOnly = false, entryRuntime = 'require', entryClaim = 'runtime', advice = true, declarations = null }) {
43
83
  const mandatory = mandatoryFor(scope, profile, staticOnly, entryClaim)
44
84
  const adjustedChecks = scope === 'entry' ? checks.map(item => adjustEntryCheck(item, entryClaim)) : checks
45
85
  const runtimeIds = new Set(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'])
@@ -59,6 +99,17 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
59
99
  const warnings = selected.filter(item => item.result === 'WARN')
60
100
  const pass = selected.filter(item => item.result === 'PASS')
61
101
 
102
+ // Coverage counts every check the report carries (14 in a full run), not just
103
+ // the mandatory subset. A report that only counts its mandatory five hides
104
+ // the runtime checks it never ran; that is the bug this block fixes.
105
+ const evaluatedAll = effectiveChecks.filter(item => ['PASS', 'WARN', 'FAIL'].includes(item.result))
106
+ const naAll = effectiveChecks.filter(item => item.result === 'NA')
107
+ const errorAll = effectiveChecks.filter(item => item.result === 'ERROR')
108
+ const notRunAll = effectiveChecks.filter(item => item.result !== 'NA' && (item.coverage === 'not-run' || item.coverage === 'partial'))
109
+ const notRunAllIds = [...new Set([...notRunAll.map(item => item.id), ...errorAll.map(item => item.id), ...missing])]
110
+ const entryBlockingIds = scope === 'entry' ? (entryClaim === 'static' ? ENTRY_BLOCKING : ENTRY_RUNTIME_BLOCKING) : null
111
+ const entryBlocking = entryBlockingIds ? summarizeBlocking(effectiveChecks, entryBlockingIds) : null
112
+
62
113
  let status
63
114
  let exitCode
64
115
  if (failed.length > 0) { status = 'fail'; exitCode = 1 }
@@ -87,6 +138,31 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
87
138
  if (warnings.length > 0) reasonParts.push(`WARN: ${warnings.map(item => item.id).join(', ')}`)
88
139
  if (advisoryWarnings.length > 0) reasonParts.push(`advisory: ${advisoryWarnings.map(item => item.id).join(', ')}`)
89
140
 
141
+ // Findings by reader-relevant bucket. A check appears in exactly one bucket:
142
+ // FAIL -> blocking, WARN -> hygiene, NA/ERROR/not-run -> notChecked.
143
+ const blockingBucket = effectiveChecks.filter(item => item.result === 'FAIL').map(item => bucketRow(item))
144
+ const hygieneBucket = effectiveChecks.filter(item => item.result === 'WARN').map(item => bucketRow(item, {
145
+ declaration: DECLARATION_CHECKS.has(item.id),
146
+ advisory: item.advisory === true,
147
+ }))
148
+ const notCheckedBucket = effectiveChecks
149
+ .filter(item => item.result === 'NA' || item.result === 'ERROR')
150
+ .map(item => bucketRow(item))
151
+ for (const id of missing) {
152
+ notCheckedBucket.push({ id, clause: null, result: 'NOT-RUN', coverage: 'not-run', reason: 'mandatory check missing' })
153
+ }
154
+ const buckets = { blocking: blockingBucket, hygiene: hygieneBucket, notChecked: notCheckedBucket }
155
+ const declaredFields = Array.isArray(declarations?.present) ? declarations.present.length : 0
156
+ const canMarkSuspect = anchor?.suspect === true
157
+
158
+ const metrics = {
159
+ checked: evaluatedAll.length,
160
+ total: effectiveChecks.length,
161
+ declaredFields,
162
+ }
163
+ const prereq = entryBlocking ? ` (prereq ${entryBlocking.checked.length}/${entryBlocking.required})` : ''
164
+ const headline = `${scope}/${profile} · runtime ${anchor?.dsh ?? 'unknown'}${anchor?.runtimeSource ? ` ${anchor.runtimeSource}` : ''}${canMarkSuspect ? ' SUSPECT' : ''} · blocking ${buckets.blocking.length}${prereq} · hygiene ${buckets.hygiene.length} · not checked ${buckets.notChecked.length}/${effectiveChecks.length}`
165
+
90
166
  const report = {
91
167
  schema: 'dsh-conformance/1',
92
168
  standard: { id: 'DS-C', version: '1.0.0' },
@@ -95,21 +171,22 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
95
171
  g4,
96
172
  checks: effectiveChecks,
97
173
  coverage: {
174
+ total: effectiveChecks.length,
98
175
  mandatory: mandatory.length,
99
- pass: pass.length,
100
- warn: warnings.length,
176
+ checked: evaluatedAll.length,
177
+ pass: evaluatedAll.filter(item => item.result === 'PASS').length,
178
+ warn: evaluatedAll.filter(item => item.result === 'WARN').length,
101
179
  advisory: advisoryWarnings.length,
102
- fail: failed.length,
103
- na: selected.filter(item => item.result === 'NA').length,
104
- notRun: notRunIds.length,
105
- error: errored.length,
106
- entryBlocking: scope === 'entry' ? (() => {
107
- const blocking = entryClaim === 'static' ? ENTRY_BLOCKING : ENTRY_RUNTIME_BLOCKING
108
- const failedItems = effectiveChecks.flatMap(item => (item.details ?? []).filter(detail => detail.result === 'FAIL' && blocking.includes(detail.item)).map(detail => detail.item))
109
- return { required: blocking.length, failed: failedItems, pass: failedItems.length === 0 }
110
- })() : null,
180
+ fail: evaluatedAll.filter(item => item.result === 'FAIL').length,
181
+ na: naAll.length,
182
+ notRun: notRunAllIds.length,
183
+ error: errorAll.length,
184
+ entryBlocking,
111
185
  },
186
+ buckets,
187
+ metrics,
112
188
  conclusion: {
189
+ headline,
113
190
  level,
114
191
  status,
115
192
  exitCode,
@@ -132,25 +209,49 @@ export function buildReport({ subject, anchor, checks, g4 = null, scope, profile
132
209
 
133
210
  const ICON = { PASS: 'PASS', WARN: 'WARN', FAIL: 'FAIL', NA: 'NA ', ERROR: 'ERR ' }
134
211
 
212
+ function renderBucket(lines, title, rows, byId) {
213
+ lines.push(`${title} (${rows.length}):`)
214
+ if (rows.length === 0) {
215
+ lines.push(' none')
216
+ return
217
+ }
218
+ for (const row of rows) {
219
+ const tags = [row.declaration ? 'declaration' : null, row.advisory ? 'advisory' : null].filter(Boolean)
220
+ const reason = String(row.reason ?? '').replace(/\s+/g, ' ').slice(0, 150)
221
+ lines.push(` ${(ICON[row.result] ?? row.result).padEnd(4)} ${String(row.id).padEnd(7)} ${String(row.clause ?? '-').padEnd(6)} ${String(row.coverage ?? '-').padEnd(8)} ${reason}${tags.length > 0 ? ` [${tags.join(',')}]` : ''}`)
222
+ const item = byId.get(row.id)
223
+ if (!item || (row.result !== 'FAIL' && row.result !== 'WARN')) continue
224
+ for (const detail of item.details ?? []) {
225
+ if (detail.result === 'PASS') continue
226
+ lines.push(` - ${detail.item}: ${detail.result} ${String(detail.reason ?? '').replace(/\s+/g, ' ').slice(0, 140)}`)
227
+ }
228
+ }
229
+ }
230
+
135
231
  export function formatText(report) {
136
232
  const lines = []
137
- const { subject, anchor, conclusion, coverage } = report
233
+ const { subject, anchor = {}, conclusion, coverage } = report
234
+ const byId = new Map((report.checks ?? []).map(item => [item.id, item]))
138
235
  lines.push(`DS-C check — ${subject.name ?? '<unknown>'}@${subject.version ?? '?'} (${subject.kind}${subject.kindInferred ? ', inferred' : ''})`)
139
- lines.push(`anchor: repo=${anchor.repo} commit=${anchor.commit.slice(0, 12)} dsh=${anchor.dsh} cordis=${anchor.cordis}`)
236
+ lines.push(`anchor: repo=${anchor.repo ?? '?'} commit=${String(anchor.commit ?? '').slice(0, 12)} dsh=${anchor.dsh ?? '?'} cordis=${anchor.cordis ?? '?'}${anchor.runtimeSource ? ` source=${anchor.runtimeSource}` : ''}${anchor.suspect ? ' suspect=true' : ''}`)
140
237
  lines.push(`scope=${conclusion.scope} profile=${conclusion.profile} status=${conclusion.status} exit=${conclusion.exitCode}${conclusion.subset ? ` subset=${conclusion.subset}` : ''}${conclusion.staticOnly ? ' static-only' : ''}`)
141
238
  if (conclusion.level) lines.push(`level: ${conclusion.level}`)
239
+ if (conclusion.headline) lines.push(`headline: ${conclusion.headline}`)
142
240
  lines.push('')
143
- lines.push('id result clause layer reason')
144
- for (const item of report.checks) {
145
- const reason = (item.reason || '').replace(/\s+/g, ' ').slice(0, 120)
146
- lines.push(`${item.id.padEnd(7)} ${ICON[item.result] ?? item.result} ${item.clause.padEnd(6)} ${item.layer.padEnd(6)} ${reason}`)
147
- for (const detail of item.details ?? []) {
148
- if (detail.result === 'PASS') continue
149
- lines.push(` - ${detail.item}: ${detail.result} ${String(detail.reason).slice(0, 140)}`)
150
- }
151
- }
241
+ renderBucket(lines, 'Blocking (FAIL)', report.buckets?.blocking ?? [], byId)
152
242
  lines.push('')
153
- lines.push(`coverage: mandatory=${coverage.mandatory} pass=${coverage.pass} warn=${coverage.warn} fail=${coverage.fail} na=${coverage.na} notRun=${coverage.notRun}`)
243
+ renderBucket(lines, 'Hygiene (WARN)', report.buckets?.hygiene ?? [], byId)
244
+ lines.push('')
245
+ renderBucket(lines, 'Not checked (NA / not-run / ERROR)', report.buckets?.notChecked ?? [], byId)
246
+ lines.push('')
247
+ const passed = (report.checks ?? []).filter(item => item.result === 'PASS')
248
+ lines.push(`passed (${passed.length}): ${passed.map(item => item.id).join(', ') || 'none'}`)
249
+ lines.push(`coverage: total=${coverage.total ?? report.checks.length} checked=${coverage.checked ?? coverage.pass + coverage.warn + coverage.fail} pass=${coverage.pass} warn=${coverage.warn} fail=${coverage.fail} na=${coverage.na} notRun=${coverage.notRun} error=${coverage.error} advisory=${coverage.advisory}`)
250
+ if (coverage.entryBlocking) {
251
+ const blocking = coverage.entryBlocking
252
+ lines.push(`entryBlocking: checked=${blocking.checked?.length ?? '?'}/${blocking.required} failed=[${(blocking.failed ?? []).join(', ')}] notChecked=[${(blocking.notChecked ?? []).join(', ')}] pass=${blocking.pass}`)
253
+ }
254
+ lines.push(`metrics: checked=${report.metrics?.checked ?? '?'}/${report.metrics?.total ?? '?'} declaredFields=${report.metrics?.declaredFields ?? 0}/5`)
154
255
  if (conclusion.reason) lines.push(`reason: ${conclusion.reason}`)
155
256
  if (conclusion.status === 'incomplete') lines.push('NOT A DS-C PASS: at least one mandatory check did not run (see notRun).')
156
257
  if (Array.isArray(report.advice) && report.advice.length > 0) {
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 1.3.0 — 2026-11-06
2
+
3
+ ### Added
4
+
5
+ - `check [dir]` 子命令:默认 cwd、Markdown 报告(一句结论、≤3 条优先修复、分桶全表、advice、免责声明),`--json` 与 `--html`(单文件、内联 CSS、零依赖)。
6
+ - `spec/schemas/advice.schema.json`(`dsh-advice/1`),advice 逐条过 schema。
7
+ - 证据主体绑定:`--flow-evidence --dir` 校验 name/version/treeSha256/tarball 哈希,不一致即 `invalid`。
8
+ - CI 归属:collector 写 `producer.runUrl/repository/commit`,audit 标 `attestation: self-declared|ci`(不升级 claimLevel)。
9
+ - sidecar 生成(`publish/sidecar.json`,`dsh-market-sidecar/1`)、每日过期/撤回 workflow、离线包(~580 KB)。
10
+ - `SUPPORTED.md`(host × node × checker 矩阵 + 证据锚)与 `CHANGELOG-RULES.md`(规则级变更与影响面)。
11
+
12
+ ### Fixed
13
+
14
+ - coverage 按全部 14 项统计,`na/notRun/error` 不再为 0;未查 C-3.1/3.2/3.5 时 `entryBlocking.pass=false`,并输出 `checked/notChecked`。
15
+ - 默认 runtime 锚改为宿主实际版本(`anchor.runtimeSource=host`),与 `--runtime-version` 不一致时 `anchor.suspect=true`;不再固定 `0.2.2-alpha.2`。
16
+ - 报告分三桶:`blocking`/`hygiene`(DSC-11/12/13 声明类)/`notChecked`,text 分组渲染,JSON 加 `buckets/metrics/headline`。
17
+
18
+ ### Verified
19
+
20
+ - `npm test` 193/193(含 root `test/`)。
21
+
1
22
  ## 1.2.2 — 2026-11-06
2
23
 
3
24
  - FLOW 沙箱:`flow collect --sandbox` 在 `unshare -Urn` + `prlimit` 下运行,记录 `sandbox{}`;env 白名单硬断言 token 不在子进程;进程组超时击杀;`producer.kind=sandbox` 仅当隔离项全部生效,否则 `sandbox-degraded` 且 audit 永不 runtime。测试 152 → 159。
package/README.en.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → dsh-standards (DSC-01…13 static) → L1–L3 runtime → report (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ The full 51-document index lives in the Chinese [README.md](README.md).
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## Three-step setup
80
85
 
81
86
  **1. Install** (either way; Node `>=24.9.0`):
package/README.es.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="Flujo de dsh-standards: directorio del plugin → dsh-standards (DSC-01…13 estáticas) → runtime L1–L3 → informe (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ El índice completo de 51 documentos está en el [README.md](README.md) en chino
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## Instalación en tres pasos
80
85
 
81
86
  **1. Instalar** (dos opciones; Node `>=24.9.0`):
package/README.hi.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards प्रवाह: प्लगइन डायरेक्टरी → dsh-standards (DSC-01…13 स्थिर) → L1–L3 रनटाइम → रिपोर्ट (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ npm पर पुराना आरक्षण `dsh-standards@0.0.3` केव
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## तीन चरणों में शुरुआत
80
85
 
81
86
  **1. इंस्टॉल** (दो में से कोई भी; Node `>=24.9.0`):
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="dsh-standards 流程:插件目录 → dsh-standards(DSC-01…13 静态)→ L1–L3 运行时 → 报告(text/json/sarif、exit 0|1|2|3)→ dsh-levels(ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-zh-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-zh-light.webp" width="100%" alt="dsh-standards:DSC-01…13 静态检查 + L1–L3 运行时,报告可复现">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -94,6 +94,11 @@ npm 上的旧占位 `dsh-standards@0.0.3` 只是名字预留,这条 1.x 线取
94
94
 
95
95
  </details>
96
96
 
97
+ <picture>
98
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
99
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards 流程:插件目录 → DSC-01…13 静态 → L1–L3 运行时 → 报告 → dsh-levels">
100
+ </picture>
101
+
97
102
  ## 三步上手
98
103
 
99
104
  **1. 安装**(二选一,Node 需 `>=24.9.0`):
package/README.pt.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <div align="center">
2
2
 
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-dark.svg">
5
- <img src="docs/assets/dsh-standards-flow-light.svg" width="100%" alt="Fluxo do dsh-standards: diretório do plugin → dsh-standards (DSC-01…13 estáticas) → runtime L1–L3 → relatório (text/json/sarif, exit 0|1|2|3) → dsh-levels (ENTRY / FLOW / BOOST)">
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-hero-en-dark.webp">
5
+ <img src="docs/assets/dsh-standards-hero-en-light.webp" width="100%" alt="dsh-standards: DSC-01…13 static checks + L1–L3 runtime, reproducible reports">
6
6
  </picture>
7
7
 
8
8
  # dsh-standards
@@ -76,6 +76,11 @@ O índice completo de 51 documentos está no [README.md](README.md) em chinês.
76
76
 
77
77
  </details>
78
78
 
79
+ <picture>
80
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/dsh-standards-flow-v4-dark.svg">
81
+ <img src="docs/assets/dsh-standards-flow-v4-light.svg" width="100%" alt="dsh-standards pipeline: plugin dir → DSC-01…13 → L1–L3 runtime → report → dsh-levels">
82
+ </picture>
83
+
79
84
  ## Instalação em três passos
80
85
 
81
86
  **1. Instalar** (duas opções; Node `>=24.9.0`):
@@ -2,6 +2,9 @@
2
2
  // Public umbrella entry point for the DSH standards checker.
3
3
  // The implementation lives in 40-checker/; this shim keeps the package-level
4
4
  // command stable while the checker remains independently runnable from source.
5
+ //
6
+ // dsh-standards check [dir] author-facing Markdown / --json / --html report
7
+ // dsh-standards <dir> raw checker report (backwards compatible)
5
8
  import { readFileSync } from 'node:fs'
6
9
  import { fileURLToPath } from 'node:url'
7
10
  import { dirname, join } from 'node:path'
@@ -15,5 +18,13 @@ if (argv.includes('--version') || argv.includes('-v')) {
15
18
  process.exit(0)
16
19
  }
17
20
 
18
- process.env.DSH_CHECKER_BRAND = `dsh-standards ${pkg.version} (checker 0.1.0)`
19
- await import('../40-checker/bin/dsh-plugin-verify.mjs')
21
+ if (argv[0] === 'check') {
22
+ const { runCheck } = await import('../40-checker/src/author-cli.mjs')
23
+ const code = await runCheck(argv.slice(1))
24
+ process.exitCode = code
25
+ // The checker may have imported a plugin; do not let stray handles hang us.
26
+ setTimeout(() => process.exit(code), 250)
27
+ } else {
28
+ process.env.DSH_CHECKER_BRAND = `dsh-standards ${pkg.version} (checker 0.1.0)`
29
+ await import('../40-checker/bin/dsh-plugin-verify.mjs')
30
+ }
@@ -0,0 +1,87 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1100" height="660" viewBox="0 0 1100 660" role="img" aria-label="dsh-standards pipeline">
2
+ <defs>
3
+ <linearGradient id="lineGrad" x1="0" y1="0" x2="1" y2="0">
4
+ <stop offset="0" stop-color="#8496f5" stop-opacity=".15"/>
5
+ <stop offset=".5" stop-color="#8496f5" stop-opacity=".9"/>
6
+ <stop offset="1" stop-color="#8496f5" stop-opacity=".15"/>
7
+ </linearGradient>
8
+ <filter id="soft" x="-40%" y="-40%" width="180%" height="200%">
9
+ <feDropShadow dx="0" dy="9" stdDeviation="12" flood-color="#1b1735" flood-opacity="0.55"/>
10
+ </filter>
11
+ <pattern id="dots" width="28" height="28" patternUnits="userSpaceOnUse">
12
+ <circle cx="1.3" cy="1.3" r="1.3" fill="#182031"/>
13
+ </pattern>
14
+ </defs>
15
+ <rect width="1100" height="660" fill="#0d1117"/>
16
+ <rect width="1100" height="660" fill="url(#dots)" opacity=".5"/>
17
+ <text x="50" y="62" fill="#f0f3f9" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="32" font-weight="700">dsh-standards</text>
18
+ <text x="50" y="90" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="16">static checks, runtime measurement, one reproducible report</text>
19
+ <rect x="50" y="108" width="1000" height="1.6" rx="1" fill="#2a3142"/>
20
+ <g filter="url(#soft)">
21
+ <rect x="50" y="190" width="176" height="118" rx="17" fill="#161b26" stroke="#2a3142" stroke-width="1.3"/>
22
+ </g>
23
+ <circle cx="74" cy="214" r="12" fill="#242d42"/>
24
+ <text x="74" y="219" text-anchor="middle" fill="#8496f5" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">01</text>
25
+ <text x="138.0" y="256" text-anchor="middle" fill="#f0f3f9" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">plugin dir</text>
26
+ <text x="138.0" y="282" text-anchor="middle" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">the repository</text>
27
+ <path d="M231 249.0 C241.0 249.0, 241.0 249.0, 251 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
28
+ <path d="M241 244.0 l8 5 -8 5" fill="none" stroke="#8496f5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
29
+ <g filter="url(#soft)">
30
+ <rect x="256" y="190" width="176" height="118" rx="17" fill="#161b26" stroke="#8496f5" stroke-width="2.6"/>
31
+ </g>
32
+ <circle cx="280" cy="214" r="12" fill="#242d42"/>
33
+ <text x="280" y="219" text-anchor="middle" fill="#8496f5" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">02</text>
34
+ <text x="344.0" y="256" text-anchor="middle" fill="#f0f3f9" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">DSC-01…13</text>
35
+ <text x="344.0" y="282" text-anchor="middle" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">static checks</text>
36
+ <path d="M437 249.0 C447.0 249.0, 447.0 249.0, 457 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
37
+ <path d="M447 244.0 l8 5 -8 5" fill="none" stroke="#8496f5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
38
+ <g filter="url(#soft)">
39
+ <rect x="462" y="190" width="176" height="118" rx="17" fill="#161b26" stroke="#2a3142" stroke-width="1.3"/>
40
+ </g>
41
+ <circle cx="486" cy="214" r="12" fill="#242d42"/>
42
+ <text x="486" y="219" text-anchor="middle" fill="#8496f5" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">03</text>
43
+ <text x="550.0" y="256" text-anchor="middle" fill="#f0f3f9" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">L1–L3</text>
44
+ <text x="550.0" y="282" text-anchor="middle" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">runtime measurement</text>
45
+ <path d="M643 249.0 C653.0 249.0, 653.0 249.0, 663 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
46
+ <path d="M653 244.0 l8 5 -8 5" fill="none" stroke="#8496f5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
47
+ <g filter="url(#soft)">
48
+ <rect x="668" y="190" width="176" height="118" rx="17" fill="#161b26" stroke="#2a3142" stroke-width="1.3"/>
49
+ </g>
50
+ <circle cx="692" cy="214" r="12" fill="#242d42"/>
51
+ <text x="692" y="219" text-anchor="middle" fill="#8496f5" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">04</text>
52
+ <text x="756.0" y="256" text-anchor="middle" fill="#f0f3f9" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">report</text>
53
+ <text x="756.0" y="282" text-anchor="middle" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">text / json / sarif</text>
54
+ <path d="M849 249.0 C859.0 249.0, 859.0 249.0, 869 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
55
+ <path d="M859 244.0 l8 5 -8 5" fill="none" stroke="#8496f5" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
56
+ <g filter="url(#soft)">
57
+ <rect x="874" y="190" width="176" height="118" rx="17" fill="#161b26" stroke="#3fb950" stroke-width="2.6"/>
58
+ </g>
59
+ <circle cx="898" cy="214" r="12" fill="#242d42"/>
60
+ <text x="898" y="219" text-anchor="middle" fill="#8496f5" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">05</text>
61
+ <text x="962.0" y="256" text-anchor="middle" fill="#f0f3f9" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">dsh-levels</text>
62
+ <text x="962.0" y="282" text-anchor="middle" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">ENTRY/FLOW/BOOST</text>
63
+ <g filter="url(#soft)">
64
+ <rect x="462" y="378" width="118" height="58" rx="14" fill="#161b26" stroke="#3fb950" stroke-width="1.2"/>
65
+ </g>
66
+ <text x="486" y="404" fill="#3fb950" font-family="ui-monospace, monospace" font-size="16" font-weight="700">0</text>
67
+ <text x="486" y="423" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit pass</text>
68
+ <g filter="url(#soft)">
69
+ <rect x="592" y="378" width="118" height="58" rx="14" fill="#161b26" stroke="#d29922" stroke-width="1.2"/>
70
+ </g>
71
+ <text x="616" y="404" fill="#d29922" font-family="ui-monospace, monospace" font-size="16" font-weight="700">1</text>
72
+ <text x="616" y="423" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit fail</text>
73
+ <g filter="url(#soft)">
74
+ <rect x="722" y="378" width="118" height="58" rx="14" fill="#161b26" stroke="#d29922" stroke-width="1.2"/>
75
+ </g>
76
+ <text x="746" y="404" fill="#d29922" font-family="ui-monospace, monospace" font-size="16" font-weight="700">2</text>
77
+ <text x="746" y="423" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit config</text>
78
+ <g filter="url(#soft)">
79
+ <rect x="852" y="378" width="118" height="58" rx="14" fill="#161b26" stroke="#d29922" stroke-width="1.2"/>
80
+ </g>
81
+ <text x="876" y="404" fill="#d29922" font-family="ui-monospace, monospace" font-size="16" font-weight="700">3</text>
82
+ <text x="876" y="423" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit runtime</text>
83
+ <rect x="50" y="476" width="1000" height="58" rx="15" fill="#161b26" stroke="#2a3142"/>
84
+ <text x="74" y="502" fill="#8496f5" font-family="ui-monospace, monospace" font-size="13" font-weight="700">static ≠ runtime</text>
85
+ <text x="74" y="522" fill="#a8b1c4" font-family="ui-sans-serif, system-ui, sans-serif" font-size="13">a static pass never substitutes for a runtime run; not-run is reported, never upgraded to PASS</text>
86
+ <text x="50" y="596" fill="#a8b1c4" font-family="ui-monospace, monospace" font-size="13">Apache-2.0 + CC-BY-4.0 · PerryLink DSH plugin family</text>
87
+ </svg>
@@ -0,0 +1,87 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1100" height="660" viewBox="0 0 1100 660" role="img" aria-label="dsh-standards pipeline">
2
+ <defs>
3
+ <linearGradient id="lineGrad" x1="0" y1="0" x2="1" y2="0">
4
+ <stop offset="0" stop-color="#4B67B4" stop-opacity=".15"/>
5
+ <stop offset=".5" stop-color="#4B67B4" stop-opacity=".9"/>
6
+ <stop offset="1" stop-color="#4B67B4" stop-opacity=".15"/>
7
+ </linearGradient>
8
+ <filter id="soft" x="-40%" y="-40%" width="180%" height="200%">
9
+ <feDropShadow dx="0" dy="9" stdDeviation="12" flood-color="#1b1735" flood-opacity="0.10"/>
10
+ </filter>
11
+ <pattern id="dots" width="28" height="28" patternUnits="userSpaceOnUse">
12
+ <circle cx="1.3" cy="1.3" r="1.3" fill="#f2f0f8"/>
13
+ </pattern>
14
+ </defs>
15
+ <rect width="1100" height="660" fill="#ffffff"/>
16
+ <rect width="1100" height="660" fill="url(#dots)" opacity=".5"/>
17
+ <text x="50" y="62" fill="#141419" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="32" font-weight="700">dsh-standards</text>
18
+ <text x="50" y="90" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="16">static checks, runtime measurement, one reproducible report</text>
19
+ <rect x="50" y="108" width="1000" height="1.6" rx="1" fill="#e6e3f0"/>
20
+ <g filter="url(#soft)">
21
+ <rect x="50" y="190" width="176" height="118" rx="17" fill="#fcfcfe" stroke="#e6e3f0" stroke-width="1.3"/>
22
+ </g>
23
+ <circle cx="74" cy="214" r="12" fill="#ece7f9"/>
24
+ <text x="74" y="219" text-anchor="middle" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">01</text>
25
+ <text x="138.0" y="256" text-anchor="middle" fill="#141419" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">plugin dir</text>
26
+ <text x="138.0" y="282" text-anchor="middle" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">the repository</text>
27
+ <path d="M231 249.0 C241.0 249.0, 241.0 249.0, 251 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
28
+ <path d="M241 244.0 l8 5 -8 5" fill="none" stroke="#4B67B4" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
29
+ <g filter="url(#soft)">
30
+ <rect x="256" y="190" width="176" height="118" rx="17" fill="#fcfcfe" stroke="#4B67B4" stroke-width="2.6"/>
31
+ </g>
32
+ <circle cx="280" cy="214" r="12" fill="#ece7f9"/>
33
+ <text x="280" y="219" text-anchor="middle" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">02</text>
34
+ <text x="344.0" y="256" text-anchor="middle" fill="#141419" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">DSC-01…13</text>
35
+ <text x="344.0" y="282" text-anchor="middle" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">static checks</text>
36
+ <path d="M437 249.0 C447.0 249.0, 447.0 249.0, 457 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
37
+ <path d="M447 244.0 l8 5 -8 5" fill="none" stroke="#4B67B4" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
38
+ <g filter="url(#soft)">
39
+ <rect x="462" y="190" width="176" height="118" rx="17" fill="#fcfcfe" stroke="#e6e3f0" stroke-width="1.3"/>
40
+ </g>
41
+ <circle cx="486" cy="214" r="12" fill="#ece7f9"/>
42
+ <text x="486" y="219" text-anchor="middle" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">03</text>
43
+ <text x="550.0" y="256" text-anchor="middle" fill="#141419" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">L1–L3</text>
44
+ <text x="550.0" y="282" text-anchor="middle" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">runtime measurement</text>
45
+ <path d="M643 249.0 C653.0 249.0, 653.0 249.0, 663 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
46
+ <path d="M653 244.0 l8 5 -8 5" fill="none" stroke="#4B67B4" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
47
+ <g filter="url(#soft)">
48
+ <rect x="668" y="190" width="176" height="118" rx="17" fill="#fcfcfe" stroke="#e6e3f0" stroke-width="1.3"/>
49
+ </g>
50
+ <circle cx="692" cy="214" r="12" fill="#ece7f9"/>
51
+ <text x="692" y="219" text-anchor="middle" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">04</text>
52
+ <text x="756.0" y="256" text-anchor="middle" fill="#141419" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">report</text>
53
+ <text x="756.0" y="282" text-anchor="middle" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">text / json / sarif</text>
54
+ <path d="M849 249.0 C859.0 249.0, 859.0 249.0, 869 249.0" stroke="url(#lineGrad)" stroke-width="2.2" fill="none"/>
55
+ <path d="M859 244.0 l8 5 -8 5" fill="none" stroke="#4B67B4" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
56
+ <g filter="url(#soft)">
57
+ <rect x="874" y="190" width="176" height="118" rx="17" fill="#fcfcfe" stroke="#16a34a" stroke-width="2.6"/>
58
+ </g>
59
+ <circle cx="898" cy="214" r="12" fill="#ece7f9"/>
60
+ <text x="898" y="219" text-anchor="middle" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="11.5" font-weight="700">05</text>
61
+ <text x="962.0" y="256" text-anchor="middle" fill="#141419" font-family="ui-monospace, monospace" font-size="15.5" font-weight="700">dsh-levels</text>
62
+ <text x="962.0" y="282" text-anchor="middle" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">ENTRY/FLOW/BOOST</text>
63
+ <g filter="url(#soft)">
64
+ <rect x="462" y="378" width="118" height="58" rx="14" fill="#fcfcfe" stroke="#16a34a" stroke-width="1.2"/>
65
+ </g>
66
+ <text x="486" y="404" fill="#16a34a" font-family="ui-monospace, monospace" font-size="16" font-weight="700">0</text>
67
+ <text x="486" y="423" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit pass</text>
68
+ <g filter="url(#soft)">
69
+ <rect x="592" y="378" width="118" height="58" rx="14" fill="#fcfcfe" stroke="#bc4c00" stroke-width="1.2"/>
70
+ </g>
71
+ <text x="616" y="404" fill="#bc4c00" font-family="ui-monospace, monospace" font-size="16" font-weight="700">1</text>
72
+ <text x="616" y="423" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit fail</text>
73
+ <g filter="url(#soft)">
74
+ <rect x="722" y="378" width="118" height="58" rx="14" fill="#fcfcfe" stroke="#bc4c00" stroke-width="1.2"/>
75
+ </g>
76
+ <text x="746" y="404" fill="#bc4c00" font-family="ui-monospace, monospace" font-size="16" font-weight="700">2</text>
77
+ <text x="746" y="423" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit config</text>
78
+ <g filter="url(#soft)">
79
+ <rect x="852" y="378" width="118" height="58" rx="14" fill="#fcfcfe" stroke="#bc4c00" stroke-width="1.2"/>
80
+ </g>
81
+ <text x="876" y="404" fill="#bc4c00" font-family="ui-monospace, monospace" font-size="16" font-weight="700">3</text>
82
+ <text x="876" y="423" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12.5">exit runtime</text>
83
+ <rect x="50" y="476" width="1000" height="58" rx="15" fill="#fcfcfe" stroke="#e6e3f0"/>
84
+ <text x="74" y="502" fill="#4B67B4" font-family="ui-monospace, monospace" font-size="13" font-weight="700">static ≠ runtime</text>
85
+ <text x="74" y="522" fill="#4d5261" font-family="ui-sans-serif, system-ui, sans-serif" font-size="13">a static pass never substitutes for a runtime run; not-run is reported, never upgraded to PASS</text>
86
+ <text x="50" y="596" fill="#4d5261" font-family="ui-monospace, monospace" font-size="13">Apache-2.0 + CC-BY-4.0 · PerryLink DSH plugin family</text>
87
+ </svg>