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,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 }