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,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* attestation-check.mjs — validate DSH attestation JSON.
|
|
4
|
+
*
|
|
5
|
+
* The attestation is a quality-grade document (A-E evidence, A-D grade). It is
|
|
6
|
+
* orthogonal to the adoption levels: a `level` field is a hard error.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node 40-checker/scripts/attestation-check.mjs --dir publish/attestations
|
|
10
|
+
* node 40-checker/scripts/attestation-check.mjs --file path/to/cert.json --json
|
|
11
|
+
*/
|
|
12
|
+
import fs from 'node:fs'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const DEFAULT_ROOT = path.resolve(HERE, '..', '..')
|
|
18
|
+
const STATUS = new Set(['draft', 'active', 'suspended', 'withdrawn', 'expired'])
|
|
19
|
+
const GRADES = new Set(['A', 'B', 'C', 'D'])
|
|
20
|
+
const DIMENSIONS = new Set(['A', 'B', 'C', 'D', 'E'])
|
|
21
|
+
|
|
22
|
+
function date(value) {
|
|
23
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateAttestation(cert) {
|
|
27
|
+
const errors = []
|
|
28
|
+
const warn = (m) => errors.push(`WARN ${m}`)
|
|
29
|
+
const err = (m) => errors.push(m)
|
|
30
|
+
if (!cert || typeof cert !== 'object') return { errors: ['attestation must be an object'] }
|
|
31
|
+
if (cert.schema !== 'dsh-attestation/1') err('schema must be dsh-attestation/1')
|
|
32
|
+
if ('level' in cert) err('attestation must not contain a level field (A-D is orthogonal to ENTRY/FLOW/BOOST)')
|
|
33
|
+
if (!/^DSH-ATT-\d{4}-\d{3}$/.test(cert.attestationId || '')) err('attestationId must match DSH-ATT-YYYY-NNN')
|
|
34
|
+
if (!cert.subject || !cert.subject.repo || !cert.subject.version) err('subject.repo and subject.version are required')
|
|
35
|
+
if (!GRADES.has(cert.grade)) err('grade must be A, B, C, or D')
|
|
36
|
+
if (!['self-reported', 'verified', 'peer-reviewed'].includes(cert.claimLevel)) {
|
|
37
|
+
err('claimLevel must be self-reported, verified, or peer-reviewed')
|
|
38
|
+
}
|
|
39
|
+
if (cert.selfAssessed === true && cert.claimLevel !== 'self-reported') {
|
|
40
|
+
err('a self-assessed record cannot claim verified or peer-reviewed')
|
|
41
|
+
}
|
|
42
|
+
if (!STATUS.has(cert.status)) err('status must be draft|active|suspended|withdrawn|expired')
|
|
43
|
+
if (!date(cert.issuedAt)) err('issuedAt must be YYYY-MM-DD')
|
|
44
|
+
if (!date(cert.expiresAt)) err('expiresAt must be YYYY-MM-DD')
|
|
45
|
+
if (date(cert.issuedAt) && date(cert.expiresAt) && Date.parse(cert.expiresAt) <= Date.parse(cert.issuedAt)) {
|
|
46
|
+
err('expiresAt must be after issuedAt')
|
|
47
|
+
}
|
|
48
|
+
if (date(cert.expiresAt) && Date.parse(cert.expiresAt) < Date.now() && cert.status === 'active') {
|
|
49
|
+
err('an expired attestation cannot be active; set status=expired')
|
|
50
|
+
}
|
|
51
|
+
if (!cert.scope || !Array.isArray(cert.scope.dimensions) || cert.scope.dimensions.length === 0) {
|
|
52
|
+
err('scope.dimensions must list A-E')
|
|
53
|
+
} else {
|
|
54
|
+
for (const dimension of cert.scope.dimensions) if (!DIMENSIONS.has(dimension)) err(`invalid scope dimension ${dimension}`)
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(cert.scope?.excluded) || cert.scope.excluded.length === 0) {
|
|
57
|
+
err('scope.excluded must state what the attestation does NOT cover')
|
|
58
|
+
}
|
|
59
|
+
if (!cert.scheme || cert.scheme.id !== 'dsh-certification' || !cert.scheme.version) err('scheme.id/version required')
|
|
60
|
+
if (!cert.assessor || !cert.assessor.handle) err('assessor.handle required')
|
|
61
|
+
if (cert.assessor && !['organizer', 'maintainer', 'reviewer', 'external-reviewer'].includes(cert.assessor.role)) {
|
|
62
|
+
err('assessor.role is invalid')
|
|
63
|
+
}
|
|
64
|
+
if (cert.assessor && !Array.isArray(cert.assessor.coi)) err('assessor.coi must be an array')
|
|
65
|
+
if (typeof cert.selfAssessed !== 'boolean') err('selfAssessed must be boolean')
|
|
66
|
+
if (cert.selfAssessed === true && !(cert.assessor?.coi || []).some((s) => /self|author|own/i.test(s))) {
|
|
67
|
+
warn('selfAssessed is true but assessor.coi does not mention the self/author conflict')
|
|
68
|
+
}
|
|
69
|
+
if (typeof cert.disclaimer !== 'string' || cert.disclaimer.length < 12) err('disclaimer must be a real sentence')
|
|
70
|
+
else if (!/非官方|not official|非认证|not a certification/i.test(cert.disclaimer)) {
|
|
71
|
+
err('disclaimer must say non-official and/or not a certification')
|
|
72
|
+
}
|
|
73
|
+
if (!/^https:\/\//.test(cert.verifyUrl || '')) err('verifyUrl must be https')
|
|
74
|
+
if (!Array.isArray(cert.evidence) || cert.evidence.length === 0) err('evidence must not be empty')
|
|
75
|
+
const ids = new Set()
|
|
76
|
+
for (const item of cert.evidence || []) {
|
|
77
|
+
if (!DIMENSIONS.has(item.id)) err(`evidence id ${item.id} must be A-E`)
|
|
78
|
+
if (ids.has(item.id)) err(`duplicate evidence id ${item.id}`)
|
|
79
|
+
ids.add(item.id)
|
|
80
|
+
if (!['PASS', 'FAIL', 'WARN', 'NOT-RUN'].includes(item.result)) err(`evidence ${item.id} result is invalid`)
|
|
81
|
+
if (!item.url && !item.sha256) err(`evidence ${item.id} needs a url or sha256`)
|
|
82
|
+
if (item.url && !/^https:\/\//.test(item.url)) err(`evidence ${item.id} url must be https`)
|
|
83
|
+
}
|
|
84
|
+
for (const item of cert.evidence || []) {
|
|
85
|
+
if (item.result === 'PASS' && !item.url && !item.sha256) err(`evidence ${item.id} cannot PASS without an artifact`)
|
|
86
|
+
}
|
|
87
|
+
return { errors: errors.filter((e) => !e.startsWith('WARN ')), warnings: errors.filter((e) => e.startsWith('WARN ')).map((e) => e.slice(5)) }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function main(argv = process.argv.slice(2)) {
|
|
91
|
+
let root = DEFAULT_ROOT
|
|
92
|
+
let dir = 'publish/attestations'
|
|
93
|
+
let file = null
|
|
94
|
+
let json = false
|
|
95
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
96
|
+
const arg = argv[i]
|
|
97
|
+
if (arg === '--root') root = path.resolve(argv[++i])
|
|
98
|
+
else if (arg === '--dir') dir = argv[++i]
|
|
99
|
+
else if (arg === '--file') file = path.resolve(argv[++i])
|
|
100
|
+
else if (arg === '--json') json = true
|
|
101
|
+
else if (arg === '--help' || arg === '-h') {
|
|
102
|
+
console.log('usage: attestation-check.mjs [--root <dir>] [--dir <dir>] [--file <json>] [--json]')
|
|
103
|
+
return 0
|
|
104
|
+
} else {
|
|
105
|
+
console.error(`unknown argument: ${arg}`)
|
|
106
|
+
return 2
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
let files = []
|
|
110
|
+
if (file) files = [file]
|
|
111
|
+
else {
|
|
112
|
+
const full = path.join(root, dir)
|
|
113
|
+
if (!fs.existsSync(full)) {
|
|
114
|
+
console.error(`attestation-check: directory not found: ${full}`)
|
|
115
|
+
return 2
|
|
116
|
+
}
|
|
117
|
+
files = fs.readdirSync(full).filter((name) => name.endsWith('.json') && name !== 'index.json').map((name) => path.join(full, name)).sort()
|
|
118
|
+
}
|
|
119
|
+
if (files.length === 0) {
|
|
120
|
+
console.error('attestation-check: no attestation JSON found')
|
|
121
|
+
return 2
|
|
122
|
+
}
|
|
123
|
+
const results = []
|
|
124
|
+
let failed = 0
|
|
125
|
+
for (const item of files) {
|
|
126
|
+
try {
|
|
127
|
+
const cert = JSON.parse(fs.readFileSync(item, 'utf8'))
|
|
128
|
+
const result = validateAttestation(cert)
|
|
129
|
+
if (result.errors.length > 0) failed += 1
|
|
130
|
+
results.push({ file: path.relative(root, item), attestationId: cert.attestationId, ...result })
|
|
131
|
+
} catch (error) {
|
|
132
|
+
failed += 1
|
|
133
|
+
results.push({ file: path.relative(root, item), errors: [`parse error: ${error.message}`], warnings: [] })
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (json) {
|
|
137
|
+
console.log(JSON.stringify({ ok: failed === 0, results }, null, 2))
|
|
138
|
+
} else {
|
|
139
|
+
for (const result of results) {
|
|
140
|
+
for (const warning of result.warnings) console.log(`WARN ${result.file}: ${warning}`)
|
|
141
|
+
for (const error of result.errors) console.log(`FAIL ${result.file}: ${error}`)
|
|
142
|
+
console.log(`${result.errors.length === 0 ? 'PASS' : 'FAIL'} ${result.file}`)
|
|
143
|
+
}
|
|
144
|
+
console.log(`${failed === 0 ? 'PASS' : 'FAIL'} attestation: ${results.length} file(s), ${failed} failed`)
|
|
145
|
+
}
|
|
146
|
+
return failed === 0 ? 0 : 2
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
150
|
+
process.exitCode = main()
|
|
151
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Minimal host-backed bundle smoke: G0 patch parse, G1 compose/row ids,
|
|
4
|
+
* G3 real boot target rows. G2 compatibility is reported from the host API.
|
|
5
|
+
*
|
|
6
|
+
* The checker stays zero-dependency: this script is only used when --host
|
|
7
|
+
* points at a built @deepseek-ai/dsh-app-boot/lib/index.js.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/bundle-smoke.mjs --package <dir> --host <app-boot/lib/index.js> \
|
|
11
|
+
* [--base <base/cordis.patch.yml>] [--json] [--timeout 30000]
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import os from 'node:os'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { pathToFileURL } from 'node:url'
|
|
17
|
+
|
|
18
|
+
function parse(argv) {
|
|
19
|
+
const options = { package: null, host: null, base: null, web: null, bare: null, json: false, timeout: 30_000 }
|
|
20
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
21
|
+
const arg = argv[i]
|
|
22
|
+
if (arg === '--package') options.package = argv[++i]
|
|
23
|
+
else if (arg === '--host') options.host = argv[++i]
|
|
24
|
+
else if (arg === '--base') options.base = argv[++i]
|
|
25
|
+
else if (arg === '--web') options.web = argv[++i]
|
|
26
|
+
else if (arg === '--bare') options.bare = argv[++i]
|
|
27
|
+
else if (arg === '--json') options.json = true
|
|
28
|
+
else if (arg === '--timeout') options.timeout = Number(argv[++i])
|
|
29
|
+
else if (arg === '--help' || arg === '-h') { console.log('usage: node scripts/bundle-smoke.mjs --package <dir> --host <app-boot/lib/index.js> [--base <patch>] [--json]'); process.exit(0) }
|
|
30
|
+
else throw new Error(`unknown option: ${arg}`)
|
|
31
|
+
}
|
|
32
|
+
if (!options.package) throw new Error('--package is required')
|
|
33
|
+
if (!options.host) throw new Error('--host is required')
|
|
34
|
+
if (!Number.isFinite(options.timeout) || options.timeout < 1000) throw new Error('--timeout must be >= 1000')
|
|
35
|
+
return options
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function withTimeout(promise, ms, label) {
|
|
39
|
+
let timer
|
|
40
|
+
const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms); timer.unref?.() })
|
|
41
|
+
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const options = parse(process.argv.slice(2))
|
|
46
|
+
const pkg = path.resolve(options.package)
|
|
47
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf8'))
|
|
48
|
+
const host = await import(pathToFileURL(path.resolve(options.host)).href)
|
|
49
|
+
const { loadOverlayPatches, composeEntries, boot, getDshRuntimeVersion, evaluatePluginCompatibility } = host
|
|
50
|
+
if (typeof loadOverlayPatches !== 'function' || typeof composeEntries !== 'function' || typeof boot !== 'function') throw new Error('host does not export loadOverlayPatches/composeEntries/boot')
|
|
51
|
+
const bundle = manifest.dsh?.bundle
|
|
52
|
+
if (!bundle?.patch) throw new Error('manifest has no dsh.bundle.patch')
|
|
53
|
+
const files = Array.isArray(bundle.patch) ? bundle.patch : [bundle.patch]
|
|
54
|
+
const patchPaths = files.map(file => path.resolve(pkg, file))
|
|
55
|
+
const targetPatches = patchPaths.flatMap(file => loadOverlayPatches('dsh', file))
|
|
56
|
+
const basePatches = options.base ? loadOverlayPatches('dsh', path.resolve(options.base)) : []
|
|
57
|
+
const webPatches = options.web ? loadOverlayPatches('dsh', path.resolve(options.web)) : []
|
|
58
|
+
const layers = [basePatches, webPatches, targetPatches].filter(layer => layer.length > 0)
|
|
59
|
+
const composed = composeEntries(layers, message => { warnings.push(message) })
|
|
60
|
+
const baseIds = new Set(composeEntries([basePatches, webPatches].filter(layer => layer.length > 0)).filter(row => row.id).map(row => row.id))
|
|
61
|
+
const targetIds = new Set(composed.filter(row => row.id && !baseIds.has(row.id)).map(row => row.id))
|
|
62
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-bundle-smoke-'))
|
|
63
|
+
const config = path.join(root, 'cordis.yml')
|
|
64
|
+
fs.writeFileSync(config, '[]\n')
|
|
65
|
+
const previousHome = process.env.DSH_HOME
|
|
66
|
+
process.env.DSH_HOME = path.join(root, 'home')
|
|
67
|
+
fs.mkdirSync(process.env.DSH_HOME, { recursive: true })
|
|
68
|
+
let ctx
|
|
69
|
+
let rows = []
|
|
70
|
+
let state = 'failed'
|
|
71
|
+
try {
|
|
72
|
+
const bare = options.bare ? pathToFileURL(path.resolve(options.bare)).href + '/' : pathToFileURL(path.join(root)).href + '/'
|
|
73
|
+
ctx = await withTimeout(boot('bundle-smoke', config, layers.flat(), undefined, bare), options.timeout, 'bundle boot')
|
|
74
|
+
const entries = [...ctx.loader.entries()]
|
|
75
|
+
// A bundle insert is composed under an `include:` group, so the group's
|
|
76
|
+
// fiber can stay null while the inserted plugin row activates. Match the
|
|
77
|
+
// inserted row's package name as well as its id, never only the wrapper.
|
|
78
|
+
const targetNames = new Set(composed
|
|
79
|
+
.filter(row => row.id && !baseIds.has(row.id) && typeof row.name === 'string')
|
|
80
|
+
.map(row => row.name))
|
|
81
|
+
rows = entries.filter(entry => [...targetIds].some(id => entry.id === id || String(entry.id).endsWith(`:${id}`))
|
|
82
|
+
|| targetNames.has(entry.name)).map(entry => ({ id: entry.id, name: entry.name, state: entry.fiber?.state ?? null, disabled: entry.disabled === true }))
|
|
83
|
+
const active = rows.filter(row => row.state === 2 || row.disabled).length
|
|
84
|
+
state = rows.length > 0 && active === rows.length ? 'verified' : rows.length > 0 ? 'partial' : 'failed'
|
|
85
|
+
} finally {
|
|
86
|
+
try { await ctx?.dispose?.() } catch { /* ignore */ }
|
|
87
|
+
if (previousHome === undefined) delete process.env.DSH_HOME
|
|
88
|
+
else process.env.DSH_HOME = previousHome
|
|
89
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
90
|
+
}
|
|
91
|
+
const compatibility = typeof evaluatePluginCompatibility === 'function' && typeof getDshRuntimeVersion === 'function'
|
|
92
|
+
? (() => { try { return evaluatePluginCompatibility(manifest, {}, getDshRuntimeVersion()) === undefined ? 'admit' : 'reject' } catch { return 'error' } })()
|
|
93
|
+
: 'unknown'
|
|
94
|
+
const hasClient = Boolean(manifest.dsh?.client)
|
|
95
|
+
const clientEntry = manifest.exports?.['./client']
|
|
96
|
+
const clientExport = typeof clientEntry === 'string'
|
|
97
|
+
|| (typeof clientEntry === 'object' && clientEntry !== null && typeof clientEntry.default === 'string')
|
|
98
|
+
// G4 is a composition property: the package's own root row must exist in
|
|
99
|
+
// the composed layer. Runtime `entries` names an `include:` wrapper instead,
|
|
100
|
+
// so reading only activated entries produced false negatives for bundles
|
|
101
|
+
// whose insert is wrapped in an include group.
|
|
102
|
+
const rootRows = composed.filter(row => row.name === manifest.name || row.name === '.' || row.name === './')
|
|
103
|
+
const result = {
|
|
104
|
+
schema: 'dsh-bundle-smoke/1',
|
|
105
|
+
package: manifest.name,
|
|
106
|
+
version: manifest.version,
|
|
107
|
+
g0: { patchFiles: patchPaths.length, parsed: true },
|
|
108
|
+
g1: { rows: composed.length, targetRows: targetIds.size, warnings },
|
|
109
|
+
g2: { compatibility },
|
|
110
|
+
g3: { state, rows, active: rows.filter(row => row.state === 2 || row.disabled).length },
|
|
111
|
+
g4: { hasClient, clientExport, rootRows, ok: !hasClient || (clientExport && rootRows.length > 0) },
|
|
112
|
+
}
|
|
113
|
+
const output = JSON.stringify(result, null, 2)
|
|
114
|
+
const exitCode = state === 'verified' ? 0 : state === 'partial' ? 2 : 1
|
|
115
|
+
// A real boot can leave host-owned timers, sockets, or workers referenced
|
|
116
|
+
// after dispose. This smoke owns the whole process, so terminate once the
|
|
117
|
+
// result is flushed instead of making the caller wait for a handle it cannot
|
|
118
|
+
// see. Without this, a completed boot looks like a timeout to CI.
|
|
119
|
+
process.stdout.write(`${output}\n`, () => process.exit(exitCode))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const warnings = []
|
|
123
|
+
try { await main() } catch (error) {
|
|
124
|
+
console.error(`bundle-smoke: ${error.message}`)
|
|
125
|
+
process.exit(3)
|
|
126
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* claim-check.mjs — conservative scan for mark misuse in public artifacts.
|
|
4
|
+
*
|
|
5
|
+
* It only scans market-facing directories by default (publish/, adoption/) plus
|
|
6
|
+
* README.md. Documentation that *quotes* forbidden words (governance, research
|
|
7
|
+
* notes) is intentionally out of scope.
|
|
8
|
+
*
|
|
9
|
+
* A line fails when it contains a forbidden claim word and no negation, e.g.
|
|
10
|
+
* "DSH-ENTRY official certification" fails, while "not official" passes.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node 40-checker/scripts/claim-check.mjs [--root <dir>] [--json]
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'node:fs'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
|
|
19
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
20
|
+
const DEFAULT_ROOT = path.resolve(HERE, '..', '..')
|
|
21
|
+
const MARKS = /\bDSH-(?:ENTRY|FLOW|BOOST)\b/i
|
|
22
|
+
const FORBIDDEN = [
|
|
23
|
+
/\bofficial\b/i,
|
|
24
|
+
/\bnational standard\b/i,
|
|
25
|
+
/\bCNAS\b/i,
|
|
26
|
+
/\baccredited\b/i,
|
|
27
|
+
/\bcertified\b/i,
|
|
28
|
+
/\bsecurity audit\b/i,
|
|
29
|
+
/\brecommended\b/i,
|
|
30
|
+
/\branking\b/i,
|
|
31
|
+
/\bguaranteed\b/i,
|
|
32
|
+
/\bapproved by deepseek\b/i,
|
|
33
|
+
/国家标准/,
|
|
34
|
+
/官方认证/,
|
|
35
|
+
/国家认可/,
|
|
36
|
+
/安全审计/,
|
|
37
|
+
/推荐标准/,
|
|
38
|
+
/排名/,
|
|
39
|
+
]
|
|
40
|
+
const NEGATION = /\bnot\b|\bno\b|\bnever\b|\bnon-|\bforbidden\b|\bprohibit|\bdoes not\b|\bcannot\b|非官方|非认证|不是|不得|禁止|不应|不能|不卖|不做|不把|不称|不提供|不收费|不推荐/i
|
|
41
|
+
const SKIP_JSON_KEYS = new Set(['forbiddenWords', 'notClaimed', 'excluded', 'forbidden', 'limitations', 'mustNot', 'cannot'])
|
|
42
|
+
// "official team" is an audience, not a claim about the mark. Strip it before scanning.
|
|
43
|
+
const ALLOWED_CONTEXT = /\bofficial (?:team|adoption|kit|directory|channel)\b|\bofficial\//gi
|
|
44
|
+
const STRONG_CLAIM = /official (?:certification|standard|endorsement)|officially certified|官方认证|国家标准|国家认可/i
|
|
45
|
+
|
|
46
|
+
function walk(dir, out) {
|
|
47
|
+
if (!fs.existsSync(dir)) return
|
|
48
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
49
|
+
const full = path.join(dir, entry.name)
|
|
50
|
+
if (entry.isDirectory()) walk(full, out)
|
|
51
|
+
else if (/\.(md|json|txt)$/.test(entry.name)) out.push(full)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function scanText(text, file) {
|
|
56
|
+
const errors = []
|
|
57
|
+
const warnings = []
|
|
58
|
+
const lines = text.split(/\r?\n/)
|
|
59
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
60
|
+
const line = lines[i]
|
|
61
|
+
const hasMark = MARKS.test(line)
|
|
62
|
+
// Without a mark, only a strong claim ("official certification") fails;
|
|
63
|
+
// a bare "official team" or a path like adoption/official/ is not a claim.
|
|
64
|
+
if (!hasMark && !STRONG_CLAIM.test(line)) continue
|
|
65
|
+
const probe = line.replace(ALLOWED_CONTEXT, ' ')
|
|
66
|
+
if (!hasMark && !FORBIDDEN.some((re) => re.test(probe))) continue
|
|
67
|
+
const context = lines.slice(Math.max(0, i - 2), i + 2).join(' ')
|
|
68
|
+
if (NEGATION.test(probe) || NEGATION.test(context)) continue
|
|
69
|
+
for (const re of FORBIDDEN) {
|
|
70
|
+
if (re.test(probe)) {
|
|
71
|
+
errors.push(`${file}:${i + 1}: forbidden claim "${line.trim().slice(0, 120)}"`)
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (MARKS.test(text) && !/非官方|not official|非认证|not a certification/i.test(text)) {
|
|
77
|
+
warnings.push(`${file}: mark used without a same-page disclaimer`)
|
|
78
|
+
}
|
|
79
|
+
return { errors, warnings }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function collectClaimStrings(value, key, out) {
|
|
83
|
+
if (typeof value === 'string') {
|
|
84
|
+
if (!SKIP_JSON_KEYS.has(key)) out.push(value)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
for (const item of value) collectClaimStrings(item, key, out)
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
if (value && typeof value === 'object') {
|
|
92
|
+
for (const [childKey, child] of Object.entries(value)) collectClaimStrings(child, childKey, out)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function main(argv = process.argv.slice(2)) {
|
|
97
|
+
let root = DEFAULT_ROOT
|
|
98
|
+
let json = false
|
|
99
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
100
|
+
const arg = argv[i]
|
|
101
|
+
if (arg === '--root') root = path.resolve(argv[++i])
|
|
102
|
+
else if (arg === '--json') json = true
|
|
103
|
+
else if (arg === '--help' || arg === '-h') {
|
|
104
|
+
console.log('usage: claim-check.mjs [--root <dir>] [--json]')
|
|
105
|
+
return 0
|
|
106
|
+
} else {
|
|
107
|
+
console.error(`unknown argument: ${arg}`)
|
|
108
|
+
return 2
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const files = [path.join(root, 'README.md')]
|
|
112
|
+
walk(path.join(root, 'publish'), files)
|
|
113
|
+
walk(path.join(root, 'adoption'), files)
|
|
114
|
+
const results = []
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
if (!fs.existsSync(file)) continue
|
|
117
|
+
if (/mark-policy\.(md|json)$/.test(file)) continue
|
|
118
|
+
let text = fs.readFileSync(file, 'utf8')
|
|
119
|
+
if (file.endsWith('.json')) {
|
|
120
|
+
try {
|
|
121
|
+
const strings = []
|
|
122
|
+
collectClaimStrings(JSON.parse(text), '', strings)
|
|
123
|
+
text = strings.join('\n')
|
|
124
|
+
} catch (error) {
|
|
125
|
+
results.push({ errors: [`${path.relative(root, file)}: parse error: ${error.message}`], warnings: [] })
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = scanText(text, path.relative(root, file))
|
|
130
|
+
if (result.errors.length || result.warnings.length) results.push(result)
|
|
131
|
+
}
|
|
132
|
+
const errors = results.flatMap((r) => r.errors)
|
|
133
|
+
const warnings = results.flatMap((r) => r.warnings)
|
|
134
|
+
if (json) console.log(JSON.stringify({ ok: errors.length === 0, errors, warnings }, null, 2))
|
|
135
|
+
else {
|
|
136
|
+
for (const warning of warnings) console.log(`WARN ${warning}`)
|
|
137
|
+
for (const error of errors) console.log(`FAIL ${error}`)
|
|
138
|
+
console.log(`${errors.length === 0 ? 'PASS' : 'FAIL'} claims: ${errors.length} error(s), ${warnings.length} warning(s)`)
|
|
139
|
+
}
|
|
140
|
+
return errors.length === 0 ? 0 : 2
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
144
|
+
process.exitCode = main()
|
|
145
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* clause-map.mjs — validate the stable clause IDs against spec/levels.json.
|
|
4
|
+
*
|
|
5
|
+
* It checks:
|
|
6
|
+
* - every gate in levels.json is covered by at least one clause;
|
|
7
|
+
* - every clause points to an existing source file;
|
|
8
|
+
* - clause IDs are unique;
|
|
9
|
+
* - no clause maps to a gate that does not exist;
|
|
10
|
+
* - every clause has a level that exists.
|
|
11
|
+
*
|
|
12
|
+
* It does not parse the prose; a human still owns the semantics.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node 40-checker/scripts/clause-map.mjs [--check] [--json]
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'node:fs'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
22
|
+
const LEVELS = path.join(ROOT, 'spec', 'levels.json')
|
|
23
|
+
const CLAUSES = path.join(ROOT, 'spec', 'clauses.json')
|
|
24
|
+
|
|
25
|
+
export function validateClauseMap({ levels, clauses, root = ROOT }) {
|
|
26
|
+
const errors = []
|
|
27
|
+
const warnings = []
|
|
28
|
+
const ids = new Set()
|
|
29
|
+
const knownGates = new Map()
|
|
30
|
+
for (const level of levels.levels || []) {
|
|
31
|
+
knownGates.set(level.id, new Set((level.gates || []).map((gate) => gate.id)))
|
|
32
|
+
}
|
|
33
|
+
const covered = new Map([...knownGates.keys()].map((id) => [id, new Set()]))
|
|
34
|
+
for (const clause of clauses.clauses || []) {
|
|
35
|
+
if (!clause.id) { errors.push('clause without id'); continue }
|
|
36
|
+
if (ids.has(clause.id)) errors.push(`duplicate clause id: ${clause.id}`)
|
|
37
|
+
ids.add(clause.id)
|
|
38
|
+
if (!knownGates.has(clause.level)) {
|
|
39
|
+
errors.push(`${clause.id}: unknown level ${clause.level}`)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(clause.gates) || clause.gates.length === 0) {
|
|
43
|
+
errors.push(`${clause.id}: no gates`)
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
for (const gate of clause.gates) {
|
|
47
|
+
if (!knownGates.get(clause.level).has(gate)) {
|
|
48
|
+
errors.push(`${clause.id}: gate ${gate} does not exist in level ${clause.level}`)
|
|
49
|
+
} else {
|
|
50
|
+
covered.get(clause.level).add(gate)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const source = String(clause.source || '').split('#')[0]
|
|
54
|
+
if (!source || !fs.existsSync(path.join(root, source))) {
|
|
55
|
+
errors.push(`${clause.id}: source file not found: ${source}`)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const [level, gates] of knownGates) {
|
|
59
|
+
for (const gate of gates) {
|
|
60
|
+
if (!covered.get(level).has(gate)) errors.push(`${level}.${gate}: no clause maps to this gate`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!clauses.normativeCount) warnings.push('clauses.json has no normativeCount; not required')
|
|
64
|
+
return { errors, warnings, covered: Object.fromEntries([...covered].map(([k, v]) => [k, [...v].sort()])) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function main(argv = process.argv.slice(2)) {
|
|
68
|
+
let json = false
|
|
69
|
+
for (const arg of argv) {
|
|
70
|
+
if (arg === '--json') json = true
|
|
71
|
+
else if (arg === '--check') { /* default behaviour */ }
|
|
72
|
+
else if (arg === '--help' || arg === '-h') {
|
|
73
|
+
console.log('usage: clause-map.mjs [--check] [--json]')
|
|
74
|
+
return 0
|
|
75
|
+
} else {
|
|
76
|
+
console.error(`unknown argument: ${arg}`)
|
|
77
|
+
return 2
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const levels = JSON.parse(fs.readFileSync(LEVELS, 'utf8'))
|
|
81
|
+
const clauses = JSON.parse(fs.readFileSync(CLAUSES, 'utf8'))
|
|
82
|
+
const result = validateClauseMap({ levels, clauses })
|
|
83
|
+
if (json) console.log(JSON.stringify(result, null, 2))
|
|
84
|
+
else {
|
|
85
|
+
for (const warning of result.warnings) console.log(`WARN ${warning}`)
|
|
86
|
+
for (const error of result.errors) console.log(`FAIL ${error}`)
|
|
87
|
+
console.log(`${result.errors.length === 0 ? 'PASS' : 'FAIL'} clause map: ${Object.keys(result.covered).map((k) => `${k}=${result.covered[k].length}`).join(' ')}`)
|
|
88
|
+
}
|
|
89
|
+
return result.errors.length === 0 ? 0 : 2
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
93
|
+
process.exitCode = main()
|
|
94
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Collect the known-answer fixture statuses for one runner (OS/Node).
|
|
4
|
+
* Output is the input format of round-robin.mjs.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node 40-checker/scripts/collect-fixtures.mjs --out runner.json
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { spawnSync } from 'node:child_process'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
15
|
+
const CHECKER = path.join(ROOT, '40-checker', 'bin', 'dsh-plugin-verify.mjs')
|
|
16
|
+
|
|
17
|
+
function run(dir) {
|
|
18
|
+
const r = spawnSync(process.execPath, [CHECKER, dir, '--scope', 'entry', '--entry-claim', 'static', '--format', 'json'], { encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 })
|
|
19
|
+
try {
|
|
20
|
+
const parsed = JSON.parse(r.stdout)
|
|
21
|
+
return parsed.conclusion?.status ?? 'error'
|
|
22
|
+
} catch {
|
|
23
|
+
return 'error'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
let out = path.join(ROOT, 'runner.json')
|
|
29
|
+
for (let i = 0; i < process.argv.length; i += 1) {
|
|
30
|
+
if (process.argv[i] === '--out') out = path.resolve(process.argv[++i])
|
|
31
|
+
}
|
|
32
|
+
const result = {
|
|
33
|
+
runner: `dsh-plugin-verify@0.1.0 (${process.platform}/${process.arch}, node ${process.version})`,
|
|
34
|
+
anchor: { dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1' },
|
|
35
|
+
results: {
|
|
36
|
+
good: { status: run(path.join(ROOT, '40-checker/fixtures/good')), artifact: 'fixtures/good' },
|
|
37
|
+
'bad-manifest': { status: run(path.join(ROOT, '40-checker/fixtures/bad-manifest')), artifact: 'fixtures/bad-manifest' },
|
|
38
|
+
skipped: { status: 'not-run', artifact: null },
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
fs.writeFileSync(out, `${JSON.stringify(result, null, 2)}\n`)
|
|
42
|
+
console.log(JSON.stringify(result, null, 2))
|
|
43
|
+
}
|
|
44
|
+
main()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dashboard.mjs — build a small, honest status dashboard from existing results.
|
|
4
|
+
* It never infers a PASS from a missing file.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node 40-checker/scripts/dashboard.mjs [--out dashboard.json]
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
|
|
12
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
14
|
+
const RESULTS = path.join(ROOT, '40-checker', 'results')
|
|
15
|
+
|
|
16
|
+
function readJson(file) {
|
|
17
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return null }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function countFiles(dir, suffix = '.json') {
|
|
21
|
+
if (!fs.existsSync(dir)) return 0
|
|
22
|
+
return fs.readdirSync(dir).filter((name) => name.endsWith(suffix)).length
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildDashboard(root = ROOT) {
|
|
26
|
+
const results = path.join(root, '40-checker', 'results')
|
|
27
|
+
const entryDir = path.join(results, 'entry', '18')
|
|
28
|
+
const entryFiles = fs.existsSync(entryDir)
|
|
29
|
+
? fs.readdirSync(entryDir).filter((name) => name.endsWith('.json') && !name.endsWith('.runtime.json')).length
|
|
30
|
+
: 0
|
|
31
|
+
const flowIndex = readJson(path.join(results, 'flow', 'real', 'index.json'))
|
|
32
|
+
const boost = readJson(path.join(results, 'boost', 'lev12458.json'))
|
|
33
|
+
const attestations = readJson(path.join(root, 'publish', 'attestations', 'index.json'))
|
|
34
|
+
const gov = readJson(path.join(root, 'governance', 'roles.json'))
|
|
35
|
+
const chain = fs.existsSync(path.join(root, 'evidence', 'chain.jsonl'))
|
|
36
|
+
? fs.readFileSync(path.join(root, 'evidence', 'chain.jsonl'), 'utf8').split('\n').filter(Boolean).length
|
|
37
|
+
: 0
|
|
38
|
+
return {
|
|
39
|
+
schema: 'dsh-dashboard/1',
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
entry: {
|
|
42
|
+
resultFiles: entryFiles,
|
|
43
|
+
claim: '18/18 recognized static; 10 G3 verified, 8 partial (see results/entry/18/README.md)',
|
|
44
|
+
},
|
|
45
|
+
flow: {
|
|
46
|
+
packages: flowIndex?.samples?.length ?? flowIndex?.results?.length ?? 0,
|
|
47
|
+
summary: flowIndex?.totals ?? flowIndex?.summary ?? null,
|
|
48
|
+
claim: 'J1/J2/J4/J6 runtime on a fixture and 10 real packages; J3/J5/J7/J8 not-run',
|
|
49
|
+
},
|
|
50
|
+
boost: {
|
|
51
|
+
measured: boost?.summary?.measured ?? null,
|
|
52
|
+
measuredCount: boost?.summary?.measured?.length ?? 0,
|
|
53
|
+
claim: 'LEV-1-8 measured at synthetic/loader level; not a production baseline',
|
|
54
|
+
},
|
|
55
|
+
attestations: {
|
|
56
|
+
count: attestations?.attestations?.length ?? 0,
|
|
57
|
+
draft: (attestations?.attestations || []).filter((a) => a.status === 'draft').length,
|
|
58
|
+
selfReported: (attestations?.attestations || []).filter((a) => a.claimLevel === 'self-reported').length,
|
|
59
|
+
},
|
|
60
|
+
governance: {
|
|
61
|
+
schema: gov?.schema ?? null,
|
|
62
|
+
stage: gov?.stage ?? null,
|
|
63
|
+
status: gov?.status ?? null,
|
|
64
|
+
},
|
|
65
|
+
evidenceChainEntries: chain,
|
|
66
|
+
notClaimed: ['official standard', 'certification', 'security audit', 'production readiness', 'ranking'],
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function main(argv = process.argv.slice(2)) {
|
|
71
|
+
let out = path.join(ROOT, 'dashboard.json')
|
|
72
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
73
|
+
if (argv[i] === '--out') out = path.resolve(argv[++i])
|
|
74
|
+
else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: dashboard.mjs [--out <file>]'); return 0 }
|
|
75
|
+
else { console.error(`unknown argument: ${argv[i]}`); return 2 }
|
|
76
|
+
}
|
|
77
|
+
const dashboard = buildDashboard(ROOT)
|
|
78
|
+
fs.writeFileSync(out, `${JSON.stringify(dashboard, null, 2)}\n`)
|
|
79
|
+
console.log(JSON.stringify(dashboard, null, 2))
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
84
|
+
process.exitCode = main()
|
|
85
|
+
}
|