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,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* entry-validate.mjs — static C1-C8 checklist for DSH plugins.
|
|
4
|
+
*
|
|
5
|
+
* This is a pre-submit helper, not a gate, certification, ranking, or security
|
|
6
|
+
* audit. C1/C3/C7 need --meta and are reported not-run without it. C5 is
|
|
7
|
+
* not-applicable when the plugin ships no client. C6 checks metadata and the
|
|
8
|
+
* entry file; code reality still needs a human sample.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { validRange } from '../src/semver.mjs'
|
|
14
|
+
|
|
15
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
16
|
+
const STD = path.resolve(HERE, '..', '..')
|
|
17
|
+
const CHECKLIST_PATH = path.join(STD, 'adoption', 'entry', 'checklist.json')
|
|
18
|
+
const FORBIDDEN = ['grade', 'score', 'rank', 'official', 'certified', 'secure', 'recommended']
|
|
19
|
+
|
|
20
|
+
function usage() {
|
|
21
|
+
console.log(`entry-validate — static DSH entry checklist (C1-C8)
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
node 40-checker/scripts/entry-validate.mjs <plugin-dir> [options]
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--meta <file> JSON: repo, topics, archived, commit, pushedAt, installCommand, claimLevel
|
|
28
|
+
--host-evidence <file> JSON: { status: pass|fail|blocked|not-run, command, host, evidence, generatedAt }
|
|
29
|
+
--format text|json default text
|
|
30
|
+
--out <file> write the JSON result
|
|
31
|
+
--help show this help
|
|
32
|
+
|
|
33
|
+
Exit codes: 0 pass/pass-with-warnings; 2 fail; 3 incomplete/not-run.`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const options = { dir: null, meta: null, hostEvidence: null, format: 'text', out: null }
|
|
38
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
39
|
+
const arg = argv[i]
|
|
40
|
+
const next = () => {
|
|
41
|
+
const value = argv[i + 1]
|
|
42
|
+
if (value === undefined) throw new Error(`${arg} needs a value`)
|
|
43
|
+
i += 1
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
if (arg === '--meta') options.meta = next()
|
|
47
|
+
else if (arg === '--host-evidence') options.hostEvidence = next()
|
|
48
|
+
else if (arg === '--json') options.format = 'json'
|
|
49
|
+
else if (arg === '--format') options.format = next()
|
|
50
|
+
else if (arg === '--out') options.out = next()
|
|
51
|
+
else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
|
|
52
|
+
else if (arg.startsWith('-')) throw new Error(`unknown option: ${arg}`)
|
|
53
|
+
else if (options.dir === null) options.dir = arg
|
|
54
|
+
else throw new Error(`unexpected argument: ${arg}`)
|
|
55
|
+
}
|
|
56
|
+
if (!options.dir) throw new Error('a plugin directory is required')
|
|
57
|
+
if (!['text', 'json'].includes(options.format)) throw new Error('--format must be text or json')
|
|
58
|
+
options.dir = path.resolve(options.dir)
|
|
59
|
+
if (!fs.existsSync(path.join(options.dir, 'package.json'))) throw new Error(`not a package directory: ${options.dir}`)
|
|
60
|
+
return options
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readJson(file, label) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`${label} is not valid JSON: ${error.message}`)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fileExists(dir, value) {
|
|
72
|
+
if (typeof value !== 'string' || value.length === 0) return false
|
|
73
|
+
const resolved = path.resolve(dir, value)
|
|
74
|
+
return resolved.startsWith(`${dir}${path.sep}`) && fs.existsSync(resolved)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveExport(value) {
|
|
78
|
+
if (typeof value === 'string') return value
|
|
79
|
+
if (!value || typeof value !== 'object') return null
|
|
80
|
+
return value.import ?? value.default ?? value.require ?? value.node ?? null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function check(id, status, detail, evidence = null) {
|
|
84
|
+
return { id, status, detail, evidence }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function run(dir, meta) {
|
|
88
|
+
const pkg = readJson(path.join(dir, 'package.json'), 'package.json')
|
|
89
|
+
const dsh = pkg.dsh && typeof pkg.dsh === 'object' ? pkg.dsh : {}
|
|
90
|
+
const checks = []
|
|
91
|
+
|
|
92
|
+
// C1 — public repo + topic
|
|
93
|
+
if (!meta || (!meta.repo && !Array.isArray(meta.topics))) {
|
|
94
|
+
checks.push(check('C1', 'not-run', 'no --meta repo/topics; cannot verify public repo or dsh-plugin topic'))
|
|
95
|
+
} else {
|
|
96
|
+
const repoOk = typeof meta.repo === 'string' && /^[^/\s]+\/[^/\s]+$/.test(meta.repo)
|
|
97
|
+
const topicOk = Array.isArray(meta.topics) && meta.topics.includes('dsh-plugin')
|
|
98
|
+
const archivedOk = meta.archived !== true
|
|
99
|
+
checks.push(check('C1', repoOk && topicOk && archivedOk ? 'pass' : 'fail',
|
|
100
|
+
`repo=${meta.repo ?? 'missing'}, topic=${topicOk}, archived=${meta.archived === true}`,
|
|
101
|
+
repoOk ? `https://github.com/${meta.repo}` : null))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// C2 — dsh.bundle + existing patch (one path or an ordered list)
|
|
105
|
+
const bundle = dsh.bundle
|
|
106
|
+
const patchValue = bundle?.patch
|
|
107
|
+
const patches = Array.isArray(patchValue)
|
|
108
|
+
? patchValue.filter(item => typeof item === 'string' && item.length > 0)
|
|
109
|
+
: typeof patchValue === 'string' && patchValue.length > 0
|
|
110
|
+
? [patchValue]
|
|
111
|
+
: null
|
|
112
|
+
if (!bundle || typeof bundle !== 'object' || Array.isArray(bundle)) {
|
|
113
|
+
checks.push(check('C2', 'fail', 'dsh.bundle is missing; dsh.client alone is not installable'))
|
|
114
|
+
} else if (!patches || patches.length === 0) {
|
|
115
|
+
checks.push(check('C2', 'fail', 'dsh.bundle.patch is missing or empty'))
|
|
116
|
+
} else {
|
|
117
|
+
const missing = patches.filter(file => !fileExists(dir, file))
|
|
118
|
+
if (missing.length > 0) checks.push(check('C2', 'fail', `dsh.bundle.patch file(s) missing: ${missing.join(', ')}`))
|
|
119
|
+
else checks.push(check('C2', 'pass', `dsh.bundle.patch exists: ${patches.join(', ')}`))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// C3 — tested compose/install line (or an explicit host-install evidence object)
|
|
123
|
+
const host = meta?.hostInstall
|
|
124
|
+
if (host) {
|
|
125
|
+
const status = ['pass', 'fail', 'blocked', 'not-run', 'not-applicable'].includes(host.status) ? host.status : 'blocked'
|
|
126
|
+
const detail = `hostInstall ${status}: ${host.command ?? 'no command'}${host.host ? ` (${host.host})` : ''}${host.evidence ? ` ${host.evidence}` : ''}`
|
|
127
|
+
checks.push(check('C3', status, detail, host.evidence ?? null))
|
|
128
|
+
} else if (!meta?.installCommand) {
|
|
129
|
+
checks.push(check('C3', 'not-applicable', 'C3 is flow-only; run J1 or pass --host-evidence for install evidence'))
|
|
130
|
+
} else {
|
|
131
|
+
const command = String(meta.installCommand)
|
|
132
|
+
const name = typeof pkg.name === 'string' ? pkg.name : ''
|
|
133
|
+
const ok = command.includes('dsh plugin') && (name.length === 0 || command.includes(name) || command.includes('add'))
|
|
134
|
+
checks.push(check('C3', ok ? 'pass' : 'fail', `installCommand (declared; FLOW J1 still required for install evidence): ${command}`))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// C4 — engines.dsh or peer range
|
|
138
|
+
const engineRange = pkg.engines?.dsh
|
|
139
|
+
const peerRange = pkg.peerDependencies?.['@deepseek-ai/dsh']
|
|
140
|
+
const range = typeof engineRange === 'string' ? engineRange : typeof peerRange === 'string' ? peerRange : null
|
|
141
|
+
const source = typeof engineRange === 'string' ? 'engines.dsh' : 'peerDependencies["@deepseek-ai/dsh"]'
|
|
142
|
+
if (!range) {
|
|
143
|
+
checks.push(check('C4', 'warn', 'no declared engines.dsh or @deepseek-ai/dsh peer range; host compatibility unknown'))
|
|
144
|
+
} else if (range.trim() === '*' || /(^|\s)(\*|x|X)(\s|$)/.test(range) || /\.(x|X|\*)/.test(range)) {
|
|
145
|
+
checks.push(check('C4', 'fail', `${source} is too wide: ${range}`))
|
|
146
|
+
} else if (!validRange(range)) {
|
|
147
|
+
checks.push(check('C4', 'fail', `${source} is not a valid range: ${range}`))
|
|
148
|
+
} else {
|
|
149
|
+
checks.push(check('C4', 'pass', `${source} = ${range} (declared range; runtime not tested)`))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// C5 — client export
|
|
153
|
+
if (!dsh.client) {
|
|
154
|
+
checks.push(check('C5', 'not-applicable', 'no dsh.client; client export not required'))
|
|
155
|
+
} else {
|
|
156
|
+
const exportValue = pkg.exports?.['./client']
|
|
157
|
+
const target = resolveExport(exportValue)
|
|
158
|
+
if (!target) {
|
|
159
|
+
checks.push(check('C5', 'fail', 'dsh.client exists but exports["./client"] is missing'))
|
|
160
|
+
} else if (!fileExists(dir, target)) {
|
|
161
|
+
checks.push(check('C5', 'warn', `exports["./client"] is declared at ${target}, but the built file is not committed; declaration present`))
|
|
162
|
+
} else {
|
|
163
|
+
checks.push(check('C5', 'pass', `exports["./client"] exists: ${target}`))
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// C6 — entry + metadata
|
|
168
|
+
const main = pkg.main ?? resolveExport(pkg.exports?.['.'])
|
|
169
|
+
const patch = Array.isArray(dsh.bundle?.patch)
|
|
170
|
+
? dsh.bundle.patch.find(item => typeof item === 'string' && item.length > 0) ?? null
|
|
171
|
+
: typeof dsh.bundle?.patch === 'string' ? dsh.bundle.patch : null
|
|
172
|
+
const missingMeta = ['description'].filter(field => {
|
|
173
|
+
const value = pkg[field]
|
|
174
|
+
return value === undefined || value === null || value === '' || (typeof value === 'object' && Object.keys(value).length === 0)
|
|
175
|
+
})
|
|
176
|
+
const missingRepository = pkg.repository === undefined || pkg.repository === null || pkg.repository === ''
|
|
177
|
+
const missingLicense = pkg.license === undefined || pkg.license === null || pkg.license === ''
|
|
178
|
+
if (!main && !patch) {
|
|
179
|
+
checks.push(check('C6', 'fail', 'no main/exports["."] and no dsh.bundle.patch'))
|
|
180
|
+
} else if (missingMeta.length > 0) {
|
|
181
|
+
checks.push(check('C6', 'fail', `package.json missing: ${missingMeta.join(', ')}`))
|
|
182
|
+
} else if (main && !fileExists(dir, main) && !(patch && fileExists(dir, patch))) {
|
|
183
|
+
checks.push(check('C6', 'warn', `entry ${main} is declared but not committed (build output); declaration present`))
|
|
184
|
+
} else if (missingRepository) {
|
|
185
|
+
checks.push(check('C6', 'warn', 'repository is missing; C1 supplies the repo URL, but a package repository field is recommended'))
|
|
186
|
+
} else {
|
|
187
|
+
checks.push(check('C6', 'pass', `entry ${main ?? patch}; description/license/repository present`))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// C7 — version + commit/digest + freshness
|
|
191
|
+
const version = typeof pkg.version === 'string' ? pkg.version : ''
|
|
192
|
+
const versionOk = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version)
|
|
193
|
+
const commit = meta?.commit ?? pkg.gitHead
|
|
194
|
+
if (!versionOk) {
|
|
195
|
+
checks.push(check('C7', 'fail', `package.json version is missing or not semver: ${version || 'missing'}`))
|
|
196
|
+
} else if (!commit && !meta?.pushedAt) {
|
|
197
|
+
checks.push(check('C7', 'not-run', 'version present, but no commit/digest or pushedAt metadata'))
|
|
198
|
+
} else if (!commit || !meta?.pushedAt) {
|
|
199
|
+
checks.push(check('C7', 'not-run', `version ${version}; need both commit and pushedAt to judge freshness`))
|
|
200
|
+
} else {
|
|
201
|
+
const ageDays = (Date.now() - Date.parse(`${meta.pushedAt}T00:00:00Z`)) / 86_400_000
|
|
202
|
+
const fresh = Number.isFinite(ageDays) && ageDays <= 365
|
|
203
|
+
checks.push(check('C7', fresh ? 'pass' : 'warn',
|
|
204
|
+
`version ${version}; commit ${String(commit).slice(0, 12)}; pushedAt ${meta.pushedAt} (${Math.round(ageDays)}d old)`))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// C8 — no grade/rank and claimLevel=reference
|
|
208
|
+
const policySurface = JSON.stringify({ dsh: pkg.dsh ?? null, keywords: pkg.keywords ?? null, labels: pkg.labels ?? null })
|
|
209
|
+
const hits = FORBIDDEN.filter(word => new RegExp(`"${word}"`, 'i').test(policySurface))
|
|
210
|
+
if (hits.length > 0) {
|
|
211
|
+
checks.push(check('C8', 'fail', `forbidden grade/rank/endorsement fields: ${hits.join(', ')}`))
|
|
212
|
+
} else if (meta?.claimLevel !== undefined && meta.claimLevel !== 'reference') {
|
|
213
|
+
checks.push(check('C8', 'fail', `claimLevel must be reference, got ${meta.claimLevel}`))
|
|
214
|
+
} else {
|
|
215
|
+
checks.push(check('C8', 'pass', 'no grade/rank/endorsement fields; claimLevel=reference (or not supplied)'))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const failed = checks.filter(row => row.status === 'fail')
|
|
219
|
+
const notRun = checks.filter(row => row.status === 'not-run')
|
|
220
|
+
const blocked = checks.filter(row => row.status === 'blocked')
|
|
221
|
+
const warnings = checks.filter(row => row.status === 'warn')
|
|
222
|
+
const advisories = []
|
|
223
|
+
if (!dsh.manifestVersion) advisories.push({ id: 'A1', detail: 'dsh.manifestVersion is missing; the reference template declares 1' })
|
|
224
|
+
if (!pkg.engines?.node) advisories.push({ id: 'A2', detail: 'engines.node is missing; the reference template recommends >=24' })
|
|
225
|
+
if (dsh.client && dsh.client.platform !== 'web') advisories.push({ id: 'A3', detail: 'dsh.client.platform is not web' })
|
|
226
|
+
if (!pkg.exports?.['./locale/en.json']) advisories.push({ id: 'A4', detail: 'exports["./locale/en.json"] is missing; display metadata may be absent' })
|
|
227
|
+
if (!pkg.icon && !pkg.exports?.['./icon']) advisories.push({ id: 'A5', detail: 'icon is missing; the host will use the default artwork' })
|
|
228
|
+
if (missingLicense) advisories.push({ id: 'A7', detail: 'license is missing; legal/market profile, not an ENTRY hard fail' })
|
|
229
|
+
if (meta?.safety && !['clean', 'mentions-only'].includes(meta.safety)) advisories.push({ id: 'A6', detail: `safety category ${meta.safety}: requires review before C3` })
|
|
230
|
+
let status = 'pass'
|
|
231
|
+
if (failed.length > 0) status = 'fail'
|
|
232
|
+
else if (notRun.length > 0 || blocked.length > 0) status = 'incomplete'
|
|
233
|
+
else if (warnings.length > 0) status = 'pass-with-warnings'
|
|
234
|
+
return {
|
|
235
|
+
schema: 'dsh-entry-audit/1',
|
|
236
|
+
generatedAt: new Date().toISOString(),
|
|
237
|
+
target: { dir, name: pkg.name ?? null, version: pkg.version ?? null },
|
|
238
|
+
checklist: { schema: 'dsh-entry-checklist/1', version: '0.1.0' },
|
|
239
|
+
checks,
|
|
240
|
+
conclusion: {
|
|
241
|
+
status,
|
|
242
|
+
passed: checks.filter(row => row.status === 'pass').length,
|
|
243
|
+
failed: failed.map(row => row.id),
|
|
244
|
+
warnings: warnings.map(row => row.id),
|
|
245
|
+
notRun: notRun.map(row => row.id),
|
|
246
|
+
blocked: blocked.map(row => row.id),
|
|
247
|
+
notApplicable: checks.filter(row => row.status === 'not-applicable').map(row => row.id),
|
|
248
|
+
},
|
|
249
|
+
advisories,
|
|
250
|
+
claimLevel: 'reference',
|
|
251
|
+
notClaimed: ['official standard', 'certification', 'security audit', 'ranking', 'market acceptance', 'production baseline'],
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function render(result) {
|
|
256
|
+
const lines = [
|
|
257
|
+
`DSH entry checklist — ${result.target.dir}`,
|
|
258
|
+
` package: ${result.target.name ?? '?'}@${result.target.version ?? '?'}`,
|
|
259
|
+
...result.checks.map(row => ` ${row.id} ${row.status.padEnd(14)} ${row.detail}`),
|
|
260
|
+
`Conclusion: ${result.conclusion.status}`,
|
|
261
|
+
'Not claimed: official, certified, security audit, ranking, market acceptance.',
|
|
262
|
+
]
|
|
263
|
+
return lines.join('\n')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function main() {
|
|
267
|
+
const options = parseArgs(process.argv.slice(2))
|
|
268
|
+
if (!fs.existsSync(CHECKLIST_PATH)) throw new Error(`missing checklist: ${CHECKLIST_PATH}`)
|
|
269
|
+
const checklist = readJson(CHECKLIST_PATH, 'checklist.json')
|
|
270
|
+
if (checklist.schema !== 'dsh-entry-checklist/1') throw new Error(`unsupported checklist schema: ${checklist.schema}`)
|
|
271
|
+
let meta = options.meta ? readJson(path.resolve(options.meta), '--meta') : null
|
|
272
|
+
if (options.hostEvidence) {
|
|
273
|
+
const host = readJson(path.resolve(options.hostEvidence), '--host-evidence')
|
|
274
|
+
meta = { ...(meta ?? {}), hostInstall: host }
|
|
275
|
+
}
|
|
276
|
+
const result = run(options.dir, meta)
|
|
277
|
+
const output = options.format === 'json' ? JSON.stringify(result, null, 2) : render(result)
|
|
278
|
+
if (options.out) {
|
|
279
|
+
fs.mkdirSync(path.dirname(path.resolve(options.out)), { recursive: true })
|
|
280
|
+
fs.writeFileSync(options.out, `${output}\n`)
|
|
281
|
+
}
|
|
282
|
+
process.stdout.write(`${output}\n`)
|
|
283
|
+
process.exit(result.conclusion.status === 'fail' ? 2 : result.conclusion.status === 'incomplete' ? 3 : 0)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
main()
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.error(`entry-validate: ${error.message}`)
|
|
290
|
+
process.exit(3)
|
|
291
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* entry18-attribution.mjs — explain the 8 partial rows without changing them.
|
|
4
|
+
* It reads the fullbase rows + logs; it does not boot anything.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
12
|
+
const DIR = path.join(ROOT, '40-checker', 'results', 'entry', '18', 'fullbase')
|
|
13
|
+
|
|
14
|
+
export function buildAttribution(dir = DIR) {
|
|
15
|
+
const rows = []
|
|
16
|
+
for (const name of fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort()) {
|
|
17
|
+
let row
|
|
18
|
+
try { row = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')) } catch { continue }
|
|
19
|
+
if (!row.id || !row.status) continue
|
|
20
|
+
const logFile = path.join(dir, `${row.id}.log`)
|
|
21
|
+
let missing = []
|
|
22
|
+
if (fs.existsSync(logFile)) {
|
|
23
|
+
const log = fs.readFileSync(logFile, 'utf8')
|
|
24
|
+
missing = [...new Set([...log.matchAll(/pending \(waiting for service: ([^)]+)\)/g)].map((m) => m[1].trim()))].sort()
|
|
25
|
+
}
|
|
26
|
+
rows.push({
|
|
27
|
+
id: row.id,
|
|
28
|
+
status: row.status,
|
|
29
|
+
g3State: row.g3State,
|
|
30
|
+
targetRows: row.smoke?.g1?.targetRows ?? null,
|
|
31
|
+
active: row.g3Active ?? null,
|
|
32
|
+
compatibility: row.compatibility ?? null,
|
|
33
|
+
missingServices: missing,
|
|
34
|
+
verdict: row.verdict ?? null,
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
return { schema: 'dsh-entry18-attribution/1', generatedAt: new Date().toISOString(), rows }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function render(report) {
|
|
41
|
+
const lines = [
|
|
42
|
+
'# ENTRY 18-package attribution',
|
|
43
|
+
'',
|
|
44
|
+
'> This explains the rows that are not `verified`. It does not turn a partial',
|
|
45
|
+
'> into a pass. A missing host/profile service is not a plugin code FAIL.',
|
|
46
|
+
'',
|
|
47
|
+
'| id | status | active/target | missing services | verdict |',
|
|
48
|
+
'|---|---|---|---|---|',
|
|
49
|
+
]
|
|
50
|
+
for (const row of report.rows) {
|
|
51
|
+
lines.push(`| \`${row.id}\` | ${row.status} | ${row.active ?? '?'}/${row.targetRows ?? '?'} | ${row.missingServices.length ? row.missingServices.join(', ') : '—'} | ${row.verdict ?? '—'} |`)
|
|
52
|
+
}
|
|
53
|
+
const partial = report.rows.filter((r) => r.status !== 'verified')
|
|
54
|
+
lines.push('', `Total: ${report.rows.length}; not verified: ${partial.length}.`)
|
|
55
|
+
lines.push('', 'A richer host profile (the missing services) is required before these can be `verified`; the plugin code itself was not the cause.')
|
|
56
|
+
return lines.join('\n')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function main() {
|
|
60
|
+
const report = buildAttribution()
|
|
61
|
+
fs.writeFileSync(path.join(DIR, 'ATTRIBUTION.md'), `${render(report)}\n`)
|
|
62
|
+
console.log(render(report))
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) process.exitCode = main()
|