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,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * governance-check.mjs — validate the machine-readable governance files.
4
+ *
5
+ * It checks the rules that must not rot:
6
+ * - exactly one human organizer;
7
+ * - every AI role is a bot and never holds a forbidden permission;
8
+ * - Class A decisions are human-only; Class B can be drafted by AI but not decided;
9
+ * - AI merge allowlist/denylist still protect spec/governance/checker;
10
+ * - contributors and reviewers reference an existing role and a known terms version.
11
+ *
12
+ * Exit codes: 0 all checks pass; 2 at least one error.
13
+ *
14
+ * Usage:
15
+ * node 40-checker/scripts/governance-check.mjs [--root <dir>] [--format text|json]
16
+ */
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
22
+ const DEFAULT_ROOT = path.resolve(HERE, '..', '..')
23
+ const TERMS_VERSION = 'dsh-ct/1'
24
+ const VALUES = new Set(['allow', 'allow-with-allowlist', 'propose', 'review', 'deny'])
25
+ const LEVELS = new Set(['signed', 'verified', 'trusted', 'entity-covered'])
26
+ const REVIEW_STATUS = new Set(['provisional', 'active', 'dormant', 'revoked'])
27
+
28
+ function readJson(root, rel) {
29
+ const file = path.join(root, rel)
30
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
31
+ }
32
+
33
+ export function validateGovernance({ roles, contributors, reviewers, coi }) {
34
+ const errors = []
35
+ const warnings = []
36
+ const err = (m) => errors.push(m)
37
+ const warn = (m) => warnings.push(m)
38
+
39
+ if (!roles || roles.schema !== 'dsh-governance/1') err('roles.schema must be dsh-governance/1')
40
+ if (!contributors || contributors.schema !== 'dsh-contributors/1') err('contributors.schema must be dsh-contributors/1')
41
+ if (!reviewers || reviewers.schema !== 'dsh-reviewers/1') err('reviewers.schema must be dsh-reviewers/1')
42
+ if (!coi || coi.schema !== 'dsh-coi/1') err('coi.schema must be dsh-coi/1')
43
+ if (!roles || !roles.roles || !roles.matrix) {
44
+ return { errors: errors.concat('roles.json must contain roles and matrix'), warnings }
45
+ }
46
+
47
+ // 1. Roles and matrix.
48
+ const roleIds = Object.keys(roles.roles)
49
+ const humanRoles = roleIds.filter((id) => roles.roles[id].humanOnly === true)
50
+ const organizers = roleIds.filter((id) => id === 'organizer' || roles.roles[id].appointment === 'creator')
51
+ if (organizers.length !== 1) err(`exactly one organizer is required, found ${organizers.length}`)
52
+ if (!humanRoles.includes('organizer')) err('organizer must be humanOnly')
53
+ if (roles.roles.organizer && roles.roles.organizer.maxHolders !== 1) err('organizer.maxHolders must be 1')
54
+ if (!humanRoles.includes('maintainer')) err('maintainer must be humanOnly')
55
+ if (!humanRoles.includes('reviewer')) err('reviewer must be humanOnly')
56
+
57
+ const permissions = roles.permissions
58
+ if (!Array.isArray(permissions) || permissions.length < 10) err('roles.permissions must list the permission keys')
59
+ for (const id of roleIds) {
60
+ const role = roles.roles[id]
61
+ if (typeof role.label !== 'string' || !role.label) err(`${id}.label is required`)
62
+ if (role.bot === true && role.humanOnly === true) err(`${id} cannot be both bot and humanOnly`)
63
+ if (typeof role.bot !== 'boolean') err(`${id}.bot must be boolean`)
64
+ if (typeof role.humanOnly !== 'boolean') err(`${id}.humanOnly must be boolean`)
65
+ const row = roles.matrix[id]
66
+ if (!row) { err(`matrix.${id} is missing`); continue }
67
+ for (const permission of permissions) {
68
+ if (!(permission in row)) err(`matrix.${id}.${permission} is missing`)
69
+ else if (!VALUES.has(row[permission])) err(`matrix.${id}.${permission} has invalid value ${row[permission]}`)
70
+ }
71
+ for (const key of Object.keys(row)) {
72
+ if (!permissions.includes(key)) err(`matrix.${id}.${key} is not a declared permission`)
73
+ }
74
+ }
75
+
76
+ // 2. AI boundaries.
77
+ const ai = roles.ai || {}
78
+ const forbidden = new Set(ai.forbiddenPermissions || [])
79
+ for (const id of roleIds.filter((rid) => roles.roles[rid].bot === true)) {
80
+ for (const permission of forbidden) {
81
+ const value = roles.matrix[id]?.[permission]
82
+ if (value && value !== 'deny' && value !== 'propose') {
83
+ err(`AI role ${id} must not hold ${permission} (got ${value})`)
84
+ }
85
+ }
86
+ if (roles.matrix[id]?.['appeal.decide'] !== 'deny') err(`AI role ${id} must not decide appeals`)
87
+ if (roles.matrix[id]?.['repo.merge.main'] === 'allow') err(`AI role ${id} needs allow-with-allowlist, not allow`)
88
+ }
89
+ if (!Array.isArray(ai.mergeAllowlist) || ai.mergeAllowlist.length === 0) err('ai.mergeAllowlist must not be empty')
90
+ if (!Array.isArray(ai.mergeDenylist)) err('ai.mergeDenylist must be an array')
91
+ for (const required of ['spec/', 'governance/', '40-checker/']) {
92
+ if (!(ai.mergeDenylist || []).includes(required)) err(`ai.mergeDenylist must include ${required}`)
93
+ }
94
+ if (!Array.isArray(ai.mustNot) || ai.mustNot.length < 3) warn('ai.mustNot should list the hard prohibitions')
95
+
96
+ // 3. Decision classes.
97
+ for (const [cls, rule] of Object.entries(roles.decisions || {})) {
98
+ const approvers = rule.approver || []
99
+ if (approvers.length === 0) err(`decision ${cls} has no approver`)
100
+ for (const approver of approvers) {
101
+ if (!roles.roles[approver]) err(`decision ${cls} references unknown role ${approver}`)
102
+ }
103
+ if (cls === 'A') {
104
+ if (rule.aiAllowed !== false) err('decision A must set aiAllowed=false')
105
+ for (const approver of approvers) {
106
+ if (roles.roles[approver]?.humanOnly !== true) err(`decision A approver ${approver} must be human`)
107
+ }
108
+ }
109
+ if (cls === 'B') {
110
+ if (rule.aiAllowed !== true) err('decision B should allow AI drafting')
111
+ if (rule.aiRole !== 'draft-only') err('decision B aiRole must be draft-only')
112
+ }
113
+ if (cls === 'D') {
114
+ for (const approver of approvers) {
115
+ if (roles.roles[approver]?.bot !== true) err(`decision D approver ${approver} must be an AI role`)
116
+ }
117
+ }
118
+ }
119
+
120
+ // 4. Quorum must never give AI a vote.
121
+ for (const [stage, rule] of Object.entries(roles.quorum || {})) {
122
+ if (typeof rule.claim !== 'string' || !/official|provisional|reviewed|consensus/i.test(rule.claim)) {
123
+ warn(`quorum.${stage}.claim should state the claim limit`)
124
+ }
125
+ }
126
+
127
+ // 5. Contributors.
128
+ const contributorHandles = new Set()
129
+ for (const entry of contributors.contributors || []) {
130
+ if (!entry.handle) { err('contributor entry without handle'); continue }
131
+ if (contributorHandles.has(entry.handle)) err(`duplicate contributor ${entry.handle}`)
132
+ contributorHandles.add(entry.handle)
133
+ if (!LEVELS.has(entry.level)) err(`contributor ${entry.handle} has invalid level ${entry.level}`)
134
+ if (entry.signedOff !== true) err(`contributor ${entry.handle} has no DCO sign-off`)
135
+ if (entry.terms !== contributors.terms) err(`contributor ${entry.handle} terms mismatch`)
136
+ if (entry.terms !== TERMS_VERSION) err(`contributor ${entry.handle} must use ${TERMS_VERSION}`)
137
+ if (entry.entityCla !== null && !/^[0-9a-f]{64}$/.test(entry.entityCla || '')) {
138
+ err(`contributor ${entry.handle} entityCla must be null or a sha256`)
139
+ }
140
+ if (!Array.isArray(entry.coi)) err(`contributor ${entry.handle} must declare coi[]`)
141
+ }
142
+
143
+ // 6. Reviewers.
144
+ for (const entry of reviewers.reviewers || []) {
145
+ if (!entry.handle) { err('reviewer entry without handle'); continue }
146
+ if (!REVIEW_STATUS.has(entry.status)) err(`reviewer ${entry.handle} has invalid status ${entry.status}`)
147
+ if (!Array.isArray(entry.scopes) || entry.scopes.length === 0) err(`reviewer ${entry.handle} needs scopes[]`)
148
+ if (!entry.expiresAt || Number.isNaN(Date.parse(entry.expiresAt))) err(`reviewer ${entry.handle} needs expiresAt`)
149
+ if (!Array.isArray(entry.coi)) err(`reviewer ${entry.handle} needs coi[]`)
150
+ if (roles.roles[entry.role]?.humanOnly !== true) err(`reviewer ${entry.handle} must reference a human role`)
151
+ }
152
+
153
+ // 7. COI declarations.
154
+ for (const entry of coi.declarations || []) {
155
+ if (!entry.handle) { err('coi entry without handle'); continue }
156
+ for (const field of roles.coi?.requiredFields || []) {
157
+ if (!(field in entry)) err(`coi ${entry.handle} is missing ${field}`)
158
+ }
159
+ if (entry.reviewDue && Number.isNaN(Date.parse(entry.reviewDue))) err(`coi ${entry.handle} reviewDue is invalid`)
160
+ if (entry.reviewDue && Date.parse(entry.reviewDue) < Date.now()) warn(`coi ${entry.handle} review is overdue`)
161
+ }
162
+
163
+ // 8. Basic consistency between registries.
164
+ for (const entry of reviewers.reviewers || []) {
165
+ if (entry.handle && !contributorHandles.has(entry.handle)) warn(`reviewer ${entry.handle} is not in contributors.json`)
166
+ }
167
+
168
+ return { errors, warnings }
169
+ }
170
+
171
+ export function main(argv = process.argv.slice(2)) {
172
+ let root = DEFAULT_ROOT
173
+ let format = 'text'
174
+ for (let i = 0; i < argv.length; i += 1) {
175
+ if (argv[i] === '--root') root = path.resolve(argv[++i])
176
+ else if (argv[i] === '--format') format = argv[++i]
177
+ else if (argv[i] === '--help' || argv[i] === '-h') {
178
+ console.log('usage: governance-check.mjs [--root <dir>] [--format text|json]')
179
+ return 0
180
+ } else {
181
+ console.error(`unknown argument: ${argv[i]}`)
182
+ return 2
183
+ }
184
+ }
185
+ let data
186
+ try {
187
+ data = {
188
+ roles: readJson(root, 'governance/roles.json'),
189
+ contributors: readJson(root, 'governance/contributors.json'),
190
+ reviewers: readJson(root, 'governance/reviewers.json'),
191
+ coi: readJson(root, 'governance/coi.json'),
192
+ }
193
+ } catch (error) {
194
+ console.error(`governance-check: cannot read input: ${error.message}`)
195
+ return 2
196
+ }
197
+ const result = validateGovernance(data)
198
+ if (format === 'json') {
199
+ console.log(JSON.stringify({ ok: result.errors.length === 0, ...result }, null, 2))
200
+ } else {
201
+ for (const warning of result.warnings) console.log(`WARN ${warning}`)
202
+ for (const error of result.errors) console.log(`FAIL ${error}`)
203
+ console.log(`${result.errors.length === 0 ? 'PASS' : 'FAIL'} governance: ${result.errors.length} error(s), ${result.warnings.length} warning(s)`)
204
+ }
205
+ return result.errors.length === 0 ? 0 : 2
206
+ }
207
+
208
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
209
+ process.exitCode = main()
210
+ }
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build a temporary host farm for L1-L3 runs.
4
+ *
5
+ * The checker itself stays zero-dependency. This script is a *test utility*:
6
+ * it links a DSH checkout's installed dependencies plus every `@deepseek-ai/*`
7
+ * workspace/vendor package into a temp `node_modules`, copies one plugin into
8
+ * `farm/plugins/<name>`, and optionally runs `npm install` / `npm run build`.
9
+ *
10
+ * Linux/WSL is the supported environment (symlinks). Windows may need
11
+ * Developer Mode or an elevated shell for directory junctions.
12
+ *
13
+ * Usage:
14
+ * node scripts/host-farm.mjs \
15
+ * --plugin /tmp/dsh-verify-samples/foo \
16
+ * --farm /tmp/dsh-farm \
17
+ * --host <dsh-checkout> \
18
+ * --install --build
19
+ */
20
+ import fs from 'node:fs'
21
+ import path from 'node:path'
22
+ import { spawnSync } from 'node:child_process'
23
+
24
+ function parse(argv) {
25
+ const options = { plugin: null, farm: null, host: '<dsh-checkout>', install: false, build: false, link: false, quiet: false }
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const arg = argv[i]
28
+ if (arg === '--plugin') options.plugin = argv[++i]
29
+ else if (arg === '--farm') options.farm = argv[++i]
30
+ else if (arg === '--host') options.host = argv[++i]
31
+ else if (arg === '--install') options.install = true
32
+ else if (arg === '--build') options.build = true
33
+ else if (arg === '--link') options.link = true
34
+ else if (arg === '--quiet') options.quiet = true
35
+ else if (arg === '--help' || arg === '-h') { console.log(fs.readFileSync(new URL(import.meta.url), 'utf8').split('\n').slice(0, 20).join('\n')); process.exit(0) }
36
+ else throw new Error(`unknown option: ${arg}`)
37
+ }
38
+ if (!options.plugin) throw new Error('--plugin is required')
39
+ if (!options.farm) throw new Error('--farm is required')
40
+ return options
41
+ }
42
+
43
+ function ensureDir(dir) {
44
+ fs.mkdirSync(dir, { recursive: true })
45
+ }
46
+
47
+ function link(source, dest) {
48
+ try {
49
+ if (fs.existsSync(dest) || fs.lstatSync(dest, { throwIfNoEntry: false })) return false
50
+ } catch { /* lstat can throw for broken links; fall through */ }
51
+ try {
52
+ fs.symlinkSync(source, dest, process.platform === 'win32' ? 'junction' : 'dir')
53
+ return true
54
+ } catch {
55
+ return false
56
+ }
57
+ }
58
+
59
+ function linkNodeModules(host, farm) {
60
+ const sourceModules = path.join(host, 'node_modules')
61
+ const destModules = path.join(farm, 'node_modules')
62
+ ensureDir(destModules)
63
+ let linked = 0
64
+ if (fs.existsSync(sourceModules)) {
65
+ for (const entry of fs.readdirSync(sourceModules, { withFileTypes: true })) {
66
+ if (entry.name === '@deepseek-ai') continue
67
+ const source = path.join(sourceModules, entry.name)
68
+ const dest = path.join(destModules, entry.name)
69
+ if (entry.name.startsWith('@') && entry.isDirectory()) {
70
+ ensureDir(dest)
71
+ for (const child of fs.readdirSync(source, { withFileTypes: true })) {
72
+ if (link(path.join(source, child.name), path.join(dest, child.name))) linked += 1
73
+ }
74
+ } else if (link(source, dest)) linked += 1
75
+ }
76
+ }
77
+ return linked
78
+ }
79
+
80
+ function linkWorkspacePackages(host, farm) {
81
+ const scope = path.join(farm, 'node_modules', '@deepseek-ai')
82
+ ensureDir(scope)
83
+ let linked = 0
84
+ const seen = new Set()
85
+ const walk = (directory) => {
86
+ if (!fs.existsSync(directory)) return
87
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
88
+ if (!entry.isDirectory()) continue
89
+ if (entry.name === 'node_modules' || entry.name === '.git') continue
90
+ const absolute = path.join(directory, entry.name)
91
+ const manifestPath = path.join(absolute, 'package.json')
92
+ if (fs.existsSync(manifestPath)) {
93
+ try {
94
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
95
+ if (typeof manifest.name === 'string' && manifest.name.startsWith('@deepseek-ai/')) {
96
+ const short = manifest.name.slice('@deepseek-ai/'.length)
97
+ if (!seen.has(short)) {
98
+ seen.add(short)
99
+ if (link(absolute, path.join(scope, short))) linked += 1
100
+ }
101
+ }
102
+ } catch { /* ignore malformed package.json */ }
103
+ }
104
+ if (entry.name === 'packages' || entry.name === 'vendor') walk(absolute)
105
+ else if (absolute.startsWith(path.join(host, 'packages')) || absolute.startsWith(path.join(host, 'vendor'))) walk(absolute)
106
+ }
107
+ }
108
+ walk(path.join(host, 'packages'))
109
+ walk(path.join(host, 'vendor'))
110
+ return linked
111
+ }
112
+
113
+ function copyPlugin(plugin, farm, asLink) {
114
+ const name = path.basename(path.resolve(plugin))
115
+ const dest = path.join(farm, 'plugins', name)
116
+ ensureDir(path.dirname(dest))
117
+ if (fs.existsSync(dest)) fs.rmSync(dest, { recursive: true, force: true })
118
+ if (asLink) {
119
+ fs.symlinkSync(path.resolve(plugin), dest, process.platform === 'win32' ? 'junction' : 'dir')
120
+ } else {
121
+ fs.cpSync(plugin, dest, { recursive: true, dereference: false })
122
+ }
123
+ return { name, dest }
124
+ }
125
+
126
+ function run(command, args, cwd) {
127
+ const result = spawnSync(command, args, { cwd, stdio: 'inherit', env: { ...process.env, npm_config_audit: 'false', npm_config_fund: 'false' } })
128
+ if (result.error) return { ok: false, error: String(result.error) }
129
+ return { ok: result.status === 0, status: result.status }
130
+ }
131
+
132
+ function main() {
133
+ const options = parse(process.argv.slice(2))
134
+ const farm = path.resolve(options.farm)
135
+ const plugin = path.resolve(options.plugin)
136
+ if (!fs.existsSync(path.join(plugin, 'package.json'))) throw new Error(`not a package directory: ${plugin}`)
137
+ if (!fs.existsSync(options.host)) throw new Error(`host checkout not found: ${options.host}`)
138
+ ensureDir(farm)
139
+ const modules = linkNodeModules(options.host, farm)
140
+ const workspace = linkWorkspacePackages(options.host, farm)
141
+ const copied = copyPlugin(plugin, farm, options.link)
142
+ let install = null
143
+ let build = null
144
+ if (options.install) {
145
+ const manifest = JSON.parse(fs.readFileSync(path.join(copied.dest, 'package.json'), 'utf8'))
146
+ if (manifest.dependencies && Object.keys(manifest.dependencies).length > 0) {
147
+ install = run('npm', ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], copied.dest)
148
+ }
149
+ }
150
+ if (options.build) {
151
+ const manifest = JSON.parse(fs.readFileSync(path.join(copied.dest, 'package.json'), 'utf8'))
152
+ if (manifest.scripts?.build) build = run('npm', ['run', 'build'], copied.dest)
153
+ }
154
+ const summary = { farm, plugin: copied.dest, linkedModules: modules, linkedWorkspace: workspace, install, build }
155
+ if (!options.quiet) console.log(JSON.stringify(summary, null, 2))
156
+ return copied.dest
157
+ }
158
+
159
+ try {
160
+ const plugin = main()
161
+ console.log(plugin)
162
+ } catch (error) {
163
+ console.error(`host-farm: ${error.message}`)
164
+ process.exitCode = 3
165
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * J3 + J7 runtime probe on a fixture plugin.
4
+ *
5
+ * J3: an invalid config value must fail loudly with a field path; a valid value
6
+ * and the default must boot.
7
+ * J7: the start line must name the package/version; a keyless boot must reach a
8
+ * diagnosable credential boundary.
9
+ *
10
+ * This is fixture-level evidence. It does not prove that every plugin validates
11
+ * its config or logs a start line.
12
+ *
13
+ * Usage:
14
+ * node 40-checker/scripts/journey-config-observe.mjs \
15
+ * --package 40-checker/fixtures/flow-config \
16
+ * --dsh /tmp/dsh022/node_modules/@deepseek-ai/dsh/lib/bin.js --json
17
+ */
18
+ import fs from 'node:fs'
19
+ import os from 'node:os'
20
+ import path from 'node:path'
21
+ import { spawnSync } from 'node:child_process'
22
+
23
+ function parse(argv) {
24
+ const out = { package: null, dsh: process.env.DSH_BIN || 'dsh', json: false, timeout: 120_000 }
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ if (argv[i] === '--package') out.package = argv[++i]
27
+ else if (argv[i] === '--dsh') out.dsh = argv[++i]
28
+ else if (argv[i] === '--json') out.json = true
29
+ else if (argv[i] === '--timeout') out.timeout = Number(argv[++i])
30
+ else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: journey-config-observe.mjs --package <dir> [--dsh <bin>] [--json]'); process.exit(0) }
31
+ else throw new Error(`unknown option: ${argv[i]}`)
32
+ }
33
+ if (!out.package) throw new Error('--package is required')
34
+ return out
35
+ }
36
+
37
+ function sh(cmd, args, env, timeout) {
38
+ return spawnSync(cmd, args, { encoding: 'utf8', timeout, env, maxBuffer: 8 * 1024 * 1024 })
39
+ }
40
+
41
+ function main() {
42
+ const options = parse(process.argv.slice(2))
43
+ const pkg = path.resolve(options.package)
44
+ const manifest = JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'), 'utf8'))
45
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-config-'))
46
+ const profile = 'flow-config'
47
+ const env = { ...process.env, DSH_HOME: home }
48
+ delete env.DEEPSEEK_API_KEY
49
+ delete env.OPENAI_API_KEY
50
+ const checks = []
51
+ let dshVersion = null
52
+ const add = (id, result, reason) => checks.push({ id, result, reason })
53
+ try {
54
+ const version = sh(options.dsh, ['--version'], env, 20_000)
55
+ dshVersion = (version.stdout || '').trim()
56
+ const create = sh(options.dsh, ['--profile', profile, '--from-default-profile', 'headless', '--dump-config'], env, options.timeout)
57
+ if (create.status !== 0) {
58
+ add('setup.profile', 'BLOCKED', `profile create exit=${create.status}`)
59
+ } else {
60
+ const install = sh(options.dsh, ['plugin', '--profile', profile, 'add', pkg], env, options.timeout)
61
+ add('setup.install', install.status === 0 ? 'PASS' : 'BLOCKED', `plain dependency install exit=${install.status}`)
62
+
63
+ const overlay = path.join(home, 'flow-config.yml')
64
+ const write = (config) => {
65
+ const body = config === undefined
66
+ ? `- insert:\n - id: flow-config\n name: '${manifest.name}'\n`
67
+ : `- insert:\n - id: flow-config\n name: '${manifest.name}'\n config:\n port: ${JSON.stringify(config)}\n`
68
+ fs.writeFileSync(overlay, body)
69
+ }
70
+ const boot = () => sh(options.dsh, ['--profile', profile, '--patch', overlay, 'reply with one short sentence'], env, 60_000)
71
+
72
+ write('not-a-number')
73
+ const invalid = boot()
74
+ const invalidText = `${invalid.stdout}${invalid.stderr}`
75
+ const fieldPath = /config\.port/.test(invalidText)
76
+ add('J3.invalid-field-path', invalid.status !== 0 && fieldPath ? 'PASS' : 'FAIL', `invalid config exit=${invalid.status}; fieldPath=${fieldPath}`)
77
+
78
+ write(8080)
79
+ const valid = boot()
80
+ const validText = `${valid.stdout}${valid.stderr}`
81
+ add('J3.valid-config', valid.status !== 0 && /started \(port=8080\)/.test(validText) ? 'PASS' : 'FAIL', `valid config exit=${valid.status}`)
82
+ add('J7.start-line', /flow-config@1\.0\.0 started/.test(validText) ? 'PASS' : 'FAIL', 'start line names package@version')
83
+ add('J7.diagnosable', /MISSING_CREDENTIAL|no API key|AUTH|credential/i.test(validText) ? 'PASS' : 'FAIL', 'keyless failure reaches a diagnosable credential boundary')
84
+
85
+ write(undefined)
86
+ const defaults = boot()
87
+ add('J3.defaults', /started \(port=default\)/.test(`${defaults.stdout}${defaults.stderr}`) ? 'PASS' : 'FAIL', 'missing config uses the default')
88
+ }
89
+ } finally {
90
+ try { fs.rmSync(home, { recursive: true, force: true }) } catch { /* ignore */ }
91
+ }
92
+ const result = {
93
+ schema: 'dsh-flow-config-observe/1',
94
+ generatedAt: new Date().toISOString(),
95
+ package: manifest.name,
96
+ version: manifest.version,
97
+ environment: { dsh: dshVersion, node: process.version, platform: process.platform },
98
+ checks,
99
+ summary: {
100
+ pass: checks.filter((c) => c.result === 'PASS').length,
101
+ fail: checks.filter((c) => c.result === 'FAIL').length,
102
+ blocked: checks.filter((c) => c.result === 'BLOCKED').length,
103
+ },
104
+ limitations: ['fixture-level only; not every plugin validates config or logs a start line'],
105
+ }
106
+ if (options.json || !process.stdout.isTTY) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
107
+ process.exitCode = result.summary.fail > 0 ? 1 : 0
108
+ }
109
+ try { main() } catch (error) { console.error(`journey-config-observe: ${error.message}`); process.exitCode = 3 }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * J5/J8 static preflight.
4
+ *
5
+ * J5: are capabilities/labels/commitments declared and does the README avoid
6
+ * forbidden claims? The final permissions judgement is still human.
7
+ * J8: is the client/server shape declared (exports["./client"], server entry)?
8
+ * This is a shape preflight, not a browser render probe (see web-probe.mjs).
9
+ *
10
+ * Usage: node 40-checker/scripts/journey-disclosure-shape.mjs --package <dir> [--json]
11
+ */
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import { spawnSync } from 'node:child_process'
15
+ import { fileURLToPath } from 'node:url'
16
+
17
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
18
+ const CHECKER = path.resolve(HERE, '..', 'bin', 'dsh-plugin-verify.mjs')
19
+ const FORBIDDEN = /\b(official|certified|secure|recommended)\b|官方认证|安全审计|推荐标准/i
20
+
21
+ function parse(argv) {
22
+ const out = { package: null, json: false }
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ if (argv[i] === '--package') out.package = argv[++i]
25
+ else if (argv[i] === '--json') out.json = true
26
+ else if (argv[i] === '--help' || argv[i] === '-h') { console.log('usage: journey-disclosure-shape.mjs --package <dir> [--json]'); process.exit(0) }
27
+ else throw new Error(`unknown option: ${argv[i]}`)
28
+ }
29
+ if (!out.package) throw new Error('--package is required')
30
+ return out
31
+ }
32
+
33
+ function main() {
34
+ const options = parse(process.argv.slice(2))
35
+ const dir = path.resolve(options.package)
36
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
37
+ const readme = ['README.md', 'readme.md', 'Readme.md'].map((n) => path.join(dir, n)).find((p) => fs.existsSync(p))
38
+ const text = readme ? fs.readFileSync(readme, 'utf8') : ''
39
+ const declared = {
40
+ capabilities: Array.isArray(manifest.dsh?.capabilities),
41
+ labels: Array.isArray(manifest.dsh?.labels),
42
+ commitments: Array.isArray(manifest.dsh?.commitments),
43
+ }
44
+ const forbidden = FORBIDDEN.test(text)
45
+ const run = spawnSync(process.execPath, [CHECKER, dir, '--scope', 'entry', '--entry-claim', 'static', '--format', 'json'], { encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 })
46
+ let checker = null
47
+ try { checker = JSON.parse(run.stdout) } catch { /* leave null */ }
48
+ const byId = new Map((checker?.checks ?? []).map((c) => [c.id, c]))
49
+ const declaration = ['DSC-11', 'DSC-12', 'DSC-13'].map((id) => ({ id, result: byId.get(id)?.result ?? 'NOT-RUN' }))
50
+ const j5 = forbidden ? 'FAIL' : declaration.some((d) => d.result === 'FAIL') ? 'FAIL' : declaration.some((d) => d.result === 'WARN') ? 'WARN' : 'PASS'
51
+ const hasClient = manifest.dsh?.client !== undefined
52
+ const clientExport = Boolean(manifest.exports?.['./client'])
53
+ const serverEntry = Boolean(manifest.exports?.['.'] || manifest.main)
54
+ const j8 = hasClient && !clientExport ? 'FAIL' : hasClient && clientExport && serverEntry ? 'PASS' : 'WARN'
55
+ const result = {
56
+ schema: 'dsh-flow-disclosure-shape/1',
57
+ generatedAt: new Date().toISOString(),
58
+ package: manifest.name,
59
+ version: manifest.version,
60
+ checks: {
61
+ J5: {
62
+ machine: j5,
63
+ humanReview: 'required',
64
+ declared,
65
+ forbiddenWords: forbidden,
66
+ declaration,
67
+ reason: forbidden ? 'README contains a forbidden claim' : 'permissions/disclosure judgement is still human',
68
+ },
69
+ J8: {
70
+ machine: j8,
71
+ humanReview: 'required for UI/render',
72
+ hasClient,
73
+ clientExport,
74
+ serverEntry,
75
+ reason: hasClient ? 'client shape checked statically; render not tested' : 'no dsh.client declaration',
76
+ },
77
+ },
78
+ limitations: ['J5 is a disclosure preflight, not a security review', 'J8 is a shape preflight, not a browser render probe'],
79
+ }
80
+ if (options.json || !process.stdout.isTTY) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`)
81
+ process.exitCode = [j5, j8].includes('FAIL') ? 1 : 0
82
+ }
83
+ try { main() } catch (error) { console.error(`journey-disclosure-shape: ${error.message}`); process.exitCode = 3 }