dsh-vibe-math 2.7.3 → 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 (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.en.md +682 -682
  3. package/README.md +683 -683
  4. package/docs/AUDIT-CHECKLIST.md +755 -744
  5. package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
  6. package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
  7. package/docs/audit-methodology.md +93 -0
  8. package/docs/final-paper.md +172 -172
  9. package/docs/formal-verification.md +613 -588
  10. package/docs/generate_framework_diagram_v2.py +114 -114
  11. package/docs/generate_framework_diagram_v3.py +127 -127
  12. package/docs/math-computation.md +195 -194
  13. package/docs/release-notes/README.en.md +1 -0
  14. package/docs/release-notes/README.md +1 -0
  15. package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
  16. package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
  17. package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
  18. package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
  19. package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
  20. package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
  21. package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
  22. package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
  23. package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
  24. package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
  25. package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
  26. package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
  27. package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
  28. package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
  29. package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
  30. package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
  31. package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
  32. package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
  33. package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
  34. package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
  35. package/docs/release-playbook.md +103 -0
  36. package/docs/slv-playbook.md +117 -0
  37. package/docs/test-timing.md +158 -157
  38. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  39. package/installer.js +792 -792
  40. package/package.json +13 -3
  41. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  42. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  43. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  44. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  45. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  46. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  47. package/scripts/clean-temp.mjs +234 -0
  48. package/tests/audit-engine-faces.mjs +298 -0
  49. package/tests/audit-formal-sensitivity.mjs +345 -342
  50. package/tests/audit-installer-compat.test.mjs +256 -256
  51. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  52. package/tests/audit-persona-sensitivity.mjs +323 -323
  53. package/tests/audit-persona-surface.test.mjs +431 -431
  54. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  55. package/tests/audit-prompt-invariants.mjs +589 -546
  56. package/tests/audit-registration.mjs +78 -78
  57. package/tests/audit-spec-traceability.mjs +193 -193
  58. package/tests/audit-status-report-fields.mjs +172 -172
  59. package/tests/audit-v5-integrity.mjs +795 -795
  60. package/tests/audit-v5-sensitivity.mjs +433 -432
  61. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  62. package/tests/formal-verify-v2.test.mjs +1806 -1800
  63. package/tests/formal-verify-v3.test.mjs +1757 -1750
  64. package/tests/formal-verify-v4.test.mjs +2114 -2106
  65. package/tests/formal-verify-v5.test.mjs +774 -767
  66. package/tests/math-computation-discovery.mutants.mjs +3 -3
  67. package/tests/math-computation-shared.test.mjs +122 -5
  68. package/tests/math-computation-v2.test.mjs +14 -2
  69. package/tests/math-computation-v3.test.mjs +14 -2
  70. package/tests/math-computation-v4.test.mjs +739 -739
  71. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  72. package/tests/run-tests.mjs +393 -332
  73. package/tests/selfdrive-v5.mjs +560 -560
  74. package/tests/temp-hygiene.mutants.mjs +80 -0
  75. package/tests/v2-fix-probes.mutants.mjs +4 -0
  76. package/tests/v2-fix-probes.test.mjs +1034 -1033
  77. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  78. package/tests/v3-fix-probes.mutants.mjs +99 -95
  79. package/tests/v3-fix-probes.test.mjs +891 -890
  80. package/tests/v4-final-paper.mutants.mjs +108 -0
  81. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  82. package/vibe-math-v2/agent.cordis.yml +376 -376
  83. package/vibe-math-v2/math-computation.js +17 -15
  84. package/vibe-math-v2/preset.yml +2 -2
  85. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  86. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  87. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  88. package/vibe-math-v3/agent.cordis.yml +420 -420
  89. package/vibe-math-v3/math-computation.js +17 -15
  90. package/vibe-math-v3/preset.yml +2 -2
  91. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  92. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  93. package/vibe-math-v4/agent.cordis.yml +438 -438
  94. package/vibe-math-v4/math-computation.js +17 -15
  95. package/vibe-math-v4/preset.yml +2 -2
  96. package/vibe-math-v4/vibe-math-v4.js +4490 -4422
  97. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  98. package/vibe-math-v5/math-computation.js +17 -15
  99. package/vibe-math-v5/vibe-math-v5.js +93 -11
  100. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  101. package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
@@ -1,172 +1,172 @@
1
- #!/usr/bin/env node
2
- /**
3
- * STATUS/REPORT FIELD TABLE vs SOURCE (G-6) — the doc's per-preset field/scope rows must be exactly the
4
- * fields the code declares, in BOTH directions, and each row's scope must match the source classification.
5
- *
6
- * DEFECT CLASS: "machine-face field documented nowhere / documented field that does not exist" (F12/F3):
7
- * `实现方案.md §14.9` claimed every `status()` field had a row while >=13 top-level keys had none; and a
8
- * table written by hand drifts the moment a field is added. Sources are derived BY MARKER (line numbers
9
- * are claims, not anchors):
10
- * v2/v3 -> `fieldScopes: { session: [...], durable: [...] }` (20 / 25 entries)
11
- * v5 -> the top-level keys of the object literal `status()` returns (its `fieldScopes` is coarse)
12
- * v4 -> has NO `fieldScopes`; the table must say so rather than invent rows
13
- *
14
- * Run: node tests/audit-status-report-fields.mjs
15
- * Env (mutant harness): FIELD_DOC, FIELD_V2_JS, FIELD_V3_JS, FIELD_V5_JS, FIELD_V4_JS (absolute or 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
- let passed = 0, failed = 0
24
- const failures = []
25
- const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
26
- const read = (envName, rel) => {
27
- const v = process.env[envName]
28
- return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
29
- }
30
- function fieldScopesOf(text) {
31
- const lines = text.split(/\r?\n/)
32
- const start = lines.findIndex((l) => /^\s*fieldScopes:\s*\{/.test(l))
33
- if (start === -1) return null
34
- let depth = 0
35
- const buf = []
36
- for (let i = start; i < lines.length; i++) {
37
- buf.push(lines[i]); depth += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length
38
- if (i > start && depth <= 0) break
39
- }
40
- const out = {}
41
- for (const m of buf.join('\n').matchAll(/(\w+):\s*\[([\s\S]*?)\]/g)) {
42
- out[m[1]] = m[2].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
43
- }
44
- return out
45
- }
46
- function statusKeysOf(text) {
47
- // Robust: brace/comment/string aware, and it reads SHORTHAND properties too —
48
- // `institute: instituteName, project, key, phase,` is FOUR keys, not one. The earlier line-anchored
49
- // version missed 8 real keys (R19 F-A: project/key/phase/running/autoDone/runId/lastProgressAt/params).
50
- const fn = text.search(/function\s+status\s*\(/)
51
- if (fn === -1) return null
52
- const ret = text.indexOf('return {', fn)
53
- if (ret === -1) return null
54
- const start = text.indexOf('{', ret)
55
- let d0 = 0, stop = start, k = start
56
- while (k < text.length) {
57
- const c = text[k]
58
- if (c === '/' && text[k + 1] === '/') { while (k < text.length && text[k] !== '\n') k++; continue }
59
- if (c === '/' && text[k + 1] === '*') { k += 2; while (k < text.length && !(text[k] === '*' && text[k + 1] === '/')) k++; k += 2; continue }
60
- if (c === '"' || c === "'" || c === '`') { const q = c; k++; while (k < text.length) { if (text[k] === '\\') k += 2; else if (text[k] === q) break; else k++ } k++; continue }
61
- if (c === '{' || c === '[' || c === '(') d0++
62
- else if (c === '}' || c === ']' || c === ')') { d0--; if (d0 === 0) { stop = k; break } }
63
- k++
64
- }
65
- if (d0 !== 0) return null
66
- const body = text.slice(start + 1, stop)
67
- const segs = []
68
- let buf = '', d = 0, j = 0
69
- while (j < body.length) {
70
- const c = body[j]
71
- if (c === '/' && body[j + 1] === '/') { while (j < body.length && body[j] !== '\n') j++; continue }
72
- if (c === '/' && body[j + 1] === '*') { j += 2; while (j < body.length && !(body[j] === '*' && body[j + 1] === '/')) j++; j += 2; continue }
73
- if (c === '"' || c === "'" || c === '`') { const q = c; buf += c; j++; while (j < body.length) { buf += body[j]; if (body[j] === '\\') { j++; buf += body[j] } else if (body[j] === q) break; j++ } j++; continue }
74
- if (c === '{' || c === '[' || c === '(') d++
75
- if (c === '}' || c === ']' || c === ')') d--
76
- if (c === ',' && d === 0) { segs.push(buf); buf = ''; j++; continue }
77
- buf += c; j++
78
- }
79
- if (buf.trim()) segs.push(buf)
80
- const keys = []
81
- for (const seg of segs) {
82
- const t = seg.trim()
83
- if (!t) continue
84
- const m = /^([A-Za-z_$][\w$]*)\s*(?::|$|,)/.exec(t)
85
- if (m) keys.push(m[1])
86
- }
87
- return [...new Set(keys)]
88
- }
89
- /** Rows of the doc section that starts with `headingPrefix`, up to the next `### `. */
90
- function docRows(doc, headingPrefix) {
91
- const lines = doc.split(/\r?\n/)
92
- const start = lines.findIndex((l) => l.startsWith(headingPrefix))
93
- if (start === -1) return null
94
- let end = lines.findIndex((l, i) => i > start && /^### /.test(l))
95
- if (end === -1) end = lines.length
96
- const rows = []
97
- for (let i = start; i < end; i++) {
98
- const m = /^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|/.exec(lines[i])
99
- if (m) rows.push({ name: m[1], scope: m[2] })
100
- }
101
- return rows
102
- }
103
- const diff = (a, b) => [...a].filter((x) => !b.has(x))
104
-
105
- const doc = read('FIELD_DOC', 'docs/status-report-fields.md')
106
- ok(doc.indexOf('G-6-FIELD-TABLE-START') !== -1 && doc.indexOf('G-6-FIELD-TABLE-END') !== -1,
107
- 'the doc carries the machine-generated field table between its markers (so the guard knows what to read)')
108
-
109
- const f2 = fieldScopesOf(read('FIELD_V2_JS', 'vibe-math-v2/vibe-math-v2.js'))
110
- const f3 = fieldScopesOf(read('FIELD_V3_JS', 'vibe-math-v3/vibe-math-v3.js'))
111
- const s5 = statusKeysOf(read('FIELD_V5_JS', 'vibe-math-v5/vibe-math-v5.js'))
112
- const v4 = read('FIELD_V4_JS', 'vibe-math-v4/vibe-math-v4.js')
113
- ok(!!f2 && !!f3 && !!s5, 'all three sources are parseable by marker (v2/v3 fieldScopes, v5 status())')
114
-
115
- if (f2 && f3 && s5) {
116
- const cases = [
117
- { preset: 'v2', src: [...(f2.session || []), ...(f2.durable || [])], head: '### v2 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f2 },
118
- { preset: 'v3', src: [...(f3.session || []), ...(f3.durable || [])], head: '### v3 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f3 },
119
- { preset: 'v5', src: s5, head: '### v5 的字段与作用域', scopeMap: null, fs: null },
120
- ]
121
- for (const c of cases) {
122
- const rows = docRows(doc, c.head)
123
- ok(!!rows && rows.length > 0, c.preset + ': the doc section exists and has rows', String(rows && rows.length))
124
- if (!rows) continue
125
- const docSet = new Set(rows.map((r) => r.name))
126
- const srcSet = new Set(c.src)
127
- const missingInDoc = diff(srcSet, docSet)
128
- const extraInDoc = diff(docSet, srcSet)
129
- ok(missingInDoc.length === 0, '★ ' + c.preset + ': every source field has a doc row (doc↔source, source→doc)', JSON.stringify(missingInDoc))
130
- ok(extraInDoc.length === 0, '★ ' + c.preset + ': every doc row names a real source field (doc↔source, doc→source)', JSON.stringify(extraInDoc))
131
- if (c.scopeMap && c.fs) {
132
- const wrong = []
133
- for (const scope of ['session', 'durable']) {
134
- for (const name of c.fs[scope] || []) {
135
- const r = rows.find((x) => x.name === name)
136
- if (r && r.scope !== c.scopeMap[scope]) wrong.push(name + '=' + r.scope)
137
- }
138
- }
139
- ok(wrong.length === 0, '★ ' + c.preset + ': every row scope matches the source classification', JSON.stringify(wrong))
140
- }
141
- }
142
- // ---- code -> doc (R19 F-A): EVERY status() top-level key lands in the v5 table OR is an explicit
143
- // exception. The exceptions list is empty today; adding one requires a reason HERE, in this file.
144
- const V5_EXCEPTIONS = []
145
- {
146
- const v5DocRows = docRows(doc, '### v5 的字段与作用域') || []
147
- const docSet5 = new Set(v5DocRows.map((r) => r.name))
148
- const frozen = s5
149
- const exceptions = V5_EXCEPTIONS
150
- console.log(' counts: frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
151
- ok(frozen.length > 0 && docSet5.size > 0, '★ the three counts are non-zero (the comparison cannot pass vacuously)',
152
- 'frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
153
- const covered = new Set([...docSet5, ...exceptions])
154
- const uncovered = frozen.filter((k) => !covered.has(k))
155
- ok(uncovered.length === 0, '★ every status() top-level key is documented or an explicit exception (code->doc)',
156
- JSON.stringify(uncovered))
157
- const phantom = [...docSet5].filter((k) => frozen.indexOf(k) === -1 && exceptions.indexOf(k) === -1)
158
- ok(phantom.length === 0, '★ no v5 doc row names a key status() does not return (doc->code)',
159
- JSON.stringify(phantom))
160
- }
161
- // v4 must be stated as having none, and the source must agree
162
- const v4Rows = docRows(doc, '### v4')
163
- ok(!v4Rows || v4Rows.length === 0, '★ v4: no rows are invented (it has no fieldScopes to derive from)', String(v4Rows && v4Rows.length))
164
- ok(doc.indexOf('**没有** `fieldScopes`') !== -1 || doc.indexOf('不存在** `fieldScopes`') !== -1,
165
- '★ v4: the doc says explicitly that v4 has no fieldScopes')
166
- ok(!/fieldScopes/.test(v4), '★ v4: the source confirms it (zero occurrences of fieldScopes)')
167
- }
168
-
169
- console.log('')
170
- console.log('=== STATUS FIELD TABLE: ' + passed + ' passed, ' + failed + ' failed ===')
171
- for (const f of failures) console.error(' - ' + f)
172
- process.exit(failed === 0 ? 0 : 1)
1
+ #!/usr/bin/env node
2
+ /**
3
+ * STATUS/REPORT FIELD TABLE vs SOURCE (G-6) — the doc's per-preset field/scope rows must be exactly the
4
+ * fields the code declares, in BOTH directions, and each row's scope must match the source classification.
5
+ *
6
+ * DEFECT CLASS: "machine-face field documented nowhere / documented field that does not exist" (F12/F3):
7
+ * `实现方案.md §14.9` claimed every `status()` field had a row while >=13 top-level keys had none; and a
8
+ * table written by hand drifts the moment a field is added. Sources are derived BY MARKER (line numbers
9
+ * are claims, not anchors):
10
+ * v2/v3 -> `fieldScopes: { session: [...], durable: [...] }` (20 / 25 entries)
11
+ * v5 -> the top-level keys of the object literal `status()` returns (its `fieldScopes` is coarse)
12
+ * v4 -> has NO `fieldScopes`; the table must say so rather than invent rows
13
+ *
14
+ * Run: node tests/audit-status-report-fields.mjs
15
+ * Env (mutant harness): FIELD_DOC, FIELD_V2_JS, FIELD_V3_JS, FIELD_V5_JS, FIELD_V4_JS (absolute or 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
+ let passed = 0, failed = 0
24
+ const failures = []
25
+ const ok = (cond, label, detail) => { if (cond) passed++; else { failed++; failures.push(label + (detail ? ' — ' + detail : '')) } }
26
+ const read = (envName, rel) => {
27
+ const v = process.env[envName]
28
+ return readFileSync(v ? (isAbsolute(v) ? v : join(REPO, v)) : join(REPO, rel), 'utf8')
29
+ }
30
+ function fieldScopesOf(text) {
31
+ const lines = text.split(/\r?\n/)
32
+ const start = lines.findIndex((l) => /^\s*fieldScopes:\s*\{/.test(l))
33
+ if (start === -1) return null
34
+ let depth = 0
35
+ const buf = []
36
+ for (let i = start; i < lines.length; i++) {
37
+ buf.push(lines[i]); depth += (lines[i].match(/\{/g) || []).length - (lines[i].match(/\}/g) || []).length
38
+ if (i > start && depth <= 0) break
39
+ }
40
+ const out = {}
41
+ for (const m of buf.join('\n').matchAll(/(\w+):\s*\[([\s\S]*?)\]/g)) {
42
+ out[m[1]] = m[2].split(',').map((s) => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
43
+ }
44
+ return out
45
+ }
46
+ function statusKeysOf(text) {
47
+ // Robust: brace/comment/string aware, and it reads SHORTHAND properties too —
48
+ // `institute: instituteName, project, key, phase,` is FOUR keys, not one. The earlier line-anchored
49
+ // version missed 8 real keys (R19 F-A: project/key/phase/running/autoDone/runId/lastProgressAt/params).
50
+ const fn = text.search(/function\s+status\s*\(/)
51
+ if (fn === -1) return null
52
+ const ret = text.indexOf('return {', fn)
53
+ if (ret === -1) return null
54
+ const start = text.indexOf('{', ret)
55
+ let d0 = 0, stop = start, k = start
56
+ while (k < text.length) {
57
+ const c = text[k]
58
+ if (c === '/' && text[k + 1] === '/') { while (k < text.length && text[k] !== '\n') k++; continue }
59
+ if (c === '/' && text[k + 1] === '*') { k += 2; while (k < text.length && !(text[k] === '*' && text[k + 1] === '/')) k++; k += 2; continue }
60
+ if (c === '"' || c === "'" || c === '`') { const q = c; k++; while (k < text.length) { if (text[k] === '\\') k += 2; else if (text[k] === q) break; else k++ } k++; continue }
61
+ if (c === '{' || c === '[' || c === '(') d0++
62
+ else if (c === '}' || c === ']' || c === ')') { d0--; if (d0 === 0) { stop = k; break } }
63
+ k++
64
+ }
65
+ if (d0 !== 0) return null
66
+ const body = text.slice(start + 1, stop)
67
+ const segs = []
68
+ let buf = '', d = 0, j = 0
69
+ while (j < body.length) {
70
+ const c = body[j]
71
+ if (c === '/' && body[j + 1] === '/') { while (j < body.length && body[j] !== '\n') j++; continue }
72
+ if (c === '/' && body[j + 1] === '*') { j += 2; while (j < body.length && !(body[j] === '*' && body[j + 1] === '/')) j++; j += 2; continue }
73
+ if (c === '"' || c === "'" || c === '`') { const q = c; buf += c; j++; while (j < body.length) { buf += body[j]; if (body[j] === '\\') { j++; buf += body[j] } else if (body[j] === q) break; j++ } j++; continue }
74
+ if (c === '{' || c === '[' || c === '(') d++
75
+ if (c === '}' || c === ']' || c === ')') d--
76
+ if (c === ',' && d === 0) { segs.push(buf); buf = ''; j++; continue }
77
+ buf += c; j++
78
+ }
79
+ if (buf.trim()) segs.push(buf)
80
+ const keys = []
81
+ for (const seg of segs) {
82
+ const t = seg.trim()
83
+ if (!t) continue
84
+ const m = /^([A-Za-z_$][\w$]*)\s*(?::|$|,)/.exec(t)
85
+ if (m) keys.push(m[1])
86
+ }
87
+ return [...new Set(keys)]
88
+ }
89
+ /** Rows of the doc section that starts with `headingPrefix`, up to the next `### `. */
90
+ function docRows(doc, headingPrefix) {
91
+ const lines = doc.split(/\r?\n/)
92
+ const start = lines.findIndex((l) => l.startsWith(headingPrefix))
93
+ if (start === -1) return null
94
+ let end = lines.findIndex((l, i) => i > start && /^### /.test(l))
95
+ if (end === -1) end = lines.length
96
+ const rows = []
97
+ for (let i = start; i < end; i++) {
98
+ const m = /^\|\s*`([^`]+)`\s*\|\s*([^|]+?)\s*\|/.exec(lines[i])
99
+ if (m) rows.push({ name: m[1], scope: m[2] })
100
+ }
101
+ return rows
102
+ }
103
+ const diff = (a, b) => [...a].filter((x) => !b.has(x))
104
+
105
+ const doc = read('FIELD_DOC', 'docs/status-report-fields.md')
106
+ ok(doc.indexOf('G-6-FIELD-TABLE-START') !== -1 && doc.indexOf('G-6-FIELD-TABLE-END') !== -1,
107
+ 'the doc carries the machine-generated field table between its markers (so the guard knows what to read)')
108
+
109
+ const f2 = fieldScopesOf(read('FIELD_V2_JS', 'vibe-math-v2/vibe-math-v2.js'))
110
+ const f3 = fieldScopesOf(read('FIELD_V3_JS', 'vibe-math-v3/vibe-math-v3.js'))
111
+ const s5 = statusKeysOf(read('FIELD_V5_JS', 'vibe-math-v5/vibe-math-v5.js'))
112
+ const v4 = read('FIELD_V4_JS', 'vibe-math-v4/vibe-math-v4.js')
113
+ ok(!!f2 && !!f3 && !!s5, 'all three sources are parseable by marker (v2/v3 fieldScopes, v5 status())')
114
+
115
+ if (f2 && f3 && s5) {
116
+ const cases = [
117
+ { preset: 'v2', src: [...(f2.session || []), ...(f2.durable || [])], head: '### v2 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f2 },
118
+ { preset: 'v3', src: [...(f3.session || []), ...(f3.durable || [])], head: '### v3 的字段与作用域', scopeMap: { session: '会话', durable: '耐久' }, fs: f3 },
119
+ { preset: 'v5', src: s5, head: '### v5 的字段与作用域', scopeMap: null, fs: null },
120
+ ]
121
+ for (const c of cases) {
122
+ const rows = docRows(doc, c.head)
123
+ ok(!!rows && rows.length > 0, c.preset + ': the doc section exists and has rows', String(rows && rows.length))
124
+ if (!rows) continue
125
+ const docSet = new Set(rows.map((r) => r.name))
126
+ const srcSet = new Set(c.src)
127
+ const missingInDoc = diff(srcSet, docSet)
128
+ const extraInDoc = diff(docSet, srcSet)
129
+ ok(missingInDoc.length === 0, '★ ' + c.preset + ': every source field has a doc row (doc↔source, source→doc)', JSON.stringify(missingInDoc))
130
+ ok(extraInDoc.length === 0, '★ ' + c.preset + ': every doc row names a real source field (doc↔source, doc→source)', JSON.stringify(extraInDoc))
131
+ if (c.scopeMap && c.fs) {
132
+ const wrong = []
133
+ for (const scope of ['session', 'durable']) {
134
+ for (const name of c.fs[scope] || []) {
135
+ const r = rows.find((x) => x.name === name)
136
+ if (r && r.scope !== c.scopeMap[scope]) wrong.push(name + '=' + r.scope)
137
+ }
138
+ }
139
+ ok(wrong.length === 0, '★ ' + c.preset + ': every row scope matches the source classification', JSON.stringify(wrong))
140
+ }
141
+ }
142
+ // ---- code -> doc (R19 F-A): EVERY status() top-level key lands in the v5 table OR is an explicit
143
+ // exception. The exceptions list is empty today; adding one requires a reason HERE, in this file.
144
+ const V5_EXCEPTIONS = []
145
+ {
146
+ const v5DocRows = docRows(doc, '### v5 的字段与作用域') || []
147
+ const docSet5 = new Set(v5DocRows.map((r) => r.name))
148
+ const frozen = s5
149
+ const exceptions = V5_EXCEPTIONS
150
+ console.log(' counts: frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
151
+ ok(frozen.length > 0 && docSet5.size > 0, '★ the three counts are non-zero (the comparison cannot pass vacuously)',
152
+ 'frozen=' + frozen.length + ' doc-rows=' + docSet5.size + ' exceptions=' + exceptions.length)
153
+ const covered = new Set([...docSet5, ...exceptions])
154
+ const uncovered = frozen.filter((k) => !covered.has(k))
155
+ ok(uncovered.length === 0, '★ every status() top-level key is documented or an explicit exception (code->doc)',
156
+ JSON.stringify(uncovered))
157
+ const phantom = [...docSet5].filter((k) => frozen.indexOf(k) === -1 && exceptions.indexOf(k) === -1)
158
+ ok(phantom.length === 0, '★ no v5 doc row names a key status() does not return (doc->code)',
159
+ JSON.stringify(phantom))
160
+ }
161
+ // v4 must be stated as having none, and the source must agree
162
+ const v4Rows = docRows(doc, '### v4')
163
+ ok(!v4Rows || v4Rows.length === 0, '★ v4: no rows are invented (it has no fieldScopes to derive from)', String(v4Rows && v4Rows.length))
164
+ ok(doc.indexOf('**没有** `fieldScopes`') !== -1 || doc.indexOf('不存在** `fieldScopes`') !== -1,
165
+ '★ v4: the doc says explicitly that v4 has no fieldScopes')
166
+ ok(!/fieldScopes/.test(v4), '★ v4: the source confirms it (zero occurrences of fieldScopes)')
167
+ }
168
+
169
+ console.log('')
170
+ console.log('=== STATUS FIELD TABLE: ' + passed + ' passed, ' + failed + ' failed ===')
171
+ for (const f of failures) console.error(' - ' + f)
172
+ process.exit(failed === 0 ? 0 : 1)