dsh-standards 1.0.3 → 1.2.0

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 (99) hide show
  1. package/40-checker/README.md +15 -0
  2. package/40-checker/bin/dsh-flow-collect.mjs +3 -0
  3. package/40-checker/bin/dsh-levels.mjs +13 -2
  4. package/40-checker/fixtures/bundle-patch-only/cordis.patch.yml +3 -0
  5. package/40-checker/fixtures/bundle-patch-only/package.json +12 -0
  6. package/40-checker/fixtures/c12-command-arg/index.mjs +7 -0
  7. package/40-checker/fixtures/c12-command-arg/package.json +10 -0
  8. package/40-checker/fixtures/c12-comment/index.mjs +3 -0
  9. package/40-checker/fixtures/c12-comment/package.json +10 -0
  10. package/40-checker/fixtures/c12-compare/index.mjs +3 -0
  11. package/40-checker/fixtures/c12-compare/package.json +10 -0
  12. package/40-checker/fixtures/c12-doc-string/index.mjs +13 -0
  13. package/40-checker/fixtures/c12-doc-string/package.json +10 -0
  14. package/40-checker/fixtures/c12-grant/index.mjs +3 -0
  15. package/40-checker/fixtures/c12-grant/package.json +10 -0
  16. package/40-checker/fixtures/c12-string/index.mjs +3 -0
  17. package/40-checker/fixtures/c12-string/package.json +10 -0
  18. package/40-checker/fixtures/declaration-file/index.d.ts +1 -0
  19. package/40-checker/fixtures/declaration-file/index.mjs +9 -0
  20. package/40-checker/fixtures/declaration-file/package.json +10 -0
  21. package/40-checker/fixtures/import-local-side-effect/index.mjs +11 -0
  22. package/40-checker/fixtures/import-local-side-effect/package.json +21 -0
  23. package/40-checker/fixtures/import-local-side-effect/side-effectful.mjs +5 -0
  24. package/40-checker/fixtures/import-open-readonly/index.mjs +14 -0
  25. package/40-checker/fixtures/import-open-readonly/package.json +19 -0
  26. package/40-checker/fixtures/import-package-only/index.mjs +11 -0
  27. package/40-checker/fixtures/import-package-only/package.json +21 -0
  28. package/40-checker/fixtures/import-parse-error/index.mjs +5 -0
  29. package/40-checker/fixtures/import-parse-error/package.json +10 -0
  30. package/40-checker/fixtures/import-side-effect/index.mjs +14 -0
  31. package/40-checker/fixtures/import-side-effect/package.json +19 -0
  32. package/40-checker/fixtures/import-side-effect-nonentry/index.mjs +9 -0
  33. package/40-checker/fixtures/import-side-effect-nonentry/package.json +19 -0
  34. package/40-checker/fixtures/import-side-effect-nonentry/selfcheck.mjs +8 -0
  35. package/40-checker/fixtures/import-void-main/index.mjs +16 -0
  36. package/40-checker/fixtures/import-void-main/package.json +19 -0
  37. package/40-checker/fixtures/no-source/package.json +19 -0
  38. package/40-checker/fixtures/parse-error-nonentry/index.mjs +2 -0
  39. package/40-checker/fixtures/parse-error-nonentry/package.json +10 -0
  40. package/40-checker/fixtures/parse-error-nonentry/references/broken.js +1 -0
  41. package/40-checker/scripts/bundle-smoke.mjs +3 -0
  42. package/40-checker/scripts/entry18-ci.mjs +4 -3
  43. package/40-checker/scripts/entry18-web.mjs +4 -1
  44. package/40-checker/scripts/flow-collect.mjs +494 -0
  45. package/40-checker/scripts/flow-sample-ci.mjs +4 -4
  46. package/40-checker/scripts/host-farm.mjs +2 -1
  47. package/40-checker/scripts/journey-config-observe.mjs +5 -3
  48. package/40-checker/scripts/journey-runtime.mjs +208 -83
  49. package/40-checker/scripts/level-audit.mjs +91 -19
  50. package/40-checker/scripts/m2-runner.mjs +4 -3
  51. package/40-checker/scripts/runtime-child.mjs +4 -0
  52. package/40-checker/src/cli.mjs +11 -3
  53. package/40-checker/src/declarations.mjs +514 -19
  54. package/40-checker/src/flow-evidence.mjs +431 -0
  55. package/40-checker/src/index.mjs +3 -0
  56. package/40-checker/src/manifest.mjs +5 -0
  57. package/40-checker/src/remediation.mjs +132 -0
  58. package/40-checker/src/report.mjs +19 -2
  59. package/40-checker/src/sandbox-env.mjs +78 -0
  60. package/40-checker/test/checks.test.mjs +243 -1
  61. package/40-checker/test/flow-evidence.test.mjs +295 -0
  62. package/40-checker/test/remediation.test.mjs +300 -0
  63. package/40-checker/vendor/babel/README.md +10 -0
  64. package/40-checker/vendor/babel/index.cjs +3 -0
  65. package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/LICENSE +22 -0
  66. package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/README.md +19 -0
  67. package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/lib/index.d.ts +41 -0
  68. package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/lib/index.js +286 -0
  69. package/40-checker/vendor/babel/node_modules/@babel/helper-string-parser/package.json +31 -0
  70. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/LICENSE +22 -0
  71. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/README.md +19 -0
  72. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/identifier-browser.js +62 -0
  73. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/identifier.js +60 -0
  74. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/index.d.ts +28 -0
  75. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/lib/index.js +27 -0
  76. package/40-checker/vendor/babel/node_modules/@babel/helper-validator-identifier/package.json +45 -0
  77. package/40-checker/vendor/babel/node_modules/@babel/parser/CHANGELOG.md +1073 -0
  78. package/40-checker/vendor/babel/node_modules/@babel/parser/LICENSE +19 -0
  79. package/40-checker/vendor/babel/node_modules/@babel/parser/README.md +19 -0
  80. package/40-checker/vendor/babel/node_modules/@babel/parser/bin/babel-parser.js +14 -0
  81. package/40-checker/vendor/babel/node_modules/@babel/parser/lib/index.d.ts +1047 -0
  82. package/40-checker/vendor/babel/node_modules/@babel/parser/lib/index.js +14612 -0
  83. package/40-checker/vendor/babel/node_modules/@babel/parser/package.json +57 -0
  84. package/40-checker/vendor/babel/node_modules/@babel/parser/typings/babel-parser.d.ts +636 -0
  85. package/40-checker/vendor/babel/node_modules/@babel/types/LICENSE +22 -0
  86. package/40-checker/vendor/babel/node_modules/@babel/types/README.md +19 -0
  87. package/40-checker/vendor/babel/node_modules/@babel/types/lib/index-legacy.d.ts +2765 -0
  88. package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.d.ts +3668 -0
  89. package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.js +10758 -0
  90. package/40-checker/vendor/babel/node_modules/@babel/types/lib/index.js.flow +2618 -0
  91. package/40-checker/vendor/babel/node_modules/@babel/types/package.json +38 -0
  92. package/CHANGELOG.md +66 -0
  93. package/bin/dsh-flow-collect.mjs +4 -0
  94. package/package.json +2 -1
  95. package/spec/README.md +2 -0
  96. package/spec/flow/README.md +23 -2
  97. package/spec/flow-evidence.schema.json +217 -0
  98. package/spec/levels.json +2 -0
  99. package/spec/remediation.json +478 -0
@@ -0,0 +1,300 @@
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 remediationSpec from '../../spec/remediation.json' with { type: 'json' }
7
+ import packageJson from '../../package.json' with { type: 'json' }
8
+ import { DOCS_BASE, joinAdvice } from '../src/remediation.mjs'
9
+ import { buildReport } from '../src/report.mjs'
10
+ import { runManifestChecks } from '../src/manifest.mjs'
11
+ import { checkCapabilities, checkCredentials, checkImportTimeAdvisory, checkLabels } from '../src/declarations.mjs'
12
+ import { readSources } from '../src/util.mjs'
13
+
14
+ const here = path.dirname(fileURLToPath(import.meta.url))
15
+ const srcDir = path.join(here, '..', 'src')
16
+ const fixturesDir = path.join(here, '..', 'fixtures')
17
+ const guideDocs = process.env.DSH_GUIDE_DOCS
18
+ ? path.resolve(process.env.DSH_GUIDE_DOCS)
19
+ : path.resolve(here, '..', '..', '..', 'dsh-plugin-guide', 'docs')
20
+ const RULES = remediationSpec
21
+
22
+ function slugify(text) {
23
+ return String(text)
24
+ .trim()
25
+ .toLowerCase()
26
+ .replace(/[^\p{L}\p{N}\s_-]/gu, '')
27
+ .replace(/\s+/g, '-')
28
+ }
29
+
30
+ function headingSlugs(text) {
31
+ const slugs = new Set()
32
+ let fence = null
33
+ for (const line of text.split(/\r?\n/)) {
34
+ const fenceMatch = /^\s*(```+|~~~+)/.exec(line)
35
+ if (fenceMatch) {
36
+ if (fence === null) fence = fenceMatch[1][0]
37
+ else if (fence === fenceMatch[1][0]) fence = null
38
+ continue
39
+ }
40
+ if (fence !== null) continue
41
+ // Explicit HTML anchors are valid targets too (older docs predate heading-only slugs).
42
+ const explicit = /<a\s+id="([^"]+)"/.exec(line)
43
+ if (explicit) slugs.add(explicit[1])
44
+ const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line)
45
+ if (match) slugs.add(slugify(match[2]))
46
+ }
47
+ return slugs
48
+ }
49
+
50
+ /** The key set the checker can emit: C-x[.y] literals, clause values, G0-G4. */
51
+ function checkerUniverse() {
52
+ const keys = new Set()
53
+ for (const file of fs.readdirSync(srcDir).filter(name => name.endsWith('.mjs'))) {
54
+ const text = fs.readFileSync(path.join(srcDir, file), 'utf8')
55
+ for (const match of text.matchAll(/'((?:C-\d+)(?:\.\d+)*)'/g)) keys.add(match[1])
56
+ for (const match of text.matchAll(/item:\s*'(G[0-4])'/g)) keys.add(match[1])
57
+ }
58
+ for (const gate of ['G0', 'G1', 'G2', 'G3', 'G4']) keys.add(gate)
59
+ return keys
60
+ }
61
+
62
+ function runtimeNotRun() {
63
+ const meta = {
64
+ 'DSC-04': ['C-4', 'L1'], 'DSC-05': ['C-5', 'L1'], 'DSC-06': ['C-6', 'L1+L2'],
65
+ 'DSC-07': ['C-7', 'L2'], 'DSC-08': ['C-8', 'L3'], 'DSC-09': ['C-9', 'L1'],
66
+ }
67
+ return Object.entries(meta).map(([id, [clause, layer]]) => ({
68
+ id, clause, layer, method: 'M', advisory: false,
69
+ result: 'ERROR', coverage: 'not-run', reason: 'test: runtime not run', details: [], evidence: [],
70
+ }))
71
+ }
72
+
73
+ function staticReportFor(dir) {
74
+ const result = runManifestChecks(dir)
75
+ const sources = readSources(dir)
76
+ const declaration = result.manifest
77
+ ? [
78
+ checkCapabilities(result.manifest, sources, result.patch),
79
+ checkCredentials(result.manifest, sources),
80
+ checkLabels(result.manifest, sources),
81
+ ].map(item => (item.result === 'WARN' && ['DSC-11', 'DSC-13'].includes(item.id) ? { ...item, advisory: true } : item))
82
+ : [
83
+ { id: 'DSC-11', clause: 'C-11', layer: 'L0', method: 'M', advisory: false, result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [] },
84
+ { id: 'DSC-12', clause: 'C-12', layer: 'L0', method: 'M', advisory: false, result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [] },
85
+ { id: 'DSC-13', clause: 'C-13', layer: 'L0', method: 'M', advisory: false, result: 'ERROR', coverage: 'not-run', reason: 'manifest unavailable', details: [], evidence: [] },
86
+ ]
87
+ const checks = [
88
+ ...result.checks,
89
+ ...runtimeNotRun(),
90
+ { id: 'DSC-14', clause: 'C-14', layer: 'L1', method: 'M', advisory: false, result: 'ERROR', coverage: 'not-run', reason: 'test: runtime not run', details: [], evidence: [] },
91
+ checkImportTimeAdvisory(sources, result.entry?.relative),
92
+ ...declaration,
93
+ ]
94
+ if (result.g4?.result === 'FAIL') {
95
+ const dsc01 = checks.find(item => item.id === 'DSC-01')
96
+ if (dsc01 && dsc01.result !== 'FAIL') {
97
+ dsc01.result = 'FAIL'
98
+ dsc01.reason = dsc01.reason ? `${dsc01.reason}; ${result.g4.reason}` : result.g4.reason
99
+ dsc01.details = [...(dsc01.details ?? []), { item: 'G4', result: 'FAIL', reason: result.g4.reason }]
100
+ }
101
+ }
102
+ return buildReport({
103
+ subject: {
104
+ mode: 'package', profile: 'package', kind: result.kind, kindInferred: true,
105
+ name: result.manifest?.name ?? null, version: result.manifest?.version ?? null, path: dir,
106
+ },
107
+ anchor: { repo: 'test', commit: '0'.repeat(40), scanner: 'test', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
108
+ checks,
109
+ g4: result.g4,
110
+ scope: 'ds-c',
111
+ profile: 'package',
112
+ kind: result.kind,
113
+ })
114
+ }
115
+
116
+ test('remediation table covers every checker key exactly', () => {
117
+ const universe = checkerUniverse()
118
+ const keys = new Set(Object.keys(RULES))
119
+ const missing = [...universe].filter(key => !keys.has(key)).sort()
120
+ const extra = [...keys].filter(key => !universe.has(key)).sort()
121
+ assert.deepEqual(missing, [], `missing remediation rules: ${missing.join(', ')}`)
122
+ assert.deepEqual(extra, [], `stale remediation rules: ${extra.join(', ')}`)
123
+ assert.ok(universe.size > 0)
124
+ })
125
+
126
+ test('remediation rules have the required shape', () => {
127
+ assert.equal(DOCS_BASE, 'https://github.com/PerryLink/dsh-plugin-guide/blob/v0.4.4/')
128
+ assert.equal(packageJson.version, '1.2.0')
129
+ assert.ok(Object.keys(RULES).length > 0)
130
+ for (const [key, rule] of Object.entries(RULES)) {
131
+ assert.match(key, /^(?:C-\d+(?:\.\d+)*|G[0-4])$/)
132
+ for (const field of ['title', 'why', 'fix']) {
133
+ assert.equal(typeof rule[field], 'string', `${key}.${field}`)
134
+ assert.ok(rule[field].trim().length > 0, `${key}.${field} is empty`)
135
+ }
136
+ assert.match(rule.docs, /^docs\/[a-z]+\.md#[a-z0-9-]+$/, `${key}.docs`)
137
+ assert.ok(['fail', 'warn', 'advisory'].includes(rule.severityHint), `${key}.severityHint`)
138
+ }
139
+ })
140
+
141
+ test('advice on every fixture has a rule (no ruleMissing)', () => {
142
+ const dirs = fs.readdirSync(fixturesDir, { withFileTypes: true })
143
+ .filter(entry => entry.isDirectory())
144
+ .map(entry => path.join(fixturesDir, entry.name))
145
+ .filter(dir => fs.existsSync(path.join(dir, 'package.json')))
146
+ assert.ok(dirs.length >= 20, `expected fixture packages, got ${dirs.length}`)
147
+ let total = 0
148
+ for (const dir of dirs) {
149
+ const report = staticReportFor(dir)
150
+ assert.ok(Array.isArray(report.advice), `${path.basename(dir)}: advice is not an array`)
151
+ const missing = report.advice.filter(item => item.ruleMissing)
152
+ assert.deepEqual(missing.map(item => `${item.source}:${item.key}`), [], `${path.basename(dir)}: ruleMissing`)
153
+ for (const item of report.advice) {
154
+ assert.ok(item.docsUrl, `${path.basename(dir)}: ${item.key} has no docsUrl`)
155
+ assert.equal(item.docsUrl, `${DOCS_BASE}${RULES[item.key].docs}`, `${path.basename(dir)}: ${item.key} docsUrl`)
156
+ assert.equal(item.version, packageJson.version)
157
+ assert.ok(['FAIL', 'WARN'].includes(item.severity))
158
+ }
159
+ total += report.advice.length
160
+ }
161
+ assert.ok(total > 0, 'fixtures produced no advice; the test would be vacuous')
162
+ })
163
+
164
+ test('runtime FAIL without details falls back to check.clause', () => {
165
+ const checks = [
166
+ {
167
+ id: 'DSC-04', clause: 'C-4', layer: 'L1', result: 'FAIL', coverage: 'full',
168
+ reason: 'load failed: boom', details: [], evidence: ['index.mjs:1'], method: 'M', advisory: false,
169
+ },
170
+ {
171
+ id: 'DSC-14', clause: 'C-14', layer: 'L1', result: 'FAIL', coverage: 'full',
172
+ reason: 'runtime failed', details: [], evidence: ['index.mjs:1'], method: 'M', advisory: false,
173
+ },
174
+ ]
175
+ const report = buildReport({
176
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
177
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
178
+ checks,
179
+ scope: 'ds-c',
180
+ profile: 'package',
181
+ kind: 'plugin',
182
+ })
183
+ const c4 = report.advice.find(item => item.source === 'DSC-04')
184
+ const c14 = report.advice.find(item => item.source === 'DSC-14')
185
+ assert.equal(c4.key, 'C-4')
186
+ assert.equal(c4.severity, 'FAIL')
187
+ assert.equal(c4.version, '1.2.0')
188
+ assert.deepEqual(c4.evidence, ['index.mjs:1'])
189
+ assert.equal(c4.docsUrl, `${DOCS_BASE}docs/levels.md#entry`)
190
+ assert.equal(c14.key, 'C-14')
191
+ assert.equal(c14.docsUrl, `${DOCS_BASE}docs/entry.md#the-two-claims`)
192
+ })
193
+
194
+ test('advice uses detail severity even when the check was adjusted to WARN', () => {
195
+ const checks = [
196
+ { id: 'DSC-01', clause: 'C-1', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
197
+ {
198
+ id: 'DSC-02', clause: 'C-2', layer: 'L0', result: 'FAIL', coverage: 'full', reason: '',
199
+ details: [{ item: 'C-2.1', result: 'FAIL', reason: 'no engines', evidence: ['package.json#engines'] }],
200
+ evidence: [], advisory: false,
201
+ },
202
+ { id: 'DSC-11', clause: 'C-11', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
203
+ { id: 'DSC-12', clause: 'C-12', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
204
+ { id: 'DSC-13', clause: 'C-13', layer: 'L0', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
205
+ { id: 'DSC-14', clause: 'C-14', layer: 'L1', result: 'PASS', coverage: 'full', reason: '', details: [], evidence: [] },
206
+ ]
207
+ const report = buildReport({
208
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
209
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
210
+ checks,
211
+ scope: 'entry',
212
+ profile: 'package',
213
+ kind: 'plugin',
214
+ })
215
+ assert.equal(report.checks.find(item => item.id === 'DSC-02').result, 'WARN')
216
+ const item = report.advice.find(entry => entry.key === 'C-2.1')
217
+ assert.equal(item.severity, 'FAIL')
218
+ assert.equal(item.source, 'DSC-02')
219
+ assert.match(item.reason, /no engines/)
220
+ })
221
+
222
+ test('duplicate details merge evidence and count once per check and key', () => {
223
+ const checks = [{
224
+ id: 'DSC-11', clause: 'C-11', layer: 'L0', result: 'WARN', coverage: 'full', reason: '',
225
+ details: [
226
+ { item: 'C-11.2', result: 'WARN', reason: 'trigger tools', evidence: ['a.mjs:1'] },
227
+ { item: 'C-11.1', result: 'WARN', reason: 'missing capabilities', evidence: ['package.json#dsh.capabilities'] },
228
+ { item: 'C-11.2', result: 'WARN', reason: 'trigger web', evidence: ['b.mjs:2'] },
229
+ ],
230
+ evidence: [], advisory: true,
231
+ }]
232
+ const report = buildReport({
233
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
234
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
235
+ checks,
236
+ scope: 'ds-c',
237
+ profile: 'package',
238
+ kind: 'plugin',
239
+ })
240
+ const merged = report.advice.find(item => item.key === 'C-11.2')
241
+ assert.ok(merged)
242
+ assert.equal(merged.occurrences, 2)
243
+ assert.deepEqual(merged.evidence, ['a.mjs:1', 'b.mjs:2'])
244
+ assert.equal(merged.advisory, true)
245
+ assert.equal(report.advice.filter(item => item.source === 'DSC-11').length, 2)
246
+ })
247
+
248
+ test('buildReport includes advice by default and omits it on advice:false', () => {
249
+ const base = {
250
+ subject: { mode: 'package', profile: 'package', kind: 'plugin', kindInferred: false, name: 'x', version: '1.0.0', path: here },
251
+ anchor: { repo: 'x', commit: '0'.repeat(40), scanner: 'x', dsh: '0.2.2-alpha.2', cordis: '4.0.6-alpha.1', runtime: null },
252
+ checks: [],
253
+ scope: 'ds-c',
254
+ profile: 'package',
255
+ kind: 'plugin',
256
+ }
257
+ const on = buildReport(base)
258
+ const off = buildReport({ ...base, advice: false })
259
+ assert.ok(Array.isArray(on.advice))
260
+ assert.equal(on.advice.length, 0)
261
+ assert.equal(Object.hasOwn(off, 'advice'), false)
262
+ })
263
+
264
+ test('every rule docs anchor exists in the local guide repository', t => {
265
+ if (!fs.existsSync(guideDocs)) {
266
+ t.skip(`guide docs not checked out at ${guideDocs}`)
267
+ return
268
+ }
269
+ const cache = new Map()
270
+ for (const [key, rule] of Object.entries(RULES)) {
271
+ const hash = rule.docs.indexOf('#')
272
+ const file = rule.docs.slice(0, hash)
273
+ const anchor = rule.docs.slice(hash + 1)
274
+ assert.ok(hash > 0 && !anchor.includes('#'), `${key}: docs must be <file>#<anchor>`)
275
+ const relative = file.replace(/^docs\//, '')
276
+ const absolute = path.join(guideDocs, relative)
277
+ assert.ok(fs.existsSync(absolute), `${key}: missing guide file ${file}`)
278
+ if (!cache.has(file)) cache.set(file, headingSlugs(fs.readFileSync(absolute, 'utf8')))
279
+ assert.ok(cache.get(file).has(anchor), `${key}: missing anchor ${rule.docs}`)
280
+ }
281
+ })
282
+
283
+ test('joinAdvice is pure and leaves the report untouched', () => {
284
+ const checks = [{
285
+ id: 'DSC-04', clause: 'C-4', layer: 'L1', result: 'FAIL', coverage: 'full',
286
+ reason: 'load failed', details: [], evidence: ['index.mjs'], method: 'M', advisory: false,
287
+ }]
288
+ const report = {
289
+ checks,
290
+ standard: { id: 'DS-C', version: '1.0.0' },
291
+ }
292
+ const snapshot = JSON.stringify(report)
293
+ const advice = joinAdvice(report, remediationSpec, DOCS_BASE)
294
+ assert.equal(JSON.stringify(report), snapshot)
295
+ assert.equal(advice.length, 1)
296
+ assert.equal(advice[0].key, 'C-4')
297
+ // A flat rule map (no version metadata) still gets the package version.
298
+ const bare = joinAdvice(report, RULES, DOCS_BASE)
299
+ assert.equal(bare[0].version, packageJson.version)
300
+ })
@@ -0,0 +1,10 @@
1
+ # Vendored @babel/parser
2
+
3
+ - Package: `@babel/parser` 8.1.0 (MIT), with `@babel/types`,
4
+ `@babel/helper-string-parser`, `@babel/helper-validator-identifier`.
5
+ - Why: the checker needs a real AST for TypeScript/JSX as well as JS. acorn
6
+ alone cannot parse `.ts`/`.tsx`; Babel can.
7
+ - Layout: `node_modules/@babel/*` so `@babel/parser` resolves its own deps
8
+ without network access. `index.cjs` is the only entry the checker imports.
9
+ - Licenses: `node_modules/@babel/parser/LICENSE` and the sibling packages.
10
+ - Update: copy the four packages from a fresh `npm install @babel/parser@8.1.0`.
@@ -0,0 +1,3 @@
1
+ // Vendored @babel/parser (MIT). Keeps the checker zero-install: no runtime npm
2
+ // dependency, and the GitHub Action can run from a codeload checkout.
3
+ module.exports = require('@babel/parser')
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2014-present Sebastian McKenzie and other contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,19 @@
1
+ # @babel/helper-string-parser
2
+
3
+ > A utility package to parse strings
4
+
5
+ See our website [@babel/helper-string-parser](https://babeljs.io/docs/babel-helper-string-parser) for more information.
6
+
7
+ ## Install
8
+
9
+ Using npm:
10
+
11
+ ```sh
12
+ npm install --save @babel/helper-string-parser
13
+ ```
14
+
15
+ or using yarn:
16
+
17
+ ```sh
18
+ yarn add @babel/helper-string-parser
19
+ ```
@@ -0,0 +1,41 @@
1
+ type StringContentsErrorHandlers = EscapedCharErrorHandlers & {
2
+ unterminated(initialPos: number, initialLineStart: number, initialCurLine: number): void;
3
+ };
4
+ declare function readStringContents(type: "single" | "double" | "template", input: string, pos: number, lineStart: number, curLine: number, errors: StringContentsErrorHandlers): {
5
+ pos: number;
6
+ str: string;
7
+ firstInvalidLoc: {
8
+ pos: number;
9
+ lineStart: number;
10
+ curLine: number;
11
+ } | null;
12
+ lineStart: number;
13
+ curLine: number;
14
+ };
15
+ type EscapedCharErrorHandlers = HexCharErrorHandlers & CodePointErrorHandlers & {
16
+ strictNumericEscape(pos: number, lineStart: number, curLine: number): void;
17
+ };
18
+ type HexCharErrorHandlers = IntErrorHandlers & {
19
+ invalidEscapeSequence(pos: number, lineStart: number, curLine: number): void;
20
+ };
21
+ type IntErrorHandlers = {
22
+ numericSeparatorInEscapeSequence(pos: number, lineStart: number, curLine: number): void;
23
+ unexpectedNumericSeparator(pos: number, lineStart: number, curLine: number): void;
24
+ invalidDigit(pos: number, lineStart: number, curLine: number, radix: number): void;
25
+ };
26
+ declare function readInt(input: string, pos: number, lineStart: number, curLine: number, radix: number, len: number | undefined, forceLen: boolean, allowNumSeparator: boolean | "bail", errors: IntErrorHandlers, bailOnError: boolean): {
27
+ n: null;
28
+ pos: number;
29
+ } | {
30
+ n: number;
31
+ pos: number;
32
+ };
33
+ type CodePointErrorHandlers = HexCharErrorHandlers & {
34
+ invalidCodePoint(pos: number, lineStart: number, curLine: number): void;
35
+ };
36
+ declare function readCodePoint(input: string, pos: number, lineStart: number, curLine: number, throwOnInvalid: boolean, errors: CodePointErrorHandlers): {
37
+ code: number | null;
38
+ pos: number;
39
+ };
40
+
41
+ export { type CodePointErrorHandlers, type IntErrorHandlers, type StringContentsErrorHandlers, readCodePoint, readInt, readStringContents };
@@ -0,0 +1,286 @@
1
+ var _isDigit = function isDigit(code) {
2
+ return code >= 48 && code <= 57;
3
+ };
4
+ const forbiddenNumericSeparatorSiblings = {
5
+ decBinOct: new Set([46, 66, 69, 79, 95, 98, 101, 111, 43, 45]),
6
+ hex: new Set([46, 88, 95, 120])
7
+ };
8
+ const isAllowedNumericSeparatorSibling = {
9
+ bin: ch => ch === 48 || ch === 49,
10
+ oct: ch => ch >= 48 && ch <= 55,
11
+ dec: ch => ch >= 48 && ch <= 57,
12
+ hex: ch => ch >= 48 && ch <= 57 || ch >= 65 && ch <= 70 || ch >= 97 && ch <= 102
13
+ };
14
+ function readStringContents(type, input, pos, lineStart, curLine, errors) {
15
+ const initialPos = pos;
16
+ const initialLineStart = lineStart;
17
+ const initialCurLine = curLine;
18
+ let out = "";
19
+ let firstInvalidLoc = null;
20
+ let chunkStart = pos;
21
+ const {
22
+ length
23
+ } = input;
24
+ for (;;) {
25
+ if (pos >= length) {
26
+ errors.unterminated(initialPos, initialLineStart, initialCurLine);
27
+ out += input.slice(chunkStart, pos);
28
+ break;
29
+ }
30
+ const ch = input.charCodeAt(pos);
31
+ if (isStringEnd(type, ch, input, pos)) {
32
+ out += input.slice(chunkStart, pos);
33
+ break;
34
+ }
35
+ if (ch === 92) {
36
+ out += input.slice(chunkStart, pos);
37
+ const res = readEscapedChar(input, pos, lineStart, curLine, type === "template", errors);
38
+ if (res.ch === null && !firstInvalidLoc) {
39
+ firstInvalidLoc = {
40
+ pos,
41
+ lineStart,
42
+ curLine
43
+ };
44
+ } else {
45
+ out += res.ch;
46
+ }
47
+ ({
48
+ pos,
49
+ lineStart,
50
+ curLine
51
+ } = res);
52
+ chunkStart = pos;
53
+ } else if (ch === 8232 || ch === 8233) {
54
+ ++pos;
55
+ ++curLine;
56
+ lineStart = pos;
57
+ } else if (ch === 10 || ch === 13) {
58
+ if (type === "template") {
59
+ out += input.slice(chunkStart, pos) + "\n";
60
+ ++pos;
61
+ if (ch === 13 && input.charCodeAt(pos) === 10) {
62
+ ++pos;
63
+ }
64
+ ++curLine;
65
+ chunkStart = lineStart = pos;
66
+ } else {
67
+ errors.unterminated(initialPos, initialLineStart, initialCurLine);
68
+ }
69
+ } else {
70
+ ++pos;
71
+ }
72
+ }
73
+ return {
74
+ pos,
75
+ str: out,
76
+ firstInvalidLoc,
77
+ lineStart,
78
+ curLine
79
+ };
80
+ }
81
+ function isStringEnd(type, ch, input, pos) {
82
+ if (type === "template") {
83
+ return ch === 96 || ch === 36 && input.charCodeAt(pos + 1) === 123;
84
+ }
85
+ return ch === (type === "double" ? 34 : 39);
86
+ }
87
+ function readEscapedChar(input, pos, lineStart, curLine, inTemplate, errors) {
88
+ const throwOnInvalid = !inTemplate;
89
+ pos++;
90
+ const res = ch => ({
91
+ pos,
92
+ ch,
93
+ lineStart,
94
+ curLine
95
+ });
96
+ const ch = input.charCodeAt(pos++);
97
+ switch (ch) {
98
+ case 110:
99
+ return res("\n");
100
+ case 114:
101
+ return res("\r");
102
+ case 120:
103
+ {
104
+ let code;
105
+ ({
106
+ code,
107
+ pos
108
+ } = readHexChar(input, pos, lineStart, curLine, 2, false, throwOnInvalid, errors));
109
+ return res(code === null ? null : String.fromCharCode(code));
110
+ }
111
+ case 117:
112
+ {
113
+ let code;
114
+ ({
115
+ code,
116
+ pos
117
+ } = readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors));
118
+ return res(code === null ? null : String.fromCodePoint(code));
119
+ }
120
+ case 116:
121
+ return res("\t");
122
+ case 98:
123
+ return res("\b");
124
+ case 118:
125
+ return res("\u000b");
126
+ case 102:
127
+ return res("\f");
128
+ case 13:
129
+ if (input.charCodeAt(pos) === 10) {
130
+ ++pos;
131
+ }
132
+ case 10:
133
+ lineStart = pos;
134
+ ++curLine;
135
+ case 8232:
136
+ case 8233:
137
+ return res("");
138
+ case 56:
139
+ case 57:
140
+ if (inTemplate) {
141
+ return res(null);
142
+ } else {
143
+ errors.strictNumericEscape(pos - 1, lineStart, curLine);
144
+ }
145
+ default:
146
+ if (ch >= 48 && ch <= 55) {
147
+ const startPos = pos - 1;
148
+ const match = /^[0-7]+/.exec(input.slice(startPos, pos + 2));
149
+ let octalStr = match[0];
150
+ let octal = parseInt(octalStr, 8);
151
+ if (octal > 255) {
152
+ octalStr = octalStr.slice(0, -1);
153
+ octal = parseInt(octalStr, 8);
154
+ }
155
+ pos += octalStr.length - 1;
156
+ const next = input.charCodeAt(pos);
157
+ if (octalStr !== "0" || next === 56 || next === 57) {
158
+ if (inTemplate) {
159
+ return res(null);
160
+ } else {
161
+ errors.strictNumericEscape(startPos, lineStart, curLine);
162
+ }
163
+ }
164
+ return res(String.fromCharCode(octal));
165
+ }
166
+ return res(String.fromCharCode(ch));
167
+ }
168
+ }
169
+ function readHexChar(input, pos, lineStart, curLine, len, forceLen, throwOnInvalid, errors) {
170
+ const initialPos = pos;
171
+ let n;
172
+ ({
173
+ n,
174
+ pos
175
+ } = readInt(input, pos, lineStart, curLine, 16, len, forceLen, false, errors, !throwOnInvalid));
176
+ if (n === null) {
177
+ if (throwOnInvalid) {
178
+ errors.invalidEscapeSequence(initialPos, lineStart, curLine);
179
+ } else {
180
+ pos = initialPos - 1;
181
+ }
182
+ }
183
+ return {
184
+ code: n,
185
+ pos
186
+ };
187
+ }
188
+ function readInt(input, pos, lineStart, curLine, radix, len, forceLen, allowNumSeparator, errors, bailOnError) {
189
+ const start = pos;
190
+ const forbiddenSiblings = radix === 16 ? forbiddenNumericSeparatorSiblings.hex : forbiddenNumericSeparatorSiblings.decBinOct;
191
+ const isAllowedSibling = radix === 16 ? isAllowedNumericSeparatorSibling.hex : radix === 10 ? isAllowedNumericSeparatorSibling.dec : radix === 8 ? isAllowedNumericSeparatorSibling.oct : isAllowedNumericSeparatorSibling.bin;
192
+ let invalid = false;
193
+ let total = 0;
194
+ for (let i = 0, e = len == null ? Infinity : len; i < e; ++i) {
195
+ const code = input.charCodeAt(pos);
196
+ let val;
197
+ if (code === 95 && allowNumSeparator !== "bail") {
198
+ const prev = input.charCodeAt(pos - 1);
199
+ const next = input.charCodeAt(pos + 1);
200
+ if (!allowNumSeparator) {
201
+ if (bailOnError) return {
202
+ n: null,
203
+ pos
204
+ };
205
+ errors.numericSeparatorInEscapeSequence(pos, lineStart, curLine);
206
+ } else if (Number.isNaN(next) || !isAllowedSibling(next) || forbiddenSiblings.has(prev)) {
207
+ if (bailOnError) return {
208
+ n: null,
209
+ pos
210
+ };
211
+ errors.unexpectedNumericSeparator(pos, lineStart, curLine);
212
+ }
213
+ ++pos;
214
+ continue;
215
+ }
216
+ if (code >= 97) {
217
+ val = code - 97 + 10;
218
+ } else if (code >= 65) {
219
+ val = code - 65 + 10;
220
+ } else if (_isDigit(code)) {
221
+ val = code - 48;
222
+ } else {
223
+ val = Infinity;
224
+ }
225
+ if (val >= radix) {
226
+ if (val <= 9) {
227
+ if (bailOnError) return {
228
+ n: null,
229
+ pos
230
+ };
231
+ errors.invalidDigit(pos, lineStart, curLine, radix);
232
+ val = 0;
233
+ } else if (forceLen) {
234
+ val = 0;
235
+ invalid = true;
236
+ } else {
237
+ break;
238
+ }
239
+ }
240
+ ++pos;
241
+ total = total * radix + val;
242
+ }
243
+ if (pos === start || len != null && pos - start !== len || invalid) {
244
+ return {
245
+ n: null,
246
+ pos
247
+ };
248
+ }
249
+ return {
250
+ n: total,
251
+ pos
252
+ };
253
+ }
254
+ function readCodePoint(input, pos, lineStart, curLine, throwOnInvalid, errors) {
255
+ const ch = input.charCodeAt(pos);
256
+ let code;
257
+ if (ch === 123) {
258
+ ++pos;
259
+ ({
260
+ code,
261
+ pos
262
+ } = readHexChar(input, pos, lineStart, curLine, input.indexOf("}", pos) - pos, true, throwOnInvalid, errors));
263
+ ++pos;
264
+ if (code !== null && code > 0x10ffff) {
265
+ if (throwOnInvalid) {
266
+ errors.invalidCodePoint(pos, lineStart, curLine);
267
+ } else {
268
+ return {
269
+ code: null,
270
+ pos
271
+ };
272
+ }
273
+ }
274
+ } else {
275
+ ({
276
+ code,
277
+ pos
278
+ } = readHexChar(input, pos, lineStart, curLine, 4, false, throwOnInvalid, errors));
279
+ }
280
+ return {
281
+ code,
282
+ pos
283
+ };
284
+ }
285
+
286
+ export { readCodePoint, readInt, readStringContents };