dsh-vibe-math 2.8.3 → 2.8.5

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 (50) hide show
  1. package/README.en.md +19 -4
  2. package/README.md +19 -4
  3. package/cordis.patch.yml +8 -8
  4. package/docs/COMPAT-AUDIT-ROUND2.md +27 -0
  5. package/docs/final-paper.md +13 -0
  6. package/docs/formal-verification.md +6 -6
  7. package/docs/release-notes/README.en.md +2 -0
  8. package/docs/release-notes/README.md +2 -0
  9. package/docs/release-notes/RELEASE-NOTES-2.8.4.en.md +48 -0
  10. package/docs/release-notes/RELEASE-NOTES-2.8.4.md +48 -0
  11. package/docs/release-notes/RELEASE-NOTES-2.8.5.en.md +75 -0
  12. package/docs/release-notes/RELEASE-NOTES-2.8.5.md +75 -0
  13. package/package.json +7 -3
  14. package/prompt-corpus-persona/persona-corpus.json +9 -9
  15. package/prompt-corpus-persona/persona-corpus.md +17 -17
  16. package/prompt-corpus-v2/formal-verify-v2.json +109 -109
  17. package/prompt-corpus-v2/formal-verify-v2.md +216 -216
  18. package/prompt-corpus-v3/formal-verify-v3.json +77 -77
  19. package/prompt-corpus-v3/formal-verify-v3.md +147 -147
  20. package/prompt-corpus-v4/formal-verify-v4.json +10 -10
  21. package/prompt-corpus-v4/formal-verify-v4.md +40 -40
  22. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  23. package/prompt-corpus-v5/prompt-corpus-v5.md +613 -186
  24. package/tests/audit-artifact-docs.mjs +38 -2
  25. package/tests/audit-artifact-docs.mutants.mjs +66 -1
  26. package/tests/audit-engine-faces.mjs +2 -2
  27. package/tests/audit-persona-surface.test.mjs +4 -1
  28. package/tests/audit-prompt-invariants.mjs +292 -2
  29. package/tests/e2e-v5-round2.test.mjs +248 -2
  30. package/tests/formal-verify-v2.test.mjs +6 -5
  31. package/tests/formal-verify-v3.test.mjs +6 -5
  32. package/tests/formal-verify-v4.test.mjs +4 -4
  33. package/tests/prompt-v5-integrity.test.mjs +6 -6
  34. package/tests/run-tests.mjs +18 -2
  35. package/tests/v4-final-paper.mutants.mjs +14 -0
  36. package/tests/v4-final-paper.test.mjs +18 -0
  37. package/tests/v5-institute-fixes.mutants.mjs +115 -0
  38. package/vibe-math-v2/agent.cordis.yml +2 -2
  39. package/vibe-math-v2/math-computation.js +5 -5
  40. package/vibe-math-v2/vibe-math-v2.js +66 -17
  41. package/vibe-math-v3/agent.cordis.yml +2 -2
  42. package/vibe-math-v3/math-computation.js +5 -5
  43. package/vibe-math-v3/vibe-math-v3.js +64 -15
  44. package/vibe-math-v4/agent.cordis.yml +6 -6
  45. package/vibe-math-v4/math-computation.js +5 -5
  46. package/vibe-math-v4/vibe-math-v4.js +71 -20
  47. package/vibe-math-v5/agent.cordis.yml +6 -6
  48. package/vibe-math-v5/math-computation.js +5 -5
  49. package/vibe-math-v5/vibe-math-v5.js +385 -34
  50. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +55 -2
@@ -11,8 +11,15 @@
11
11
  * sections must list it.
12
12
  *
13
13
  * Run: node tests/audit-artifact-docs.mjs
14
- * Env (mutant harness): ARTIFACT_README / ARTIFACT_README_EN / ARTIFACT_DOC / ARTIFACT_V2_JS /
15
- * ARTIFACT_V3_JS / ARTIFACT_V4_JS / ARTIFACT_V5_JS (absolute or repo-relative).
14
+ * Env (mutant harness): ARTIFACT_README / ARTIFACT_README_EN / ARTIFACT_DOC / ARTIFACT_INSTALL_DOC /
15
+ * ARTIFACT_V2_JS / ARTIFACT_V3_JS / ARTIFACT_V4_JS / ARTIFACT_V5_JS (absolute or repo-relative).
16
+ *
17
+ * Section 5 (issue #10, plugin-market install failure) guards the INSTALL/UPGRADE docs: DSH forwards
18
+ * plugin installs to pnpm and does not bundle it (missing pnpm ⇒ `exit 1`), and a bare package name is
19
+ * resolved through the profile's package.json / pnpm-lock.yaml, so it can stay on an OLD version —
20
+ * measured: `@latest`, `@^2`, `pnpm update --latest` and `pnpm add …@latest` all failed to upgrade,
21
+ * only an explicit `@<version>` did. Both are outside this package, but the docs must not send a user
22
+ * into either trap (and must never claim `@latest` is the upgrade recipe).
16
23
  */
17
24
  import { readFileSync } from 'node:fs'
18
25
  import { dirname, isAbsolute, join } from 'node:path'
@@ -66,6 +73,35 @@ ok(docScope, '★ docs/final-paper.md scopes ' + ARTIFACT + ' to v2/v3 AND state
66
73
  ok(/NOT IMPLEMENTED/.test(presets.v4) && /claim_write/.test(presets.v4),
67
74
  'vibe-math-v4.js still carries the explicit NOT-IMPLEMENTED shared-lock disclaimer (the source of the doc denial)')
68
75
 
76
+ // 5) INSTALL/UPGRADE DOCS (issue #10): the plugin market forwards installs to pnpm (which DSH does not
77
+ // bundle) and resolves a bare package name through the profile's package.json / pnpm-lock.yaml, so the
78
+ // install can stay on an OLD version. Both traps are outside this package — but the docs must not walk a
79
+ // user into either, and must never present `@latest` as the upgrade recipe (measured ineffective).
80
+ const INSTALL_DOC = 'docs/COMPAT-AUDIT-ROUND2.md'
81
+ for (const [name, envName, rel] of [['README.md', 'ARTIFACT_README', 'README.md'], ['README.en.md', 'ARTIFACT_README_EN', 'README.en.md']]) {
82
+ const text = read(envName, rel)
83
+ ok(/(\*\*前置\*\*|\*\*Prerequisite\*\*)[^\n]*pnpm/.test(text),
84
+ '★ ' + name + ': states the `pnpm` prerequisite (DSH forwards plugin installs to pnpm and does not bundle it)')
85
+ ok(/hub\.log/.test(text),
86
+ '★ ' + name + ': points at ~/.dsh/profiles/<profile>/hub.log (the failure the market hides)')
87
+ const bare = count(text, 'add dsh-vibe-math')
88
+ const pinned = count(text, 'add dsh-vibe-math@')
89
+ ok(pinned > 0 && bare === pinned,
90
+ '★ ' + name + ': EVERY `add dsh-vibe-math` pins an explicit version (a bare name can stay on an old version)',
91
+ 'bare=' + (bare - pinned))
92
+ ok(!/dsh-vibe-math@latest/.test(text),
93
+ '★★ ' + name + ': never presents `dsh-vibe-math@latest` as the install/upgrade recipe (measured ineffective)')
94
+ ok(/^### (安装\/升级排查|Install\/upgrade troubleshooting)/m.test(text),
95
+ '★ ' + name + ': carries an install/upgrade troubleshooting section')
96
+ }
97
+ const idoc = read('ARTIFACT_INSTALL_DOC', INSTALL_DOC)
98
+ ok(/pnpm/.test(idoc) && /hub\.log/.test(idoc),
99
+ '★ ' + INSTALL_DOC + ' documents the pnpm prerequisite AND the hub.log pointer')
100
+ ok(/add dsh-vibe-math@/.test(idoc) && !/dsh-vibe-math@latest/.test(idoc),
101
+ '★ ' + INSTALL_DOC + ' shows the explicit-version install/upgrade (and never `@latest`)')
102
+ ok(/pnpm-lock\.yaml|package\.json/.test(idoc),
103
+ '★ ' + INSTALL_DOC + ' explains WHY a bare name can stay on an old version (profile package.json / pnpm-lock.yaml wins)')
104
+
69
105
  console.log('')
70
106
  console.log('=== ARTIFACT DOCS: ' + passed + ' passed, ' + failed + ' failed ===')
71
107
  for (const f of failures) console.error(' - ' + f)
@@ -52,7 +52,72 @@ const namedB = b.failLines.some((l) => l.indexOf('the writing presets are exactl
52
52
  ok(b.status !== 0 && namedB, '★ B: a v4 that mentions the artifact reddens the "writers are exactly v2,v3" assertion', 'exit=' + b.status)
53
53
  if (!namedB) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
54
54
 
55
+ // ---- issue #10: the install/upgrade docs must keep teaching the pnpm prerequisite and an EXPLICIT
56
+ // version, and must never present `@latest` as the recipe. One single-site mutation per requirement.
57
+ const zhAll = readFileSync(join(REPO, 'README.md'), 'utf8')
58
+ const enAll = readFileSync(join(REPO, 'README.en.md'), 'utf8')
59
+ const probes = [
60
+ {
61
+ tag: 'C',
62
+ file: 'README-c.md',
63
+ env: { ARTIFACT_README: null },
64
+ from: /^> \*\*前置\*\*:[^\n]*$/m,
65
+ to: '> 前置未写明。',
66
+ expect: 'states the `pnpm` prerequisite',
67
+ what: 'dropping the pnpm prerequisite',
68
+ },
69
+ {
70
+ tag: 'D',
71
+ file: 'README-d.md',
72
+ env: { ARTIFACT_README: null },
73
+ from: 'add dsh-vibe-math@<版本>',
74
+ to: 'add dsh-vibe-math@latest',
75
+ expect: 'never presents `dsh-vibe-math@latest`',
76
+ what: 'teaching `@latest` as the install/upgrade command',
77
+ },
78
+ {
79
+ tag: 'E',
80
+ file: 'README-e.md',
81
+ env: { ARTIFACT_README: null },
82
+ from: 'hub.log',
83
+ to: '安装日志',
84
+ expect: 'points at ~/.dsh/profiles/<profile>/hub.log',
85
+ what: 'dropping the hub.log pointer',
86
+ },
87
+ {
88
+ tag: 'F',
89
+ file: 'README-f.md',
90
+ env: { ARTIFACT_README_EN: null },
91
+ from: '### Install/upgrade troubleshooting (pnpm and versions)',
92
+ to: '### Install/upgrade notes',
93
+ expect: 'carries an install/upgrade troubleshooting section',
94
+ what: 'dropping the troubleshooting section',
95
+ },
96
+ {
97
+ tag: 'G',
98
+ file: 'compat-doc.md',
99
+ env: { ARTIFACT_INSTALL_DOC: null },
100
+ from: 'add dsh-vibe-math@<版本>',
101
+ to: 'add dsh-vibe-math',
102
+ expect: 'shows the explicit-version install/upgrade',
103
+ what: 'dropping the explicit version from the install/upgrade doc',
104
+ doc: 'docs/COMPAT-AUDIT-ROUND2.md',
105
+ },
106
+ ]
107
+ for (const p of probes) {
108
+ const base = p.doc ? readFileSync(join(REPO, p.doc), 'utf8') : (p.tag === 'F' ? enAll : zhAll)
109
+ const anchor = p.from instanceof RegExp ? (base.match(p.from) || [])[0] : p.from
110
+ const n = anchor ? base.split(anchor).length - 1 : 0
111
+ if (!anchor || n < 1) { ok(false, p.tag + ': the anchor for ' + p.what + ' exists (harness and guard agree)', 'ANCHOR MISS'); continue }
112
+ writeFileSync(join(dir, p.file), base.split(anchor).join(p.to))
113
+ const envKey = Object.keys(p.env)[0]
114
+ const r = run({ [envKey]: join(dir, p.file) })
115
+ const named = r.failLines.some((l) => l.indexOf(p.expect) !== -1)
116
+ ok(r.status !== 0 && named, '★ ' + p.tag + ': ' + p.what + ' reddens the guard by name', 'exit=' + r.status)
117
+ if (!named) for (const l of r.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
118
+ }
119
+
55
120
  rmSync(dir, { recursive: true, force: true })
56
121
  console.log('')
57
122
  console.log('=== ARTIFACT DOCS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
58
- process.exit(failed === 0 ? 0 : 1)
123
+ process.exit(failed === 0 ? 0 : 1)
@@ -250,8 +250,8 @@ if (!XELATEX) {
250
250
  ok(bad && typeof bad === 'object' && bad.ok === true && (bad.started === true || bad.code === undefined),
251
251
  '★ engine-faces latex: a NONEXISTENT `paperLatexCommand` neither crashes nor raises an error code (entry stays ok:true, code undefined) (' + JSON.stringify({ ok: bad && bad.ok, code: bad && bad.code, note: String(bad && bad.note || '').slice(0, 50) }) + ')')
252
252
  console.log(' note: the LaTeX COMPILE (paper writer subagent) is not reachable from this probe; it asserts the ENTRY only — crash-freedom and no error code. The named degradation for an unresolvable command is a COMPILE-time behaviour (docs/final-paper.md) and is deliberately NOT claimed here.')
253
- console.log(' This probe runs the REAL xelatex only because it carries its own candidate list')
254
- console.log(' (D:\\texlive\\2025\\bin\\windows) - it is NOT evidence that the product discovers a non-standard TeX installation.')
253
+ console.log(' This probe carries its own candidate list (D:\\texlive\\2025\\bin\\windows); since task-28 the')
254
+ console.log(' PRODUCT probes that documented TeX Live drive-root position too (audit-prompt-invariants I19).')
255
255
  } finally { rmSync(WS, { recursive: true, force: true }) }
256
256
  }
257
257
 
@@ -196,7 +196,7 @@ const PRESETS = [
196
196
  dir: 'vibe-math-v5',
197
197
  js: 'vibe-math-v5.js',
198
198
  prefix: 'vibe_v5_',
199
- tools: 40,
199
+ tools: 41,
200
200
  // Member- and academician-facing tools that the v5 persona describes GENERICALLY; the office (main
201
201
  // agent) holds only the institute-level controls plus the hiring authority. The final-paper pair is
202
202
  // an OFFICE control and IS named in the persona (vibe_v5_paper / vibe_v5_finalize_paper), so it must
@@ -207,6 +207,9 @@ const PRESETS = [
207
207
  'vibe_v5_propose_verify', 'vibe_v5_verdict', 'vibe_v5_task_create',
208
208
  'vibe_v5_task_list', 'vibe_v5_task_get', 'vibe_v5_task_update',
209
209
  'vibe_v5_overview', 'vibe_v5_assign', 'vibe_v5_prioritize', 'vibe_v5_nudge',
210
+ // task-30: the methodology/collaboration feedback tool is member-facing and described GENERICALLY
211
+ // by the persona; the plugin's per-round hint names it and explains the three routes.
212
+ 'vibe_v5_feedback',
210
213
  ],
211
214
  required: [],
212
215
  lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib', 'vibe_v5_lean_read', 'vibe_v5_lean_job'], extra: [] },
@@ -20,6 +20,17 @@
20
20
  * I13 the Lean switch unreachable THROUGH the closed tool schema (v3 2.3.1: all four params
21
21
  * missing from vibe_math_set_params, invisible to every suite because suites call handlers)
22
22
  * I14 a parameter the tool schema advertises but the parameter layer silently drops
23
+ * I15 the LaTeX-missing guidance drifting between presets (four steps + three hard boundaries)
24
+ * I16 a member prompt demanding an action the member CANNOT perform (direct user contact)
25
+ * I17 the Lean-3 `--search-path` spelling surviving in an agent-facing STRING (only exception: the
26
+ * guard's explicit legacy-compatibility array)
27
+ * I18 the same spelling surviving in the AGENT-FACING preset text (`vibe-math-vN/agent.cordis.yml`,
28
+ * checked per preset) or in its generated copy (`cordis.patch.yml`, I18b)
29
+ * I19 the paper-engine detection missing TeX Live's documented install positions, or losing its
30
+ * explicit BOUND on candidate probes (never a whole-drive scan) — asserted for all four presets
31
+ * I20 the env-gated TeX sandbox seam (V2/V3/V4/V5_TEX_ROOTS_SANDBOX): every known-install probe site
32
+ * must stay rebased through it (unset = identity), and with it set the stage finds nothing while
33
+ * still naming its probes — static (I20) plus behavioural (I20b)
23
34
  *
24
35
  * Run: node tests/audit-prompt-invariants.mjs (add --json for a machine-readable report)
25
36
  * node tests/audit-prompt-invariants.mjs --self-probe
@@ -129,6 +140,111 @@ const SELF_PROBE_MUTATIONS = [
129
140
  to: ', then continue',
130
141
  expect: 'v2 I15: the LaTeX-missing guidance carries the four steps + the three hard boundaries',
131
142
  },
143
+ {
144
+ name: 'v5: the member prompt sends the TeX approval back to the USER instead of the OFFICE (I16)',
145
+ rel: 'vibe-math-v5/vibe-math-v5.js',
146
+ from: '如实上报所办(或群聊)**,由**所办**向用户确认',
147
+ to: '向用户问一次',
148
+ expect: 'v5 I16: a member-visible prompt never demands an action the member cannot perform',
149
+ },
150
+ {
151
+ name: 'v3: the member prompt sends the TeX approval back to the USER instead of the OFFICE (I16)',
152
+ rel: 'vibe-math-v3/vibe-math-v3.js',
153
+ from: 'REPORT IT TO THE OFFICE (or the group chat) and let the OFFICE confirm with the user',
154
+ to: 'ask the user once',
155
+ expect: 'v3 I16: a member-visible prompt never demands an action the member cannot perform',
156
+ },
157
+ {
158
+ name: 'v5: a tool description teaches the Lean-3 `--search-path` again (I17 must read the string, not the comment)',
159
+ rel: 'vibe-math-v5/vibe-math-v5.js',
160
+ from: 'leanSearchPaths (string[]) adds extra compiler search roots',
161
+ to: 'leanSearchPaths (string[]) adds extra --search-path roots',
162
+ expect: 'v5 I17:',
163
+ },
164
+ {
165
+ name: 'v5: the AGENT-FACING preset text reverts to the Lean-3 `--search-path` (I18 — the real-host SLV shape)',
166
+ rel: 'vibe-math-v5/agent.cordis.yml',
167
+ from: 'gets `-R <VibeMath root>`, preceded by any leanSearchPaths',
168
+ to: 'gets `--search-path <VibeMath root>`, preceded by any leanSearchPaths',
169
+ expect: 'v5 I18:',
170
+ },
171
+ {
172
+ name: 'the generated bundle patch keeps the Lean-3 `--search-path` (I18b: source fixed but not regenerated)',
173
+ rel: 'cordis.patch.yml',
174
+ from: '-R <VibeMath root>',
175
+ to: '--search-path <VibeMath root>',
176
+ expect: 'I18b:',
177
+ },
178
+ {
179
+ name: 'v5: the DRIVE-ROOT TeX Live candidates are deleted (I19 — the real-host "installed but not on PATH" shape)',
180
+ rel: 'vibe-math-v5/vibe-math-v5.js',
181
+ from: "const TEX_DRIVE_YEAR_ROOTS = ['D:/texlive', 'C:/texlive']",
182
+ to: 'const TEX_DRIVE_YEAR_ROOTS = []',
183
+ expect: 'v5 I19:',
184
+ },
185
+ {
186
+ name: 'v5: the TeX probe cap is removed (I19: the known-root stage must stay bounded)',
187
+ rel: 'vibe-math-v5/vibe-math-v5.js',
188
+ from: 'const TEX_CANDIDATE_CAP = 40',
189
+ to: 'const TEX_CANDIDATE_CAP = 100000',
190
+ expect: 'v5 I19:',
191
+ },
192
+ {
193
+ name: 'v5: a bare DRIVE ROOT enters the TeX candidates (I19: never a whole-drive scan)',
194
+ rel: 'vibe-math-v5/vibe-math-v5.js',
195
+ from: "const TEX_DRIVE_YEAR_ROOTS = ['D:/texlive', 'C:/texlive']",
196
+ to: "const TEX_DRIVE_YEAR_ROOTS = ['D:/texlive', 'C:/texlive', 'D:/']",
197
+ expect: 'v5 I19:',
198
+ },
199
+ {
200
+ name: 'v5: the TeX sandbox gate is ignored (I20 — a suite can no longer pin "no LaTeX on this host")',
201
+ rel: 'vibe-math-v5/vibe-math-v5.js',
202
+ from: "const texSandbox = kind === 'tex' ? String(env.V5_TEX_ROOTS_SANDBOX || '').trim() : ''",
203
+ to: "const texSandbox = ''",
204
+ expect: 'v5 I20:',
205
+ },
206
+ {
207
+ name: 'v5: one known-install TeX probe site stops rebasing into the sandbox (I20)',
208
+ rel: 'vibe-math-v5/vibe-math-v5.js',
209
+ from: "const p = await resolvePath(texProbeBase(base) + '/' + name)",
210
+ to: "const p = await resolvePath(base + '/' + name)",
211
+ expect: 'v5 I20:',
212
+ },
213
+ {
214
+ name: 'v4: one known-install TeX probe site stops rebasing into the sandbox (I20, second preset — not a v5-only check)',
215
+ rel: 'vibe-math-v4/vibe-math-v4.js',
216
+ from: "const p = await resolvePath(texProbeBase(base) + '/' + name)",
217
+ to: "const p = await resolvePath(base + '/' + name)",
218
+ expect: 'v4 I20:',
219
+ },
220
+ {
221
+ name: 'v3: one known-install TeX probe site stops rebasing into the sandbox (I20)',
222
+ rel: 'vibe-math-v3/vibe-math-v3.js',
223
+ from: "const p = await resolvePath(texProbeBase(base) + '/' + name)",
224
+ to: "const p = await resolvePath(base + '/' + name)",
225
+ expect: 'v3 I20:',
226
+ },
227
+ {
228
+ name: 'v2: one known-install TeX probe site stops rebasing into the sandbox (I20)',
229
+ rel: 'vibe-math-v2/vibe-math-v2.js',
230
+ from: "const p = await resolvePath(texProbeBase(base) + '/' + name)",
231
+ to: "const p = await resolvePath(base + '/' + name)",
232
+ expect: 'v2 I20:',
233
+ },
234
+ {
235
+ name: 'run-tests: the gate stops sandboxing the TeX install roots (X4 — the sweep becomes host-dependent again)',
236
+ rel: 'tests/run-tests.mjs',
237
+ from: 'process.env.V4_TEX_ROOTS_SANDBOX = TEX_ROOTS_SANDBOX_DIR',
238
+ to: '',
239
+ expect: 'X4:',
240
+ },
241
+ {
242
+ name: 'v5: a drive-letter absolute path is written back into the agent-facing guidance (I9b — reddens before any corpus regeneration)',
243
+ rel: 'vibe-math-v5/vibe-math-v5.js',
244
+ from: 'macOS at `/Library/TeX/texbin`',
245
+ to: 'macOS at `C:/TeX/texbin`',
246
+ expect: 'v5 I9b:',
247
+ },
132
248
  ]
133
249
 
134
250
  if (process.argv.includes('--self-probe')) {
@@ -352,14 +468,92 @@ for (const P of PRESETS) {
352
468
  // redden: requiring "either language" would have made the zh mutations no-ops once both exist).
353
469
  const I15_LANG = { v4: 'zh', v5: 'zh', v3: 'en', v2: 'en' }
354
470
  const I15_MARKERS = {
355
- zh: ['检测不到 LaTeX 引擎时', '有界核查', '不要全盘扫描', '并重新检测', '向用户问一次', '照旧降级', '绝不自动安装', '绝不写工作区之外', '当失败'],
356
- en: ['When no LaTeX engine is detected', 'never scan whole drives', 're-detect', 'ask the user once', 'degrade exactly as today', 'never auto-install', 'never write outside the workspace'],
471
+ zh: ['检测不到 LaTeX 引擎时', '有界核查', '不要全盘扫描', '并重新检测', '如实上报所办', '照旧降级', '绝不自动安装', '绝不写工作区之外', '当失败'],
472
+ en: ['When no LaTeX engine is detected', 'never scan whole drives', 're-detect', 'REPORT IT TO THE OFFICE', 'degrade exactly as today', 'never auto-install', 'never write outside the workspace'],
357
473
  }
358
474
  if (I15_LANG[P.tag]) {
359
475
  check(I15_MARKERS[I15_LANG[P.tag]].every((s) => js.includes(s)),
360
476
  P.tag + ' I15: the LaTeX-missing guidance carries the four steps + the three hard boundaries')
361
477
  }
362
478
 
479
+ // I16 — a MEMBER-VISIBLE prompt must never demand an action the member cannot perform. A member's
480
+ // only channels are its own tools (the group chat / private messages): it cannot reach the user
481
+ // directly, so the TeX-install approval must be REPORTED to the OFFICE, which DOES own that channel.
482
+ // Reads the CODE stream (a comment may legitimately quote the old wording as history) and requires
483
+ // the reworded clause POSITIVELY, so putting the old instruction back reddens this invariant BY NAME
484
+ // (two self-probe mutations below: the zh clause on v5 and the en clause on v3).
485
+ check(!code.includes('向用户问一次') && !code.includes('ask the user once'),
486
+ P.tag + ' I16: a member-visible prompt never demands an action the member cannot perform (no direct user contact)')
487
+ check(code.includes('如实上报所办') || code.includes('let the OFFICE confirm with the user'),
488
+ P.tag + ' I16: the TeX-approval clause routes the confirmation through the OFFICE (the party that can reach the user)')
489
+
490
+ // I17 — the Lean-3 spelling `--search-path` must not survive in ANY agent-facing string. `code` has
491
+ // comments blanked, so a surviving occurrence is inside a string/template literal; the ONE exception a
492
+ // preset may keep is its explicit legacy-compatibility array (`ALSO_ACCEPTED`), and only there: that
493
+ // is what makes an agent-supplied old spelling still suppress injection. Everything else is the
494
+ // defect class task-27 removed (`lean --search-path <dir> <f>` fails at argument parsing, rc=1).
495
+ {
496
+ const hits = code.split(/\r?\n/).map((l, i) => [i + 1, l]).filter(([, l]) => l.includes('--search-path'))
497
+ const bad = hits.filter(([, l]) => !/ALSO_ACCEPTED/.test(l))
498
+ check(hits.length === 1 && bad.length === 0,
499
+ P.tag + ' I17: `--search-path` survives only in the legacy-compat array, never in agent-facing text',
500
+ bad.length ? 'code-stream lines ' + JSON.stringify(bad.map(([n]) => n))
501
+ : 'expected exactly 1 occurrence (the ALSO_ACCEPTED guard line), found ' + hits.length)
502
+ }
503
+
504
+ // I18 — the AGENT-FACING preset text is a prompt too. `vibe-math-vN/agent.cordis.yml` is the persona/
505
+ // instruction surface the MAIN agent actually receives, and the plugin NEVER contains it — so I17
506
+ // cannot see it. The real-host SLV v5 run (15.2 min, candidate PASS) still showed exactly ONE
507
+ // `--search-path <VibeMath root>` in the v5 lean_lib "reuse first" paragraph. Guarded at the SOURCE;
508
+ // I18b below pins the generated copy so "edited the source but forgot to regenerate" also reddens.
509
+ {
510
+ const yml = read('vibe-math-' + P.tag + '/agent.cordis.yml')
511
+ const hits = yml ? yml.split(/\r?\n/).map((l, i) => [i + 1, l]).filter(([, l]) => l.includes('--search-path')) : []
512
+ check(yml !== null && hits.length === 0,
513
+ P.tag + ' I18: the agent-facing preset text never teaches the Lean-3 `--search-path`',
514
+ yml === null ? 'preset text unreadable (' + P.tag + '/agent.cordis.yml)'
515
+ : 'lines ' + JSON.stringify(hits.map(([n]) => n)))
516
+ }
517
+
518
+ // I19 (all four presets carry the same candidates) — the paper-engine detection must cover TeX Live's
519
+ // DOCUMENTED install positions. A real-host SLV v5 run (15.2 min, candidate PASS) degraded to
520
+ // 'not-detected' although D:\texlive\2025\bin\windows\xelatex.exe existed, because only
521
+ // <ProgramFiles>/texlive was ever probed ("installed but not on PATH"). The stage must stay BOUNDED
522
+ // (an explicit cap plus a recent-year window) and must never become a whole-drive scan. Static check
523
+ // on the CODE stream: I15 guards the TEXT of the guidance, this guards the ROOTS it acts on.
524
+ {
525
+ const roots = ["'D:/texlive'", "'C:/texlive'", "'/usr/local/texlive'", "'/opt/texlive'", "'/Library/TeX/texbin'"]
526
+ const missing = roots.filter((s) => !code.includes(s))
527
+ check(missing.length === 0,
528
+ P.tag + ' I19: the TeX candidate roots cover the documented TeX Live install positions',
529
+ missing.length ? 'missing ' + JSON.stringify(missing) : '')
530
+ const cap = /const TEX_CANDIDATE_CAP = (\d+)/.exec(code)
531
+ const capEnforced = code.indexOf('texProbes++') >= 0 && code.indexOf('TEX_CANDIDATE_CAP', code.indexOf('texProbes++')) >= 0
532
+ check(!!cap && Number(cap[1]) >= 1 && Number(cap[1]) <= 40 && capEnforced,
533
+ P.tag + ' I19: the TeX known-root stage stays BOUNDED by an explicit cap <= 40',
534
+ (cap ? 'cap=' + cap[1] : 'no TEX_CANDIDATE_CAP declaration') + (capEnforced ? '' : '; declared but never enforced'))
535
+ const win = /const TEX_YEAR_LOOKBACK = (\d+)/.exec(code)
536
+ check(!!win && Number(win[1]) >= 1 && Number(win[1]) <= 4 && /Number\(b\) - Number\(a\)/.test(code),
537
+ P.tag + ' I19: the TeX Live year dirs stay bounded (on-disk newest first, then the recent-year window)',
538
+ win ? 'lookback=' + win[1] : 'no TEX_YEAR_LOOKBACK declaration')
539
+ check(!/['"][A-Za-z]:[\\/]['"]/.test(code),
540
+ P.tag + ' I19: no bare drive root among the candidate roots (never a whole-drive scan)')
541
+
542
+ // I20 (all four presets) — the TEST/DIAGNOSTIC sandbox seam must stay a RELOCATION of where the
543
+ // documented roots are looked for, never a product switch: every known-install TeX probe site goes
544
+ // through `texProbeBase`, and with the gate unset the helper returns its argument unchanged
545
+ // (identity = today's behaviour). Each preset uses its OWN gate name (V2/V3/V4/V5_TEX_ROOTS_SANDBOX).
546
+ // Static here; the behavioural half is I20b. The named-red proof is the probe-site mutations in
547
+ // SELF_PROBE_MUTATIONS: the audit's mutation mechanism is read-time, so it can redden this check but
548
+ // cannot reach a dynamically imported module.
549
+ const sandboxEnv = 'V' + P.tag.slice(1) + '_TEX_ROOTS_SANDBOX'
550
+ const allRebased = code.includes(sandboxEnv) && /if \(!texSandbox\) return p/.test(code) &&
551
+ /listDirs\(texProbeBase\(base\)\)/.test(code) && /resolvePath\(texProbeBase\(base\) \+ '\//.test(code) &&
552
+ !/listDirs\(base\)/.test(code) && !/resolvePath\(base \+ '\//.test(code)
553
+ check(allRebased,
554
+ P.tag + ' I20: every known-install TeX probe site is rebased through the ' + sandboxEnv + ' gate (unset = identity)')
555
+ }
556
+
363
557
  // I3 — the defect rule is in the injected text.
364
558
  check(js.includes('不要投 0'), P.tag + ' I3: injected text forbids a 0 vote on a defect')
365
559
  check(/'defect'/.test(js) || /"defect"/.test(js), P.tag + ' I3: injected text names decision=\'defect\'')
@@ -411,6 +605,37 @@ for (const P of PRESETS) {
411
605
  check(!/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(corpus), P.tag + ' I9: corpus carries no timestamp (deterministic)')
412
606
  check(!/vibe-v[0-9]-[a-z]+-[A-Za-z0-9]{4,}/.test(corpus), P.tag + ' I9: corpus leaks no temp-dir name')
413
607
 
608
+ // I9b (task-29 follow-up) — the SOURCE-LEVEL early warning that I9 lacks: a drive-letter absolute path
609
+ // written into AGENT-FACING text must redden HERE, immediately, not only after the corpus is
610
+ // regenerated (I9 is the later, mirror-level net). It reads the comment-blanked `code` stream, so a
611
+ // comment may still quote a real host path as evidence (task-28's `D:\texlive\2025\...` line, and v5's
612
+ // task-17 `Z:/no/such/xelatex.exe` comment): "assertions read code, not comments".
613
+ // CRITERION — I9's `[A-Za-z]:[\\/]` is a CORPUS-level test (the corpus holds DECODED text). Applied to
614
+ // SOURCE it false-positives on every escape sequence that merely looks like `<letter>:\`
615
+ // (`'…:\n'`, `/…limit:\s*/`; measured on this tree: 24 / 27 / 15 / 11 such lines in v2/v3/v4/v5). The
616
+ // source SPELLING of the same path is `D:/…` or `D:\\…`, so the equivalent criterion is
617
+ // `/[A-Za-z]:(?:\/|\\\\)/` — a bare `:\n` escape never matches it.
618
+ // EXCLUSIONS (documented, auditable; PURE CODE constructs that never reach a prompt):
619
+ // 1. `TEX_DRIVE_YEAR_ROOTS = ['D:/texlive', 'C:/texlive']` — the candidate-root list the detector
620
+ // walks; resolveKnownTool reads it, nothing concatenates it into agent text.
621
+ // 2. resolveKnownTool's PATH-fallback defaults `'C:/Users/Default'`, `'C:/Program Files'`,
622
+ // `'C:/Program Files (x86)'` — used only when the host env vars are absent.
623
+ // Where they live today: v2 L136/146/148/149 · v3 L169/179/181/182 · v4 L57/67/69/70 · v5 L67/77/79/80.
624
+ // The anti-vacuity check re-derives them on every run, so a refactor that drops them is CAUGHT instead
625
+ // of silently widening the exclusion.
626
+ {
627
+ const DRIVE_ABS = /[A-Za-z]:(?:\/|\\\\)/
628
+ const CODE_ONLY = /TEX_DRIVE_YEAR_ROOTS|env\.ProgramFiles|ProgramFiles\(x86\)|env\.USERPROFILE/
629
+ const flagged = [], excluded = []
630
+ code.split(/\r?\n/).forEach((l, i) => { if (DRIVE_ABS.test(l)) (CODE_ONLY.test(l) ? excluded : flagged).push(i + 1) })
631
+ check(flagged.length === 0,
632
+ P.tag + ' I9b: no drive-letter absolute path in agent-facing text (guidance, warnings, tool/param descriptions)',
633
+ flagged.length ? 'lines ' + JSON.stringify(flagged) + ' — ' + JSON.stringify(code.split(/\r?\n/)[flagged[0] - 1].trim().slice(0, 120)) : '')
634
+ check(excluded.length >= 3,
635
+ P.tag + ' I9b: the documented code-only exclusions are still present (the guard is not vacuous)',
636
+ 'excluded lines ' + JSON.stringify(excluded))
637
+ }
638
+
414
639
  // I10 — the prompt-rule probes exist for this preset (a guard nobody can prove is a guard nobody has).
415
640
  const probeSrc = read('tests/audit-formal-sensitivity.mjs') || ''
416
641
  for (const kind of ['fidelity-defect-rule-removed', 'abbreviated-tool-name-injected', 'require-wording-removed', 'defect-decision-not-offered']) {
@@ -506,6 +731,48 @@ for (const P of PRESETS) {
506
731
  notes.push(P.tag + ': plugin ' + js.length + 'B · suite ' + suite.length + 'B · corpus ' + corpus.length + 'B')
507
732
  }
508
733
 
734
+ // I20b — the sandbox seam, BEHAVIOURALLY and host-independently (ALL FOUR presets). Point each preset's
735
+ // own gate at a fresh EMPTY directory and require the known-install stage to
736
+ // (a) find NOTHING — a suite can pin "no LaTeX here" on ANY host,
737
+ // (b) still report a NAMED list — triedPaths does not go silent (task-9),
738
+ // (c) probe ONLY inside the sandbox — no real install is touched, and
739
+ // (d) stay within the cap — the relocation does not unbound the stage.
740
+ // Portable by construction: on a host WITH TeX Live (a) is the property under test, on one WITHOUT it
741
+ // is trivially true, and (c) is host-independent either way.
742
+ {
743
+ const items = PRESETS.map((P) => ({
744
+ tag: P.tag, file: join(HERE, P.js), env: 'V' + P.tag.slice(1) + '_TEX_ROOTS_SANDBOX', sandbox: '',
745
+ }))
746
+ const dir = mkdtempSync(join(tmpdir(), 'prompt-invariants-tex-sandbox-'))
747
+ try {
748
+ for (const it of items) it.sandbox = join(dir, it.tag)
749
+ const probe = join(dir, 'probe.mjs')
750
+ writeFileSync(probe, [
751
+ "import { pathToFileURL } from 'node:url'",
752
+ 'const items = JSON.parse(process.argv[2])',
753
+ 'for (const it of items) process.env[it.env] = it.sandbox',
754
+ 'const out = []',
755
+ 'for (const it of items) {',
756
+ ' const mod = await import(pathToFileURL(it.file).href)',
757
+ " const r = await mod.resolveKnownTool({ resolveExecutable: async () => { throw new Error('no PATH') } }, { name: 'xelatex', explicit: '', kind: 'tex' })",
758
+ ' out.push({ tag: it.tag, exe: r.exe, via: r.via, tried: r.tried })',
759
+ '}',
760
+ 'console.log(JSON.stringify(out))',
761
+ ].join('\n'))
762
+ const r = spawnSync(process.execPath, [probe, JSON.stringify(items)], { encoding: 'utf8' })
763
+ let rows = null
764
+ try { rows = JSON.parse(r.stdout) } catch (e) { /* reported below */ }
765
+ for (const it of items) {
766
+ const row = rows && rows.find((x) => x.tag === it.tag)
767
+ const slash = (s) => String(s).split('\\').join('/')
768
+ const inside = !!row && row.tried.length > 0 && row.tried.every((p) => slash(p).indexOf(slash(it.sandbox)) === 0)
769
+ check(!!row && row.exe === null && inside && row.tried.length <= 40,
770
+ 'I20b: ' + it.tag + ' — with ' + it.env + ' set the known-install stage finds nothing, probes only inside the sandbox, and names <= 40 paths',
771
+ row ? JSON.stringify({ exe: row.exe, via: row.via, triedCount: row.tried.length, inside }) : 'probe failed: ' + String(r.stderr || r.stdout || '').slice(0, 160))
772
+ }
773
+ } finally { rmSync(dir, { recursive: true, force: true }) }
774
+ }
775
+
509
776
  // Cross-preset: the probe script must refuse to report success on an empty selection (false green).
510
777
  {
511
778
  const probeSrc = read('tests/audit-formal-sensitivity.mjs') || ''
@@ -513,6 +780,29 @@ for (const P of PRESETS) {
513
780
  const runner = read('tests/run-tests.mjs') || ''
514
781
  check(/no suites matched/.test(runner), 'X2: the suite runner fails when no suite matches')
515
782
  check(/argv\[i \+ 1\]/.test(runner) && /argv\[\+\+i\]/.test(runner), 'X3: the suite runner accepts both --flag=x and --flag x')
783
+ // X4 (task-29) — the GATE must be independent of whether the HOST has TeX installed: the runner points
784
+ // every child suite's documented-root sandbox gate at ONE empty scratch dir (children inherit env), and
785
+ // removes it on exit. Static here; the behavioural proof is the gate itself (the "no LaTeX on this
786
+ // host" suites pass on a machine that really HAS TeX Live). Named red: the mutation below drops one of
787
+ // the four assignments.
788
+ const texGateVars = ['V2_TEX_ROOTS_SANDBOX', 'V3_TEX_ROOTS_SANDBOX', 'V4_TEX_ROOTS_SANDBOX', 'V5_TEX_ROOTS_SANDBOX']
789
+ const texGateMissing = texGateVars.filter((v) => !runner.includes(v))
790
+ check(texGateMissing.length === 0 && /mkdtempSync\(/.test(runner) && /rmSync\(/.test(runner) && /process\.on\('exit'/.test(runner),
791
+ 'X4: run-tests.mjs sandboxes the documented TeX install roots for every child suite (host-independent gate)',
792
+ texGateMissing.length ? 'missing ' + JSON.stringify(texGateMissing) : 'no mkdtemp/exit-cleanup found')
793
+ }
794
+
795
+ // I18b — the GENERATED bundle patch repeats the preset text (`node scripts/build-preset-rows.mjs`
796
+ // writes cordis.patch.yml from the four agent.cordis.yml). Guarding the copy catches the second failure
797
+ // mode: the preset source is fixed but the patch was never regenerated, so the shipped prompt keeps the
798
+ // Lean-3 spelling. tests/audit-preset-rows.test.mjs already pins byte-identity with the generator; this
799
+ // pins the SEMANTICS (the -R text is really there, and the old spelling is not).
800
+ {
801
+ const patch = read('cordis.patch.yml')
802
+ const hits = patch ? patch.split(/\r?\n/).map((l, i) => [i + 1, l]).filter(([, l]) => l.includes('--search-path')) : []
803
+ check(patch !== null && patch.includes('-R <VibeMath root>') && hits.length === 0,
804
+ 'I18b: the generated cordis.patch.yml teaches no `--search-path` (regenerate: node scripts/build-preset-rows.mjs)',
805
+ patch === null ? 'cordis.patch.yml unreadable' : 'lines ' + JSON.stringify(hits.map(([n]) => n)))
516
806
  }
517
807
 
518
808
  // X5–X7: guard the guard. I1/I13/I14 trust stripComments(), and all four presets contain a regex