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.
- package/LICENSE +21 -21
- package/README.en.md +682 -682
- package/README.md +683 -683
- package/docs/AUDIT-CHECKLIST.md +755 -744
- package/docs/COMPAT-AUDIT-ROUND2.md +351 -351
- package/docs/COMPAT-AUDIT-ROUND3.md +154 -154
- package/docs/audit-methodology.md +93 -0
- package/docs/final-paper.md +172 -172
- package/docs/formal-verification.md +613 -588
- package/docs/generate_framework_diagram_v2.py +114 -114
- package/docs/generate_framework_diagram_v3.py +127 -127
- package/docs/math-computation.md +195 -194
- package/docs/release-notes/README.en.md +1 -0
- package/docs/release-notes/README.md +1 -0
- package/docs/release-notes/RELEASE-NOTES-2.0.22.md +112 -112
- package/docs/release-notes/RELEASE-NOTES-2.1.0.md +143 -143
- package/docs/release-notes/RELEASE-NOTES-2.2.0.md +266 -266
- package/docs/release-notes/RELEASE-NOTES-2.2.1.md +43 -43
- package/docs/release-notes/RELEASE-NOTES-2.2.2.md +88 -88
- package/docs/release-notes/RELEASE-NOTES-2.3.0.md +207 -207
- package/docs/release-notes/RELEASE-NOTES-2.3.1.md +134 -134
- package/docs/release-notes/RELEASE-NOTES-2.3.10.md +105 -105
- package/docs/release-notes/RELEASE-NOTES-2.3.11.md +57 -57
- package/docs/release-notes/RELEASE-NOTES-2.3.12.md +80 -80
- package/docs/release-notes/RELEASE-NOTES-2.3.2.md +145 -145
- package/docs/release-notes/RELEASE-NOTES-2.3.3.md +115 -115
- package/docs/release-notes/RELEASE-NOTES-2.3.4.md +69 -69
- package/docs/release-notes/RELEASE-NOTES-2.3.5.md +63 -63
- package/docs/release-notes/RELEASE-NOTES-2.3.6.md +66 -66
- package/docs/release-notes/RELEASE-NOTES-2.3.7.md +59 -59
- package/docs/release-notes/RELEASE-NOTES-2.3.8.md +45 -45
- package/docs/release-notes/RELEASE-NOTES-2.3.9.md +70 -70
- package/docs/release-notes/RELEASE-NOTES-2.8.0.en.md +65 -0
- package/docs/release-notes/RELEASE-NOTES-2.8.0.md +65 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/test-timing.md +158 -157
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +792 -792
- package/package.json +13 -3
- package/prompt-corpus-v2/formal-verify-v2.json +2 -2
- package/prompt-corpus-v2/formal-verify-v2.md +4 -0
- package/prompt-corpus-v3/formal-verify-v3.json +7 -7
- package/prompt-corpus-v3/formal-verify-v3.md +14 -0
- package/prompt-corpus-v5/prompt-corpus-v5.json +61 -61
- package/prompt-corpus-v5/prompt-corpus-v5.md +122 -0
- package/scripts/clean-temp.mjs +234 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-compat.test.mjs +256 -256
- package/tests/audit-math-computation-sensitivity.mjs +1 -1
- package/tests/audit-persona-sensitivity.mjs +323 -323
- package/tests/audit-persona-surface.test.mjs +431 -431
- package/tests/audit-prompt-duplication.mutants.mjs +3 -2
- package/tests/audit-prompt-invariants.mjs +589 -546
- package/tests/audit-registration.mjs +78 -78
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -172
- package/tests/audit-v5-integrity.mjs +795 -795
- package/tests/audit-v5-sensitivity.mjs +433 -432
- package/tests/e2e-v5-round2.test.mjs +2453 -2426
- package/tests/formal-verify-v2.test.mjs +1806 -1800
- package/tests/formal-verify-v3.test.mjs +1757 -1750
- package/tests/formal-verify-v4.test.mjs +2114 -2106
- package/tests/formal-verify-v5.test.mjs +774 -767
- package/tests/math-computation-discovery.mutants.mjs +3 -3
- package/tests/math-computation-shared.test.mjs +122 -5
- package/tests/math-computation-v2.test.mjs +14 -2
- package/tests/math-computation-v3.test.mjs +14 -2
- package/tests/math-computation-v4.test.mjs +739 -739
- package/tests/prompt-v5-integrity.test.mjs +1291 -1291
- package/tests/run-tests.mjs +393 -332
- package/tests/selfdrive-v5.mjs +560 -560
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +4 -0
- package/tests/v2-fix-probes.test.mjs +1034 -1033
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -95
- package/tests/v3-fix-probes.test.mjs +891 -890
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -190
- package/vibe-math-v2/agent.cordis.yml +376 -376
- package/vibe-math-v2/math-computation.js +17 -15
- package/vibe-math-v2/preset.yml +2 -2
- package/vibe-math-v2/v2/347/232/204/351/242/235/345/244/226/350/246/201/346/261/202.txt +21 -21
- package/vibe-math-v2/vibe-math-v2.js +5078 -5011
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +486 -486
- package/vibe-math-v3/agent.cordis.yml +420 -420
- package/vibe-math-v3/math-computation.js +17 -15
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6215 -6149
- package/vibe-math-v3//345/256/236/347/216/260/346/226/271/346/241/210.md +649 -649
- package/vibe-math-v4/agent.cordis.yml +438 -438
- package/vibe-math-v4/math-computation.js +17 -15
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4422
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1015
- package/vibe-math-v5/math-computation.js +17 -15
- package/vibe-math-v5/vibe-math-v5.js +93 -11
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1728
- 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)
|