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,194 @@
1
+ /**
2
+ * Minimal SemVer range matcher for the checker.
3
+ *
4
+ * It is intentionally small: the host evaluates plugin requirements with
5
+ * `semver.satisfies(runtime, range, { includePrerelease: true })`
6
+ * (deepseek-harness/packages/boot/app-boot/src/plugin-compatibility.ts:52).
7
+ * This module implements the subset that appears in package manifests:
8
+ * exact versions, `>= > <= <`, `^`, `~`, `*`/`x`, space-separated AND,
9
+ * and `||` alternatives. It does NOT implement hyphen ranges, `!=`,
10
+ * or npm's "prerelease only matches same tuple" exclusion; the host uses
11
+ * includePrerelease, so we include prereleases everywhere.
12
+ */
13
+
14
+ const VERSION_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
15
+
16
+ export function parseVersion(value) {
17
+ if (typeof value !== 'string') return null
18
+ const match = VERSION_RE.exec(value.trim())
19
+ if (!match) return null
20
+ return {
21
+ major: Number(match[1]),
22
+ minor: Number(match[2]),
23
+ patch: Number(match[3]),
24
+ prerelease: match[4] ? match[4].split('.') : [],
25
+ }
26
+ }
27
+
28
+ function compareIdentifier(left, right) {
29
+ const leftNumeric = /^\d+$/.test(left)
30
+ const rightNumeric = /^\d+$/.test(right)
31
+ if (leftNumeric && rightNumeric) return Number(left) - Number(right)
32
+ if (leftNumeric) return -1
33
+ if (rightNumeric) return 1
34
+ return left < right ? -1 : left > right ? 1 : 0
35
+ }
36
+
37
+ export function compareVersions(left, right) {
38
+ for (const key of ['major', 'minor', 'patch']) {
39
+ if (left[key] !== right[key]) return left[key] - right[key]
40
+ }
41
+ const a = left.prerelease
42
+ const b = right.prerelease
43
+ if (a.length === 0 && b.length === 0) return 0
44
+ if (a.length === 0) return 1
45
+ if (b.length === 0) return -1
46
+ const length = Math.max(a.length, b.length)
47
+ for (let index = 0; index < length; index += 1) {
48
+ if (a[index] === undefined) return -1
49
+ if (b[index] === undefined) return 1
50
+ const result = compareIdentifier(a[index], b[index])
51
+ if (result !== 0) return result
52
+ }
53
+ return 0
54
+ }
55
+
56
+ function upperForCaret(version) {
57
+ if (version.major > 0) return { ...version, major: version.major + 1, minor: 0, patch: 0, prerelease: [] }
58
+ if (version.minor > 0) return { ...version, minor: version.minor + 1, patch: 0, prerelease: [] }
59
+ return { ...version, patch: version.patch + 1, prerelease: [] }
60
+ }
61
+
62
+ function upperForTilde(version) {
63
+ return { ...version, minor: version.minor + 1, patch: 0, prerelease: [] }
64
+ }
65
+
66
+ function parsePartial(text) {
67
+ const match = /^v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(text.trim())
68
+ if (!match) return null
69
+ return {
70
+ major: Number(match[1]),
71
+ minor: match[2] === undefined || /[xX*]/.test(match[2]) ? null : Number(match[2]),
72
+ patch: match[3] === undefined || /[xX*]/.test(match[3]) ? null : Number(match[3]),
73
+ prerelease: match[4] ? match[4].split('.') : [],
74
+ }
75
+ }
76
+
77
+ function fullVersion(partial) {
78
+ return { major: partial.major, minor: partial.minor ?? 0, patch: partial.patch ?? 0, prerelease: partial.prerelease }
79
+ }
80
+ function nextMinor(partial) { return { major: partial.major, minor: (partial.minor ?? 0) + 1, patch: 0, prerelease: [] } }
81
+ function nextMajor(partial) { return { major: partial.major + 1, minor: 0, patch: 0, prerelease: [] } }
82
+ function fmt(version) {
83
+ const base = `${version.major}.${version.minor}.${version.patch}`
84
+ return version.prerelease && version.prerelease.length ? `${base}-${version.prerelease.join('.')}` : base
85
+ }
86
+
87
+ function expandToken(raw) {
88
+ if (raw === '*' || raw === 'x' || raw === 'X') return '*'
89
+ const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+)(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/.exec(raw)
90
+ if (!match) return null
91
+ const op = match[1] ?? '='
92
+ const partial = {
93
+ major: Number(match[2]),
94
+ minor: match[3] === undefined || /[xX*]/.test(match[3]) ? null : Number(match[3]),
95
+ patch: match[4] === undefined || /[xX*]/.test(match[4]) ? null : Number(match[4]),
96
+ prerelease: match[5] ? match[5].split('.') : [],
97
+ }
98
+ const low = fullVersion(partial)
99
+ const noMinor = partial.minor === null
100
+ const noPatch = partial.patch === null
101
+ if (!noMinor && !noPatch) return `${op}${fmt(low)}`
102
+ switch (op) {
103
+ case '^':
104
+ case '~':
105
+ case '=':
106
+ return noMinor ? `>=${fmt(low)} <${fmt(nextMajor(partial))}` : `>=${fmt(low)} <${fmt(nextMinor(partial))}`
107
+ case '>=': return `>=${fmt(low)}`
108
+ case '>': return noMinor ? `>=${fmt(nextMajor(partial))}` : `>=${fmt(nextMinor(partial))}`
109
+ case '<': return `<${fmt(low)}`
110
+ case '<=': return noMinor ? `<${fmt(nextMajor(partial))}` : `<${fmt(nextMinor(partial))}`
111
+ default: return null
112
+ }
113
+ }
114
+
115
+ /** Hyphen ranges and x-ranges are normalized before tokenizing. */
116
+ function expandAlternative(alternative) {
117
+ const text = alternative.trim()
118
+ if (text === '' || text === '*' || text === 'x' || text === 'X') return '*'
119
+ const hyphen = /^(.+?)\s+-\s+(.+)$/.exec(text)
120
+ if (hyphen) {
121
+ const left = parsePartial(hyphen[1])
122
+ const right = parsePartial(hyphen[2])
123
+ if (!left || !right) return null
124
+ const low = `>=${fmt(fullVersion(left))}`
125
+ if (right.minor === null) return `${low} <${fmt(nextMajor(right))}`
126
+ if (right.patch === null) return `${low} <${fmt(nextMinor(right))}`
127
+ return `${low} <=${fmt(fullVersion(right))}`
128
+ }
129
+ const parts = []
130
+ for (const raw of text.split(/\s+/)) {
131
+ const expanded = expandToken(raw)
132
+ if (expanded === null) return null
133
+ parts.push(expanded)
134
+ }
135
+ return parts.join(' ')
136
+ }
137
+
138
+ function tokenizeAlternative(alternative) {
139
+ const expanded = expandAlternative(alternative)
140
+ if (expanded === null) return null
141
+ if (expanded === '*') return [{ op: '*', version: null }]
142
+ const tokens = []
143
+ for (const raw of expanded.split(/\s+/)) {
144
+ const match = /^(>=|<=|>|<|=|\^|~)?\s*v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/.exec(raw)
145
+ if (!match) return null
146
+ const op = match[1] ?? '='
147
+ const version = parseVersion(match[2])
148
+ if (!version) return null
149
+ tokens.push({ op, version })
150
+ }
151
+ return tokens
152
+ }
153
+
154
+ function satisfiesToken(version, token) {
155
+ if (token.op === '*') return true
156
+ const cmp = compareVersions(version, token.version)
157
+ switch (token.op) {
158
+ case '=': return cmp === 0
159
+ case '>': return cmp > 0
160
+ case '>=': return cmp >= 0
161
+ case '<': return cmp < 0
162
+ case '<=': return cmp <= 0
163
+ case '^': return cmp >= 0 && compareVersions(version, upperForCaret(token.version)) < 0
164
+ case '~': return cmp >= 0 && compareVersions(version, upperForTilde(token.version)) < 0
165
+ default: return false
166
+ }
167
+ }
168
+
169
+ /**
170
+ * @param {string} version exact version to test
171
+ * @param {string} range SemVer range
172
+ * @returns {boolean}
173
+ */
174
+ export function satisfies(version, range) {
175
+ const parsed = parseVersion(version)
176
+ if (!parsed || typeof range !== 'string') return false
177
+ if (range.trim() === '') return false
178
+ const alternatives = range.split('||')
179
+ for (const alternative of alternatives) {
180
+ const tokens = tokenizeAlternative(alternative)
181
+ if (!tokens || tokens.length === 0) continue
182
+ if (tokens.every(token => satisfiesToken(parsed, token))) return true
183
+ }
184
+ return false
185
+ }
186
+
187
+ /** @returns {boolean} whether `range` is a syntactically usable range for this checker. */
188
+ export function validRange(range) {
189
+ if (typeof range !== 'string' || range.trim() === '') return false
190
+ for (const alternative of range.split('||')) {
191
+ if (!tokenizeAlternative(alternative)) return false
192
+ }
193
+ return true
194
+ }
@@ -0,0 +1,148 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ export const SOURCE_EXTENSIONS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx'])
5
+
6
+ export function exists(target) {
7
+ try { fs.accessSync(target); return true } catch { return false }
8
+ }
9
+
10
+ export function isFile(target) {
11
+ try { return fs.statSync(target).isFile() } catch { return false }
12
+ }
13
+
14
+ export function isDirectory(target) {
15
+ try { return fs.statSync(target).isDirectory() } catch { return false }
16
+ }
17
+
18
+ export function readText(target) {
19
+ return fs.readFileSync(target, 'utf8')
20
+ }
21
+
22
+ export function readJson(target) {
23
+ return JSON.parse(readText(target))
24
+ }
25
+
26
+ export function readJsonMaybe(target) {
27
+ try { return { value: readJson(target), error: null } } catch (error) {
28
+ return { value: null, error }
29
+ }
30
+ }
31
+
32
+ export function isObject(value) {
33
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
34
+ }
35
+
36
+ export function asArray(value) {
37
+ if (value === undefined || value === null) return []
38
+ return Array.isArray(value) ? value : [value]
39
+ }
40
+
41
+ export function escapeRegExp(value) {
42
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
43
+ }
44
+
45
+ export function posixRelative(from, to) {
46
+ return path.relative(from, to).split(path.sep).join('/')
47
+ }
48
+
49
+ const SKIP_DIRECTORIES = new Set([
50
+ 'node_modules', '.git', '.hg', '.svn', 'test', 'tests', '__tests__', 'docs', 'doc',
51
+ 'assets', 'images', 'screenshots', 'coverage', '.github', '.agents', '.claude',
52
+ 'tools', 'scripts', 'examples', 'benchmarks', 'fixtures', 'demo', 'dev', 'deploy', 'quality', 'bench',
53
+ ])
54
+
55
+ const BUILD_FILE = /(?:^|\/)(?:build|esbuild|vite|rollup|webpack)\.config\.[cm]?[jt]s$|(?:^|\/)(?:build|esbuild)\.mjs$|(?:^|\/)(?:make|render|verify|check)-[^/]+\.mjs$/
56
+
57
+ /**
58
+ * Collect source files without following symlinks. Deterministic order, bounded.
59
+ * @param {string} root
60
+ * @param {{maxFiles?: number, maxBytes?: number}} [options]
61
+ */
62
+ export function listSourceFiles(root, options = {}) {
63
+ const maxFiles = options.maxFiles ?? 400
64
+ const maxBytes = options.maxBytes ?? 2_000_000
65
+ const files = []
66
+ let bytes = 0
67
+ const walk = (directory) => {
68
+ if (files.length >= maxFiles || bytes >= maxBytes) return
69
+ let entries
70
+ try { entries = fs.readdirSync(directory, { withFileTypes: true }) } catch { return }
71
+ entries.sort((a, b) => a.name.localeCompare(b.name))
72
+ for (const entry of entries) {
73
+ if (files.length >= maxFiles || bytes >= maxBytes) return
74
+ const absolute = path.join(directory, entry.name)
75
+ if (entry.isDirectory()) {
76
+ if (SKIP_DIRECTORIES.has(entry.name)) continue
77
+ if (entry.name.startsWith('.')) continue
78
+ walk(absolute)
79
+ continue
80
+ }
81
+ if (!entry.isFile()) continue
82
+ if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) continue
83
+ const relative = path.relative(root, absolute).split(path.sep).join('/')
84
+ if (BUILD_FILE.test(relative)) continue
85
+ let size = 0
86
+ try { size = fs.statSync(absolute).size } catch { continue }
87
+ if (bytes + size > maxBytes) continue
88
+ bytes += size
89
+ files.push(absolute)
90
+ }
91
+ }
92
+ walk(root)
93
+ return files
94
+ }
95
+
96
+ /** Read all source files once, returning {file, relative, text} records. */
97
+ export function readSources(root, options) {
98
+ return listSourceFiles(root, options).map(file => ({
99
+ file,
100
+ relative: posixRelative(root, file),
101
+ text: readText(file),
102
+ }))
103
+ }
104
+
105
+ export function regexHits(sources, pattern) {
106
+ const hits = []
107
+ for (const source of sources) {
108
+ const re = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`)
109
+ let match
110
+ while ((match = re.exec(source.text))) {
111
+ const before = source.text.slice(0, match.index)
112
+ const line = before.split('\n').length
113
+ hits.push({ file: source.relative, line, text: match[0] })
114
+ if (match[0] === '') re.lastIndex += 1
115
+ }
116
+ }
117
+ return hits
118
+ }
119
+
120
+ /** Resolve an exports target for the root subpath. Returns a relative path or null. */
121
+ export function rootExportTarget(manifest) {
122
+ const exportsField = manifest.exports
123
+ if (exportsField === undefined) return null
124
+ const pick = (value) => {
125
+ if (typeof value === 'string') return value
126
+ if (Array.isArray(value)) {
127
+ for (const item of value) {
128
+ const picked = pick(item)
129
+ if (picked) return picked
130
+ }
131
+ return null
132
+ }
133
+ if (!isObject(value)) return null
134
+ for (const key of ['import', 'default', 'node', 'require', 'module-sync', 'browser']) {
135
+ if (Object.hasOwn(value, key)) {
136
+ const picked = pick(value[key])
137
+ if (picked) return picked
138
+ }
139
+ }
140
+ return null
141
+ }
142
+ if (typeof exportsField === 'string' || Array.isArray(exportsField)) return pick(exportsField)
143
+ if (!isObject(exportsField)) return null
144
+ if (Object.hasOwn(exportsField, '.')) return pick(exportsField['.'])
145
+ // A conditional object without subpath keys is the root export.
146
+ if (Object.hasOwn(exportsField, 'import') || Object.hasOwn(exportsField, 'default') || Object.hasOwn(exportsField, 'require')) return pick(exportsField)
147
+ return null
148
+ }
@@ -0,0 +1,59 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { validateAttestation } from '../scripts/attestation-check.mjs'
7
+
8
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
9
+ const ROOT = path.resolve(HERE, '..', '..')
10
+ const base = () => JSON.parse(fs.readFileSync(path.join(ROOT, 'publish/attestations/DSH-ATT-2026-001.json'), 'utf8'))
11
+
12
+ test('attestation: the checked-in example passes', () => {
13
+ const result = validateAttestation(base())
14
+ assert.deepEqual(result.errors, [])
15
+ })
16
+
17
+ test('attestation: a level field is forbidden', () => {
18
+ const cert = base()
19
+ cert.level = 'entry'
20
+ const result = validateAttestation(cert)
21
+ assert.ok(result.errors.some((e) => /level field/.test(e)))
22
+ })
23
+
24
+ test('attestation: selfAssessed cannot be verified', () => {
25
+ const cert = base()
26
+ cert.claimLevel = 'verified'
27
+ const result = validateAttestation(cert)
28
+ assert.ok(result.errors.some((e) => /cannot claim verified/.test(e)))
29
+ })
30
+
31
+ test('attestation: an expired active attestation fails', () => {
32
+ const cert = base()
33
+ cert.status = 'active'
34
+ cert.issuedAt = '2024-01-01'
35
+ cert.expiresAt = '2025-01-01'
36
+ const result = validateAttestation(cert)
37
+ assert.ok(result.errors.some((e) => /cannot be active/.test(e)))
38
+ })
39
+
40
+ test('attestation: excluded scope is required', () => {
41
+ const cert = base()
42
+ cert.scope.excluded = []
43
+ const result = validateAttestation(cert)
44
+ assert.ok(result.errors.some((e) => /scope\.excluded/.test(e)))
45
+ })
46
+
47
+ test('attestation: evidence needs a URL or hash', () => {
48
+ const cert = base()
49
+ cert.evidence[0] = { id: 'A', result: 'PASS' }
50
+ const result = validateAttestation(cert)
51
+ assert.ok(result.errors.some((e) => /needs a url or sha256/.test(e)))
52
+ })
53
+
54
+ test('attestation: disclaimer must deny official/certification', () => {
55
+ const cert = base()
56
+ cert.disclaimer = 'A great plugin, approved.'
57
+ const result = validateAttestation(cert)
58
+ assert.ok(result.errors.some((e) => /non-official/.test(e)))
59
+ })
@@ -0,0 +1,172 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+ import { runManifestChecks } from '../src/manifest.mjs'
6
+ import { checkCapabilities, checkCredentials, checkLabels } from '../src/declarations.mjs'
7
+ import { readSources } from '../src/util.mjs'
8
+ import { buildReport } from '../src/report.mjs'
9
+ import { parseInstallSpec } from '../src/install-spec.mjs'
10
+
11
+ const here = path.dirname(fileURLToPath(import.meta.url))
12
+ const good = path.join(here, '..', 'fixtures', 'good')
13
+ const bad = path.join(here, '..', 'fixtures', 'bad')
14
+
15
+ test('good fixture passes manifest and version checks', () => {
16
+ const result = runManifestChecks(good)
17
+ assert.equal(result.kind, 'plugin')
18
+ assert.equal(result.checks[0].id, 'DSC-01')
19
+ assert.equal(result.checks[0].result, 'PASS')
20
+ assert.equal(result.checks[1].result, 'PASS')
21
+ assert.equal(result.checks[2].result, 'NA')
22
+ })
23
+
24
+ test('bad fixture fails version and warns licence', () => {
25
+ const result = runManifestChecks(bad)
26
+ assert.equal(result.checks[0].result, 'WARN')
27
+ assert.equal(result.checks[1].result, 'FAIL')
28
+ assert.match(result.checks[1].reason, /engines\.dsh|peer/)
29
+ })
30
+
31
+ test('good fixture declarations are consistent', () => {
32
+ const result = runManifestChecks(good)
33
+ const sources = readSources(good)
34
+ const caps = checkCapabilities(result.manifest, sources, result.patch)
35
+ const creds = checkCredentials(result.manifest, sources)
36
+ const labels = checkLabels(result.manifest, sources)
37
+ assert.equal(caps.result, 'PASS', caps.reason)
38
+ assert.equal(creds.result, 'PASS', creds.reason)
39
+ assert.equal(labels.result, 'PASS', labels.reason)
40
+ })
41
+
42
+ test('bad fixture rejects official label and unknown capability', () => {
43
+ const result = runManifestChecks(bad)
44
+ const sources = readSources(bad)
45
+ const caps = checkCapabilities(result.manifest, sources, result.patch)
46
+ const labels = checkLabels(result.manifest, sources)
47
+ assert.equal(labels.result, 'FAIL')
48
+ assert.match(labels.reason, /official|forbidden/)
49
+ assert.equal(caps.result, 'WARN')
50
+ })
51
+
52
+ test('parseInstallSpec pins git and rejects floating git', () => {
53
+ const pinned = parseInstallSpec('github:acme/dsh-greet#0123456789abcdef0123456789abcdef01234567')
54
+ assert.equal(pinned.kind, 'git')
55
+ assert.equal(pinned.pinned, true)
56
+ assert.throws(() => parseInstallSpec('github:acme/dsh-greet'), /pin/)
57
+ assert.equal(parseInstallSpec('@acme/dsh-greet@1.2.3').pinned, true)
58
+ assert.equal(parseInstallSpec('@acme/dsh-greet').pinned, false)
59
+ })
60
+
61
+ test('report marks missing mandatory checks as incomplete', () => {
62
+ const checks = [
63
+ { id: 'DSC-01', clause: 'C-1', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
64
+ { id: 'DSC-02', clause: 'C-2', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
65
+ ]
66
+ const report = buildReport({
67
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
68
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
69
+ checks,
70
+ scope: 'ds-c',
71
+ profile: 'package',
72
+ kind: 'plugin',
73
+ })
74
+ assert.equal(report.conclusion.status, 'incomplete')
75
+ assert.equal(report.conclusion.exitCode, 2)
76
+ assert.equal(report.conclusion.level, null)
77
+ assert.ok(report.coverage.notRun > 0)
78
+ })
79
+
80
+ test('report passes when all mandatory checks are full and clean', () => {
81
+ const checks = ['DSC-01', 'DSC-02', 'DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09', 'DSC-11', 'DSC-12', 'DSC-13']
82
+ .map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] }))
83
+ const report = buildReport({
84
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
85
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
86
+ checks,
87
+ scope: 'ds-c',
88
+ profile: 'package',
89
+ kind: 'plugin',
90
+ })
91
+ assert.equal(report.conclusion.status, 'pass')
92
+ assert.equal(report.conclusion.exitCode, 0)
93
+ assert.equal(report.conclusion.level, 'DS-C (package)')
94
+ })
95
+
96
+ test('static-only M1 report maps L1-L3 to NA and issues DS-C static', () => {
97
+ const checks = ['DSC-01', 'DSC-02', 'DSC-03', 'DSC-11', 'DSC-12', 'DSC-13']
98
+ .map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] }))
99
+ .concat(['DSC-04', 'DSC-05', 'DSC-06', 'DSC-07', 'DSC-08', 'DSC-09'].map(id => ({ id, clause: id.replace('DSC', 'C'), layer: 'L1', result: 'ERROR', coverage: 'not-run', reason: 'no runtime', details: [], evidence: [] })))
100
+ const report = buildReport({
101
+ subject: { mode: 'listing', profile: 'listing', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
102
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
103
+ checks,
104
+ scope: 'ds-c',
105
+ profile: 'listing',
106
+ kind: 'plugin',
107
+ staticOnly: true,
108
+ })
109
+ assert.equal(report.conclusion.status, 'pass')
110
+ assert.equal(report.conclusion.level, 'DS-C static')
111
+ assert.equal(report.conclusion.staticOnly, true)
112
+ assert.equal(report.checks.find(item => item.id === 'DSC-04').result, 'NA')
113
+ })
114
+
115
+ function entryChecks(overrides = {}) {
116
+ const make = (id, clause, result = 'PASS', details = []) => ({ id, clause, layer: 'L0', result, coverage: result === 'ERROR' ? 'not-run' : 'full', reason: '', details, evidence: [] })
117
+ return [
118
+ make('DSC-01', 'C-1'),
119
+ make('DSC-02', 'C-2', overrides.c2Result ?? 'PASS', overrides.c2Details ?? []),
120
+ make('DSC-11', 'C-11'),
121
+ make('DSC-12', 'C-12'),
122
+ make('DSC-13', 'C-13'),
123
+ make('DSC-14', 'C-14', overrides.runResult ?? 'PASS'),
124
+ ]
125
+ }
126
+
127
+ test('entry: C-2.1 only is WARN, runtime level', () => {
128
+ const report = buildReport({
129
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
130
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
131
+ checks: entryChecks({ c2Result: 'FAIL', c2Details: [{ item: 'C-2.1', result: 'FAIL', reason: 'no engines' }] }),
132
+ scope: 'entry', profile: 'package', kind: 'plugin',
133
+ })
134
+ assert.equal(report.conclusion.status, 'warn')
135
+ assert.equal(report.conclusion.level, 'DSH-ENTRY runtime (warnings)')
136
+ })
137
+
138
+ test('entry: C-2.2 remains blocking', () => {
139
+ const report = buildReport({
140
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
141
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
142
+ checks: entryChecks({ c2Result: 'FAIL', c2Details: [{ item: 'C-2.2', result: 'FAIL', reason: 'peer mismatch' }] }),
143
+ scope: 'entry', profile: 'package', kind: 'plugin',
144
+ })
145
+ assert.equal(report.conclusion.status, 'fail')
146
+ assert.ok(report.coverage.entryBlocking.failed.includes('C-2.2'))
147
+ })
148
+
149
+ test('entry: no runtime is incomplete, not static pass', () => {
150
+ const report = buildReport({
151
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
152
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
153
+ checks: entryChecks({ runResult: 'ERROR' }),
154
+ scope: 'entry', profile: 'package', kind: 'plugin',
155
+ })
156
+ assert.equal(report.conclusion.status, 'incomplete')
157
+ assert.equal(report.conclusion.exitCode, 2)
158
+ assert.equal(report.conclusion.level, null)
159
+ })
160
+
161
+ test('entry: static claim recognizes without runtime', () => {
162
+ const checks = entryChecks().filter(c => c.id !== 'DSC-14')
163
+ const report = buildReport({
164
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
165
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
166
+ checks,
167
+ scope: 'entry', profile: 'package', kind: 'plugin', entryClaim: 'static',
168
+ })
169
+ assert.equal(report.conclusion.status, 'pass')
170
+ assert.equal(report.conclusion.level, 'DSH-ENTRY recognized')
171
+ assert.equal(report.conclusion.exitCode, 0)
172
+ })
@@ -0,0 +1,28 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { scanText } from '../scripts/claim-check.mjs'
4
+
5
+ test('claim: an official claim fails', () => {
6
+ const r = scanText('DSH-ENTRY is an official certification.', 'x.md')
7
+ assert.equal(r.errors.length, 1)
8
+ })
9
+
10
+ test('claim: a negated claim passes', () => {
11
+ const r = scanText('DSH-ENTRY is not an official certification.', 'x.md')
12
+ assert.deepEqual(r.errors, [])
13
+ })
14
+
15
+ test('claim: Chinese negation passes', () => {
16
+ const r = scanText('DSH-ENTRY 不是官方认证,也不做安全审计。', 'x.md')
17
+ assert.deepEqual(r.errors, [])
18
+ })
19
+
20
+ test('claim: a mark without a disclaimer warns', () => {
21
+ const r = scanText('DSH-FLOW manual', 'x.md')
22
+ assert.equal(r.warnings.length, 1)
23
+ })
24
+
25
+ test('claim: a mark with a disclaimer does not warn', () => {
26
+ const r = scanText('DSH-FLOW manual\n非官方 · 非认证 · 非安全审计', 'x.md')
27
+ assert.deepEqual(r.warnings, [])
28
+ })
@@ -0,0 +1,35 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import fs from 'node:fs'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { runManifestChecks } from '../src/manifest.mjs'
7
+
8
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
9
+ const ROOT = path.resolve(HERE, '..')
10
+
11
+ test('G4: no dsh.client is not-applicable, not failed', () => {
12
+ const { g4 } = runManifestChecks(path.join(ROOT, 'fixtures/good'))
13
+ assert.equal(g4.hasClient, false)
14
+ assert.equal(g4.ok, true)
15
+ assert.equal(g4.result, 'NA')
16
+ })
17
+
18
+ test('G4: dsh.client without exports["./client"] fails statically', () => {
19
+ const { g4 } = runManifestChecks(path.join(ROOT, 'fixtures/client-missing'))
20
+ assert.equal(g4.hasClient, true)
21
+ assert.equal(g4.ok, false)
22
+ assert.equal(g4.result, 'FAIL')
23
+ assert.match(g4.reason, /exports\["\.\/client"\]/)
24
+ })
25
+
26
+ test('G4: a client package with the export passes statically', () => {
27
+ const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/client-missing/package.json'), 'utf8'))
28
+ manifest.exports['./client'] = './client.js'
29
+ const dir = fs.mkdtempSync(path.join(ROOT, 'fixtures', 'client-ok-'))
30
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(manifest))
31
+ const { g4 } = runManifestChecks(dir)
32
+ assert.equal(g4.ok, true)
33
+ assert.equal(g4.result, 'PASS')
34
+ fs.rmSync(dir, { recursive: true, force: true })
35
+ })