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,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* FLOW real-package CI: run J1/J2/J4 (+J6 when a previous version is supplied)
|
|
4
|
+
* on a small representative slice of 40-checker/samples/entry18.json.
|
|
5
|
+
*
|
|
6
|
+
* What this script does, exactly:
|
|
7
|
+
* 1. npm sample -> `npm pack <name>@<version>`, extract to /tmp/flow-sample/<id>
|
|
8
|
+
* GitHub sample -> `git clone --depth 1`, HEAD/commit recorded
|
|
9
|
+
* 2. dependencies -> `npm install --omit=dev --ignore-scripts --legacy-peer-deps
|
|
10
|
+
* --no-package-lock` (only when the manifest has dependencies)
|
|
11
|
+
* 3. journey -> `node 40-checker/scripts/journey-runtime.mjs --package <dir>
|
|
12
|
+
* --dsh <bin> --json` (plus `--previous <dir>` for ci-diet)
|
|
13
|
+
* 4. gate sample -> a preflight `dsh plugin add` only; a compatibility rejection is
|
|
14
|
+
* recorded as BLOCKED. This script NEVER calls allow-version and
|
|
15
|
+
* never grants an exemption.
|
|
16
|
+
*
|
|
17
|
+
* Saved to 40-checker/results/flow/real/<id>.json plus `index.json`.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* node 40-checker/scripts/flow-sample-ci.mjs [--only id,id] [--refresh]
|
|
21
|
+
* [--dsh <bin>] [--out <dir>] [--timeout ms]
|
|
22
|
+
*/
|
|
23
|
+
import fs from 'node:fs'
|
|
24
|
+
import os from 'node:os'
|
|
25
|
+
import path from 'node:path'
|
|
26
|
+
import { spawnSync } from 'node:child_process'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
|
|
29
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
30
|
+
const STD = path.resolve(HERE, '..', '..')
|
|
31
|
+
const SAMPLE_BASE = '/tmp/flow-sample'
|
|
32
|
+
const CACHE = path.join(SAMPLE_BASE, '_cache')
|
|
33
|
+
const JOURNEY = path.join(STD, '40-checker/scripts/journey-runtime.mjs')
|
|
34
|
+
const DEFAULT_DSH = process.env.DSH_BIN || 'dsh'
|
|
35
|
+
const REAL = path.join(STD, '40-checker/results/flow/real')
|
|
36
|
+
|
|
37
|
+
const SAMPLES = [
|
|
38
|
+
{
|
|
39
|
+
id: 'ci-diet', name: 'dsh-ci-diet', version: '0.5.2', source: 'npm', spec: 'dsh-ci-diet@0.5.2',
|
|
40
|
+
why: 'simple npm bundle',
|
|
41
|
+
previous: { spec: 'dsh-ci-diet@0.5.1', dir: 'ci-diet-v0.5.1', version: '0.5.1' },
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'DDDMUC__dsh-free-search', name: 'dsh-free-search', version: '0.9.26', source: 'github', spec: 'DDDMUC/dsh-free-search',
|
|
45
|
+
why: 'GitHub bundle',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'ui-auth', name: 'dsh-ui-auth', version: '0.7.1', source: 'npm', spec: 'dsh-ui-auth@0.7.1',
|
|
49
|
+
why: 'bundle with a client',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'telemetry', name: 'dsh-telemetry-redactor', version: '0.1.0', source: 'npm', spec: 'dsh-telemetry-redactor@0.1.0',
|
|
53
|
+
why: 'expected host compatibility-gate rejection', gate: 'expect-reject',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'YuJunZhiXue__dsh-purge', name: 'dsh-purge', version: '1.1.85', source: 'github', spec: 'YuJunZhiXue/dsh-purge',
|
|
57
|
+
why: 'postinstall/profile behavior', j1Partial: true, j2Partial: true, layerProbe: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'daily-log', name: '@zzerx/dsh-plugin-daily-log', version: '0.4.3', source: 'npm', spec: '@zzerx/dsh-plugin-daily-log@0.4.3',
|
|
61
|
+
why: 'second wave: scoped npm plugin',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'project-memory', name: '@yolk_vat-y/dsh-project-memory', version: '0.6.7', source: 'npm', spec: '@yolk_vat-y/dsh-project-memory@0.6.7',
|
|
65
|
+
why: 'second wave: scoped npm plugin',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'qiushi', name: 'qiushi-dsh-evidence-audit', version: '0.1.0', source: 'npm', spec: 'qiushi-dsh-evidence-audit@0.1.0',
|
|
69
|
+
why: 'second wave: evidence-oriented plugin',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'status-rotator', name: 'dsh-status-rotator', version: '0.38.0', source: 'npm', spec: 'dsh-status-rotator@0.38.0',
|
|
73
|
+
why: 'second wave: npm plugin',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'verification', name: 'dsh-verification-receipt', version: '0.1.0', source: 'npm', spec: 'dsh-verification-receipt@0.1.0',
|
|
77
|
+
why: 'second wave: expected gate/host edge',
|
|
78
|
+
},
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
function parseArgs(argv) {
|
|
82
|
+
const out = { only: null, refresh: false, dsh: process.env.DSH_BIN || DEFAULT_DSH, out: REAL, timeout: 180_000 }
|
|
83
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
84
|
+
if (argv[i] === '--only') out.only = argv[++i].split(',').map(s => s.trim()).filter(Boolean)
|
|
85
|
+
else if (argv[i] === '--refresh') out.refresh = true
|
|
86
|
+
else if (argv[i] === '--dsh') out.dsh = argv[++i]
|
|
87
|
+
else if (argv[i] === '--out') out.out = path.resolve(argv[++i])
|
|
88
|
+
else if (argv[i] === '--timeout') out.timeout = Number(argv[++i])
|
|
89
|
+
else if (argv[i] === '--help' || argv[i] === '-h') {
|
|
90
|
+
console.log('usage: node 40-checker/scripts/flow-sample-ci.mjs [--only id,id] [--refresh] [--dsh <bin>] [--out <dir>] [--timeout ms]')
|
|
91
|
+
process.exit(0)
|
|
92
|
+
} else throw new Error(`unknown option: ${argv[i]}`)
|
|
93
|
+
}
|
|
94
|
+
return out
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function quote(arg) {
|
|
98
|
+
return /^[A-Za-z0-9_./:@=+-]+$/.test(arg) ? arg : `'${String(arg).replace(/'/g, "'\\''")}'`
|
|
99
|
+
}
|
|
100
|
+
function cmdline(cmd, args) { return [cmd, ...args].map(quote).join(' ') }
|
|
101
|
+
|
|
102
|
+
function run(cmd, args, { cwd, env, timeout = 180_000 } = {}) {
|
|
103
|
+
const started = Date.now()
|
|
104
|
+
const r = spawnSync(cmd, args, { cwd, env, encoding: 'utf8', timeout, maxBuffer: 16 * 1024 * 1024 })
|
|
105
|
+
return {
|
|
106
|
+
command: cmdline(cmd, args),
|
|
107
|
+
status: r.status,
|
|
108
|
+
signal: r.signal ?? null,
|
|
109
|
+
stdout: r.stdout || '',
|
|
110
|
+
stderr: r.stderr || '',
|
|
111
|
+
error: r.error ? String(r.error.message) : null,
|
|
112
|
+
durationMs: Date.now() - started,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function tail(text, n = 4000) { return String(text ?? '').slice(-n) }
|
|
116
|
+
function existsPackage(dir) { return fs.existsSync(path.join(dir, 'package.json')) }
|
|
117
|
+
function readJson(file) { return JSON.parse(fs.readFileSync(file, 'utf8')) }
|
|
118
|
+
|
|
119
|
+
function assertUnderSampleBase(target) {
|
|
120
|
+
const base = path.resolve(SAMPLE_BASE)
|
|
121
|
+
const resolved = path.resolve(target)
|
|
122
|
+
if (resolved === base || !resolved.startsWith(`${base}${path.sep}`)) throw new Error(`refusing to remove outside ${base}: ${resolved}`)
|
|
123
|
+
return resolved
|
|
124
|
+
}
|
|
125
|
+
function removeDir(dir) {
|
|
126
|
+
if (!fs.existsSync(dir)) return
|
|
127
|
+
fs.rmSync(assertUnderSampleBase(dir), { recursive: true, force: true })
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function childEnv() {
|
|
131
|
+
const env = { ...process.env }
|
|
132
|
+
delete env.DEEPSEEK_API_KEY
|
|
133
|
+
delete env.OPENAI_API_KEY
|
|
134
|
+
return env
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function manifestSummary(dir) {
|
|
138
|
+
const m = readJson(path.join(dir, 'package.json'))
|
|
139
|
+
return {
|
|
140
|
+
name: m.name,
|
|
141
|
+
version: m.version,
|
|
142
|
+
engines: m.engines ?? null,
|
|
143
|
+
peerDependencies: m.peerDependencies ?? null,
|
|
144
|
+
dependencies: m.dependencies ?? null,
|
|
145
|
+
scripts: m.scripts ? { postinstall: m.scripts.postinstall ?? null } : {},
|
|
146
|
+
dsh: m.dsh ?? null,
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function prepareNpm(sample, opts) {
|
|
151
|
+
fs.mkdirSync(CACHE, { recursive: true })
|
|
152
|
+
const dir = path.join(SAMPLE_BASE, sample.id)
|
|
153
|
+
if (opts.refresh) removeDir(dir)
|
|
154
|
+
const pack = run('npm', ['pack', sample.spec, '--pack-destination', CACHE], { timeout: opts.timeout })
|
|
155
|
+
const filename = pack.stdout.trim().split(/\r?\n/).filter(Boolean).pop()
|
|
156
|
+
if (pack.status !== 0 || !filename) {
|
|
157
|
+
return { ok: false, dir, download: { kind: 'npm-pack', command: pack.command, exit: pack.status, error: tail(pack.stderr || pack.error) } }
|
|
158
|
+
}
|
|
159
|
+
const tarball = path.join(CACHE, filename)
|
|
160
|
+
if (!existsPackage(dir)) {
|
|
161
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
162
|
+
const extract = run('tar', ['-xzf', tarball, '-C', dir, '--strip-components=1'], { timeout: opts.timeout })
|
|
163
|
+
if (extract.status !== 0) {
|
|
164
|
+
return { ok: false, dir, download: { kind: 'npm-pack', command: pack.command, tarball, extract: extract.command, exit: extract.status, error: tail(extract.stderr) } }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
ok: true, dir,
|
|
169
|
+
download: {
|
|
170
|
+
kind: 'npm-pack', command: pack.command, tarball,
|
|
171
|
+
registry: run('npm', ['config', 'get', 'registry']).stdout.trim(),
|
|
172
|
+
actualVersion: readJson(path.join(dir, 'package.json')).version,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function prepareGithub(sample, opts) {
|
|
178
|
+
const dir = path.join(SAMPLE_BASE, sample.id)
|
|
179
|
+
if (opts.refresh) removeDir(dir)
|
|
180
|
+
if (!existsPackage(dir)) {
|
|
181
|
+
const clone = run('git', ['clone', '--depth', '1', `https://github.com/${sample.spec}.git`, dir], { timeout: opts.timeout })
|
|
182
|
+
if (clone.status !== 0) return { ok: false, dir, download: { kind: 'git-clone', command: clone.command, exit: clone.status, error: tail(clone.stderr) } }
|
|
183
|
+
}
|
|
184
|
+
const head = run('git', ['-C', dir, 'rev-parse', 'HEAD'])
|
|
185
|
+
const subject = run('git', ['-C', dir, 'log', '-1', '--format=%s'])
|
|
186
|
+
return {
|
|
187
|
+
ok: true, dir,
|
|
188
|
+
download: {
|
|
189
|
+
kind: 'git-clone', command: `git clone --depth 1 https://github.com/${sample.spec}.git ${dir}`,
|
|
190
|
+
url: `https://github.com/${sample.spec}`, gitHead: head.stdout.trim() || null, gitSubject: subject.stdout.trim() || null,
|
|
191
|
+
actualVersion: readJson(path.join(dir, 'package.json')).version,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function installDeps(dir, opts) {
|
|
197
|
+
const m = readJson(path.join(dir, 'package.json'))
|
|
198
|
+
const deps = { ...(m.dependencies || {}), ...(m.optionalDependencies || {}) }
|
|
199
|
+
if (Object.keys(deps).length === 0) {
|
|
200
|
+
return { needed: false, command: null, exit: 0, ignoredScripts: true, note: 'no dependencies in package.json; npm install skipped' }
|
|
201
|
+
}
|
|
202
|
+
const r = run('npm', ['install', '--omit=dev', '--ignore-scripts', '--legacy-peer-deps', '--no-package-lock'], { cwd: dir, timeout: opts.timeout })
|
|
203
|
+
return {
|
|
204
|
+
needed: true, command: r.command, exit: r.status, ignoredScripts: true,
|
|
205
|
+
note: 'third-party lifecycle scripts were not run (--ignore-scripts)',
|
|
206
|
+
stdout: tail(r.stdout, 1200), stderr: tail(r.stderr, 1200), error: r.error,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function journeyRun(sample, dir, previousDir, opts) {
|
|
211
|
+
const args = [JOURNEY, '--package', dir, '--dsh', opts.dsh, '--json']
|
|
212
|
+
if (previousDir) args.push('--previous', previousDir)
|
|
213
|
+
const r = run(process.execPath, args, { cwd: STD, env: childEnv(), timeout: opts.timeout })
|
|
214
|
+
let result = null
|
|
215
|
+
try { result = JSON.parse(r.stdout) } catch { /* leave null; recorded below */ }
|
|
216
|
+
return { command: r.command, exit: r.status, stderr: tail(r.stderr), parseError: result ? null : tail(r.stdout, 1200), result }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeResult(value) {
|
|
220
|
+
const v = String(value || '').toLowerCase()
|
|
221
|
+
if (v === 'pass') return 'pass'
|
|
222
|
+
if (v === 'fail') return 'fail'
|
|
223
|
+
if (v === 'warn') return 'warn'
|
|
224
|
+
if (v === 'blocked') return 'blocked'
|
|
225
|
+
if (v === 'partial') return 'partial'
|
|
226
|
+
return 'notRun'
|
|
227
|
+
}
|
|
228
|
+
function countChecks(checks) {
|
|
229
|
+
const c = { pass: 0, warn: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 }
|
|
230
|
+
for (const ch of checks) c[normalizeResult(ch.result)] += 1
|
|
231
|
+
return c
|
|
232
|
+
}
|
|
233
|
+
function groupChecks(checks, prefix) { return checks.filter(c => c.id.startsWith(`${prefix}.`)) }
|
|
234
|
+
function groupVerdict(checks, prefix, forcePartial = false) {
|
|
235
|
+
const cs = groupChecks(checks, prefix)
|
|
236
|
+
if (cs.length === 0) return 'not-run'
|
|
237
|
+
if (cs.some(c => c.result === 'FAIL')) return 'fail'
|
|
238
|
+
if (cs.some(c => c.result === 'BLOCKED')) return 'blocked'
|
|
239
|
+
if (forcePartial) return 'partial'
|
|
240
|
+
if (cs.every(c => c.result === 'not-run')) return 'not-run'
|
|
241
|
+
if (cs.some(c => c.result === 'not-run')) return 'partial'
|
|
242
|
+
return 'pass'
|
|
243
|
+
}
|
|
244
|
+
function verdictsOf(checks, sample) {
|
|
245
|
+
return {
|
|
246
|
+
J1: groupVerdict(checks, 'J1', sample.j1Partial === true),
|
|
247
|
+
J2: groupVerdict(checks, 'J2', sample.j2Partial === true),
|
|
248
|
+
J4: groupVerdict(checks, 'J4'),
|
|
249
|
+
J6: groupVerdict(checks, 'J6'),
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function countVerdicts(verdicts) {
|
|
253
|
+
const c = { pass: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 }
|
|
254
|
+
for (const v of Object.values(verdicts)) c[normalizeResult(v)] += 1
|
|
255
|
+
return c
|
|
256
|
+
}
|
|
257
|
+
function conclusionOf(verdicts) {
|
|
258
|
+
const values = Object.values(verdicts)
|
|
259
|
+
if (values.includes('fail')) return 'flow-fail'
|
|
260
|
+
if (values.includes('blocked')) return 'flow-blocked'
|
|
261
|
+
if (values.includes('partial')) return 'flow-partial'
|
|
262
|
+
return 'flow-warn'
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Preflight only: a rejection is evidence. Never call allow-version here. */
|
|
266
|
+
function gatePreflight(sample, dir) {
|
|
267
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-flow-gate-'))
|
|
268
|
+
const env = { ...childEnv(), DSH_HOME: home }
|
|
269
|
+
const profile = 'flow-gate'
|
|
270
|
+
const create = run(optsDsh, ['--profile', profile, '--from-default-profile', 'headless', '--dump-config'], { env, timeout: 180_000 })
|
|
271
|
+
const add = run(optsDsh, ['plugin', '--profile', profile, 'add', dir], { env, timeout: 180_000 })
|
|
272
|
+
const output = `${add.stdout}${add.stderr}`
|
|
273
|
+
const rejected = add.status !== 0 && /installation rejected/.test(output)
|
|
274
|
+
const compat = path.join(home, 'profiles', profile, 'compatibility.json')
|
|
275
|
+
const requirementMatch = /requirements (\{.*?\})\./.exec(output)
|
|
276
|
+
const result = {
|
|
277
|
+
command: add.command,
|
|
278
|
+
dshHome: home,
|
|
279
|
+
profile,
|
|
280
|
+
createExit: create.status,
|
|
281
|
+
addExit: add.status,
|
|
282
|
+
rejected,
|
|
283
|
+
diagnostic: tail(output, 3000),
|
|
284
|
+
requirements: requirementMatch ? requirementMatch[1] : null,
|
|
285
|
+
exemptionGranted: false,
|
|
286
|
+
compatibilityFilePresent: fs.existsSync(compat),
|
|
287
|
+
cleanedUp: true,
|
|
288
|
+
}
|
|
289
|
+
try { fs.rmSync(home, { recursive: true, force: true }) } catch { result.cleanedUp = false }
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
let optsDsh = DEFAULT_DSH
|
|
293
|
+
|
|
294
|
+
/** Show why a skipped postinstall matters: the main row is absent, the patch rows exist. */
|
|
295
|
+
function layerProbe(sample, dir) {
|
|
296
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-flow-layer-'))
|
|
297
|
+
const env = { ...childEnv(), DSH_HOME: home }
|
|
298
|
+
const profile = 'flow-layer'
|
|
299
|
+
const create = run(optsDsh, ['--profile', profile, '--from-default-profile', 'headless', '--dump-config'], { env, timeout: 180_000 })
|
|
300
|
+
const add = run(optsDsh, ['plugin', '--profile', profile, 'add', dir], { env, timeout: 180_000 })
|
|
301
|
+
const dump = run(optsDsh, ['--profile', profile, '--dump-config'], { env, timeout: 180_000 })
|
|
302
|
+
const mainRowPresent = new RegExp(`^- id: ${sample.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm').test(dump.stdout)
|
|
303
|
+
const redteamRowIds = [...dump.stdout.matchAll(/^- id: (dsh-purge-redteam\S*)$/gm)].map(m => m[1])
|
|
304
|
+
const missingPatchEntries = [...dump.stderr.matchAll(/entry "([^"]+)" not found/g)].map(m => m[1])
|
|
305
|
+
let profileBundles = null
|
|
306
|
+
try { profileBundles = readJson(path.join(home, 'profiles', profile, 'package.json')).dsh?.profile?.bundles ?? null } catch { /* ignore */ }
|
|
307
|
+
const result = {
|
|
308
|
+
command: dump.command,
|
|
309
|
+
dshHome: home,
|
|
310
|
+
profile,
|
|
311
|
+
createExit: create.status,
|
|
312
|
+
addExit: add.status,
|
|
313
|
+
dumpExit: dump.status,
|
|
314
|
+
mainRowPresent,
|
|
315
|
+
redteamRowIds,
|
|
316
|
+
missingPatchEntries,
|
|
317
|
+
profileBundles,
|
|
318
|
+
dumpStderr: tail(dump.stderr, 2000),
|
|
319
|
+
cleanedUp: true,
|
|
320
|
+
}
|
|
321
|
+
try { fs.rmSync(home, { recursive: true, force: true }) } catch { result.cleanedUp = false }
|
|
322
|
+
return result
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function emptyChecks(reason) {
|
|
326
|
+
const ids = ['J1.profile', 'J1.install', 'J1.dump', 'J1.layer', 'J1.remove', 'J1.clean', 'J2.disable', 'J2.enable', 'J4.tree-load', 'J6.upgrade', 'J6.rollback', 'J3.config', 'J5.permissions', 'J7.observe', 'J8.client']
|
|
327
|
+
return ids.map(id => ({ id, result: 'not-run', reason }))
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function telemetryChecks(gate) {
|
|
331
|
+
const why = 'compatibility gate rejected the install; nothing was installed'
|
|
332
|
+
const checks = [
|
|
333
|
+
{ id: 'J1.profile', result: gate.createExit === 0 ? 'PASS' : 'FAIL', reason: `create profile exit=${gate.createExit}` },
|
|
334
|
+
{ id: 'J1.install', result: 'BLOCKED', reason: `installation rejected: ${gate.requirements || 'unmet dsh requirement'}`, stderr: gate.diagnostic },
|
|
335
|
+
{ id: 'J1.dump', result: 'not-run', reason: why },
|
|
336
|
+
{ id: 'J1.layer', result: 'not-run', reason: why },
|
|
337
|
+
{ id: 'J1.remove', result: 'not-run', reason: why },
|
|
338
|
+
{ id: 'J1.clean', result: 'not-run', reason: why },
|
|
339
|
+
{ id: 'J2.disable', result: 'not-run', reason: why },
|
|
340
|
+
{ id: 'J2.enable', result: 'not-run', reason: why },
|
|
341
|
+
{ id: 'J4.tree-load', result: 'not-run', reason: why },
|
|
342
|
+
{ id: 'J6.upgrade', result: 'not-run', reason: why },
|
|
343
|
+
{ id: 'J6.rollback', result: 'not-run', reason: why },
|
|
344
|
+
{ id: 'J3.config', result: 'not-run', reason: 'invalid-config harness not implemented' },
|
|
345
|
+
{ id: 'J5.permissions', result: 'not-run', reason: 'permissions/disclosure review is manual' },
|
|
346
|
+
{ id: 'J7.observe', result: 'not-run', reason: 'observability probe not implemented' },
|
|
347
|
+
{ id: 'J8.client', result: 'not-run', reason: 'client-shape probe not implemented' },
|
|
348
|
+
]
|
|
349
|
+
return checks
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function main() {
|
|
353
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
354
|
+
optsDsh = opts.dsh
|
|
355
|
+
fs.mkdirSync(opts.out, { recursive: true })
|
|
356
|
+
const selected = SAMPLES.filter(s => !opts.only || opts.only.includes(s.id))
|
|
357
|
+
if (selected.length === 0) throw new Error(`no sample matched --only ${opts.only}`)
|
|
358
|
+
const dshVersion = run(opts.dsh, ['--version']).stdout.trim()
|
|
359
|
+
const envInfo = {
|
|
360
|
+
node: process.version,
|
|
361
|
+
platform: process.platform,
|
|
362
|
+
arch: process.arch,
|
|
363
|
+
dsh: { path: path.resolve(opts.dsh), version: dshVersion },
|
|
364
|
+
pnpm: run('pnpm', ['--version']).stdout.trim(),
|
|
365
|
+
npm: run('npm', ['--version']).stdout.trim(),
|
|
366
|
+
registry: run('npm', ['config', 'get', 'registry']).stdout.trim(),
|
|
367
|
+
sampleBase: SAMPLE_BASE,
|
|
368
|
+
resultsDir: opts.out,
|
|
369
|
+
command: cmdline(process.execPath, process.argv.slice(1)),
|
|
370
|
+
}
|
|
371
|
+
const rows = []
|
|
372
|
+
for (const sample of selected) {
|
|
373
|
+
console.log(`\n=== ${sample.id} (${sample.name}@${sample.version}, ${sample.source})`)
|
|
374
|
+
const prep = sample.source === 'npm' ? prepareNpm(sample, opts) : prepareGithub(sample, opts)
|
|
375
|
+
if (!prep.ok) {
|
|
376
|
+
const result = {
|
|
377
|
+
schema: 'dsh-flow-real/1', generatedAt: new Date().toISOString(), id: sample.id,
|
|
378
|
+
package: sample.name, entry18Version: sample.version, source: sample.source, spec: sample.spec,
|
|
379
|
+
packageDir: prep.dir, download: prep.download, dependencyInstall: null, journey: null,
|
|
380
|
+
checks: emptyChecks(`download/prepare failed: ${prep.download.error || 'unknown error'}`),
|
|
381
|
+
verdicts: { J1: 'not-run', J2: 'not-run', J4: 'not-run', J6: 'not-run' },
|
|
382
|
+
classification: 'not-run', caveats: ['Download/prepare failed; this is not a plugin defect.'],
|
|
383
|
+
summary: { checks: countChecks(emptyChecks('x')), verdicts: { pass: 0, fail: 0, blocked: 0, partial: 0, notRun: 4 } },
|
|
384
|
+
conclusion: 'flow-warn',
|
|
385
|
+
}
|
|
386
|
+
fs.writeFileSync(path.join(opts.out, `${sample.id}.json`), `${JSON.stringify(result, null, 2)}\n`)
|
|
387
|
+
rows.push(result)
|
|
388
|
+
console.log(`not-run: ${prep.download.error || 'prepare failed'}`)
|
|
389
|
+
continue
|
|
390
|
+
}
|
|
391
|
+
const manifest = manifestSummary(prep.dir)
|
|
392
|
+
const deps = installDeps(prep.dir, opts)
|
|
393
|
+
console.log(`manifest ${manifest.name}@${manifest.version}; deps ${deps.needed ? `install exit=${deps.exit}` : 'none'}`)
|
|
394
|
+
if (deps.needed && deps.exit !== 0) {
|
|
395
|
+
const result = {
|
|
396
|
+
schema: 'dsh-flow-real/1', generatedAt: new Date().toISOString(), id: sample.id,
|
|
397
|
+
package: manifest.name, entry18Version: sample.version, source: sample.source, spec: sample.spec,
|
|
398
|
+
packageDir: prep.dir, download: prep.download, manifest, dependencyInstall: deps, journey: null,
|
|
399
|
+
checks: emptyChecks(`dependency install failed (exit=${deps.exit}); not a plugin failure`),
|
|
400
|
+
verdicts: { J1: 'not-run', J2: 'not-run', J4: 'not-run', J6: 'not-run' },
|
|
401
|
+
classification: 'not-run', caveats: ['Dependency install failed; recorded not-run rather than a plugin failure.'],
|
|
402
|
+
summary: { checks: countChecks(emptyChecks('x')), verdicts: { pass: 0, fail: 0, blocked: 0, partial: 0, notRun: 4 } },
|
|
403
|
+
conclusion: 'flow-warn',
|
|
404
|
+
}
|
|
405
|
+
fs.writeFileSync(path.join(opts.out, `${sample.id}.json`), `${JSON.stringify(result, null, 2)}\n`)
|
|
406
|
+
rows.push(result)
|
|
407
|
+
console.log(`not-run: dependency install failed`)
|
|
408
|
+
continue
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let previous = null
|
|
412
|
+
let previousInstall = null
|
|
413
|
+
let previousDir = null
|
|
414
|
+
if (sample.previous) {
|
|
415
|
+
const prev = prepareNpm({ ...sample.previous, id: sample.previous.dir, source: 'npm' }, opts)
|
|
416
|
+
if (prev.ok) {
|
|
417
|
+
previousDir = prev.dir
|
|
418
|
+
previousInstall = installDeps(prev.dir, opts)
|
|
419
|
+
previous = { dir: prev.dir, download: prev.download, dependencyInstall: previousInstall, manifest: manifestSummary(prev.dir) }
|
|
420
|
+
} else {
|
|
421
|
+
previous = { error: prev.download.error || 'previous version prepare failed' }
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
let gate = null
|
|
426
|
+
let checks
|
|
427
|
+
let journey = null
|
|
428
|
+
let layer = null
|
|
429
|
+
let classification = 'clean'
|
|
430
|
+
const caveats = []
|
|
431
|
+
if (sample.gate === 'expect-reject') {
|
|
432
|
+
gate = gatePreflight(sample, prep.dir)
|
|
433
|
+
console.log(`gate rejected=${gate.rejected} requirements=${gate.requirements}`)
|
|
434
|
+
if (gate.rejected) {
|
|
435
|
+
checks = telemetryChecks(gate)
|
|
436
|
+
classification = 'host-gate'
|
|
437
|
+
caveats.push('Compatibility gate rejected this exact version; no exemption was granted and no retry was attempted.')
|
|
438
|
+
caveats.push('J2/J4 were not run because nothing was installed. This is a host gate, not a plugin defect.')
|
|
439
|
+
} else {
|
|
440
|
+
journey = journeyRun(sample, prep.dir, previousDir, opts)
|
|
441
|
+
checks = journey.result?.checks || emptyChecks('gate did not reject and journey produced no JSON')
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
journey = journeyRun(sample, prep.dir, previousDir, opts)
|
|
445
|
+
checks = journey.result?.checks || emptyChecks(journey.parseError || `journey exit=${journey.exit} with no JSON`)
|
|
446
|
+
if (!journey.result) caveats.push(`journey-runtime produced no parseable JSON (exit=${journey.exit}); recorded not-run.`)
|
|
447
|
+
}
|
|
448
|
+
if (sample.layerProbe && journey?.result) {
|
|
449
|
+
layer = layerProbe(sample, prep.dir)
|
|
450
|
+
classification = 'harness-partial'
|
|
451
|
+
console.log(`layer probe mainRowPresent=${layer.mainRowPresent} missing=${layer.missingPatchEntries.join(',')}`)
|
|
452
|
+
caveats.push('J1 is partial: the package postinstall was not run (`--ignore-scripts`); the main `dsh-purge` profile row is absent even though add/dump/remove succeeded.')
|
|
453
|
+
caveats.push('J2 is partial: the overlay toggled `dsh-purge-redteam`, not the main `dsh-purge` row.')
|
|
454
|
+
caveats.push('J4 reached the credential boundary, but the main plugin was not mounted; this proves tree-load only.')
|
|
455
|
+
}
|
|
456
|
+
if (sample.id === 'ui-auth') caveats.push('J4 reached MISSING_CREDENTIAL, but dsh-ui-auth stayed pending on services webServer/connection in headless; client activation (J8) is not proven.')
|
|
457
|
+
if (sample.id === 'DDDMUC__dsh-free-search') caveats.push('J6 not run: only one version was fetched; the clone HEAD is 7c30dd059e1d9de4ffc8b779978b2152af84b0ec.')
|
|
458
|
+
if (sample.id === 'ci-diet') caveats.push('J6 used the published 0.5.1 tarball as --previous; no persisted data/migration was exercised.')
|
|
459
|
+
if (manifest.version !== sample.version) caveats.push(`Actual package version ${manifest.version} differs from entry18.json ${sample.version}.`)
|
|
460
|
+
|
|
461
|
+
const verdicts = verdictsOf(checks, sample)
|
|
462
|
+
const checkCounts = countChecks(checks)
|
|
463
|
+
const verdictCounts = countVerdicts(verdicts)
|
|
464
|
+
const result = {
|
|
465
|
+
schema: 'dsh-flow-real/1',
|
|
466
|
+
generatedAt: new Date().toISOString(),
|
|
467
|
+
id: sample.id,
|
|
468
|
+
package: manifest.name,
|
|
469
|
+
version: manifest.version,
|
|
470
|
+
entry18Version: sample.version,
|
|
471
|
+
source: sample.source,
|
|
472
|
+
spec: sample.spec,
|
|
473
|
+
why: sample.why,
|
|
474
|
+
packageDir: prep.dir,
|
|
475
|
+
download: prep.download,
|
|
476
|
+
manifest,
|
|
477
|
+
dependencyInstall: deps,
|
|
478
|
+
previous,
|
|
479
|
+
gate,
|
|
480
|
+
journey: journey ? { command: journey.command, exit: journey.exit, parseError: journey.parseError, stderr: journey.stderr, result: journey.result } : null,
|
|
481
|
+
layerProbe: layer,
|
|
482
|
+
checks,
|
|
483
|
+
verdicts,
|
|
484
|
+
classification,
|
|
485
|
+
caveats,
|
|
486
|
+
limitations: ['J3/J5/J7/J8 are not covered by journey-runtime.', 'This is a local-directory install (`dsh plugin add <dir>`), not a registry tarball install.'],
|
|
487
|
+
summary: { checks: checkCounts, verdicts: verdictCounts },
|
|
488
|
+
conclusion: conclusionOf(verdicts),
|
|
489
|
+
}
|
|
490
|
+
const file = path.join(opts.out, `${sample.id}.json`)
|
|
491
|
+
fs.writeFileSync(file, `${JSON.stringify(result, null, 2)}\n`)
|
|
492
|
+
rows.push(result)
|
|
493
|
+
console.log(`saved ${file}`)
|
|
494
|
+
console.log(`verdicts ${JSON.stringify(verdicts)} checks ${JSON.stringify(checkCounts)}`)
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const totals = {
|
|
498
|
+
packages: rows.length,
|
|
499
|
+
checks: { pass: 0, warn: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 },
|
|
500
|
+
verdicts: { pass: 0, fail: 0, blocked: 0, partial: 0, notRun: 0 },
|
|
501
|
+
classifications: {},
|
|
502
|
+
}
|
|
503
|
+
for (const row of rows) {
|
|
504
|
+
for (const [k, v] of Object.entries(row.summary.checks)) totals.checks[k] += v
|
|
505
|
+
for (const [k, v] of Object.entries(row.summary.verdicts)) totals.verdicts[k] += v
|
|
506
|
+
totals.classifications[row.classification] = (totals.classifications[row.classification] || 0) + 1
|
|
507
|
+
}
|
|
508
|
+
const index = {
|
|
509
|
+
schema: 'dsh-flow-real-index/1',
|
|
510
|
+
generatedAt: new Date().toISOString(),
|
|
511
|
+
environment: envInfo,
|
|
512
|
+
samples: rows.map(r => ({
|
|
513
|
+
id: r.id, package: r.package, version: r.version, source: r.source, why: r.why,
|
|
514
|
+
verdicts: r.verdicts, classification: r.classification, conclusion: r.conclusion,
|
|
515
|
+
checks: r.summary.checks, caveats: r.caveats, file: path.join(opts.out, `${r.id}.json`),
|
|
516
|
+
})),
|
|
517
|
+
totals,
|
|
518
|
+
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' },
|
|
519
|
+
notes: [
|
|
520
|
+
'A gate rejection is recorded as BLOCKED, never as PASS; this script never calls allow-version.',
|
|
521
|
+
'The purge sample is J1/J2 partial because its postinstall was deliberately not run.',
|
|
522
|
+
'J3/J5/J7/J8 remain not-run: this evidence does not claim full FLOW coverage.',
|
|
523
|
+
],
|
|
524
|
+
}
|
|
525
|
+
fs.writeFileSync(path.join(opts.out, 'index.json'), `${JSON.stringify(index, null, 2)}\n`)
|
|
526
|
+
console.log(`\n${JSON.stringify(totals)}`)
|
|
527
|
+
console.log(`index: ${path.join(opts.out, 'index.json')}`)
|
|
528
|
+
}
|
|
529
|
+
try { main() } catch (error) { console.error(`flow-sample-ci: ${error.message}`); process.exitCode = 3 }
|