dsh-vibe-math 2.7.3 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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 +758 -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 +2 -0
  14. package/docs/release-notes/README.md +2 -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-notes/RELEASE-NOTES-2.8.1.en.md +49 -0
  36. package/docs/release-notes/RELEASE-NOTES-2.8.1.md +49 -0
  37. package/docs/release-playbook.md +111 -0
  38. package/docs/slv-playbook.md +117 -0
  39. package/docs/test-timing.md +158 -157
  40. package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
  41. package/installer.js +792 -792
  42. package/package.json +18 -3
  43. package/prompt-corpus-v2/formal-verify-v2.json +2 -2
  44. package/prompt-corpus-v2/formal-verify-v2.md +4 -0
  45. package/prompt-corpus-v3/formal-verify-v3.json +7 -7
  46. package/prompt-corpus-v3/formal-verify-v3.md +14 -0
  47. package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
  48. package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
  49. package/scripts/clean-temp.mjs +234 -0
  50. package/scripts/release-check.mjs +181 -0
  51. package/tests/audit-engine-faces.mjs +298 -0
  52. package/tests/audit-formal-sensitivity.mjs +345 -342
  53. package/tests/audit-installer-compat.test.mjs +256 -256
  54. package/tests/audit-math-computation-sensitivity.mjs +1 -1
  55. package/tests/audit-persona-sensitivity.mjs +323 -323
  56. package/tests/audit-persona-surface.test.mjs +431 -431
  57. package/tests/audit-prompt-duplication.mutants.mjs +3 -2
  58. package/tests/audit-prompt-invariants.mjs +589 -546
  59. package/tests/audit-registration.mjs +78 -78
  60. package/tests/audit-spec-traceability.mjs +193 -193
  61. package/tests/audit-status-report-fields.mjs +172 -172
  62. package/tests/audit-v5-integrity.mjs +795 -795
  63. package/tests/audit-v5-sensitivity.mjs +433 -432
  64. package/tests/e2e-v5-round2.test.mjs +2453 -2426
  65. package/tests/formal-verify-v2.test.mjs +1806 -1800
  66. package/tests/formal-verify-v3.test.mjs +1757 -1750
  67. package/tests/formal-verify-v4.test.mjs +2114 -2106
  68. package/tests/formal-verify-v5.test.mjs +774 -767
  69. package/tests/math-computation-discovery.mutants.mjs +3 -3
  70. package/tests/math-computation-shared.test.mjs +122 -5
  71. package/tests/math-computation-v2.test.mjs +14 -2
  72. package/tests/math-computation-v3.test.mjs +14 -2
  73. package/tests/math-computation-v4.test.mjs +739 -739
  74. package/tests/prompt-v5-integrity.test.mjs +1291 -1291
  75. package/tests/release-check.mutants.mjs +78 -0
  76. package/tests/run-tests.mjs +393 -332
  77. package/tests/selfdrive-v5.mjs +560 -560
  78. package/tests/temp-hygiene.mutants.mjs +80 -0
  79. package/tests/v2-fix-probes.mutants.mjs +4 -0
  80. package/tests/v2-fix-probes.test.mjs +1034 -1033
  81. package/tests/v2v3-interrupt.mutants.mjs +111 -0
  82. package/tests/v3-fix-probes.mutants.mjs +99 -95
  83. package/tests/v3-fix-probes.test.mjs +891 -890
  84. package/tests/v4-final-paper.mutants.mjs +116 -0
  85. package/tests/v4-final-paper.test.mjs +628 -0
  86. package/tests/v5-institute-fixes.mutants.mjs +218 -190
  87. package/vibe-math-v2/agent.cordis.yml +376 -376
  88. package/vibe-math-v2/math-computation.js +17 -15
  89. package/vibe-math-v2/preset.yml +2 -2
  90. package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
  91. package/vibe-math-v2/vibe-math-v2.js +5078 -5011
  92. package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
  93. package/vibe-math-v3/agent.cordis.yml +420 -420
  94. package/vibe-math-v3/math-computation.js +17 -15
  95. package/vibe-math-v3/preset.yml +2 -2
  96. package/vibe-math-v3/vibe-math-v3.js +6215 -6149
  97. package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
  98. package/vibe-math-v4/agent.cordis.yml +438 -438
  99. package/vibe-math-v4/math-computation.js +17 -15
  100. package/vibe-math-v4/preset.yml +2 -2
  101. package/vibe-math-v4/vibe-math-v4.js +4520 -4422
  102. package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
  103. package/vibe-math-v5/math-computation.js +17 -15
  104. package/vibe-math-v5/vibe-math-v5.js +93 -11
  105. package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
  106. 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)