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.
- package/40-checker/README.md +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- 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
|
+
}
|