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,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()