dsh-vibe-math 2.7.2 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -681
  3. package/README.md +683 -682
  4. package/cordis.patch.yml +112 -16
  5. package/docs/AUDIT-CHECKLIST.md +755 -518
  6. package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
  7. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  8. package/docs/audit-methodology.md +93 -0
  9. package/docs/final-paper.md +172 -172
  10. package/docs/formal-verification.md +613 -588
  11. package/docs/generate_framework_diagram_v2.py +114 -114
  12. package/docs/generate_framework_diagram_v3.py +127 -127
  13. package/docs/math-computation.md +195 -187
  14. package/docs/parameter-schema.md +37 -0
  15. package/docs/release-notes/README.en.md +2 -0
  16. package/docs/release-notes/README.md +2 -0
  17. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  18. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  19. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  20. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  21. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  22. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  23. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  24. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  25. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  26. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  27. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  28. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  29. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  30. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  31. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  32. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  33. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  34. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  35. package/docs/release-notes/RELEASE-NOTES-2.7.2.en.md +2 -2
  36. package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
  37. package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
  38. package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
  39. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  40. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  41. package/docs/release-notes/TEMPLATE.md +6 -0
  42. package/docs/release-playbook.md +103 -0
  43. package/docs/slv-playbook.md +117 -0
  44. package/docs/status-report-fields.md +167 -0
  45. package/docs/test-timing.md +158 -154
  46. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  47. package/installer.js +793 -783
  48. package/package.json +260 -203
  49. package/prompt-corpus-persona/persona-corpus.json +9 -9
  50. package/prompt-corpus-persona/persona-corpus.md +225 -33
  51. package/prompt-corpus-v2/formal-verify-v2.json +295 -115
  52. package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
  53. package/prompt-corpus-v3/formal-verify-v3.json +112 -112
  54. package/prompt-corpus-v3/formal-verify-v3.md +227 -143
  55. package/prompt-corpus-v4/formal-verify-v4.json +18 -18
  56. package/prompt-corpus-v4/formal-verify-v4.md +49 -44
  57. package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
  58. package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
  59. package/scripts/clean-temp.mjs +234 -0
  60. package/scripts/update-doc-counts.mjs +71 -0
  61. package/tests/audit-artifact-docs.mjs +72 -0
  62. package/tests/audit-artifact-docs.mutants.mjs +58 -0
  63. package/tests/audit-engine-faces.mjs +298 -0
  64. package/tests/audit-formal-sensitivity.mjs +345 -342
  65. package/tests/audit-installer-assertions.mutants.mjs +63 -0
  66. package/tests/audit-installer-compat.test.mjs +256 -208
  67. package/tests/audit-installer-policy.test.mjs +41 -8
  68. package/tests/audit-math-computation-contract.mjs +64 -8
  69. package/tests/audit-math-computation-contract.mutants.mjs +106 -0
  70. package/tests/audit-math-computation-parity.mjs +164 -3
  71. package/tests/audit-math-computation-sensitivity.mjs +5 -3
  72. package/tests/audit-package-membership.mjs +57 -0
  73. package/tests/audit-package-membership.mutants.mjs +28 -0
  74. package/tests/audit-participant-set-parity.mjs +160 -0
  75. package/tests/audit-path-discipline.mjs +83 -0
  76. package/tests/audit-path-discipline.mutants.mjs +51 -0
  77. package/tests/audit-persona-sensitivity.mjs +323 -309
  78. package/tests/audit-persona-surface.mutants.mjs +69 -0
  79. package/tests/audit-persona-surface.test.mjs +431 -390
  80. package/tests/audit-prompt-duplication.mjs +143 -0
  81. package/tests/audit-prompt-duplication.mutants.mjs +51 -0
  82. package/tests/audit-prompt-invariants.mjs +589 -508
  83. package/tests/audit-readme-bilingual.test.mjs +152 -0
  84. package/tests/audit-readme-counts.mjs +95 -0
  85. package/tests/audit-readme-counts.mutants.mjs +52 -0
  86. package/tests/audit-registration.mjs +78 -0
  87. package/tests/audit-spec-traceability.mjs +193 -193
  88. package/tests/audit-status-report-fields.mjs +172 -0
  89. package/tests/audit-status-report-fields.mutants.mjs +64 -0
  90. package/tests/audit-v3-registration-parity.mjs +156 -0
  91. package/tests/audit-v5-integrity.mjs +795 -508
  92. package/tests/audit-v5-lean-abstention.mjs +392 -0
  93. package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
  94. package/tests/audit-v5-prompt-duplication.mjs +88 -0
  95. package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
  96. package/tests/audit-v5-sensitivity.mjs +433 -427
  97. package/tests/e2e-identity-a6.mutants.mjs +65 -0
  98. package/tests/e2e-v5-round2.test.mjs +2453 -1981
  99. package/tests/formal-verify-v2.test.mjs +1778 -1724
  100. package/tests/formal-verify-v3.mutants.mjs +117 -0
  101. package/tests/formal-verify-v3.test.mjs +1724 -1671
  102. package/tests/formal-verify-v4.mutants.mjs +152 -0
  103. package/tests/formal-verify-v4.test.mjs +2114 -1574
  104. package/tests/formal-verify-v5.test.mjs +790 -724
  105. package/tests/helpers/doc-counts-format.mjs +41 -0
  106. package/tests/helpers/math-computation-fake-seam.mjs +4 -0
  107. package/tests/math-computation-a1.mutants.mjs +72 -0
  108. package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
  109. package/tests/math-computation-discovery.mutants.mjs +76 -0
  110. package/tests/math-computation-shared.test.mjs +392 -6
  111. package/tests/math-computation-v2.test.mjs +779 -0
  112. package/tests/math-computation-v3.test.mjs +771 -0
  113. package/tests/math-computation-v4.mutants.mjs +133 -0
  114. package/tests/math-computation-v4.test.mjs +739 -0
  115. package/tests/prompt-v5-integrity.test.mjs +1291 -1280
  116. package/tests/run-tests.mjs +393 -202
  117. package/tests/run-tests.mutants.mjs +70 -0
  118. package/tests/selfdrive-v5.mjs +560 -508
  119. package/tests/selfdrive-v5.mutants.mjs +132 -0
  120. package/tests/temp-hygiene.mutants.mjs +80 -0
  121. package/tests/v2-fix-probes.mutants.mjs +104 -0
  122. package/tests/v2-fix-probes.test.mjs +1040 -0
  123. package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
  124. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  125. package/tests/v3-fix-probes.mutants.mjs +99 -0
  126. package/tests/v3-fix-probes.test.mjs +892 -0
  127. package/tests/v4-final-paper.mutants.mjs +108 -0
  128. package/tests/v5-institute-fixes.mutants.mjs +218 -0
  129. package/vibe-math-v2/agent.cordis.yml +378 -322
  130. package/vibe-math-v2/math-computation.js +144 -53
  131. package/vibe-math-v2/math-engines.js +12 -12
  132. package/vibe-math-v2/preset.yml +2 -2
  133. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  134. package/vibe-math-v2/vibe-math-v2.js +5097 -4755
  135. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
  136. package/vibe-math-v3/agent.cordis.yml +422 -356
  137. package/vibe-math-v3/math-computation.js +144 -53
  138. package/vibe-math-v3/math-engines.js +12 -12
  139. package/vibe-math-v3/preset.yml +2 -2
  140. package/vibe-math-v3/vibe-math-v3.js +6235 -5873
  141. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  142. package/vibe-math-v4/agent.cordis.yml +438 -402
  143. package/vibe-math-v4/math-computation.js +144 -53
  144. package/vibe-math-v4/math-engines.js +12 -12
  145. package/vibe-math-v4/preset.yml +2 -2
  146. package/vibe-math-v4/vibe-math-v4.js +4490 -4269
  147. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
  148. package/vibe-math-v5/agent.cordis.yml +46 -12
  149. package/vibe-math-v5/math-computation.js +144 -53
  150. package/vibe-math-v5/math-engines.js +12 -12
  151. package/vibe-math-v5/vibe-math-v5.js +1266 -229
  152. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
  153. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * TEMP HYGIENE (task-12) — sweep STALE test scratch directories and prefer a roomier temp root.
4
+ *
5
+ * WHY THIS EXISTS (measured, 2026-10 on this machine): the suites create their workspace with
6
+ * `mkdtempSync(join(tmpdir(), '<prefix>-…'))` and remove it in `finally` — but a suite KILLED by the
7
+ * runner's per-suite timeout cannot clean up, and some suites create many directories per run. After
8
+ * hundreds of sweeps `%TEMP%` had reached **248,737 top-level entries, 245,288 of them ours**
9
+ * (`vibe-*` 221,939, `v2-fix-*` 23,160, …), with 111,223 older than 24 h (oldest ~2 weeks).
10
+ *
11
+ * TWO RULES, both bounded:
12
+ * 1. SWEEP: delete only entries at the TOP LEVEL of the temp root, only whose name starts with a
13
+ * prefix ACTUALLY USED BY THE SUITES (extracted from `tests/*.mjs`, never hand-written), and only
14
+ * when older than `--age-hours` (default 6 h — a concurrently running gate's dirs stay safe).
15
+ * 2. PREFER D:: when `D:\_tmp` can be created, point TEMP/TMP/TMPDIR at it (the suites follow
16
+ * `os.tmpdir()` automatically, so this needs NO product change). Never hard-fail: if the drive is
17
+ * absent the ambient temp root is kept.
18
+ *
19
+ * Usage:
20
+ * node scripts/clean-temp.mjs # sweep (age 6 h) and print counts
21
+ * node scripts/clean-temp.mjs --dry-run # print the plan, delete nothing
22
+ * node scripts/clean-temp.mjs --age-hours=12 # change the threshold
23
+ * node scripts/clean-temp.mjs --root=<dir> # sweep another temp root (mainly for tests)
24
+ * node scripts/clean-temp.mjs --self-test # prove the guards below (fresh dirs are NEVER swept)
25
+ */
26
+ import { execFileSync } from 'node:child_process'
27
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'
28
+ import { tmpdir } from 'node:os'
29
+ import { dirname, join, resolve } from 'node:path'
30
+ import { fileURLToPath } from 'node:url'
31
+
32
+ const HERE = dirname(fileURLToPath(import.meta.url))
33
+ const REPO = resolve(HERE, '..')
34
+ const TESTS = join(REPO, 'tests')
35
+
36
+ /** Prefixes the suites really use — extracted from their own `mkdtempSync(join(tmpdir(), '<prefix>'))` calls. */
37
+ export function suitePrefixes(testsDir = TESTS) {
38
+ const out = new Set()
39
+ let files = []
40
+ try { files = readdirSync(testsDir) } catch (e) { return [] }
41
+ for (const f of files) {
42
+ if (!f.endsWith('.mjs')) continue
43
+ let text = ''
44
+ try { text = readFileSync(join(testsDir, f), 'utf8') } catch (e) { continue }
45
+ for (const m of text.matchAll(/mkdtempSync\(\s*join\(\s*tmpdir\(\)\s*,\s*'([^']+)'/g)) out.add(m[1])
46
+ for (const m of text.matchAll(/mkdtempSync\(\s*join\(\s*tmpdir\(\)\s*,\s*"([^"]+)"/g)) out.add(m[1])
47
+ }
48
+ return [...out].sort()
49
+ }
50
+
51
+ /**
52
+ * PURE planner: which entries may be deleted. `entries` are `{name, mtimeMs}` already read from the
53
+ * temp root. An entry qualifies only if BOTH hold: its name starts with a suite prefix, and it is older
54
+ * than the threshold. (This is the function the mutant family attacks: replacing the age test with
55
+ * `true` must make the "a FRESH dir is never swept" check go red BY NAME.)
56
+ */
57
+ export function planSweep(entries, { prefixes, ageHours = 6, now = Date.now() } = {}) {
58
+ const cut = now - ageHours * 3600 * 1000
59
+ return entries
60
+ .filter((e) => Array.isArray(prefixes) ? prefixes.some((p) => String(e.name).startsWith(p)) : false)
61
+ .filter((e) => Number(e.mtimeMs) < cut)
62
+ .map((e) => e.name)
63
+ }
64
+
65
+ /** Read the top level of a temp root into planner entries. Only DIRECTORIES are considered. */
66
+ export function listTop(root) {
67
+ const out = []
68
+ let names = []
69
+ try { names = readdirSync(root, { withFileTypes: true }) } catch (e) { return out }
70
+ for (const d of names) {
71
+ if (!d.isDirectory()) continue
72
+ let mtimeMs = 0
73
+ try { mtimeMs = statSync(join(root, d.name)).mtimeMs } catch (e) { continue }
74
+ out.push({ name: d.name, mtimeMs })
75
+ }
76
+ return out
77
+ }
78
+
79
+ /** Delete the planned entries. Returns counts so callers can print a checkable summary. */
80
+ export function sweep({ root = tmpdir(), prefixes, ageHours = 6, dryRun = false, log = () => {} } = {}) {
81
+ const all = listTop(root)
82
+ const doomed = planSweep(all, { prefixes, ageHours })
83
+ if (dryRun) return { root, scanned: all.length, planned: doomed.length, deleted: 0, failed: 0, dryRun: true }
84
+ let deleted = 0
85
+ let failed = 0
86
+ for (const name of doomed) {
87
+ try { rmSync(join(root, name), { recursive: true, force: true }); deleted++ } catch (e) { failed++ }
88
+ }
89
+ log('temp-hygiene: root=' + root + ' scanned=' + all.length + ' stale=' + doomed.length + ' deleted=' + deleted + ' failed=' + failed + ' ageHours=' + ageHours)
90
+ return { root, scanned: all.length, planned: doomed.length, deleted, failed }
91
+ }
92
+
93
+ /** `D:\_tmp` when the D: drive is usable; otherwise null (keep the ambient temp root). Never throws. */
94
+ export function preferredTempRoot(candidate = 'D:\\_tmp') {
95
+ try {
96
+ if (!existsSync(candidate)) mkdirSync(candidate, { recursive: true })
97
+ return existsSync(candidate) ? candidate : null
98
+ } catch (e) { return null }
99
+ }
100
+
101
+ /** Point TEMP/TMP/TMPDIR at `root` (children inherit; `os.tmpdir()` follows). No-op when root is falsy. */
102
+ export function useTempRoot(root) {
103
+ if (!root) return false
104
+ process.env.TEMP = root
105
+ process.env.TMP = root
106
+ process.env.TMPDIR = root
107
+ return true
108
+ }
109
+
110
+ /**
111
+ * Leftover-PROCESS detector (task-15). Measured motivation: a leftover DSH host (`--profile vmfix7`,
112
+ * started two days earlier, its parent gone) had burned **197,410 s of CPU** on this 4-core machine - a
113
+ * full core, permanently. With it, whole-gate `sum of suite times` went 2822 s -> 7875 s (x2.8) and
114
+ * individual suites ran 1.3x-4.5x slower, so honest suites reported failures that never reproduced
115
+ * standalone. The rule is deliberately NARROW: flag a process only when its command line matches THIS
116
+ * project/team AND its parent is gone (a process the running gate spawned has a live parent, so a healthy
117
+ * gate is never flagged - that property is asserted in `--self-test` and mutated by the family).
118
+ */
119
+ export const DEFAULT_PROCESS_PATTERNS = [
120
+ /--profile\s+\S+/, // a DSH host started against any profile (including our clones)
121
+ /[\\/]review[\\/][^\\/]*\.mjs/, // the dev-only harness scripts kept under _oneoff/review
122
+ /[\\/]tests[\\/][^\\/]*\.mjs/, // a test/probe script (gates, mutant families)
123
+ /--temp-dry-run/, // another gate run
124
+ ]
125
+
126
+ export function suspectProcesses(entries, { selfPids = [], patterns = DEFAULT_PROCESS_PATTERNS } = {}) {
127
+ const self = new Set((selfPids || []).map((p) => String(p)))
128
+ return (entries || []).filter((e) => {
129
+ if (!e || self.has(String(e.pid))) return false
130
+ if (!patterns.some((re) => re.test(String(e.cmd || '')))) return false
131
+ return e.parentGone === true // the safety property: a live parent means something owns it
132
+ })
133
+ }
134
+
135
+ /**
136
+ * List node processes with a `parentGone` flag. Windows-only in practice (PowerShell JSON); other
137
+ * platforms return [] rather than pretending. `exec` is injectable so the self-test can feed fixtures.
138
+ */
139
+ export function listProcesses({ exec = null } = {}) {
140
+ if (process.platform !== 'win32' && !exec) return []
141
+ const ps = "$live=@{}; Get-Process | ForEach-Object { $live[[int]$_.Id]=1 }; " +
142
+ "Get-CimInstance Win32_Process | Where-Object { $_.Name -eq 'node.exe' } | ForEach-Object { " +
143
+ "[pscustomobject]@{ pid=[int]$_.ProcessId; parent=[int]$_.ParentProcessId; cmd=[string]$_.CommandLine; " +
144
+ "parentGone=(-not $live.ContainsKey([int]$_.ParentProcessId)); created=[string]$_.CreationDate } } | ConvertTo-Json -Compress"
145
+ try {
146
+ const run = exec || ((args) => execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', args], { encoding: 'utf8', timeout: 30000 }))
147
+ const raw = String(run(ps) || '').trim()
148
+ if (!raw) return []
149
+ const parsed = JSON.parse(raw)
150
+ return Array.isArray(parsed) ? parsed : [parsed]
151
+ } catch (e) { return [] }
152
+ }
153
+
154
+ export function formatSuspect(s) {
155
+ const cmd = String(s.cmd || '')
156
+ return ' pid=' + s.pid + ' parent=' + s.parent + (s.parentGone ? ' (GONE)' : '') + ' ' + (s.created || '') + ' ' + cmd.slice(0, 120)
157
+ }
158
+
159
+ const argv = process.argv.slice(2)
160
+ const has = (n) => argv.includes('--' + n)
161
+ const val = (n, d) => { const a = argv.find((x) => x.startsWith('--' + n + '=')); return a ? a.split('=').slice(1).join('=') : d }
162
+
163
+ if (has('self-test')) {
164
+ // The GUARD: these three checks fail BY NAME if the planner's prefix test or age test is broken.
165
+ const now = Date.now()
166
+ const prefixes = ['vibe-', 'v2-fix-']
167
+ const entries = [
168
+ { name: 'vibe-fresh-abc', mtimeMs: now - 60 * 1000 }, // fresh ⇒ must NOT be planned
169
+ { name: 'vibe-stale-abc', mtimeMs: now - 48 * 3600 * 1000 }, // stale ⇒ MUST be planned
170
+ { name: 'v2-fix-stale', mtimeMs: now - 48 * 3600 * 1000 }, // stale ⇒ MUST be planned
171
+ { name: 'unrelated-stale', mtimeMs: now - 48 * 3600 * 1000 }, // foreign⇒ must NOT be planned
172
+ { name: 'vibestale-nodash', mtimeMs: now - 48 * 3600 * 1000 }, // prefix must match exactly
173
+ ]
174
+ const planned = planSweep(entries, { prefixes, ageHours: 6, now })
175
+ const failures = []
176
+ const check = (cond, label) => { console.log((cond ? 'PASS ' : 'FAIL - ') + label); if (!cond) failures.push(label) }
177
+ check(!planned.includes('vibe-fresh-abc'), 'TEMP-HYGIENE: a FRESH suite dir is never swept (age threshold respected)')
178
+ check(planned.includes('vibe-stale-abc') && planned.includes('v2-fix-stale'), 'TEMP-HYGIENE: STALE dirs with a suite prefix ARE swept')
179
+ check(!planned.includes('unrelated-stale') && !planned.includes('vibestale-nodash'), 'TEMP-HYGIENE: only exact suite prefixes are swept (no glob-like overreach)')
180
+ const live = suitePrefixes()
181
+ check(live.length >= 3, 'TEMP-HYGIENE: the prefix table is EXTRACTED from the suites (found ' + live.length + ': ' + live.slice(0, 4).join(', ') + ' …)')
182
+ // task-15: the leftover-PROCESS predicate, tested on fixtures so the property is checkable and mutable.
183
+ const procs = [
184
+ { pid: 1, parent: 999, parentGone: true, cmd: 'node tests/run-tests.mjs --temp-dry-run' }, // excluded via selfPids ONLY
185
+ { pid: 5, parent: 995, parentGone: false, cmd: 'node tests/run-tests.mjs --temp-dry-run' }, // LIVE parent -> never suspect
186
+ { pid: 2, parent: 998, parentGone: true, cmd: 'node tests/run-tests.mjs --temp-dry-run' }, // an ORPHANED gate
187
+ { pid: 3, parent: 997, parentGone: true, cmd: 'node bin.js --profile vmfix7 --port 7791' }, // the measured leftover
188
+ { pid: 4, parent: 996, parentGone: true, cmd: 'C:/apps/editor/editor.exe --open notes.md' },// unrelated program
189
+ ]
190
+ const susp = suspectProcesses(procs, { selfPids: [1] })
191
+ check(!susp.some((s) => s.pid === 1), 'TEMP-HYGIENE: an explicitly excluded pid (self / own parent) is never reported')
192
+ // This one is what the "parent-gone" mutation attacks: pid 5 is NOT excluded by selfPids, so only the
193
+ // live-parent property keeps it out of the report.
194
+ check(!susp.some((s) => s.pid === 5), 'TEMP-HYGIENE: a process with a LIVE parent (our running gate) is never called a leftover')
195
+ check(susp.some((s) => s.pid === 2) && susp.some((s) => s.pid === 3), 'TEMP-HYGIENE: ORPHANED project/team processes are reported (incl. a DSH host started against a clone profile)')
196
+ check(!susp.some((s) => s.pid === 4), 'TEMP-HYGIENE: an unrelated program is never reported (the command-line gate holds)')
197
+ console.log('temp-hygiene self-test: ' + (failures.length ? failures.length + ' failure(s)' : 'all checks passed'))
198
+ process.exit(failures.length ? 1 : 0)
199
+ }
200
+
201
+ // CLI entry detection must survive being IMPORTED (then `process.argv[1]` is undefined — the naive
202
+ // `process.argv[1].replace(...)` form threw a TypeError for every importer).
203
+ const isCli = (() => {
204
+ try { return !!process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url) } catch (e) { return false }
205
+ })()
206
+
207
+ if (isCli) {
208
+ // task-15: read-only leftover-process report. `--kill` must be asked for explicitly, and the current
209
+ // process + its parent are always excluded (a running gate must never kill itself).
210
+ if (has('ps')) {
211
+ const procs = listProcesses()
212
+ const susp = suspectProcesses(procs, { selfPids: [process.pid, process.ppid] })
213
+ console.log('temp-hygiene --ps: scanned ' + procs.length + ' node process(es); suspects=' + susp.length)
214
+ for (const s of susp) console.log(formatSuspect(s))
215
+ if (has('kill')) {
216
+ let killed = 0
217
+ for (const s of susp) { try { process.kill(Number(s.pid), 'SIGKILL'); killed++ } catch (e) { /* already gone */ } }
218
+ console.log('temp-hygiene --ps --kill: killed=' + killed)
219
+ } else if (susp.length) {
220
+ console.log('temp-hygiene --ps: read-only. Re-run with --kill to stop them (each one steals CPU from the gate).')
221
+ }
222
+ process.exit(0)
223
+ }
224
+ const root = val('root', null)
225
+ if (!root) { const pref = preferredTempRoot(); if (useTempRoot(pref)) console.log('temp-hygiene: using temp root ' + pref) }
226
+ const res = sweep({
227
+ root: val('root', tmpdir()),
228
+ prefixes: suitePrefixes(),
229
+ ageHours: Number(val('age-hours', '6')),
230
+ dryRun: has('dry-run'),
231
+ log: (m) => console.log(m),
232
+ })
233
+ console.log('temp-hygiene: ' + JSON.stringify(res))
234
+ }
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Regenerate the count sentences quoted by shipped docs FROM THE TREE (D1: derive, don't type).
4
+ *
5
+ * node scripts/update-doc-counts.mjs # rewrite in place
6
+ * node scripts/update-doc-counts.mjs --check # exit 1 if anything would change (converges: whole-line rewrites)
7
+ *
8
+ * Authority: `node tests/run-tests.mjs --counts` (the gate's own JOB list) + `package.json#files`
9
+ * (shipped FILE counts). Shapes come from tests/helpers/doc-counts-format.mjs, shared with the guard
10
+ * (tests/audit-readme-counts.mjs) and its mutant harness. Adding a test file reddens the guard; the fix
11
+ * is to run this script. Whole lines are rebuilt (no partial regex edits), so a corrupted previous
12
+ * shape is repaired rather than half-matched.
13
+ *
14
+ * Grep anchors: update-doc-counts, doc-counts-format.
15
+ */
16
+ import { readFileSync, writeFileSync } from 'node:fs'
17
+ import { dirname, join } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+ import { spawnSync } from 'node:child_process'
20
+ import { claims, MARKERS } from '../tests/helpers/doc-counts-format.mjs'
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url))
23
+ const REPO = join(HERE, '..')
24
+ const checkOnly = process.argv.includes('--check')
25
+
26
+ const derived = JSON.parse(spawnSync(process.execPath, [join(REPO, 'tests', 'run-tests.mjs'), '--counts'], { cwd: REPO, encoding: 'utf8' }).stdout.trim())
27
+ const pkg = JSON.parse(readFileSync(join(REPO, 'package.json'), 'utf8'))
28
+ const shippedFiles = pkg.files.filter((f) => f.startsWith('tests/') && f.endsWith('.mjs'))
29
+ const shipped = { total: shippedFiles.length, suites: shippedFiles.filter((f) => f.endsWith('.test.mjs')).length }
30
+ shipped.probes = shipped.total - shipped.suites
31
+ const c = claims(derived, shipped)
32
+
33
+ function replaceLine(text, marker, canonical) {
34
+ const lines = text.split(/\r?\n/)
35
+ const i = lines.findIndex((l) => l.indexOf(marker) !== -1)
36
+ if (i === -1) return null
37
+ if (lines[i] === canonical) return text
38
+ lines[i] = canonical
39
+ return lines.join(text.includes('\r\n') ? '\r\n' : '\n')
40
+ }
41
+
42
+ const log = []
43
+ let changed = 0
44
+ const apply = (file, fn) => {
45
+ const p = join(REPO, file)
46
+ const before = readFileSync(p, 'utf8')
47
+ const after = fn(before)
48
+ if (after === null) { log.push('MARKER MISS: ' + file); return }
49
+ if (after !== before) {
50
+ changed++
51
+ if (checkOnly) log.push('WOULD CHANGE ' + file)
52
+ else { writeFileSync(p, after); log.push('updated ' + file) }
53
+ }
54
+ }
55
+
56
+ apply('README.md', (s) => replaceLine(s, MARKERS.zhBullet, c.zhBullet))
57
+ apply('README.en.md', (s) => replaceLine(s, MARKERS.enBullet, c.enBullet))
58
+ apply('docs/test-timing.md', (s) => replaceLine(s, MARKERS.ttRow, c.ttRow))
59
+ apply('docs/AUDIT-CHECKLIST.md', (s) => {
60
+ let out = replaceLine(s, MARKERS.d1Row, c.d1Row)
61
+ if (out === null) return null
62
+ out = out.replace(/\d+ 项作业(job count)= \d+ 套件 \+ \d+ 探针\/变体/, c.ckClaim)
63
+ out = out.replace(/只发 `tests\/` 的 .*?([^)]*)/, c.ckShipped)
64
+ return out
65
+ })
66
+
67
+ log.push((checkOnly ? 'check: ' : '') + 'derived JOBS total=' + derived.total + ' suites=' + derived.suites + ' probes=' + derived.probes
68
+ + ' | shipped FILES tests/*.mjs=' + shipped.total + ' (' + shipped.suites + ' suites + ' + shipped.probes + ' probes/scripts)'
69
+ + ' | files changed=' + changed)
70
+ console.log(log.join('\n'))
71
+ process.exit(checkOnly && changed ? 1 : 0)
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ARTIFACT DOCS vs CODE — the READMEs and the contract doc must not name an artifact a preset does not
4
+ * write, and must name the ones it does.
5
+ *
6
+ * DEFECT CLASS (D2): `Paper/<id>/paper.lock.json` was listed in the README's v4 section as "the directory's
7
+ * write lock, serialising concurrent rewrites/recompiles" — while v4 writes NO such file and
8
+ * `vibe-math-v4.js` says the shared-file lock is **NOT IMPLEMENTED** ("nothing is reserved, nothing is
9
+ * serialized"). The README promised a mechanism the code explicitly disclaims. The inverse direction
10
+ * matters too: v2/v3 DO write it (vibe-math-v2.js:3725/3732, vibe-math-v3.js:4318/4325) and their
11
+ * sections must list it.
12
+ *
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).
16
+ */
17
+ import { readFileSync } from 'node:fs'
18
+ import { dirname, isAbsolute, join } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const HERE = dirname(fileURLToPath(import.meta.url))
22
+ const REPO = join(HERE, '..')
23
+ const ARTIFACT = 'paper.lock.json'
24
+ let passed = 0, failed = 0
25
+ const failures = []
26
+ const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
27
+
28
+ const read = (envName, rel) => {
29
+ const v = process.env[envName]
30
+ return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
31
+ }
32
+ const count = (text, needle) => text.split(needle).length - 1
33
+
34
+ // 1) CODE TRUTH: which presets write the artifact?
35
+ const presets = {
36
+ v2: read('ARTIFACT_V2_JS', 'vibe-math-v2/vibe-math-v2.js'),
37
+ v3: read('ARTIFACT_V3_JS', 'vibe-math-v3/vibe-math-v3.js'),
38
+ v4: read('ARTIFACT_V4_JS', 'vibe-math-v4/vibe-math-v4.js'),
39
+ v5: read('ARTIFACT_V5_JS', 'vibe-math-v5/vibe-math-v5.js'),
40
+ }
41
+ const writers = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) > 0)
42
+ const nonWriters = Object.keys(presets).filter((k) => count(presets[k], ARTIFACT) === 0)
43
+ ok(writers.length > 0, 'at least one preset writes ' + ARTIFACT + ' (else the doc expectation is empty)', JSON.stringify(writers))
44
+ ok(writers.join(',') === 'v2,v3', 'the writing presets are exactly v2,v3 (derived from code)', JSON.stringify({ writers, nonWriters }))
45
+
46
+ // 2) every README mention must sit in a writer's context OR be an explicit denial
47
+ const DENY = /(不写|未实现|无此锁|not implemented|does NOT write|no such|not write)/i
48
+ for (const [name, envName, rel] of [['README.md', 'ARTIFACT_README', 'README.md'], ['README.en.md', 'ARTIFACT_README_EN', 'README.en.md']]) {
49
+ const lines = read(envName, rel).split(/\r?\n/)
50
+ const mentions = lines.map((l, i) => ({ n: i + 1, l })).filter((x) => x.l.indexOf(ARTIFACT) !== -1)
51
+ ok(mentions.length > 0, name + ' mentions ' + ARTIFACT + ' somewhere (it is a real artifact for v2/v3)')
52
+ const bad = mentions.filter((m) => !/v2|v3/.test(m.l) && !DENY.test(m.l))
53
+ ok(bad.length === 0, '★ ' + name + ': every ' + ARTIFACT + ' mention names a WRITING preset or denies the claim', JSON.stringify(bad.map((b) => b.n)))
54
+ const listed = mentions.filter((m) => /Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json/.test(m.l))
55
+ ok(listed.length >= 2, '★ ' + name + ' lists ' + ARTIFACT + ' inside the v2/v3 artifact braces', 'brace mentions=' + listed.length)
56
+ }
57
+
58
+ // 3) the contract doc must state the scope (v2/v3 write it; v4/v5 do not)
59
+ const doc = read('ARTIFACT_DOC', 'docs/final-paper.md')
60
+ const docLines = doc.split(/\r?\n/).filter((l) => l.indexOf(ARTIFACT) !== -1)
61
+ ok(docLines.length > 0, 'docs/final-paper.md documents ' + ARTIFACT)
62
+ const docScope = docLines.some((l) => /v2\/v3/.test(l) && DENY.test(l))
63
+ ok(docScope, '★ docs/final-paper.md scopes ' + ARTIFACT + ' to v2/v3 AND states that v4/v5 do not write it', docLines[0] ? docLines[0].slice(0, 80) : '(no line)')
64
+
65
+ // 4) the non-writer's own disclaimer must exist in code (so the doc denial is backed by the source)
66
+ ok(/NOT IMPLEMENTED/.test(presets.v4) && /claim_write/.test(presets.v4),
67
+ 'vibe-math-v4.js still carries the explicit NOT-IMPLEMENTED shared-lock disclaimer (the source of the doc denial)')
68
+
69
+ console.log('')
70
+ console.log('=== ARTIFACT DOCS: ' + passed + ' passed, ' + failed + ' failed ===')
71
+ for (const f of failures) console.error(' - ' + f)
72
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mutant harness for tests/audit-artifact-docs.mjs (ships: `tests/*.mutants.mjs`).
4
+ *
5
+ * Two single-site mutations, ONE PER DIRECTION (the guard must bite both ways):
6
+ * A) a COPY of README.md whose v2/v3 brace list loses `paper.lock.json` -> the listing assertion reddens
7
+ * B) a COPY of vibe-math-v4.js that mentions `paper.lock.json` -> the "writers are exactly v2,v3"
8
+ * assertion reddens (i.e. if v4 ever gains the artifact, the docs expectation moves with the code)
9
+ *
10
+ * Run: node tests/audit-artifact-docs.mutants.mjs
11
+ */
12
+ import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { spawnSync } from 'node:child_process'
16
+ import { fileURLToPath } from 'node:url'
17
+
18
+ const HERE = fileURLToPath(new URL('./', import.meta.url))
19
+ const REPO = join(HERE, '..')
20
+ const GUARD = join(HERE, 'audit-artifact-docs.mjs')
21
+ const ARTIFACT = 'paper.lock.json'
22
+ let passed = 0, failed = 0
23
+ const ok = (c, l, d) => { if (c) { passed++; console.log(' ok ' + l) } else { failed++; console.log(' FAIL ' + l + (d ? ' — ' + d : '')) } }
24
+
25
+ const run = (env) => {
26
+ const r = spawnSync(process.execPath, [GUARD], { cwd: REPO, encoding: 'utf8', env: Object.assign({}, process.env, env || {}) })
27
+ const out = String(r.stdout || '') + String(r.stderr || '')
28
+ return { status: r.status, out, failLines: out.split('\n').filter((l) => /^ - /.test(l)) }
29
+ }
30
+ const baseline = run(null)
31
+ ok(baseline.status === 0, 'baseline: the artifact-docs guard is green',
32
+ (baseline.out.trim().split('\n').filter(Boolean).slice(-1)[0] || '').slice(0, 80))
33
+
34
+ const dir = mkdtempSync(join(tmpdir(), 'artifact-mut-'))
35
+ // A) README loses the v2/v3 brace listing
36
+ const zh = readFileSync(join(REPO, 'README.md'), 'utf8')
37
+ const braces = zh.match(/Paper\/<[^>]*>\/\{[^}]*paper\.lock\.json[^}]*\}/g) || []
38
+ if (!braces.length) ok(false, 'A: the README v2/v3 brace listing exists (harness and guard agree on the shape)', 'ANCHOR MISS')
39
+ else {
40
+ const tampered = zh.split(braces[0]).join(braces[0].replace(',' + ARTIFACT, ''))
41
+ writeFileSync(join(dir, 'README.md'), tampered)
42
+ const a = run({ ARTIFACT_README: join(dir, 'README.md') })
43
+ const named = a.failLines.some((l) => l.indexOf('lists ' + ARTIFACT + ' inside the v2/v3 artifact braces') !== -1)
44
+ ok(a.status !== 0 && named, '★ A: dropping the artifact from the v2/v3 brace list reddens the guard by name', 'exit=' + a.status)
45
+ if (!named) for (const l of a.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
46
+ }
47
+ // B) v4 "gains" the artifact
48
+ const v4 = readFileSync(join(REPO, 'vibe-math-v4/vibe-math-v4.js'), 'utf8')
49
+ writeFileSync(join(dir, 'vibe-math-v4.js'), v4 + '\n// mutant: pretend v4 writes ' + ARTIFACT + '\n')
50
+ const b = run({ ARTIFACT_V4_JS: join(dir, 'vibe-math-v4.js') })
51
+ const namedB = b.failLines.some((l) => l.indexOf('the writing presets are exactly v2,v3') !== -1)
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
+ if (!namedB) for (const l of b.failLines.slice(0, 3)) console.log(' guard failure: ' + l.trim().slice(0, 130))
54
+
55
+ rmSync(dir, { recursive: true, force: true })
56
+ console.log('')
57
+ console.log('=== ARTIFACT DOCS MUTANTS: ' + passed + ' passed, ' + failed + ' failed ===')
58
+ process.exit(failed === 0 ? 0 : 1)