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,178 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FLOW runtime probe: J1 install/remove, J2 enable/disable, J4 keyless tree-load,
4
+ * and J6 upgrade/rollback when `--previous` is supplied.
5
+ *
6
+ * J3 (config validation), J5 (permissions), J7 (observability) and J8 (client
7
+ * shapes) are reported `not-run`: this probe does not pretend to cover them.
8
+ *
9
+ * Usage:
10
+ * node scripts/journey-runtime.mjs --package <dir> [--dsh <bin>] [--previous <dir>] [--json]
11
+ */
12
+ import fs from 'node:fs'
13
+ import os from 'node:os'
14
+ import path from 'node:path'
15
+ import { spawnSync } from 'node:child_process'
16
+
17
+ function parse(argv) {
18
+ const out = { package: null, previous: null, dsh: process.env.DSH_BIN || null, allowExemption: false, json: false, timeout: 120_000 }
19
+ for (let i = 0; i < argv.length; i += 1) {
20
+ if (argv[i] === '--package') out.package = argv[++i]
21
+ else if (argv[i] === '--previous') out.previous = argv[++i]
22
+ else if (argv[i] === '--dsh') out.dsh = argv[++i]
23
+ else if (argv[i] === '--allow-exemption') out.allowExemption = true
24
+ else if (argv[i] === '--json') out.json = true
25
+ else if (argv[i] === '--timeout') out.timeout = Number(argv[++i])
26
+ else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: node scripts/journey-runtime.mjs --package <dir> [--previous <dir>] [--dsh <bin>] [--allow-exemption] [--json]'); process.exit(0) }
27
+ else throw new Error(`unknown option: ${argv[i]}`)
28
+ }
29
+ if (!out.package) throw new Error('--package is required')
30
+ return out
31
+ }
32
+
33
+ function sh(cmd, args, env, timeout) {
34
+ return spawnSync(cmd, args, { encoding: 'utf8', timeout, env, maxBuffer: 8 * 1024 * 1024 })
35
+ }
36
+
37
+ /** Find the row id whose `name:` is the package name or a relative server entry. */
38
+ function rowIdFromDump(dump, pkgName) {
39
+ for (const block of dump.split(/\n(?=- )/)) {
40
+ const id = /^\s*-\s*id:\s*(\S+)/m.exec(block)
41
+ if (!id) continue
42
+ // `name` is either inline or a `>-` block whose first indented line holds
43
+ // the value (a file: URL for a local bundle).
44
+ const inline = /\n\s*name:\s*['"]?([^'"\n]+)['"]?/.exec(block)
45
+ const blockValue = /\n\s*name:\s*>-\s*\n\s*([^\n]+)/.exec(block)
46
+ const name = (blockValue ? blockValue[1] : inline ? inline[1] : '').trim()
47
+ if (name === pkgName || name.startsWith('./') || name.includes(pkgName)) return id[1]
48
+ }
49
+ return null
50
+ }
51
+
52
+ function installedVersion(home, profile, pkgName) {
53
+ const file = path.join(home, 'profiles', profile, 'node_modules', ...pkgName.split('/'), 'package.json')
54
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')).version ?? null } catch { return null }
55
+ }
56
+
57
+ function main() {
58
+ const options = parse(process.argv.slice(2))
59
+ const pkg = path.resolve(options.package)
60
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf8'))
61
+ const previous = options.previous ? path.resolve(options.previous) : null
62
+ const previousManifest = previous ? JSON.parse(fs.readFileSync(path.join(previous, 'package.json'), 'utf8')) : null
63
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-journey-'))
64
+ const profile = 'flow-journey'
65
+ const env = { ...process.env, DSH_HOME: home }
66
+ delete env.DEEPSEEK_API_KEY
67
+ delete env.OPENAI_API_KEY
68
+ const dsh = options.dsh || (process.platform === 'win32' ? 'dsh.cmd' : 'dsh')
69
+ const checks = []
70
+ let dshVersion = null
71
+ const add = (id, result, reason, extra = {}) => checks.push({ id, result, reason, ...extra })
72
+ try {
73
+ const version = sh(dsh, ['--version'], env, 30_000)
74
+ dshVersion = (version.stdout || '').trim()
75
+ const create = sh(dsh, ['--profile', profile, '--from-default-profile', 'headless', '--dump-config'], env, options.timeout)
76
+ add('J1.profile', create.status === 0 ? 'PASS' : 'FAIL', `create profile exit=${create.status}`, { stderr: create.stderr.slice(0, 500) })
77
+ let addResult = sh(dsh, ['plugin', '--profile', profile, 'add', pkg], env, options.timeout)
78
+ let addText = `${addResult.stdout}${addResult.stderr}`
79
+ const rejected = addResult.status !== 0 && /installation rejected/.test(addText)
80
+ // Never grant an exact-version exemption by default: a gate rejection is a
81
+ // measured BLOCKED result, not something the harness may silently accept.
82
+ if (rejected && (options.allowExemption || process.env.DSH_JOURNEY_ALLOW_EXEMPTION === '1')) {
83
+ const allow = sh(dsh, ['plugin', '--profile', profile, 'allow-version', `${manifest.name}@${manifest.version}`, '--dsh-version', dshVersion, '--accept-risk'], env, options.timeout)
84
+ add('J1.exemption', allow.status === 0 ? 'WARN' : 'FAIL', `allow-version exit=${allow.status}`, { stderr: allow.stderr.slice(0, 500) })
85
+ addResult = sh(dsh, ['plugin', '--profile', profile, 'add', pkg], env, options.timeout)
86
+ addText = `${addResult.stdout}${addResult.stderr}`
87
+ }
88
+ add('J1.install', rejected && !options.allowExemption && process.env.DSH_JOURNEY_ALLOW_EXEMPTION !== '1' ? 'BLOCKED' : addResult.status === 0 ? 'PASS' : 'FAIL', rejected ? 'host compatibility gate rejected the install (no exemption granted)' : `add exit=${addResult.status}`, { stderr: addText.slice(0, 800) })
89
+ const dump1 = sh(dsh, ['--profile', profile, '--dump-config'], env, options.timeout)
90
+ add('J1.dump', dump1.status === 0 ? 'PASS' : 'FAIL', `dump exit=${dump1.status}`, { stderr: dump1.stderr.slice(0, 500) })
91
+ const appears = dump1.stdout.includes(manifest.name) || /fixture-bundle-server/.test(dump1.stdout)
92
+ add('J1.layer', appears ? 'PASS' : 'FAIL', appears ? 'layer/entry appears in dump' : 'plugin/bundle not visible in dump')
93
+
94
+ // J2: disable/enable through a profile overlay. A missing row id is a real
95
+ // failure of the probe (not a pass): report it and skip the toggle checks.
96
+ const rowId = rowIdFromDump(dump1.stdout, manifest.name)
97
+ if (!rowId) {
98
+ add('J2.toggle', 'not-run', 'could not locate the inserted row id in dump-config')
99
+ } else {
100
+ const overlay = path.join(home, 'j2-overlay.yml')
101
+ fs.writeFileSync(overlay, `- id: ${rowId}\n disabled: true\n`)
102
+ const disabled = sh(dsh, ['--profile', profile, '--patch', overlay, '--dump-config'], env, options.timeout)
103
+ const disabledOK = disabled.status === 0 && /disabled:\s*true/.test(disabled.stdout)
104
+ add('J2.disable', disabledOK ? 'PASS' : 'FAIL', disabledOK ? `row ${rowId} disabled` : `disable exit=${disabled.status}`, { stderr: disabled.stderr.slice(0, 500) })
105
+ fs.writeFileSync(overlay, `- id: ${rowId}\n disabled: false\n`)
106
+ const enabled = sh(dsh, ['--profile', profile, '--patch', overlay, '--dump-config'], env, options.timeout)
107
+ const enabledOK = enabled.status === 0 && !new RegExp(`id:\\s*${rowId}[\\s\\S]{0,80}disabled:\\s*true`).test(enabled.stdout)
108
+ add('J2.enable', enabledOK ? 'PASS' : 'FAIL', enabledOK ? `row ${rowId} enabled again` : `enable exit=${enabled.status}`, { stderr: enabled.stderr.slice(0, 500) })
109
+ }
110
+
111
+ const remove = sh(dsh, ['plugin', '--profile', profile, 'remove', manifest.name], env, options.timeout)
112
+ add('J1.remove', remove.status === 0 ? 'PASS' : 'FAIL', `remove exit=${remove.status}`, { stderr: (remove.stderr || remove.stdout).slice(0, 800) })
113
+ const dump2 = sh(dsh, ['--profile', profile, '--dump-config'], env, options.timeout)
114
+ const gone = !dump2.stdout.includes(manifest.name)
115
+ add('J1.clean', remove.status === 0 && gone ? 'PASS' : 'FAIL', gone ? 'layer/dependency gone after remove' : 'still present after remove')
116
+
117
+ // J4: with no API key, the tree must load far enough to reach the model
118
+ // boundary. This is a tree-load proof, not a successful first task.
119
+ if (process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY) {
120
+ add('J4.tree-load', 'not-run', 'an API key is present in the launching environment')
121
+ } else {
122
+ const boot = sh(dsh, ['plugin', '--profile', profile, 'add', pkg], env, options.timeout)
123
+ if (boot.status !== 0) add('J4.tree-load', 'FAIL', `reinstall exit=${boot.status}`, { stderr: boot.stderr.slice(0, 500) })
124
+ else {
125
+ const run = sh(dsh, ['--profile', profile, 'Reply with one short sentence.'], env, 60_000)
126
+ const text = `${run.stdout}${run.stderr}`
127
+ const reached = run.status !== 0 && /MISSING_CREDENTIAL|no API key|AUTH|credential/i.test(text)
128
+ add('J4.tree-load', reached ? 'PASS' : 'FAIL', reached ? 'keyless boot reached the credential boundary' : `boot exit=${run.status}; no credential-boundary message`, { stderr: text.slice(0, 500) })
129
+ }
130
+ }
131
+
132
+ // J6: upgrade then rollback. Only meaningful when a previous package is
133
+ // supplied; otherwise it is honestly not-run.
134
+ if (!previous || !previousManifest) {
135
+ add('J6.upgrade', 'not-run', 'no --previous package supplied')
136
+ add('J6.rollback', 'not-run', 'no --previous package supplied')
137
+ } else {
138
+ const u1 = sh(dsh, ['plugin', '--profile', profile, 'add', previous], env, options.timeout)
139
+ const v1 = installedVersion(home, profile, manifest.name)
140
+ add('J6.upgrade', u1.status === 0 && v1 === previousManifest.version ? 'PASS' : 'FAIL', `upgrade ${previousManifest.version}->${manifest.version} exit=${u1.status}, installed=${v1}`, { stderr: (u1.stderr || u1.stdout).slice(0, 500) })
141
+ const u2 = sh(dsh, ['plugin', '--profile', profile, 'add', pkg], env, options.timeout)
142
+ const v2 = installedVersion(home, profile, manifest.name)
143
+ add('J6.upgrade-current', u2.status === 0 && v2 === manifest.version ? 'PASS' : 'FAIL', `current install exit=${u2.status}, installed=${v2}`, { stderr: (u2.stderr || u2.stdout).slice(0, 500) })
144
+ const u3 = sh(dsh, ['plugin', '--profile', profile, 'add', previous], env, options.timeout)
145
+ const v3 = installedVersion(home, profile, manifest.name)
146
+ add('J6.rollback', u3.status === 0 && v3 === previousManifest.version ? 'PASS' : 'FAIL', `rollback exit=${u3.status}, installed=${v3}`, { stderr: (u3.stderr || u3.stdout).slice(0, 500) })
147
+ }
148
+
149
+ for (const [id, reason] of [
150
+ ['J3.config', 'invalid-config harness not implemented; do not claim J3'],
151
+ ['J5.permissions', 'permissions/disclosure review is manual'],
152
+ ['J7.observe', 'observability probe not implemented'],
153
+ ['J8.client', 'client-shape probe not implemented'],
154
+ ]) add(id, 'not-run', reason)
155
+ } finally {
156
+ try { fs.rmSync(home, { recursive: true, force: true }) } catch { /* ignore */ }
157
+ }
158
+ const result = {
159
+ schema: 'dsh-flow-runtime/1',
160
+ environment: { dsh: dshVersion, node: process.version, platform: process.platform },
161
+ package: manifest.name,
162
+ version: manifest.version,
163
+ previous: previousManifest ? previousManifest.version : null,
164
+ mode: 'j1-j2-j4-j6-runtime',
165
+ checks,
166
+ summary: {
167
+ pass: checks.filter(c => c.result === 'PASS').length,
168
+ warn: checks.filter(c => c.result === 'WARN').length,
169
+ fail: checks.filter(c => c.result === 'FAIL').length,
170
+ blocked: checks.filter(c => c.result === 'BLOCKED').length,
171
+ notRun: checks.filter(c => c.result === 'not-run').length,
172
+ },
173
+ conclusion: checks.some(c => c.result === 'FAIL') ? 'flow-fail' : checks.some(c => c.result === 'BLOCKED') ? 'flow-blocked' : 'flow-warn',
174
+ }
175
+ if (options.json || !process.stdout.isTTY) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
176
+ process.exitCode = result.conclusion === 'flow-fail' ? 1 : 0
177
+ }
178
+ try { main() } catch (error) { console.error(`journey-runtime: ${error.message}`); process.exitCode = 3 }
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * DSH-FLOW static journey prep: J1-J8 as far as possible without a host.
4
+ * Runtime install/boot journeys are reported not-run, never PASS.
5
+ *
6
+ * Usage: node scripts/journey.mjs --package <dir> [--json]
7
+ */
8
+ import fs from 'node:fs'
9
+ import path from 'node:path'
10
+
11
+ function parse(argv) {
12
+ const out = { package: null, json: false }
13
+ for (let i = 0; i < argv.length; i += 1) {
14
+ if (argv[i] === '--package') out.package = argv[++i]
15
+ else if (argv[i] === '--json') out.json = true
16
+ else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: node scripts/journey.mjs --package <dir> [--json]'); process.exit(0) }
17
+ else throw new Error(`unknown option: ${argv[i]}`)
18
+ }
19
+ if (!out.package) throw new Error('--package is required')
20
+ return out
21
+ }
22
+
23
+ const read = file => { try { return fs.readFileSync(file, 'utf8') } catch { return '' } }
24
+ const has = (text, re) => re.test(text)
25
+
26
+ function main() {
27
+ const { package: pkg, json } = parse(process.argv.slice(2))
28
+ const dir = path.resolve(pkg)
29
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
30
+ const readme = read(path.join(dir, 'README.md'))
31
+ const sources = ['index.js', 'lib/index.js', 'src/index.ts', 'src/index.js', 'dist/index.js']
32
+ .map(file => read(path.join(dir, file))).join('\n')
33
+ const patchFiles = manifest.dsh?.bundle?.patch
34
+ ? (Array.isArray(manifest.dsh.bundle.patch) ? manifest.dsh.bundle.patch : [manifest.dsh.bundle.patch])
35
+ : []
36
+ const patch = patchFiles.map(file => read(path.join(dir, file))).join('\n')
37
+ const hasClient = Boolean(manifest.dsh?.client)
38
+ const hasWorkspace = JSON.stringify(manifest).includes('workspace:')
39
+ const hasFiles = Array.isArray(manifest.files) && manifest.files.length > 0
40
+ const hasEngines = Boolean(manifest.engines?.dsh)
41
+
42
+ const checks = []
43
+ const add = (id, result, reason) => checks.push({ id, result, reason })
44
+ add('J1.install', !hasWorkspace && (manifest.main || manifest.exports) && hasFiles ? 'PASS' : hasWorkspace ? 'FAIL' : 'WARN', hasWorkspace ? 'workspace: in manifest' : hasFiles ? 'entry + files present' : 'files[] missing (prebuilt tarball may omit)')
45
+ add('J2.toggle', patch.includes('disabled') || hasClient ? 'PASS' : 'WARN', patch.includes('disabled') ? 'patch has disabled' : 'no disabled/stable toggle evidence')
46
+ add('J3.config', has(sources, /Config|Schema|schemastery/) ? 'PASS' : 'WARN', has(sources, /Config|Schema|schemastery/) ? 'schema found' : 'no schema/Config found in known entries')
47
+ add('J4.first-run', has(readme, /quickstart|getting started|安装|快速开始/i) ? 'PASS' : 'WARN', has(readme, /quickstart|getting started|安装|快速开始/i) ? 'README quickstart' : 'README quickstart missing')
48
+ add('J5.permissions', manifest.dsh?.capabilities || manifest.dsh?.labels || has(readme, /permission|权限/i) ? 'PASS' : 'WARN', 'permissions declaration/README')
49
+ add('J6.upgrade', hasEngines && hasFiles ? 'PASS' : 'WARN', hasEngines ? 'engines + files' : 'engines.dsh missing')
50
+ add('J7.observe', has(sources, /logger|ctx\.logger/) || has(readme, /troubleshoot|日志|log/i) ? 'PASS' : 'WARN', 'logger/README troubleshooting')
51
+ add('J8.client', hasClient ? (typeof manifest.exports?.['./client'] === 'string' ? 'PASS' : 'FAIL') : 'NA', hasClient ? 'client export present' : 'no dsh.client')
52
+ add('J1.runtime', 'not-run', 'requires temp DSH_HOME + real dsh plugin add/remove')
53
+ add('J2.runtime', 'not-run', 'requires real profile boot')
54
+ add('J4.runtime', 'not-run', 'requires real first-run')
55
+ add('J6.runtime', 'not-run', 'requires real upgrade/rollback')
56
+ add('J8.runtime', 'not-run', 'requires browser/host page')
57
+
58
+ const result = {
59
+ schema: 'dsh-flow-journey/1',
60
+ package: manifest.name,
61
+ version: manifest.version,
62
+ mode: 'static-prep',
63
+ checks,
64
+ summary: {
65
+ pass: checks.filter(c => c.result === 'PASS').length,
66
+ warn: checks.filter(c => c.result === 'WARN').length,
67
+ fail: checks.filter(c => c.result === 'FAIL').length,
68
+ na: checks.filter(c => c.result === 'NA').length,
69
+ notRun: checks.filter(c => c.result === 'not-run').length,
70
+ },
71
+ conclusion: 'flow-not-run',
72
+ }
73
+ if (json || !process.stdout.isTTY) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
74
+ process.exitCode = result.summary.fail > 0 ? 1 : 0
75
+ }
76
+ try { main() } catch (error) { console.error(`journey: ${error.message}`); process.exitCode = 3 }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /** label-drift — validate a published label index, hashes, expiry, and forbidden fields. */
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { effectiveStatus, validateIndex } from '../src/label.mjs'
7
+
8
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
9
+ const STD = path.resolve(HERE, '..', '..')
10
+
11
+ function usage() {
12
+ console.log(`label-drift — check a label index for schema/hash/expiry/forbidden-word drift
13
+
14
+ Usage:
15
+ node 40-checker/scripts/label-drift.mjs --index <index.json> [--now YYYY-MM-DD] [--json]
16
+
17
+ Exit: 0 no issues, 1 drift found, 3 input error.`)
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const options = { index: path.join(STD, 'publish', 'labels', 'index.json'), now: new Date().toISOString().slice(0, 10), json: false }
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i]
24
+ const next = () => {
25
+ const value = argv[i + 1]
26
+ if (value === undefined) throw new Error(`${arg} needs a value`)
27
+ i += 1
28
+ return value
29
+ }
30
+ if (arg === '--index') options.index = path.resolve(next())
31
+ else if (arg === '--now') options.now = next()
32
+ else if (arg === '--json') options.json = true
33
+ else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
34
+ else throw new Error(`unknown argument: ${arg}`)
35
+ }
36
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(options.now)) throw new Error('--now must be YYYY-MM-DD')
37
+ return options
38
+ }
39
+
40
+ function main() {
41
+ const options = parseArgs(process.argv.slice(2))
42
+ const index = JSON.parse(fs.readFileSync(options.index, 'utf8'))
43
+ const validation = validateIndex(index)
44
+ const issues = [...validation.errors]
45
+ const now = new Date(`${options.now}T00:00:00Z`)
46
+ let stale = 0
47
+ let withdrawn = 0
48
+ for (const [i, record] of (index.records ?? []).entries()) {
49
+ const effective = effectiveStatus(record, now)
50
+ if (effective === 'stale') { stale += 1; issues.push(`records[${i}]: stale at ${options.now}`) }
51
+ if (effective === 'withdrawn') withdrawn += 1
52
+ }
53
+ const result = {
54
+ schema: 'dsh-label-drift/1',
55
+ checkedAt: options.now,
56
+ index: path.relative(STD, options.index),
57
+ total: (index.records ?? []).length,
58
+ stale,
59
+ withdrawn,
60
+ issues,
61
+ ok: issues.length === 0,
62
+ }
63
+ if (options.json) console.log(JSON.stringify(result, null, 2))
64
+ else {
65
+ console.log(`label-drift ${result.ok ? 'PASS' : 'FAIL'} — ${result.total} record(s), stale ${stale}, withdrawn ${withdrawn}`)
66
+ for (const issue of issues) console.log(` - ${issue}`)
67
+ }
68
+ process.exit(result.ok ? 0 : 1)
69
+ }
70
+
71
+ try { main() } catch (error) {
72
+ console.error(`label-drift: ${error.message}`)
73
+ process.exit(3)
74
+ }
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /** label-sync — validate immutable labels, compute recordHash, emit a static index. */
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { labelId, normalizeLabel, validateLabel } from '../src/label.mjs'
7
+
8
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
9
+ const STD = path.resolve(HERE, '..', '..')
10
+
11
+ function usage() {
12
+ console.log(`label-sync — validate labels and write publish/labels/<id>.json + index.json
13
+
14
+ Usage:
15
+ node 40-checker/scripts/label-sync.mjs --source <labels.json> [--out <dir>] [--check]
16
+
17
+ Source: an array of labels, or { "records": [ ... ] }.
18
+ --check validates and prints a summary without writing.
19
+ Exit: 0 valid, 1 invalid, 3 input error.`)
20
+ }
21
+
22
+ function parseArgs(argv) {
23
+ const options = { source: null, out: path.join(STD, 'publish', 'labels'), check: false }
24
+ for (let i = 0; i < argv.length; i += 1) {
25
+ const arg = argv[i]
26
+ const next = () => {
27
+ const value = argv[i + 1]
28
+ if (value === undefined) throw new Error(`${arg} needs a value`)
29
+ i += 1
30
+ return value
31
+ }
32
+ if (arg === '--source') options.source = next()
33
+ else if (arg === '--out') options.out = path.resolve(next())
34
+ else if (arg === '--check') options.check = true
35
+ else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
36
+ else throw new Error(`unknown argument: ${arg}`)
37
+ }
38
+ if (!options.source) throw new Error('--source is required')
39
+ options.source = path.resolve(options.source)
40
+ return options
41
+ }
42
+
43
+ function main() {
44
+ const options = parseArgs(process.argv.slice(2))
45
+ const raw = JSON.parse(fs.readFileSync(options.source, 'utf8'))
46
+ const records = Array.isArray(raw) ? raw : Array.isArray(raw.records) ? raw.records : null
47
+ if (!records) throw new Error('source must be an array or { records: [...] }')
48
+ const errors = []
49
+ const out = []
50
+ for (const [index, record] of records.entries()) {
51
+ if (!record || typeof record !== 'object') { errors.push(`[${index}] not an object`); continue }
52
+ const provided = record.recordHash
53
+ const normalized = normalizeLabel(record)
54
+ const validation = validateLabel(normalized)
55
+ if (provided && provided !== normalized.recordHash) errors.push(`[${index}] recordHash does not match canonical record`)
56
+ for (const error of validation.errors) errors.push(`[${index}] ${error}`)
57
+ if (validation.valid) out.push(normalized)
58
+ }
59
+ if (errors.length > 0) {
60
+ console.error(`label-sync: ${errors.length} error(s)`)
61
+ for (const error of errors) console.error(` - ${error}`)
62
+ process.exit(1)
63
+ }
64
+ const index = {
65
+ schema: 'dsh-label-index/1',
66
+ generatedAt: new Date().toISOString().slice(0, 10),
67
+ source: path.relative(STD, options.source),
68
+ spec: { labels: 'dsh-market-label/1', checklist: 'dsh-entry-checklist/1' },
69
+ records: out,
70
+ }
71
+ if (options.check) {
72
+ console.log(`PASS label-sync: ${out.length} record(s); no files written (--check)`)
73
+ return
74
+ }
75
+ fs.mkdirSync(options.out, { recursive: true })
76
+ for (const record of out) {
77
+ const file = path.join(options.out, `${labelId(record)}.json`)
78
+ fs.writeFileSync(file, `${JSON.stringify(record, null, 2)}\n`)
79
+ }
80
+ fs.writeFileSync(path.join(options.out, 'index.json'), `${JSON.stringify(index, null, 2)}\n`)
81
+ console.log(`PASS label-sync: ${out.length} record(s) -> ${path.relative(STD, options.out)}`)
82
+ }
83
+
84
+ try { main() } catch (error) {
85
+ console.error(`label-sync: ${error.message}`)
86
+ process.exit(3)
87
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /** label-verify — look up one immutable label by recordHash or id; no grade, no ranking. */
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { effectiveStatus, labelId, validateLabel } from '../src/label.mjs'
7
+
8
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
9
+ const STD = path.resolve(HERE, '..', '..')
10
+
11
+ function usage() {
12
+ console.log(`label-verify — look up one label without grades or rankings
13
+
14
+ Usage:
15
+ node 40-checker/scripts/label-verify.mjs --index <index.json> (--hash sha256:... | --id <id>) [--json]
16
+
17
+ Exit: 0 found and valid, 2 not found/invalid, 3 input error.`)
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const options = { index: path.join(STD, 'publish', 'labels', 'index.json'), hash: null, id: null, json: false }
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i]
24
+ const next = () => {
25
+ const value = argv[i + 1]
26
+ if (value === undefined) throw new Error(`${arg} needs a value`)
27
+ i += 1
28
+ return value
29
+ }
30
+ if (arg === '--index') options.index = path.resolve(next())
31
+ else if (arg === '--hash') options.hash = next()
32
+ else if (arg === '--id') options.id = next()
33
+ else if (arg === '--json') options.json = true
34
+ else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
35
+ else throw new Error(`unknown argument: ${arg}`)
36
+ }
37
+ if (!options.hash && !options.id) throw new Error('--hash or --id is required')
38
+ return options
39
+ }
40
+
41
+ function main() {
42
+ const options = parseArgs(process.argv.slice(2))
43
+ const index = JSON.parse(fs.readFileSync(options.index, 'utf8'))
44
+ const records = Array.isArray(index.records) ? index.records : []
45
+ const record = options.hash
46
+ ? records.find(row => row.recordHash === options.hash)
47
+ : records.find(row => labelId(row) === options.id)
48
+ if (!record) {
49
+ console.error(`label-verify: not found (${options.hash ?? options.id})`)
50
+ process.exit(2)
51
+ }
52
+ const validation = validateLabel(record)
53
+ if (!validation.valid) {
54
+ console.error(`label-verify: invalid record: ${validation.errors.join('; ')}`)
55
+ process.exit(2)
56
+ }
57
+ const summary = {
58
+ schema: 'dsh-label-verify/1',
59
+ ok: true,
60
+ id: labelId(record),
61
+ recordHash: record.recordHash,
62
+ subject: record.subject,
63
+ level: record.level,
64
+ status: record.status,
65
+ effectiveStatus: effectiveStatus(record),
66
+ claimLevel: record.claimLevel,
67
+ scope: record.scope,
68
+ validity: record.validity,
69
+ evidence: record.evidence,
70
+ disclaimer: record.disclaimer,
71
+ issuer: record.issuer,
72
+ verifyUrl: record.verifyUrl,
73
+ notClaimed: ['official standard', 'certification', 'security audit', 'ranking', 'market acceptance'],
74
+ }
75
+ if (options.json) console.log(JSON.stringify(summary, null, 2))
76
+ else {
77
+ console.log(`${summary.id} ${summary.level}/${summary.effectiveStatus}`)
78
+ console.log(`subject: ${summary.subject.repo}@${summary.subject.version} (${summary.subject.commit.slice(0, 12)})`)
79
+ console.log(`claimLevel: ${summary.claimLevel}; issuer: ${summary.issuer}`)
80
+ console.log(`validity: ${summary.validity.generatedAt} -> ${summary.validity.expiresAt}`)
81
+ console.log(`evidence: ${summary.evidence[0]?.url ?? 'none'}`)
82
+ console.log(`disclaimer: ${summary.disclaimer}`)
83
+ }
84
+ process.exit(0)
85
+ }
86
+
87
+ try { main() } catch (error) {
88
+ console.error(`label-verify: ${error.message}`)
89
+ process.exit(3)
90
+ }
@@ -0,0 +1,101 @@
1
+ // Minimal Landlock launcher: allow read/execute everywhere, read-write only
2
+ // under explicitly listed directories, then exec the target.
3
+ //
4
+ // Linux 5.13+ (Landlock ABI 1); uses TRUNCATE/REFER when the ABI supports them.
5
+ // Build: cc -O2 scripts/landlock-exec.c -o /tmp/landlock-exec
6
+ // Usage: landlock-exec --rw <dir> [--rw <dir> ...] -- <command> [args...]
7
+ #define _GNU_SOURCE
8
+ #include <linux/landlock.h>
9
+ #include <sys/prctl.h>
10
+ #include <sys/syscall.h>
11
+ #include <sys/stat.h>
12
+ #include <fcntl.h>
13
+ #include <unistd.h>
14
+ #include <stdio.h>
15
+ #include <stdlib.h>
16
+ #include <string.h>
17
+ #include <errno.h>
18
+
19
+ #ifndef LANDLOCK_ACCESS_FS_REFER
20
+ #define LANDLOCK_ACCESS_FS_REFER (1ULL << 13)
21
+ #endif
22
+ #ifndef LANDLOCK_ACCESS_FS_TRUNCATE
23
+ #define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14)
24
+ #endif
25
+
26
+ static long landlock_abi(void) {
27
+ return syscall(SYS_landlock_create_ruleset, NULL, 0, LANDLOCK_CREATE_RULESET_VERSION);
28
+ }
29
+
30
+ static int add_rule(int ruleset_fd, const char *path, __u64 access) {
31
+ int parent = open(path, O_PATH | O_CLOEXEC);
32
+ if (parent < 0) {
33
+ fprintf(stderr, "landlock-exec: open %s: %s\n", path, strerror(errno));
34
+ return -1;
35
+ }
36
+ struct landlock_path_beneath_attr attr;
37
+ memset(&attr, 0, sizeof(attr));
38
+ attr.allowed_access = access;
39
+ attr.parent_fd = parent;
40
+ int rc = syscall(SYS_landlock_add_rule, ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, &attr, 0);
41
+ if (rc != 0) fprintf(stderr, "landlock-exec: add_rule %s: %s\n", path, strerror(errno));
42
+ close(parent);
43
+ return rc;
44
+ }
45
+
46
+ int main(int argc, char **argv) {
47
+ long abi = landlock_abi();
48
+ if (abi < 1) {
49
+ fprintf(stderr, "landlock-exec: Landlock unavailable (abi=%ld, errno=%s)\n", abi, strerror(errno));
50
+ return 3;
51
+ }
52
+
53
+ __u64 handled = LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_WRITE_FILE |
54
+ LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR |
55
+ LANDLOCK_ACCESS_FS_REMOVE_DIR | LANDLOCK_ACCESS_FS_REMOVE_FILE |
56
+ LANDLOCK_ACCESS_FS_MAKE_CHAR | LANDLOCK_ACCESS_FS_MAKE_DIR |
57
+ LANDLOCK_ACCESS_FS_MAKE_REG | LANDLOCK_ACCESS_FS_MAKE_SOCK |
58
+ LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK |
59
+ LANDLOCK_ACCESS_FS_MAKE_SYM;
60
+ if (abi >= 2) handled |= LANDLOCK_ACCESS_FS_REFER;
61
+ if (abi >= 3) handled |= LANDLOCK_ACCESS_FS_TRUNCATE;
62
+
63
+ struct landlock_ruleset_attr ruleset = { .handled_access_fs = handled };
64
+ int ruleset_fd = syscall(SYS_landlock_create_ruleset, &ruleset, sizeof(ruleset), 0);
65
+ if (ruleset_fd < 0) {
66
+ fprintf(stderr, "landlock-exec: create_ruleset: %s\n", strerror(errno));
67
+ return 3;
68
+ }
69
+
70
+ // Read + execute everywhere; write access is denied unless a --rw rule follows.
71
+ if (add_rule(ruleset_fd, "/", LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR) != 0) return 3;
72
+
73
+ int i = 1;
74
+ int saw_separator = 0;
75
+ for (; i < argc; i++) {
76
+ if (strcmp(argv[i], "--") == 0) { saw_separator = 1; i++; break; }
77
+ if (strcmp(argv[i], "--rw") != 0 || i + 1 >= argc) {
78
+ fprintf(stderr, "usage: landlock-exec --rw <dir> [--rw <dir> ...] -- <command> [args...]\n");
79
+ return 3;
80
+ }
81
+ if (add_rule(ruleset_fd, argv[++i], handled) != 0) return 3;
82
+ }
83
+ if (!saw_separator || i >= argc) {
84
+ fprintf(stderr, "usage: landlock-exec --rw <dir> [--rw <dir> ...] -- <command> [args...]\n");
85
+ return 3;
86
+ }
87
+
88
+ if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) != 0) {
89
+ fprintf(stderr, "landlock-exec: PR_SET_NO_NEW_PRIVS: %s\n", strerror(errno));
90
+ return 3;
91
+ }
92
+ if (syscall(SYS_landlock_restrict_self, ruleset_fd, 0) != 0) {
93
+ fprintf(stderr, "landlock-exec: restrict_self: %s\n", strerror(errno));
94
+ return 3;
95
+ }
96
+ close(ruleset_fd);
97
+
98
+ execvp(argv[i], &argv[i]);
99
+ fprintf(stderr, "landlock-exec: exec %s: %s\n", argv[i], strerror(errno));
100
+ return 127;
101
+ }