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,147 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dco-check.mjs — enforce DCO 1.1 sign-off and the DSH contribution terms.
4
+ *
5
+ * Rules:
6
+ * 1. every commit has at least one `Signed-off-by: Name <email>`;
7
+ * 2. the author email matches one of the sign-off emails;
8
+ * 3. `AI-Assisted: yes` requires `AI-Tool:` and a human sign-off;
9
+ * 4. with --require-terms, at least one commit carries `DSH-Contribution: dsh-ct/1`
10
+ * or the PR body contains the terms checkbox.
11
+ *
12
+ * This checks attestations, not identity. It cannot verify legal capacity,
13
+ * employment, or patent status.
14
+ *
15
+ * Usage:
16
+ * node 40-checker/scripts/dco-check.mjs --range origin/main..HEAD --require-terms --pr-body-env PR_BODY
17
+ * node 40-checker/scripts/dco-check.mjs --range HEAD~1..HEAD --json
18
+ */
19
+ import fs from 'node:fs'
20
+ import path from 'node:path'
21
+ import { spawnSync } from 'node:child_process'
22
+ import { fileURLToPath } from 'node:url'
23
+
24
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
25
+ const DEFAULT_ROOT = path.resolve(HERE, '..', '..')
26
+ const TERMS = 'dsh-ct/1'
27
+
28
+ export function parseLog(text) {
29
+ return text
30
+ .split('\x1e')
31
+ .map((record) => record.trim())
32
+ .filter(Boolean)
33
+ .map((record) => {
34
+ const [sha, authorName, authorEmail, ...rest] = record.split('\x1f')
35
+ return { sha: (sha || '').trim(), authorName: authorName || '', authorEmail: authorEmail || '', body: rest.join('\x1f') }
36
+ })
37
+ }
38
+
39
+ export function checkCommit(commit) {
40
+ const errors = []
41
+ const warnings = []
42
+ const body = commit.body || ''
43
+ const signoffs = [...body.matchAll(/^Signed-off-by:\s*(.+?)\s*<([^>]+)>\s*$/gim)]
44
+ .map((m) => ({ name: m[1].trim(), email: m[2].trim().toLowerCase() }))
45
+ if (signoffs.length === 0) {
46
+ errors.push(`${commit.sha.slice(0, 8)}: missing Signed-off-by`)
47
+ } else {
48
+ const emails = new Set(signoffs.map((s) => s.email))
49
+ if (commit.authorEmail && !emails.has(commit.authorEmail.toLowerCase())) {
50
+ errors.push(`${commit.sha.slice(0, 8)}: author email ${commit.authorEmail} is not among the sign-offs`)
51
+ }
52
+ }
53
+ const aiAssisted = /^AI-Assisted:\s*yes\s*$/im.test(body) || /^AI-Generated:\s*yes\s*$/im.test(body)
54
+ if (aiAssisted) {
55
+ if (!/^AI-Tool:\s*\S+/im.test(body)) errors.push(`${commit.sha.slice(0, 8)}: AI-Assisted without AI-Tool`)
56
+ const human = signoffs.some((s) => !/\bbot\b|\[bot\]/i.test(s.name))
57
+ if (!human) errors.push(`${commit.sha.slice(0, 8)}: AI-Assisted needs a human sign-off`)
58
+ }
59
+ const terms = /^(?:DSH-Contribution|Contribution-Agreement):\s*dsh-ct\/1\s*$/im.test(body)
60
+ const entity = /^Entity-CLA:\s*(\S+)\s*$/im.exec(body)
61
+ const onBehalf = /^On-Behalf-Of:\s*(.+?)\s*$/im.exec(body)
62
+ const agent = /^AI-Agent:\s*(\S+)\s*$/im.exec(body)
63
+ if (onBehalf && !entity) warnings.push(`${commit.sha.slice(0, 8)}: On-Behalf-Of present; verify the Entity CLA manually`)
64
+ return {
65
+ sha: commit.sha,
66
+ errors,
67
+ warnings,
68
+ terms,
69
+ entityCla: entity ? entity[1] : null,
70
+ onBehalfOf: onBehalf ? onBehalf[1] : null,
71
+ aiAgent: agent ? agent[1] : null,
72
+ }
73
+ }
74
+
75
+ export function checkTerms({ commits, prBody, requireTerms }) {
76
+ if (!requireTerms) return { errors: [], warnings: [] }
77
+ const fromCommit = commits.some((c) => /^(?:DSH-Contribution|Contribution-Agreement):\s*dsh-ct\/1\s*$/im.test(c.body || ''))
78
+ const fromPr = /\[[xX]\]\s*I agree to the DSH Contribution Terms v1/i.test(prBody || '')
79
+ || /dsh-ct\/1/i.test(prBody || '')
80
+ if (!fromCommit && !fromPr) {
81
+ return { errors: ['no DSH Contribution Terms acceptance: add the PR checkbox or DSH-Contribution: dsh-ct/1'], warnings: [] }
82
+ }
83
+ return { errors: [], warnings: [] }
84
+ }
85
+
86
+ export function run({ root = DEFAULT_ROOT, range = 'HEAD~1..HEAD', prBody = '', requireTerms = false } = {}) {
87
+ const result = spawnSync('git', ['-C', root, 'log', '--format=%H%x1f%an%x1f%ae%x1f%B%x1e', range], { encoding: 'utf8' })
88
+ if (result.status !== 0) {
89
+ return { ok: false, errors: [`git log failed: ${(result.stderr || '').trim()}`], warnings: [], commits: [] }
90
+ }
91
+ const commits = parseLog(result.stdout)
92
+ if (commits.length === 0) {
93
+ return { ok: false, errors: [`no commits in range ${range}`], warnings: [], commits: [] }
94
+ }
95
+ const errors = []
96
+ const warnings = []
97
+ const checked = commits.map((commit) => {
98
+ const r = checkCommit(commit)
99
+ errors.push(...r.errors)
100
+ warnings.push(...r.warnings)
101
+ return { sha: commit.sha, terms: r.terms, entityCla: r.entityCla, aiAgent: r.aiAgent }
102
+ })
103
+ const terms = checkTerms({ commits, prBody, requireTerms })
104
+ errors.push(...terms.errors)
105
+ warnings.push(...terms.warnings)
106
+ if (checked.some((c) => c.entityCla)) warnings.push('Entity-CLA trailer present; verify the archived document manually')
107
+ return { ok: errors.length === 0, errors, warnings, commits: checked }
108
+ }
109
+
110
+ export function main(argv = process.argv.slice(2)) {
111
+ let root = DEFAULT_ROOT
112
+ let range = 'HEAD~1..HEAD'
113
+ let prBody = ''
114
+ let prBodyEnv = null
115
+ let requireTerms = false
116
+ let json = false
117
+ for (let i = 0; i < argv.length; i += 1) {
118
+ const arg = argv[i]
119
+ if (arg === '--root') root = path.resolve(argv[++i])
120
+ else if (arg === '--range') range = argv[++i]
121
+ else if (arg === '--pr-body') prBody = fs.readFileSync(argv[++i], 'utf8')
122
+ else if (arg === '--pr-body-env') prBodyEnv = argv[++i]
123
+ else if (arg === '--require-terms') requireTerms = true
124
+ else if (arg === '--json') json = true
125
+ else if (arg === '--help' || arg === '-h') {
126
+ console.log('usage: dco-check.mjs [--root <dir>] [--range <git-range>] [--pr-body <file>] [--pr-body-env <ENV>] [--require-terms] [--json]')
127
+ return 0
128
+ } else {
129
+ console.error(`unknown argument: ${arg}`)
130
+ return 2
131
+ }
132
+ }
133
+ if (prBodyEnv) prBody = process.env[prBodyEnv] || ''
134
+ const result = run({ root, range, prBody, requireTerms })
135
+ if (json) {
136
+ console.log(JSON.stringify(result, null, 2))
137
+ } else {
138
+ for (const warning of result.warnings) console.log(`WARN ${warning}`)
139
+ for (const error of result.errors) console.log(`FAIL ${error}`)
140
+ console.log(`${result.ok ? 'PASS' : 'FAIL'} dco: ${result.commits.length} commit(s), ${result.errors.length} error(s), ${result.warnings.length} warning(s)`)
141
+ }
142
+ return result.ok ? 0 : 2
143
+ }
144
+
145
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
146
+ process.exitCode = main()
147
+ }
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dependabot-check.mjs — enforce the owner policy: no Dependabot in this repo.
4
+ *
5
+ * Fails when `.github/dependabot.yml`/`.yaml` exists, or when a workflow still
6
+ * references Dependabot (for example an auto-merge job). The policy is recorded
7
+ * in `46`/`51`; this script makes it machine-checkable.
8
+ */
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+
13
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
14
+ const ROOT = path.resolve(HERE, '..', '..')
15
+ const errors = []
16
+
17
+ for (const name of ['dependabot.yml', 'dependabot.yaml']) {
18
+ const file = path.join(ROOT, '.github', name)
19
+ if (fs.existsSync(file)) errors.push(`found ${path.relative(ROOT, file)}`)
20
+ }
21
+
22
+ const workflows = path.join(ROOT, '.github', 'workflows')
23
+ if (fs.existsSync(workflows)) {
24
+ for (const name of fs.readdirSync(workflows)) {
25
+ if (!/\.ya?ml$/.test(name)) continue
26
+ const text = fs.readFileSync(path.join(workflows, name), 'utf8')
27
+ // Only automation references count. The policy guard itself lives in a
28
+ // workflow and must not trip this check.
29
+ if (/dependabot\[bot\]|dependabot\/fetch-metadata|dependabot-auto-merge|dependabot\/merge|dependabot\/.*action/i.test(text)) {
30
+ errors.push(`workflow ${name} references Dependabot automation`)
31
+ }
32
+ }
33
+ }
34
+
35
+ if (errors.length) {
36
+ for (const error of errors) console.error(`FAIL dependabot: ${error}`)
37
+ console.error(`FAIL dependabot: ${errors.length} error(s); owner policy is no Dependabot`)
38
+ process.exit(1)
39
+ }
40
+ console.log('PASS dependabot: no config or automation reference')
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * doc-commands-check.mjs — keep the developer path first-party and runnable.
4
+ *
5
+ * Two checks:
6
+ * 1. Current developer docs must not send readers to `npx dsh-levels`,
7
+ * `npx dsh-plugin-kit`, or `npx dsh-plugin-verify`; those bare npm names
8
+ * belong to other projects, not this repository.
9
+ * 2. The first-party template in `examples/plugin-template` must still audit
10
+ * to a non-`not-run` ENTRY state.
11
+ *
12
+ * This is a documentation/CLI guard, not a runtime or security claim.
13
+ */
14
+ import fs from 'node:fs'
15
+ import os from 'node:os'
16
+ import path from 'node:path'
17
+ import { spawnSync } from 'node:child_process'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
21
+ const STANDARDS = path.resolve(HERE, '..', '..')
22
+ const DOCS = [
23
+ '45-三用途落地-官方市场开发者.md',
24
+ ...fs.readdirSync(path.join(STANDARDS, 'adoption', 'developer'))
25
+ .filter((name) => name.endsWith('.md'))
26
+ .map((name) => path.join('adoption', 'developer', name)),
27
+ ]
28
+ const FORBIDDEN = [
29
+ { re: /\bnpx\s+dsh-levels\b/, name: 'npx dsh-levels' },
30
+ { re: /\bnpx\s+dsh-plugin-kit\b/, name: 'npx dsh-plugin-kit' },
31
+ { re: /\bnpx\s+dsh-plugin-verify\b/, name: 'npx dsh-plugin-verify' },
32
+ ]
33
+
34
+ let errors = 0
35
+ for (const rel of DOCS) {
36
+ const text = fs.readFileSync(path.join(STANDARDS, rel), 'utf8')
37
+ const lines = text.split(/\r?\n/)
38
+ let inFence = false
39
+ for (let index = 0; index < lines.length; index += 1) {
40
+ const line = lines[index]
41
+ if (/^\s*```/.test(line)) {
42
+ inFence = !inFence
43
+ continue
44
+ }
45
+ if (!inFence) continue
46
+ for (const rule of FORBIDDEN) {
47
+ if (rule.re.test(line)) {
48
+ console.error(`FAIL ${rel}:${index + 1}: ${rule.name} resolves to a third-party npm package`)
49
+ errors += 1
50
+ }
51
+ }
52
+ }
53
+ }
54
+ if (errors === 0) console.log(`PASS docs: 0 third-party npx command(s) in ${DOCS.length} file(s)`)
55
+
56
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-doc-commands-'))
57
+ try {
58
+ const out = path.join(tmp, 'audit.json')
59
+ const run = spawnSync(process.execPath, [
60
+ path.join(STANDARDS, '40-checker', 'bin', 'dsh-levels.mjs'),
61
+ 'audit',
62
+ path.join(STANDARDS, 'examples', 'plugin-template'),
63
+ '--format', 'json',
64
+ '--out', out,
65
+ ], { encoding: 'utf8' })
66
+
67
+ if (run.status !== 0) {
68
+ console.error(`FAIL template: dsh-levels audit exited ${run.status}`)
69
+ if (run.stderr) console.error(run.stderr.trim())
70
+ errors += 1
71
+ } else {
72
+ const report = JSON.parse(fs.readFileSync(out, 'utf8'))
73
+ const state = report?.levels?.entry?.state
74
+ if (!['recognized', 'static', 'runtime'].includes(state)) {
75
+ console.error(`FAIL template: ENTRY state is ${JSON.stringify(state)}, expected recognized/static/runtime`)
76
+ errors += 1
77
+ } else {
78
+ console.log(`PASS template: ENTRY state=${state}`)
79
+ }
80
+ }
81
+ } catch (error) {
82
+ console.error(`FAIL template check: ${error.message}`)
83
+ errors += 1
84
+ } finally {
85
+ fs.rmSync(tmp, { recursive: true, force: true })
86
+ }
87
+
88
+ if (errors > 0) {
89
+ console.error(`FAIL doc-commands: ${errors} error(s)`)
90
+ process.exit(1)
91
+ }
92
+ console.log('PASS doc-commands: first-party developer path is runnable')
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * entry-scan.mjs — read-only C1-C8 scan of public GitHub plugins.
4
+ *
5
+ * It fetches package.json and checks that the referenced patch/client/entry
6
+ * files exist via the GitHub contents API. It stores only package.json and
7
+ * placeholder files; it never executes plugin code. C3 is left not-run because
8
+ * a real install line was not tested on a host.
9
+ */
10
+ import fs from 'node:fs'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { spawnSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
17
+ const STD = path.resolve(HERE, '..', '..')
18
+ const VALIDATOR = path.join(STD, '40-checker', 'scripts', 'entry-validate.mjs')
19
+ const TOKEN = process.env.GH_TOKEN
20
+ if (!TOKEN) throw new Error('GH_TOKEN is required')
21
+
22
+ const headers = {
23
+ Authorization: `Bearer ${TOKEN}`,
24
+ Accept: 'application/vnd.github+json',
25
+ 'User-Agent': 'dsh-entry-scan',
26
+ 'X-GitHub-Api-Version': '2022-11-28',
27
+ }
28
+
29
+ function usage() {
30
+ console.log(`entry-scan — static C1-C8 scan of public DSH plugins
31
+
32
+ Usage:
33
+ node 40-checker/scripts/entry-scan.mjs [--limit 10] [--out report.json] [--repos owner/repo,...]
34
+
35
+ Without --repos, searches GitHub topic:dsh-plugin sorted by stars.
36
+ It writes only package.json and placeholder files in a temp directory; no plugin code is executed.
37
+ C3 is not-run because no real install line is tested.
38
+ Exit: 0 scan completed, 3 input/API error.`)
39
+ }
40
+
41
+ function parseArgs(argv) {
42
+ const options = { limit: 10, out: null, repos: null }
43
+ for (let i = 0; i < argv.length; i += 1) {
44
+ const arg = argv[i]
45
+ const next = () => {
46
+ const value = argv[i + 1]
47
+ if (value === undefined) throw new Error(`${arg} needs a value`)
48
+ i += 1
49
+ return value
50
+ }
51
+ if (arg === '--limit') options.limit = Number(next())
52
+ else if (arg === '--out') options.out = path.resolve(next())
53
+ else if (arg === '--repos') options.repos = next().split(',').map(s => s.trim()).filter(Boolean)
54
+ else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
55
+ else throw new Error(`unknown argument: ${arg}`)
56
+ }
57
+ if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 30) throw new Error('--limit must be 1..30')
58
+ return options
59
+ }
60
+
61
+ async function api(pathname) {
62
+ const r = await fetch(`https://api.github.com${pathname}`, { headers })
63
+ if (r.status === 404) return { ok: false, status: 404, json: null }
64
+ const text = await r.text()
65
+ let json = null
66
+ try { json = JSON.parse(text) } catch { json = { raw: text } }
67
+ if (!r.ok) throw new Error(`${pathname} -> ${r.status}: ${text.slice(0, 300)}`)
68
+ return { ok: true, status: r.status, json }
69
+ }
70
+
71
+ async function fileExists(repo, ref, file) {
72
+ if (typeof file !== 'string' || file.length === 0) return false
73
+ const clean = file.replace(/^\.\//, '')
74
+ const encoded = clean.split('/').map(encodeURIComponent).join('/')
75
+ const r = await api(`/repos/${repo}/contents/${encoded}?ref=${encodeURIComponent(ref)}`)
76
+ return r.ok
77
+ }
78
+
79
+ function writePlaceholder(dir, file) {
80
+ const clean = file.replace(/^\.\//, '')
81
+ const full = path.resolve(dir, clean)
82
+ if (!full.startsWith(`${dir}${path.sep}`)) return
83
+ fs.mkdirSync(path.dirname(full), { recursive: true })
84
+ fs.writeFileSync(full, '// existence verified via GitHub contents API; content not stored\n')
85
+ }
86
+
87
+ async function scanRepo(repo, meta) {
88
+ const pkgRes = await api(`/repos/${repo}/contents/package.json?ref=${encodeURIComponent(meta.default_branch)}`)
89
+ if (!pkgRes.ok) return null
90
+ const pkg = JSON.parse(Buffer.from(pkgRes.json.content, 'base64').toString('utf8'))
91
+ if (!pkg.dsh || typeof pkg.dsh !== 'object') return null
92
+ if (!pkg.dsh.bundle && !pkg.dsh.client) return null
93
+ const commitRes = await api(`/repos/${repo}/commits?per_page=1`)
94
+ const commit = commitRes.json?.[0]?.sha ?? null
95
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-entry-scan-'))
96
+ try {
97
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
98
+ const files = []
99
+ if (typeof pkg.dsh.bundle?.patch === 'string') files.push(pkg.dsh.bundle.patch)
100
+ else if (Array.isArray(pkg.dsh.bundle?.patch)) files.push(...pkg.dsh.bundle.patch.filter(item => typeof item === 'string'))
101
+ if (pkg.dsh.client) {
102
+ const client = typeof pkg.exports?.['./client'] === 'string'
103
+ ? pkg.exports['./client']
104
+ : pkg.exports?.['./client']?.import ?? pkg.exports?.['./client']?.default
105
+ if (typeof client === 'string') files.push(client)
106
+ }
107
+ const main = typeof pkg.main === 'string' ? pkg.main : typeof pkg.exports?.['.'] === 'string' ? pkg.exports['.'] : pkg.exports?.['.']?.import
108
+ if (typeof main === 'string') files.push(main)
109
+ const paths = new Set()
110
+ const treeRes = await api(`/repos/${repo}/git/trees/${encodeURIComponent(meta.default_branch)}?recursive=1`)
111
+ if (treeRes.ok && Array.isArray(treeRes.json.tree)) {
112
+ for (const item of treeRes.json.tree) paths.add(item.path)
113
+ }
114
+ for (const file of [...new Set(files)]) {
115
+ const clean = file.replace(/^\.\//, '')
116
+ if (paths.has(clean) || (!treeRes.ok && await fileExists(repo, meta.default_branch, file))) {
117
+ writePlaceholder(dir, file)
118
+ }
119
+ }
120
+ const metaFile = path.join(dir, 'meta.json')
121
+ fs.writeFileSync(metaFile, JSON.stringify({
122
+ repo,
123
+ topics: meta.topics ?? [],
124
+ archived: meta.archived === true,
125
+ commit,
126
+ pushedAt: String(meta.pushed_at ?? '').slice(0, 10),
127
+ claimLevel: 'reference',
128
+ }, null, 2))
129
+ const run = spawnSync(process.execPath, [VALIDATOR, dir, '--meta', metaFile, '--json'], { encoding: 'utf8', timeout: 30_000 })
130
+ const result = JSON.parse(run.stdout)
131
+ return {
132
+ repo,
133
+ stars: meta.stargazers_count,
134
+ package: pkg.name ?? null,
135
+ version: pkg.version ?? null,
136
+ filesChecked: [...new Set(files)],
137
+ conclusion: result.conclusion,
138
+ checks: result.checks,
139
+ exit: run.status,
140
+ }
141
+ } finally {
142
+ fs.rmSync(dir, { recursive: true, force: true })
143
+ }
144
+ }
145
+
146
+ async function main() {
147
+ const options = parseArgs(process.argv.slice(2))
148
+ let repos = options.repos
149
+ if (!repos) {
150
+ const search = await api(`/search/repositories?q=${encodeURIComponent('topic:dsh-plugin')}&sort=stars&order=desc&per_page=30`)
151
+ repos = (search.json.items ?? []).map(item => item.full_name)
152
+ }
153
+ const scanned = []
154
+ for (const repo of repos) {
155
+ if (scanned.length >= options.limit) break
156
+ const r = await api(`/repos/${repo}`)
157
+ if (!r.ok) continue
158
+ const row = await scanRepo(repo, r.json)
159
+ if (row) {
160
+ scanned.push(row)
161
+ console.log(`${repo}: ${row.conclusion.status}; ${row.conclusion.failed.join(',') || 'no fail'}${row.conclusion.notRun.length ? `; not-run ${row.conclusion.notRun.join(',')}` : ''}`)
162
+ }
163
+ }
164
+ const report = {
165
+ schema: 'dsh-entry-scan/1',
166
+ generatedAt: new Date().toISOString(),
167
+ method: 'static GitHub contents API; C3 not-run; no plugin code executed; file existence only for patch/client/main',
168
+ disclaimer: '非官方 · 非认证 · 非安全审计 · 非背书 · 无担保',
169
+ notClaimed: ['official standard', 'certification', 'security audit', 'ranking', 'market acceptance', 'external adoption'],
170
+ total: scanned.length,
171
+ summary: {
172
+ pass: scanned.filter(r => r.conclusion.status === 'pass').length,
173
+ passWithWarnings: scanned.filter(r => r.conclusion.status === 'pass-with-warnings').length,
174
+ incomplete: scanned.filter(r => r.conclusion.status === 'incomplete').length,
175
+ fail: scanned.filter(r => r.conclusion.status === 'fail').length,
176
+ },
177
+ repos: scanned,
178
+ }
179
+ const output = `${JSON.stringify(report, null, 2)}\n`
180
+ if (options.out) {
181
+ fs.mkdirSync(path.dirname(options.out), { recursive: true })
182
+ fs.writeFileSync(options.out, output)
183
+ console.log(`wrote ${options.out}`)
184
+ } else process.stdout.write(output)
185
+ }
186
+
187
+ main().catch(error => {
188
+ console.error(`entry-scan: ${error.message}`)
189
+ process.exit(3)
190
+ })