dsh-standards 0.0.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/40-checker/README.md +251 -0
  2. package/40-checker/bin/dsh-levels.mjs +15 -0
  3. package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
  4. package/40-checker/fixtures/bad/index.mjs +12 -0
  5. package/40-checker/fixtures/bad/package.json +15 -0
  6. package/40-checker/fixtures/bad-manifest/index.js +1 -0
  7. package/40-checker/fixtures/bad-manifest/package.json +5 -0
  8. package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
  9. package/40-checker/fixtures/bundle/package.json +15 -0
  10. package/40-checker/fixtures/bundle/server.mjs +7 -0
  11. package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
  12. package/40-checker/fixtures/bundle-v0/package.json +21 -0
  13. package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
  14. package/40-checker/fixtures/client-missing/client.js +1 -0
  15. package/40-checker/fixtures/client-missing/index.js +1 -0
  16. package/40-checker/fixtures/client-missing/package.json +6 -0
  17. package/40-checker/fixtures/flow-config/index.js +9 -0
  18. package/40-checker/fixtures/flow-config/package.json +10 -0
  19. package/40-checker/fixtures/fs-write/index.mjs +7 -0
  20. package/40-checker/fixtures/fs-write/package.json +10 -0
  21. package/40-checker/fixtures/good/index.mjs +10 -0
  22. package/40-checker/fixtures/good/package.json +19 -0
  23. package/40-checker/fixtures/hang/index.mjs +5 -0
  24. package/40-checker/fixtures/hang/package.json +10 -0
  25. package/40-checker/fixtures/known-answer/expected.json +18 -0
  26. package/40-checker/fixtures/market-index-real.json +18 -0
  27. package/40-checker/fixtures/market-index.json +18 -0
  28. package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
  29. package/40-checker/fixtures/negative/no-entry/package.json +1 -0
  30. package/40-checker/fixtures/negative/no-patch/package.json +1 -0
  31. package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
  32. package/40-checker/fixtures/negative/root-missing/package.json +1 -0
  33. package/40-checker/fixtures/negative/workspace/package.json +1 -0
  34. package/40-checker/package.json +22 -0
  35. package/40-checker/samples/README.md +8 -0
  36. package/40-checker/samples/entry18.json +139 -0
  37. package/40-checker/samples/frame.json +485 -0
  38. package/40-checker/scripts/apply-overlay.mjs +59 -0
  39. package/40-checker/scripts/attestation-check.mjs +151 -0
  40. package/40-checker/scripts/bundle-smoke.mjs +126 -0
  41. package/40-checker/scripts/claim-check.mjs +145 -0
  42. package/40-checker/scripts/clause-map.mjs +94 -0
  43. package/40-checker/scripts/collect-fixtures.mjs +44 -0
  44. package/40-checker/scripts/dashboard.mjs +85 -0
  45. package/40-checker/scripts/dco-check.mjs +147 -0
  46. package/40-checker/scripts/dependabot-check.mjs +40 -0
  47. package/40-checker/scripts/doc-commands-check.mjs +92 -0
  48. package/40-checker/scripts/entry-scan.mjs +190 -0
  49. package/40-checker/scripts/entry-validate.mjs +291 -0
  50. package/40-checker/scripts/entry18-attribution.mjs +66 -0
  51. package/40-checker/scripts/entry18-ci.mjs +685 -0
  52. package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
  53. package/40-checker/scripts/entry18-web.mjs +307 -0
  54. package/40-checker/scripts/evidence-chain.mjs +100 -0
  55. package/40-checker/scripts/flow-index.mjs +82 -0
  56. package/40-checker/scripts/flow-sample-ci.mjs +529 -0
  57. package/40-checker/scripts/governance-check.mjs +210 -0
  58. package/40-checker/scripts/host-farm.mjs +165 -0
  59. package/40-checker/scripts/journey-config-observe.mjs +109 -0
  60. package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
  61. package/40-checker/scripts/journey-runtime.mjs +178 -0
  62. package/40-checker/scripts/journey.mjs +76 -0
  63. package/40-checker/scripts/label-drift.mjs +74 -0
  64. package/40-checker/scripts/label-sync.mjs +87 -0
  65. package/40-checker/scripts/label-verify.mjs +90 -0
  66. package/40-checker/scripts/landlock-exec.c +101 -0
  67. package/40-checker/scripts/level-audit.mjs +336 -0
  68. package/40-checker/scripts/m1-sidecar.mjs +126 -0
  69. package/40-checker/scripts/m2-runner.mjs +213 -0
  70. package/40-checker/scripts/round-robin.mjs +88 -0
  71. package/40-checker/scripts/sample-frame.mjs +72 -0
  72. package/40-checker/scripts/sample20-ci.mjs +25 -0
  73. package/40-checker/scripts/sample60-ci.mjs +25 -0
  74. package/40-checker/scripts/stale-check.mjs +59 -0
  75. package/40-checker/scripts/surveillance.mjs +88 -0
  76. package/40-checker/scripts/web-probe.mjs +504 -0
  77. package/40-checker/src/cli.mjs +300 -0
  78. package/40-checker/src/client-shape.mjs +37 -0
  79. package/40-checker/src/declarations.mjs +242 -0
  80. package/40-checker/src/index.mjs +8 -0
  81. package/40-checker/src/install-spec.mjs +67 -0
  82. package/40-checker/src/label.mjs +130 -0
  83. package/40-checker/src/manifest.mjs +381 -0
  84. package/40-checker/src/permissions.mjs +60 -0
  85. package/40-checker/src/report.mjs +204 -0
  86. package/40-checker/src/result.mjs +60 -0
  87. package/40-checker/src/runtime.mjs +299 -0
  88. package/40-checker/src/semver.mjs +194 -0
  89. package/40-checker/src/util.mjs +148 -0
  90. package/40-checker/test/attestation.test.mjs +59 -0
  91. package/40-checker/test/checks.test.mjs +172 -0
  92. package/40-checker/test/claim.test.mjs +28 -0
  93. package/40-checker/test/client-shape.test.mjs +35 -0
  94. package/40-checker/test/dco.test.mjs +51 -0
  95. package/40-checker/test/entry-validate.test.mjs +172 -0
  96. package/40-checker/test/governance.test.mjs +76 -0
  97. package/40-checker/test/label.test.mjs +97 -0
  98. package/40-checker/test/level-audit.test.mjs +99 -0
  99. package/40-checker/test/levels.test.mjs +31 -0
  100. package/40-checker/test/ops.test.mjs +117 -0
  101. package/40-checker/test/permissions.test.mjs +40 -0
  102. package/40-checker/test/semver.test.mjs +32 -0
  103. package/40-checker/test/verify.test.mjs +57 -0
  104. package/CHANGELOG.md +24 -0
  105. package/LICENSE +28 -0
  106. package/LICENSE-APACHE +190 -0
  107. package/LICENSE-CC-BY +23 -0
  108. package/NOTICE +14 -0
  109. package/NOTICE.md +11 -0
  110. package/README.md +146 -3
  111. package/bin/dsh-levels.mjs +4 -0
  112. package/bin/dsh-standards.mjs +19 -0
  113. package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
  114. package/docs/DEVELOPER-JOURNEY.md +40 -0
  115. package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
  116. package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
  117. package/docs/NAMING-FALLBACK.md +11 -0
  118. package/docs/fork-policy.md +12 -0
  119. package/docs/full-test-plan-2026-11-02.md +207 -0
  120. package/docs/incident.md +37 -0
  121. package/docs/onboarding.md +27 -0
  122. package/docs/privacy.md +12 -0
  123. package/docs/release.md +16 -0
  124. package/docs/scope-security.md +62 -0
  125. package/docs/status-taxonomy.md +54 -0
  126. package/docs/sunset.md +44 -0
  127. package/docs/translating.md +12 -0
  128. package/package.json +58 -12
  129. package/spec/README.md +27 -0
  130. package/spec/boost/README.md +8 -0
  131. package/spec/clauses.json +253 -0
  132. package/spec/entry/README.md +9 -0
  133. package/spec/flow/README.md +8 -0
  134. package/spec/levels.json +371 -0
  135. package/spec/levels.schema.json +45 -0
  136. package/spec/reference-suite.json +35 -0
  137. package/spec/schemas/attestation.schema.json +284 -0
  138. package/spec/schemas/evidence.schema.json +331 -0
  139. package/spec/schemas/report.schema.json +64 -0
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * entry18-realweb-summary.mjs — aggregate the real-web-profile ENTRY rows.
4
+ *
5
+ * The summary is a manifest: each row's SHA-256 is recorded so the evidence
6
+ * chain can hash one file without losing the per-row identity. Missing rows are
7
+ * `missing`, never a pass.
8
+ */
9
+ import crypto from 'node:crypto'
10
+ import fs from 'node:fs'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
15
+ const STANDARDS = path.resolve(HERE, '..', '..')
16
+ const dir = process.argv[2] ? path.resolve(process.argv[2]) : path.join(STANDARDS, '40-checker', 'results', 'entry', '18', 'realweb')
17
+ if (!fs.existsSync(dir)) {
18
+ console.error(`entry18-realweb-summary: not found: ${dir}`)
19
+ process.exit(2)
20
+ }
21
+ const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')
22
+ const rows = fs.readdirSync(dir)
23
+ .filter((name) => name.endsWith('.json') && name !== 'summary.json')
24
+ .sort()
25
+ .map((name) => {
26
+ const file = path.join(dir, name)
27
+ const row = JSON.parse(fs.readFileSync(file, 'utf8'))
28
+ const baseFile = path.join(STANDARDS, '40-checker', 'results', 'entry', '18', 'fullbase', `${row.id}.json`)
29
+ const base = fs.existsSync(baseFile) ? JSON.parse(fs.readFileSync(baseFile, 'utf8')) : null
30
+ const localCompat = row.compatibility && row.compatibility !== 'error' ? row.compatibility : null
31
+ const compatibility = localCompat ?? base?.compatibility ?? null
32
+ const compatibilitySource = localCompat ? 'realweb' : base ? 'fullbase' : 'unknown'
33
+ const verdict = row.status === 'verified' && compatibility === 'reject' ? 'verified-gate-reject' : row.status
34
+ return {
35
+ id: row.id,
36
+ name: row.name,
37
+ version: row.version,
38
+ spec: row.spec,
39
+ status: row.status,
40
+ verdict,
41
+ compatibility,
42
+ compatibilitySource,
43
+ compatibilityError: row.compatibilityError ?? null,
44
+ targetRows: row.targetRows ?? 0,
45
+ active: row.active ?? 0,
46
+ baseTargetRows: base?.g3Rows ?? null,
47
+ baseActive: base?.g3Active ?? null,
48
+ targetSetMatches: base?.g3Rows === (row.targetRows ?? 0),
49
+ entries: row.entries ?? 0,
50
+ error: row.error ?? null,
51
+ record: path.relative(STANDARDS, file).split(path.sep).join('/'),
52
+ recordSha256: sha256(file),
53
+ baseRecord: base ? path.relative(STANDARDS, baseFile).split(path.sep).join('/') : null,
54
+ baseRecordSha256: base ? sha256(baseFile) : null,
55
+ }
56
+ })
57
+ const counts = {}
58
+ for (const row of rows) counts[row.verdict] = (counts[row.verdict] ?? 0) + 1
59
+ const summary = {
60
+ schema: 'dsh-entry18-realweb-summary/1',
61
+ generatedAt: new Date().toISOString(),
62
+ profile: 'web',
63
+ method: 'real dsh web profile via runProfile; manual profile symlink; redacted launch token',
64
+ rowCount: rows.length,
65
+ status: counts,
66
+ rows,
67
+ notClaimed: ['18/18 clean', 'UI correctness', 'security', 'production readiness', 'official endorsement'],
68
+ note: 'A missing row is missing, not a pass. Compatibility is G2; a reject stays verified-gate-reject.',
69
+ }
70
+ fs.writeFileSync(path.join(dir, 'summary.json'), `${JSON.stringify(summary, null, 2)}\n`)
71
+ console.log(JSON.stringify({ rowCount: rows.length, status: counts, out: path.join(dir, 'summary.json') }, null, 1))
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * entry18-web.mjs — boot one ENTRY sample in the REAL DSH web profile and
4
+ * inspect the target rows of the composed loader tree.
5
+ *
6
+ * This is the "real web runner" for the 18/18 rich-profile experiment. It differs
7
+ * from `bundle-smoke --web`: the web app's launcher provides `webStartup`,
8
+ * `cmdlineArgs`, `webApp`, `fileUploads`, and `connection`, so the target rows
9
+ * can actually activate. The install is a manual profile symlink (not
10
+ * `dsh plugin add`) so the experiment does not depend on pnpm; that deviation is
11
+ * recorded in the row.
12
+ *
13
+ * Evidence is a row JSON only. The launch token printed by the web app is
14
+ * redacted from stdout/stderr and never written to the row.
15
+ */
16
+ import fs from 'node:fs'
17
+ import os from 'node:os'
18
+ import path from 'node:path'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { fileURLToPath } from 'node:url'
21
+
22
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
23
+ const STANDARDS = path.resolve(HERE, '..', '..')
24
+ const DEFAULT_OUT = path.join(STANDARDS, '40-checker', 'results', 'entry', '18', 'realweb')
25
+
26
+ function usage() {
27
+ console.log(`usage: node 40-checker/scripts/entry18-web.mjs --id <id> --spec <spec> --pkg <name> [options]
28
+
29
+ --id <id> result id (manifest id)
30
+ --spec <spec> npm spec (pkg@version) or github:owner/repo#ref
31
+ --pkg <name> expected package name
32
+ --version <v> expected package version (optional)
33
+ --out <dir> result directory (default: 40-checker/results/entry/18/realweb)
34
+ --dsh-root <dir> installed @deepseek-ai/dsh package root
35
+ --port <n> web port (default: 18000 + pid % 1000)
36
+ --boot-timeout <ms> web boot budget (default 240000)
37
+ --keep-work keep the temp profile/package (debug)
38
+ --help show this help`)
39
+ }
40
+
41
+ function parseArgs(argv) {
42
+ const options = {
43
+ id: null, spec: null, pkg: null, version: null, out: DEFAULT_OUT,
44
+ dshRoot: null, port: 18000 + (process.pid % 1000),
45
+ bootTimeout: 240_000, keepWork: false,
46
+ }
47
+ for (let i = 0; i < argv.length; i += 1) {
48
+ const arg = argv[i]
49
+ const next = () => {
50
+ const value = argv[++i]
51
+ if (value === undefined) throw new Error(`${arg} needs a value`)
52
+ return value
53
+ }
54
+ if (arg === '--id') options.id = next()
55
+ else if (arg === '--spec') options.spec = next()
56
+ else if (arg === '--pkg') options.pkg = next()
57
+ else if (arg === '--version') options.version = next()
58
+ else if (arg === '--out') options.out = path.resolve(next())
59
+ else if (arg === '--dsh-root') options.dshRoot = path.resolve(next())
60
+ else if (arg === '--port') options.port = Number(next())
61
+ else if (arg === '--boot-timeout') options.bootTimeout = Number(next())
62
+ else if (arg === '--keep-work') options.keepWork = true
63
+ else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
64
+ else throw new Error(`unknown argument: ${arg}`)
65
+ }
66
+ if (!options.id || !options.spec || !options.pkg) throw new Error('--id, --spec and --pkg are required')
67
+ if (!Number.isFinite(options.port) || options.port < 1 || options.port > 65535) throw new Error('--port must be 1-65535')
68
+ if (!Number.isFinite(options.bootTimeout) || options.bootTimeout < 30_000) throw new Error('--boot-timeout must be >= 30000')
69
+ return options
70
+ }
71
+
72
+ function redact(text) {
73
+ return String(text ?? '')
74
+ .replace(/([?&]token=)[^&\s]+/g, '$1<redacted>')
75
+ .replace(/(密码\s*[::]\s*).*/g, '$1<redacted>')
76
+ }
77
+
78
+ // Redact the launch token and any bootstrap password from every stream.
79
+ for (const stream of [process.stdout, process.stderr]) {
80
+ const write = stream.write.bind(stream)
81
+ stream.write = (chunk, ...args) => write(redact(chunk), ...args)
82
+ }
83
+
84
+ function run(command, args, { cwd, timeout, env } = {}) {
85
+ const result = spawnSync(command, args, {
86
+ cwd,
87
+ timeout,
88
+ encoding: 'utf8',
89
+ maxBuffer: 64 * 1024 * 1024,
90
+ env: { ...process.env, NO_COLOR: '1', ...env },
91
+ })
92
+ return {
93
+ status: result.status,
94
+ signal: result.signal,
95
+ error: result.error?.message ?? null,
96
+ stdout: result.stdout ?? '',
97
+ stderr: result.stderr ?? '',
98
+ }
99
+ }
100
+
101
+ function defaultDshRoot() {
102
+ const root = run('npm', ['root', '-g'], { timeout: 60_000 })
103
+ if (root.status !== 0) throw new Error(`npm root -g failed: ${root.stderr.trim()}`)
104
+ return path.join(root.stdout.trim(), '@deepseek-ai', 'dsh')
105
+ }
106
+
107
+ function fetchGithub(spec, destination) {
108
+ const match = /^github:([^#]+)(?:#(.+))?$/.exec(spec)
109
+ if (!match) throw new Error(`unsupported github spec: ${spec}`)
110
+ const repo = match[1]
111
+ const ref = match[2]
112
+ const url = `https://github.com/${repo}.git`
113
+ const clone = run('git', ['clone', '--depth', '1', url, destination], { timeout: 240_000 })
114
+ if (clone.status !== 0) throw new Error(`git clone ${repo} failed: ${(clone.stderr || clone.stdout).trim().slice(-800)}`)
115
+ if (ref) {
116
+ const head = run('git', ['rev-parse', 'HEAD'], { cwd: destination })
117
+ if (head.status !== 0 || head.stdout.trim() !== ref) {
118
+ const fetch = run('git', ['fetch', '--depth', '1', 'origin', ref], { cwd: destination, timeout: 240_000 })
119
+ if (fetch.status !== 0) throw new Error(`git fetch ${ref} failed: ${(fetch.stderr || fetch.stdout).trim().slice(-800)}`)
120
+ const checkout = run('git', ['checkout', '--detach', 'FETCH_HEAD'], { cwd: destination })
121
+ if (checkout.status !== 0) throw new Error(`git checkout ${ref} failed: ${(checkout.stderr || checkout.stdout).trim().slice(-800)}`)
122
+ }
123
+ }
124
+ return { source: 'github', resolved: ref ?? 'default-branch' }
125
+ }
126
+
127
+ function fetchNpm(spec, work, destination) {
128
+ const pack = run('npm', ['pack', spec, '--pack-destination', work, '--json'], { timeout: 300_000 })
129
+ if (pack.status !== 0) throw new Error(`npm pack ${spec} failed: ${(pack.stderr || pack.stdout).trim().slice(-800)}`)
130
+ let filename
131
+ try {
132
+ const parsed = JSON.parse(pack.stdout)
133
+ filename = (Array.isArray(parsed) ? parsed.at(-1) : parsed)?.filename
134
+ } catch {
135
+ throw new Error(`npm pack ${spec} returned unparseable JSON`)
136
+ }
137
+ if (!filename) throw new Error(`npm pack ${spec} did not report a filename`)
138
+ const tarball = path.join(work, filename)
139
+ if (!fs.existsSync(tarball)) throw new Error(`npm pack ${spec} reported a missing tarball`)
140
+ fs.mkdirSync(destination, { recursive: true })
141
+ const extract = run('tar', ['-xzf', tarball, '-C', destination, '--strip-components=1'], { timeout: 120_000 })
142
+ if (extract.status !== 0) throw new Error(`tar extraction failed: ${(extract.stderr || extract.stdout).trim().slice(-800)}`)
143
+ return { source: 'npm', resolved: spec, tarball: filename }
144
+ }
145
+
146
+ function targetRows(pkgDir, manifest, loadOverlayPatches, composeEntries) {
147
+ const patch = manifest.dsh?.bundle?.patch
148
+ if (!patch) throw new Error('package has no dsh.bundle.patch')
149
+ const patches = (Array.isArray(patch) ? patch : [patch])
150
+ .map((file) => path.resolve(pkgDir, file))
151
+ .flatMap((file) => loadOverlayPatches('dsh', file))
152
+ const composed = composeEntries([patches])
153
+ return {
154
+ ids: new Set(composed.filter((row) => row.id).map((row) => row.id)),
155
+ names: new Set(composed.filter((row) => typeof row.name === 'string').map((row) => row.name)),
156
+ composed: composed.length,
157
+ }
158
+ }
159
+
160
+ async function main() {
161
+ const options = parseArgs(process.argv.slice(2))
162
+ const startedAt = new Date().toISOString()
163
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-entry18-web-'))
164
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-entry18-home-'))
165
+ process.env.DSH_HOME = home
166
+ let ctx
167
+ let shutdown
168
+ let row = {
169
+ schema: 'dsh-entry18-web/1',
170
+ id: options.id,
171
+ name: options.pkg,
172
+ version: options.version,
173
+ spec: options.spec,
174
+ profile: 'web',
175
+ method: {
176
+ runner: 'real-dsh-web-profile',
177
+ install: 'manual-profile-symlink',
178
+ base: 'dsh-base/cordis.patch.yml',
179
+ web: 'dsh-web-app/cordis.patch.yml',
180
+ args: ['--no-open', `--port`, String(options.port)],
181
+ },
182
+ environment: { node: process.version, platform: process.platform, dshVersion: null },
183
+ startedAt,
184
+ finishedAt: null,
185
+ elapsedMs: null,
186
+ status: 'error',
187
+ verdict: null,
188
+ compatibility: null,
189
+ compatibilityError: null,
190
+ targetRows: 0,
191
+ active: 0,
192
+ rows: [],
193
+ entries: 0,
194
+ notClaimed: ['UI correctness', 'security', 'production readiness', 'official endorsement'],
195
+ error: null,
196
+ }
197
+ try {
198
+ const dshRoot = options.dshRoot ?? defaultDshRoot()
199
+ const profileBoot = path.join(dshRoot, 'lib', 'profile-boot.js')
200
+ const appBoot = path.join(dshRoot, '..', 'dsh-app-boot', 'lib', 'index.js')
201
+ if (!fs.existsSync(profileBoot)) throw new Error(`not a DSH install: ${profileBoot}`)
202
+
203
+ const pkgDir = path.join(work, 'pkg')
204
+ const fetched = options.spec.startsWith('github:')
205
+ ? fetchGithub(options.spec, pkgDir)
206
+ : fetchNpm(options.spec, work, pkgDir)
207
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'))
208
+ if (manifest.name !== options.pkg) throw new Error(`installed package name ${manifest.name} != ${options.pkg}`)
209
+ if (options.version && manifest.version !== options.version) throw new Error(`installed version ${manifest.version} != ${options.version}`)
210
+ row.version = manifest.version
211
+ row.source = fetched.source
212
+ row.artifact = fetched
213
+
214
+
215
+ // Standalone install: strip dev-only specs/scripts, never run third-party scripts.
216
+ const rewrite = []
217
+ if (manifest.devDependencies) { delete manifest.devDependencies; rewrite.push('devDependencies') }
218
+ if (manifest.scripts) { delete manifest.scripts; rewrite.push('scripts') }
219
+ if (rewrite.length) fs.writeFileSync(path.join(pkgDir, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`)
220
+ row.method.manifestRewrite = rewrite.length ? `removed ${rewrite.join('+')}` : 'none'
221
+ const install = run('npm', ['install', '--omit=dev', '--ignore-scripts', '--legacy-peer-deps', '--no-package-lock', '--no-audit', '--no-fund'], { cwd: pkgDir, timeout: 300_000 })
222
+ if (install.status !== 0) throw new Error(`npm install in sample failed: ${(install.stderr || install.stdout).trim().slice(-800)}`)
223
+
224
+ const { prepareProfile, runProfile } = await import(profileBoot)
225
+ const { loadOverlayPatches, composeEntries, loadLayeredEnv, evaluatePluginCompatibility, getDshRuntimeVersion } = await import(appBoot)
226
+
227
+ // G2 is the host compatibility gate. The manual install bypasses `dsh plugin add`,
228
+ // so evaluate it explicitly; a reject stays `verified-gate-reject`, not clean.
229
+ try {
230
+ row.compatibility = evaluatePluginCompatibility(manifest, {}, getDshRuntimeVersion()) === undefined ? 'admit' : 'reject'
231
+ } catch (error) {
232
+ row.compatibility = 'error'
233
+ row.compatibilityError = error.message
234
+ }
235
+ const profile = prepareProfile('wr', true, 'web')
236
+ const dest = path.join(profile.dir, 'node_modules', ...options.pkg.split('/'))
237
+ fs.mkdirSync(path.dirname(dest), { recursive: true })
238
+ fs.symlinkSync(pkgDir, dest, 'dir')
239
+ const profilePkgPath = path.join(profile.dir, 'package.json')
240
+ const profilePkg = JSON.parse(fs.readFileSync(profilePkgPath, 'utf8'))
241
+ profilePkg.dependencies = { ...(profilePkg.dependencies || {}), [options.pkg]: `file:${pkgDir}` }
242
+ profilePkg.dsh.profile.bundles = [...new Set([...(profilePkg.dsh.profile.bundles || []), options.pkg])]
243
+ fs.writeFileSync(profilePkgPath, `${JSON.stringify(profilePkg, null, 2)}\n`)
244
+
245
+ const targets = targetRows(pkgDir, manifest, loadOverlayPatches, composeEntries)
246
+ row.targetRows = targets.ids.size
247
+ row.targetNames = targets.names.size
248
+
249
+ let timer
250
+ const timeout = new Promise((_, reject) => {
251
+ timer = setTimeout(() => reject(new Error(`web boot exceeded ${options.bootTimeout}ms`)), options.bootTimeout)
252
+ })
253
+ let booted
254
+ try {
255
+ booted = await Promise.race([
256
+ runProfile({
257
+ environment: loadLayeredEnv('dsh'),
258
+ profile: 'wr',
259
+ patchFiles: [],
260
+ args: ['--no-open', '--port', String(options.port)],
261
+ }),
262
+ timeout,
263
+ ])
264
+ } finally {
265
+ clearTimeout(timer)
266
+ }
267
+ ctx = booted.ctx
268
+ shutdown = booted.shutdown
269
+ row.environment.dshVersion = ctx.get?.('profileContext')?.version ?? null
270
+
271
+ const entries = [...ctx.loader.entries()]
272
+ row.entries = entries.length
273
+ row.rows = entries
274
+ .filter((entry) => [...targets.ids].some((id) => entry.id === id || String(entry.id).endsWith(`:${id}`)) || targets.names.has(entry.name))
275
+ .map((entry) => ({ id: entry.id, name: entry.name, state: entry.fiber?.state ?? null, disabled: entry.disabled === true }))
276
+ row.active = row.rows.filter((entry) => entry.state === 2 || entry.disabled).length
277
+ row.status = row.rows.length > 0 && row.active === row.rows.length
278
+ ? 'verified'
279
+ : row.rows.length > 0 ? 'partial' : 'failed'
280
+ row.verdict = row.status === 'verified' && row.compatibility === 'reject' ? 'verified-gate-reject' : row.status
281
+ if (row.status === 'partial') row.error = { stage: 'target-rows', message: 'some target rows did not activate' }
282
+ if (row.status === 'failed') row.error = { stage: 'target-rows', message: 'no target rows found in the booted tree' }
283
+ } catch (error) {
284
+ row.status = 'blocked'
285
+ row.error = { stage: 'runner', message: redact(error.message) }
286
+ } finally {
287
+ row.finishedAt = new Date().toISOString()
288
+ row.elapsedMs = Date.now() - Date.parse(startedAt)
289
+ try { await shutdown?.shutdown(0) } catch { /* best effort */ }
290
+ try { await ctx?.fiber?.dispose?.() } catch { /* best effort */ }
291
+ fs.mkdirSync(options.out, { recursive: true })
292
+ fs.writeFileSync(path.join(options.out, `${options.id}.json`), `${JSON.stringify(row, null, 2)}\n`)
293
+ if (!options.keepWork) {
294
+ try { fs.rmSync(home, { recursive: true, force: true }) } catch { /* best effort */ }
295
+ try { fs.rmSync(work, { recursive: true, force: true }) } catch { /* best effort */ }
296
+ }
297
+ console.log(`[${options.id}] ${row.status} targetRows=${row.targetRows} active=${row.active} entries=${row.entries} ${row.elapsedMs}ms`)
298
+ }
299
+ return row.status === 'blocked' ? 3 : 0
300
+ }
301
+
302
+ try {
303
+ process.exit(await main())
304
+ } catch (error) {
305
+ console.error(`entry18-web: ${error.message}`)
306
+ process.exit(3)
307
+ }
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * evidence-chain.mjs — append-only, hash-chained evidence index.
4
+ *
5
+ * It does not make evidence immutable. It makes a change detectable: each line
6
+ * contains the previous line's hash and the record's sha256. A rewrite breaks
7
+ * the chain.
8
+ *
9
+ * Usage:
10
+ * node 40-checker/scripts/evidence-chain.mjs append --record publish/attestations/DSH-ATT-2026-001.json
11
+ * node 40-checker/scripts/evidence-chain.mjs verify
12
+ * node ... append --chain <file> --record <file> --ts <date>
13
+ */
14
+ import crypto from 'node:crypto'
15
+ import fs from 'node:fs'
16
+ import path from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
20
+ const ROOT = path.resolve(HERE, '..', '..')
21
+ const DEFAULT_CHAIN = path.join(ROOT, 'evidence', 'chain.jsonl')
22
+
23
+ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex')
24
+ const entryHash = ({ seq, prev, record, recordSha256, ts }) => sha256(`${seq}\n${prev}\n${record}\n${recordSha256}\n${ts}\n`)
25
+
26
+ export function verifyChain(lines, { root = null } = {}) {
27
+ const errors = []
28
+ let prev = 'GENESIS'
29
+ let expectedSeq = 1
30
+ for (const [index, line] of lines.entries()) {
31
+ if (!line.trim()) continue
32
+ let entry
33
+ try { entry = JSON.parse(line) } catch { errors.push(`line ${index + 1}: not JSON`); continue }
34
+ if (entry.seq !== expectedSeq) errors.push(`line ${index + 1}: seq ${entry.seq} != ${expectedSeq}`)
35
+ if (entry.prev !== prev) errors.push(`line ${index + 1}: prev does not match the previous entry`)
36
+ const expected = entryHash(entry)
37
+ if (entry.entrySha256 !== expected) errors.push(`line ${index + 1}: entry hash mismatch`)
38
+ if (entry.recordSha256 && !/^[0-9a-f]{64}$/.test(entry.recordSha256)) errors.push(`line ${index + 1}: bad record sha256`)
39
+ if (root && entry.record) {
40
+ const file = path.join(root, entry.record)
41
+ if (!fs.existsSync(file)) errors.push(`line ${index + 1}: record not found: ${entry.record}`)
42
+ else if (entry.recordSha256 !== sha256(fs.readFileSync(file))) errors.push(`line ${index + 1}: record sha256 does not match ${entry.record}`)
43
+ }
44
+ prev = entry.entrySha256
45
+ expectedSeq += 1
46
+ }
47
+ return { errors, entries: expectedSeq - 1 }
48
+ }
49
+
50
+ export function appendRecord({ chain = DEFAULT_CHAIN, record, ts = new Date().toISOString() }) {
51
+ const absolute = path.isAbsolute(record) ? record : path.join(ROOT, record)
52
+ if (!fs.existsSync(absolute)) throw new Error(`record not found: ${absolute}`)
53
+ const relative = path.relative(ROOT, absolute).split(path.sep).join('/')
54
+ const recordSha256 = sha256(fs.readFileSync(absolute))
55
+ const lines = fs.existsSync(chain) ? fs.readFileSync(chain, 'utf8').split('\n').filter((line) => line.trim()) : []
56
+ const previous = lines.length > 0 ? JSON.parse(lines[lines.length - 1]) : null
57
+ const entry = {
58
+ seq: lines.length + 1,
59
+ prev: previous ? previous.entrySha256 : 'GENESIS',
60
+ record: relative,
61
+ recordSha256,
62
+ ts,
63
+ }
64
+ entry.entrySha256 = entryHash(entry)
65
+ fs.mkdirSync(path.dirname(chain), { recursive: true })
66
+ fs.appendFileSync(chain, `${JSON.stringify(entry)}\n`)
67
+ return entry
68
+ }
69
+
70
+ export function main(argv = process.argv.slice(2)) {
71
+ const [command, ...rest] = argv
72
+ let chain = DEFAULT_CHAIN
73
+ let record = null
74
+ let ts = new Date().toISOString()
75
+ for (let i = 0; i < rest.length; i += 1) {
76
+ if (rest[i] === '--chain') chain = path.resolve(rest[++i])
77
+ else if (rest[i] === '--record') record = rest[++i]
78
+ else if (rest[i] === '--ts') ts = rest[++i]
79
+ else { console.error(`unknown argument: ${rest[i]}`); return 2 }
80
+ }
81
+ if (command === 'append') {
82
+ if (!record) { console.error('append needs --record'); return 2 }
83
+ const entry = appendRecord({ chain, record, ts })
84
+ console.log(JSON.stringify(entry, null, 2))
85
+ return 0
86
+ }
87
+ if (command === 'verify') {
88
+ if (!fs.existsSync(chain)) { console.error(`chain not found: ${chain}`); return 2 }
89
+ const result = verifyChain(fs.readFileSync(chain, 'utf8').split('\n'), { root: ROOT })
90
+ for (const error of result.errors) console.log(`FAIL ${error}`)
91
+ console.log(`${result.errors.length === 0 ? 'PASS' : 'FAIL'} evidence chain: ${result.entries} entries`)
92
+ return result.errors.length === 0 ? 0 : 2
93
+ }
94
+ console.error('usage: evidence-chain.mjs append --record <file> | verify [--chain <file>]')
95
+ return 2
96
+ }
97
+
98
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
99
+ process.exitCode = main()
100
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * flow-index.mjs — rebuild 40-checker/results/flow/real/index.json from all
4
+ * per-package result files. It never reruns a journey and never upgrades a
5
+ * verdict; it only aggregates what is on disk.
6
+ *
7
+ * Usage: node 40-checker/scripts/flow-index.mjs [--dir <dir>]
8
+ */
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
14
+ const ROOT = path.resolve(HERE, '..', '..')
15
+ const DEFAULT_DIR = path.join(ROOT, '40-checker', 'results', 'flow', 'real')
16
+
17
+ export function buildFlowIndex(dir = DEFAULT_DIR, environment = null) {
18
+ const files = fs.readdirSync(dir).filter((name) => name.endsWith('.json') && name !== 'index.json').sort()
19
+ const rows = []
20
+ for (const name of files) {
21
+ let row
22
+ try { row = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')) } catch { continue }
23
+ if (row?.schema !== 'dsh-flow-real/1') continue
24
+ rows.push(row)
25
+ }
26
+ const totals = {
27
+ packages: rows.length,
28
+ checks: { pass: 0, warn: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 },
29
+ verdicts: { pass: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 },
30
+ classifications: {},
31
+ }
32
+ for (const row of rows) {
33
+ for (const [key, value] of Object.entries(row.summary?.checks || {})) totals.checks[key] += value
34
+ for (const [key, value] of Object.entries(row.summary?.verdicts || {})) totals.verdicts[key] += value
35
+ totals.classifications[row.classification] = (totals.classifications[row.classification] || 0) + 1
36
+ }
37
+ return {
38
+ schema: 'dsh-flow-real-index/1',
39
+ generatedAt: new Date().toISOString(),
40
+ environment,
41
+ samples: rows.map((row) => ({
42
+ id: row.id,
43
+ package: row.package,
44
+ version: row.version,
45
+ source: row.source,
46
+ why: row.why,
47
+ verdicts: row.verdicts,
48
+ classification: row.classification,
49
+ conclusion: row.conclusion,
50
+ checks: row.summary?.checks ?? null,
51
+ caveats: row.caveats,
52
+ file: path.join(dir, `${row.id}.json`),
53
+ })),
54
+ totals,
55
+ coverage: { J1: 'runtime', J2: 'runtime', J3: 'not-run', J4: 'runtime (tree-load only)', J5: 'not-run', J6: 'runtime for ci-diet only', J7: 'not-run', J8: 'not-run' },
56
+ notes: [
57
+ 'A gate rejection is recorded as BLOCKED, never as PASS; this index never calls allow-version.',
58
+ 'The purge sample is J1/J2 partial because its postinstall was deliberately not run.',
59
+ 'J3/J5/J7/J8 remain not-run: this evidence does not claim full FLOW coverage.',
60
+ 'Second-wave npm samples are real packages, not fixtures; their J4 failure is recorded, not hidden.',
61
+ ],
62
+ }
63
+ }
64
+
65
+ export function main(argv = process.argv.slice(2)) {
66
+ let dir = DEFAULT_DIR
67
+ for (let i = 0; i < argv.length; i += 1) {
68
+ if (argv[i] === '--dir') dir = path.resolve(argv[++i])
69
+ else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: flow-index.mjs [--dir <dir>]'); return 0 }
70
+ else { console.error(`unknown argument: ${argv[i]}`); return 2 }
71
+ }
72
+ const old = fs.existsSync(path.join(dir, 'index.json')) ? JSON.parse(fs.readFileSync(path.join(dir, 'index.json'), 'utf8')) : null
73
+ const index = buildFlowIndex(dir, old?.environment ?? null)
74
+ fs.writeFileSync(path.join(dir, 'index.json'), `${JSON.stringify(index, null, 2)}\n`)
75
+ console.log(JSON.stringify(index.totals))
76
+ console.log(`wrote ${path.join(dir, 'index.json')}`)
77
+ return 0
78
+ }
79
+
80
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
81
+ process.exitCode = main()
82
+ }