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.
- package/LICENSE +21 -21
- package/README.en.md +682 -681
- package/README.md +683 -682
- package/cordis.patch.yml +112 -16
- package/docs/AUDIT-CHECKLIST.md +755 -518
- package/docs/COMPAT-AUDIT-ROUND2.md +354 -328
- 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 -187
- package/docs/parameter-schema.md +37 -0
- package/docs/release-notes/README.en.md +2 -0
- package/docs/release-notes/README.md +2 -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.7.2.en.md +2 -2
- package/docs/release-notes/RELEASE-NOTES-2.7.2.md +3 -3
- package/docs/release-notes/RELEASE-NOTES-2.7.3.en.md +67 -0
- package/docs/release-notes/RELEASE-NOTES-2.7.3.md +67 -0
- 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-notes/TEMPLATE.md +6 -0
- package/docs/release-playbook.md +103 -0
- package/docs/slv-playbook.md +117 -0
- package/docs/status-report-fields.md +167 -0
- package/docs/test-timing.md +158 -154
- package/docs//346/236/266/346/236/204/345/233/276.md +77 -77
- package/installer.js +793 -783
- package/package.json +260 -203
- package/prompt-corpus-persona/persona-corpus.json +9 -9
- package/prompt-corpus-persona/persona-corpus.md +225 -33
- package/prompt-corpus-v2/formal-verify-v2.json +295 -115
- package/prompt-corpus-v2/formal-verify-v2.md +2290 -282
- package/prompt-corpus-v3/formal-verify-v3.json +112 -112
- package/prompt-corpus-v3/formal-verify-v3.md +227 -143
- package/prompt-corpus-v4/formal-verify-v4.json +18 -18
- package/prompt-corpus-v4/formal-verify-v4.md +49 -44
- package/prompt-corpus-v5/prompt-corpus-v5.json +82 -82
- package/prompt-corpus-v5/prompt-corpus-v5.md +573 -207
- package/scripts/clean-temp.mjs +234 -0
- package/scripts/update-doc-counts.mjs +71 -0
- package/tests/audit-artifact-docs.mjs +72 -0
- package/tests/audit-artifact-docs.mutants.mjs +58 -0
- package/tests/audit-engine-faces.mjs +298 -0
- package/tests/audit-formal-sensitivity.mjs +345 -342
- package/tests/audit-installer-assertions.mutants.mjs +63 -0
- package/tests/audit-installer-compat.test.mjs +256 -208
- package/tests/audit-installer-policy.test.mjs +41 -8
- package/tests/audit-math-computation-contract.mjs +64 -8
- package/tests/audit-math-computation-contract.mutants.mjs +106 -0
- package/tests/audit-math-computation-parity.mjs +164 -3
- package/tests/audit-math-computation-sensitivity.mjs +5 -3
- package/tests/audit-package-membership.mjs +57 -0
- package/tests/audit-package-membership.mutants.mjs +28 -0
- package/tests/audit-participant-set-parity.mjs +160 -0
- package/tests/audit-path-discipline.mjs +83 -0
- package/tests/audit-path-discipline.mutants.mjs +51 -0
- package/tests/audit-persona-sensitivity.mjs +323 -309
- package/tests/audit-persona-surface.mutants.mjs +69 -0
- package/tests/audit-persona-surface.test.mjs +431 -390
- package/tests/audit-prompt-duplication.mjs +143 -0
- package/tests/audit-prompt-duplication.mutants.mjs +51 -0
- package/tests/audit-prompt-invariants.mjs +589 -508
- package/tests/audit-readme-bilingual.test.mjs +152 -0
- package/tests/audit-readme-counts.mjs +95 -0
- package/tests/audit-readme-counts.mutants.mjs +52 -0
- package/tests/audit-registration.mjs +78 -0
- package/tests/audit-spec-traceability.mjs +193 -193
- package/tests/audit-status-report-fields.mjs +172 -0
- package/tests/audit-status-report-fields.mutants.mjs +64 -0
- package/tests/audit-v3-registration-parity.mjs +156 -0
- package/tests/audit-v5-integrity.mjs +795 -508
- package/tests/audit-v5-lean-abstention.mjs +392 -0
- package/tests/audit-v5-lean-abstention.mutants.mjs +106 -0
- package/tests/audit-v5-prompt-duplication.mjs +88 -0
- package/tests/audit-v5-prompt-duplication.mutants.mjs +34 -0
- package/tests/audit-v5-sensitivity.mjs +433 -427
- package/tests/e2e-identity-a6.mutants.mjs +65 -0
- package/tests/e2e-v5-round2.test.mjs +2453 -1981
- package/tests/formal-verify-v2.test.mjs +1778 -1724
- package/tests/formal-verify-v3.mutants.mjs +117 -0
- package/tests/formal-verify-v3.test.mjs +1724 -1671
- package/tests/formal-verify-v4.mutants.mjs +152 -0
- package/tests/formal-verify-v4.test.mjs +2114 -1574
- package/tests/formal-verify-v5.test.mjs +790 -724
- package/tests/helpers/doc-counts-format.mjs +41 -0
- package/tests/helpers/math-computation-fake-seam.mjs +4 -0
- package/tests/math-computation-a1.mutants.mjs +72 -0
- package/tests/math-computation-archive-rerun.mutants.mjs +65 -0
- package/tests/math-computation-discovery.mutants.mjs +76 -0
- package/tests/math-computation-shared.test.mjs +392 -6
- package/tests/math-computation-v2.test.mjs +779 -0
- package/tests/math-computation-v3.test.mjs +771 -0
- package/tests/math-computation-v4.mutants.mjs +133 -0
- package/tests/math-computation-v4.test.mjs +739 -0
- package/tests/prompt-v5-integrity.test.mjs +1291 -1280
- package/tests/run-tests.mjs +393 -202
- package/tests/run-tests.mutants.mjs +70 -0
- package/tests/selfdrive-v5.mjs +560 -508
- package/tests/selfdrive-v5.mutants.mjs +132 -0
- package/tests/temp-hygiene.mutants.mjs +80 -0
- package/tests/v2-fix-probes.mutants.mjs +104 -0
- package/tests/v2-fix-probes.test.mjs +1040 -0
- package/tests/v2-list-agents-and-next-step.mutants.mjs +106 -0
- package/tests/v2v3-interrupt.mutants.mjs +111 -0
- package/tests/v3-fix-probes.mutants.mjs +99 -0
- package/tests/v3-fix-probes.test.mjs +892 -0
- package/tests/v4-final-paper.mutants.mjs +108 -0
- package/tests/v5-institute-fixes.mutants.mjs +218 -0
- package/vibe-math-v2/agent.cordis.yml +378 -322
- package/vibe-math-v2/math-computation.js +144 -53
- package/vibe-math-v2/math-engines.js +12 -12
- 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 +5097 -4755
- package/vibe-math-v2//345/256/236/347/216/260/346/226/271/346/241/210.md +491 -487
- package/vibe-math-v3/agent.cordis.yml +422 -356
- package/vibe-math-v3/math-computation.js +144 -53
- package/vibe-math-v3/math-engines.js +12 -12
- package/vibe-math-v3/preset.yml +2 -2
- package/vibe-math-v3/vibe-math-v3.js +6235 -5873
- 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 -402
- package/vibe-math-v4/math-computation.js +144 -53
- package/vibe-math-v4/math-engines.js +12 -12
- package/vibe-math-v4/preset.yml +2 -2
- package/vibe-math-v4/vibe-math-v4.js +4490 -4269
- package/vibe-math-v4//345/256/236/347/216/260/346/226/271/346/241/210.md +1015 -1013
- package/vibe-math-v5/agent.cordis.yml +46 -12
- package/vibe-math-v5/math-computation.js +144 -53
- package/vibe-math-v5/math-engines.js +12 -12
- package/vibe-math-v5/vibe-math-v5.js +1266 -229
- package/vibe-math-v5//345/256/236/347/216/260/346/226/271/346/241/210.md +1728 -1660
- package/vibe-math-v5//346/236/266/346/236/204/345/233/276.md +693 -693
|
@@ -1,390 +1,431 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* PERSONA SURFACE — prompt/allocation consistency for the four presets.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
],
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PERSONA SURFACE — prompt/allocation consistency for the four presets.
|
|
4
|
+
*
|
|
5
|
+
* DEFECT CLASS (why this guard exists; it belongs to the dual-persona-block parity family of §3)
|
|
6
|
+
* "capability advertised but never granted, or granted but never advertised": the persona block is
|
|
7
|
+
* the main agent view of the tool surface, so a registered-but-unnamed tool is invisible and a
|
|
8
|
+
* named-but-unregistered tool is a trap. Every other suite calls apply(ctx) directly and never
|
|
9
|
+
* parses the YAML, so this class cannot fail anywhere else.
|
|
10
|
+
*
|
|
11
|
+
* Run: node tests/audit-persona-surface.test.mjs
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS EXISTS
|
|
14
|
+
* The persona row of each `agent.cordis.yml` is the prompt the MAIN agent actually
|
|
15
|
+
* receives, and it is the only place the main agent learns which `vibe_*` tools exist
|
|
16
|
+
* and which parameters it may tune. Two failure modes are invisible to every other
|
|
17
|
+
* suite, because those suites call `apply(ctx)` directly and never load the YAML:
|
|
18
|
+
*
|
|
19
|
+
* 1. a tool is registered but never named in the persona → the agent does not know
|
|
20
|
+
* the capability exists (it cannot even guess the exact name);
|
|
21
|
+
* 2. the persona names a tool that is NOT registered → the agent calls a tool
|
|
22
|
+
* that will fail, and (in the worst case) is told to rely on it.
|
|
23
|
+
*
|
|
24
|
+
* The Lean formal-verification feature shipped with exactly this bug in three of four
|
|
25
|
+
* presets: the three `*_lean_*` tools were registered unconditionally but the v2/v3/v4
|
|
26
|
+
* personas never listed them (v5's did), and v4's `vibe_v4_set` parameter list omitted
|
|
27
|
+
* `formalVerify`/`leanCommand`/`leanArgs`/`leanTimeoutMs`, so the coordinator could not
|
|
28
|
+
* discover the switch at all. Nothing in the build noticed. Hence this guard.
|
|
29
|
+
*
|
|
30
|
+
* INVARIANTS (per preset)
|
|
31
|
+
* A. the persona row carries both `prefix` and `text` literal blocks, and both parse
|
|
32
|
+
* into a non-empty body (both keys are required for DSH schema/back-compat);
|
|
33
|
+
* B. the two blocks are IDENTICAL apart from their first line — an old host that uses
|
|
34
|
+
* `text` must not receive a different tool surface from a new host that uses `prefix`;
|
|
35
|
+
* C. mentions ⊆ registered: every `vibe_*` token in the persona is a registered tool
|
|
36
|
+
* (a `name*` mention is allowed when some registered tool carries that prefix);
|
|
37
|
+
* D. every registered tool is either MENTIONED in the persona or consciously ALLOW-LISTED, and the
|
|
38
|
+
* allow-list is itself validated (real tools only, empty by default) — so adding a tool forcing a
|
|
39
|
+
* decision: name it, or add it to the list with a reason. (Earlier this was a snapshot equality,
|
|
40
|
+
* which went red whenever a legitimately newly-named tool emptied the "undocumented" set.)
|
|
41
|
+
* without documenting it (or deleting a tool the persona still advertises) fails
|
|
42
|
+
* loudly and forces the author to make a decision;
|
|
43
|
+
* E. the parameter surface of the Lean feature (the four names) and its semantics
|
|
44
|
+
* (three modes, the fidelity switch, the project/global paths) appear in BOTH blocks.
|
|
45
|
+
*
|
|
46
|
+
* The snapshots below are the reviewed answer to "is this tool intentionally absent from
|
|
47
|
+
* the coordinator's persona?" — virtually all of them are `(member)` / `(academician)` /
|
|
48
|
+
* `(resident)` tools that only subagents call.
|
|
49
|
+
*/
|
|
50
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs'
|
|
51
|
+
import { join, resolve } from 'node:path'
|
|
52
|
+
import { fileURLToPath } from 'node:url'
|
|
53
|
+
|
|
54
|
+
const ROOT = fileURLToPath(new URL('../', import.meta.url))
|
|
55
|
+
/**
|
|
56
|
+
* `PERSONA_ROOT` points the preset lookup at a prepared copy. It exists so that
|
|
57
|
+
* `audit-persona-sensitivity.mjs` can prove this suite is not vacuous: each probe mutates a
|
|
58
|
+
* copy of one persona (or one plugin) and requires this suite to go RED. Without the
|
|
59
|
+
* override the mutation would never be loaded and every probe would be a false green.
|
|
60
|
+
* `docs/formal-verification.md` is always read from the repository root, because the
|
|
61
|
+
* shared contract is not part of a preset copy.
|
|
62
|
+
*/
|
|
63
|
+
const BASE = process.env.PERSONA_ROOT ? resolve(process.env.PERSONA_ROOT) : ROOT
|
|
64
|
+
|
|
65
|
+
let passed = 0
|
|
66
|
+
let failed = 0
|
|
67
|
+
const failures = []
|
|
68
|
+
function ok(cond, msg) {
|
|
69
|
+
if (cond) { passed++; return true }
|
|
70
|
+
failed++
|
|
71
|
+
failures.push(msg)
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
function eq(actual, expected, msg) {
|
|
75
|
+
const a = JSON.stringify(actual)
|
|
76
|
+
const e = JSON.stringify(expected)
|
|
77
|
+
return ok(a === e, `${msg}\n expected: ${e}\n actual: ${a}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Extract a YAML literal block scalar (`prefix: |-`) by indentation. Deliberately does
|
|
82
|
+
* not depend on a YAML library: this file must run inside the published package, where
|
|
83
|
+
* only the DSH host (not this repo) has one. The block's own indentation is stripped so
|
|
84
|
+
* the `prefix` and `text` bodies can be compared line by line.
|
|
85
|
+
*/
|
|
86
|
+
function literalBlocks(yml) {
|
|
87
|
+
const out = {}
|
|
88
|
+
const lines = yml.split(/\r?\n/)
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const m = /^(\s*)([A-Za-z_][\w-]*):\s*\|-?\s*$/.exec(lines[i])
|
|
91
|
+
if (!m) continue
|
|
92
|
+
const ind = m[1].length
|
|
93
|
+
const body = []
|
|
94
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
95
|
+
const L = lines[j]
|
|
96
|
+
if (L.trim() === '') { body.push(''); continue }
|
|
97
|
+
const li = L.length - L.trimStart().length
|
|
98
|
+
if (li <= ind) break
|
|
99
|
+
body.push(L.slice(ind + 2))
|
|
100
|
+
}
|
|
101
|
+
out[m[2]] = body
|
|
102
|
+
}
|
|
103
|
+
return out
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The slice of a plugin that describes and implements its `/vN` slash command: the
|
|
108
|
+
* `commands.register({...})` block, plus — for v2/v3, which dispatch through a named function —
|
|
109
|
+
* the `dispatchVibeCommand` body (that is where their branch list and usage string live).
|
|
110
|
+
* The anchor includes the `{` on purpose: the files also mention `commands.register()` in a
|
|
111
|
+
* prose comment *before* the real call, and anchoring on the call's object literal skips it.
|
|
112
|
+
*/
|
|
113
|
+
function commandRegion(src) {
|
|
114
|
+
const i = src.indexOf('commands.register({')
|
|
115
|
+
if (i < 0) return null
|
|
116
|
+
const end = src.indexOf('\n }))', i)
|
|
117
|
+
let region = src.slice(i, end > i ? end : Math.min(src.length, i + 8000))
|
|
118
|
+
if (!/cmd\s*===\s*'/.test(region)) {
|
|
119
|
+
const d = src.indexOf('dispatchVibeCommand(cmd, args)')
|
|
120
|
+
if (d >= 0) {
|
|
121
|
+
const dEnd = src.indexOf('\n }', d)
|
|
122
|
+
region += '\n' + src.slice(d, dEnd > d ? dEnd : d + 8000)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return region
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Split a `a|b <x|y>|c` advertised list into bare subcommand names. */
|
|
129
|
+
function cmdsFrom(list) {
|
|
130
|
+
const out = []
|
|
131
|
+
const bare = String(list).replace(/\[[^\]]*\]/g, ' ').replace(/<[^>]*>/g, ' ')
|
|
132
|
+
for (const tok of bare.split('|')) {
|
|
133
|
+
const t = tok.trim()
|
|
134
|
+
if (!t || t === '...') continue
|
|
135
|
+
const name = t.split(/\s+/)[0]
|
|
136
|
+
if (/^[a-z][a-z0-9-]*$/.test(name)) out.push(name)
|
|
137
|
+
}
|
|
138
|
+
return out
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const PRESETS = [
|
|
142
|
+
{
|
|
143
|
+
dir: 'vibe-math-v2',
|
|
144
|
+
js: 'vibe-math-v2.js',
|
|
145
|
+
prefix: 'vibe_math_',
|
|
146
|
+
tools: 28,
|
|
147
|
+
// D. registered \ mentioned ⊆ allow-list: EVERY registered tool must be mentioned in the persona,
|
|
148
|
+
// unless its name is listed here as a conscious exception (the allow-list is empty by default, so a
|
|
149
|
+
// newly registered tool cannot slip in unnamed). An entry must be a REAL registered tool, and a
|
|
150
|
+
// preset may also pin names that must be mentioned (never allow-listed).
|
|
151
|
+
allowList: [],
|
|
152
|
+
required: [],
|
|
153
|
+
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
|
|
154
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
155
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
156
|
+
math: { tools: ['math_computation'] },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
dir: 'vibe-math-v3',
|
|
160
|
+
js: 'vibe-math-v3.js',
|
|
161
|
+
prefix: 'vibe_math_',
|
|
162
|
+
tools: 36,
|
|
163
|
+
// `vibe_math_sync_meta` is named by the promotion-contract line, and the write-lock pair is now
|
|
164
|
+
// named too (the lock is described in the persona, so leaving the two tools unmentioned was the
|
|
165
|
+
// last hole). Both stay REQUIRED: they must be mentioned, never allow-listed.
|
|
166
|
+
allowList: [],
|
|
167
|
+
required: ['vibe_math_claim_write', 'vibe_math_release_write'],
|
|
168
|
+
lean: { tools: ['vibe_math_lean_run', 'vibe_math_lean_archive', 'vibe_math_lean_lib', 'vibe_math_lean_read', 'vibe_math_lean_job'], extra: [] },
|
|
169
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
170
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
171
|
+
math: { tools: ['math_computation'] },
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
dir: 'vibe-math-v4',
|
|
175
|
+
js: 'vibe-math-v4.js',
|
|
176
|
+
prefix: 'vibe_v4_',
|
|
177
|
+
tools: 35,
|
|
178
|
+
// Member-facing tools (mail, library cards, task board, write lock) that the v4 persona describes
|
|
179
|
+
// GENERICALLY rather than naming one by one; the coordinator drives residents through
|
|
180
|
+
// vibe_v4_message / _meeting / _add_member. Allow-listed ON PURPOSE, so a NEWLY registered tool
|
|
181
|
+
// still has to be named (or added here consciously).
|
|
182
|
+
allowList: [
|
|
183
|
+
'vibe_v4_send_message', 'vibe_v4_publish_progress', 'vibe_v4_record_proposition',
|
|
184
|
+
'vibe_v4_record_method', 'vibe_v4_record_subproblem', 'vibe_v4_read_progress',
|
|
185
|
+
'vibe_v4_list_residents', 'vibe_v4_propose_task', 'vibe_v4_claim_task',
|
|
186
|
+
'vibe_v4_task_done', 'vibe_v4_list_tasks', 'vibe_v4_report_context',
|
|
187
|
+
'vibe_v4_claim_write', 'vibe_v4_release_write',
|
|
188
|
+
],
|
|
189
|
+
required: [],
|
|
190
|
+
lean: { tools: ['vibe_v4_lean_run', 'vibe_v4_lean_archive', 'vibe_v4_lean_lib', 'vibe_v4_lean_read', 'vibe_v4_lean_job'], extra: ['vibe_v4_formal_report'] },
|
|
191
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
192
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
193
|
+
math: { tools: ['math_computation'] },
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
dir: 'vibe-math-v5',
|
|
197
|
+
js: 'vibe-math-v5.js',
|
|
198
|
+
prefix: 'vibe_v5_',
|
|
199
|
+
tools: 40,
|
|
200
|
+
// Member- and academician-facing tools that the v5 persona describes GENERICALLY; the office (main
|
|
201
|
+
// agent) holds only the institute-level controls plus the hiring authority. The final-paper pair is
|
|
202
|
+
// an OFFICE control and IS named in the persona (vibe_v5_paper / vibe_v5_finalize_paper), so it must
|
|
203
|
+
// NOT be allow-listed here. Allow-listed ON PURPOSE: a NEWLY registered tool still has to be named.
|
|
204
|
+
allowList: [
|
|
205
|
+
'vibe_v5_wait', 'vibe_v5_record_progress', 'vibe_v5_record_proposition',
|
|
206
|
+
'vibe_v5_record_method', 'vibe_v5_record_subproblem', 'vibe_v5_read_library',
|
|
207
|
+
'vibe_v5_propose_verify', 'vibe_v5_verdict', 'vibe_v5_task_create',
|
|
208
|
+
'vibe_v5_task_list', 'vibe_v5_task_get', 'vibe_v5_task_update',
|
|
209
|
+
'vibe_v5_overview', 'vibe_v5_assign', 'vibe_v5_prioritize', 'vibe_v5_nudge',
|
|
210
|
+
],
|
|
211
|
+
required: [],
|
|
212
|
+
lean: { tools: ['vibe_v5_lean_run', 'vibe_v5_lean_archive', 'vibe_v5_lean_lib', 'vibe_v5_lean_read', 'vibe_v5_lean_job'], extra: [] },
|
|
213
|
+
// the shared math-computation tool (docs/math-computation.md) is prefix-less by
|
|
214
|
+
// design (INTERFACE-FREEZE.md §3), so it is pinned explicitly here.
|
|
215
|
+
math: { tools: ['math_computation'] },
|
|
216
|
+
},
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
const LEAN_PARAMS = ['formalVerify', 'leanCommand', 'leanArgs', 'leanTimeoutMs', 'leanAsync', 'leanJobsMaxParallel', 'leanInitiative', 'leanSearchPaths']
|
|
220
|
+
|
|
221
|
+
for (const P of PRESETS) {
|
|
222
|
+
const ymlPath = join(BASE, P.dir, 'agent.cordis.yml')
|
|
223
|
+
const jsPath = join(BASE, P.dir, P.js)
|
|
224
|
+
const yml = readFileSync(ymlPath, 'utf8')
|
|
225
|
+
const src = readFileSync(jsPath, 'utf8')
|
|
226
|
+
|
|
227
|
+
// ---- registered tools -------------------------------------------------
|
|
228
|
+
// Two registration shapes exist now:
|
|
229
|
+
// 1. a literal `registerTool('<name>', …)` (most tools, including the shared math tool in
|
|
230
|
+
// v2/v3, which register it by its frozen prefix-less name);
|
|
231
|
+
// 2. the shared math tool in v4/v5, where the plugin only wires a host callback and the module
|
|
232
|
+
// calls `host.register(MATH_TOOL_NAME, …)`. The module's name is FROZEN
|
|
233
|
+
// (MATH_TOOL_NAME = 'math_computation', INTERFACE-FREEZE.md §3), so a source that wires
|
|
234
|
+
// `registerMathComputation(` contributes exactly that one tool. Without this the shared tool
|
|
235
|
+
// would be invisible to this guard on v4/v5 - a silent coverage hole.
|
|
236
|
+
const registered = new Set()
|
|
237
|
+
for (const m of src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)) registered.add(m[1])
|
|
238
|
+
if (/registerMathComputation\s*\(/.test(src)) registered.add('math_computation')
|
|
239
|
+
eq(registered.size, P.tools, `${P.dir}: registered tool count changed (update this snapshot deliberately)`)
|
|
240
|
+
for (const tool of (P.math ? P.math.tools : [])) ok(registered.has(tool), `${P.dir}: the shared math tool ${tool} is not registered`)
|
|
241
|
+
|
|
242
|
+
// ---- A. both literal blocks exist and are non-empty -------------------
|
|
243
|
+
const blocks = literalBlocks(yml)
|
|
244
|
+
const hasPersona = /^\s*-\s*id:\s*persona\s*$/m.test(yml)
|
|
245
|
+
ok(hasPersona, `${P.dir}: no persona row in agent.cordis.yml`)
|
|
246
|
+
for (const key of ['prefix', 'text']) {
|
|
247
|
+
ok(Array.isArray(blocks[key]) && blocks[key].length > 0, `${P.dir}: persona.config.${key} is missing or not a literal block`)
|
|
248
|
+
}
|
|
249
|
+
if (!blocks.prefix || !blocks.text) { ok(false, `${P.dir}: cannot continue without both blocks`); continue }
|
|
250
|
+
|
|
251
|
+
// ---- B. the two blocks must not drift --------------------------------
|
|
252
|
+
const trimTail = (a) => { const b = a.slice(); while (b.length && b[b.length - 1] === '') b.pop(); return b }
|
|
253
|
+
const p = trimTail(blocks.prefix)
|
|
254
|
+
const t = trimTail(blocks.text)
|
|
255
|
+
eq(p.length, t.length, `${P.dir}: prefix and text have different line counts`)
|
|
256
|
+
const drift = []
|
|
257
|
+
for (let i = 1; i < Math.max(p.length, t.length); i++) if (p[i] !== t[i]) drift.push(i)
|
|
258
|
+
ok(drift.length === 0, `${P.dir}: prefix and text differ on line(s) ${drift.slice(0, 5).join(', ')} (only line 0 may differ)`)
|
|
259
|
+
ok(/\{\{model\}\}/.test(p[0]) || /\{\{model\}\}/.test(t[0]), `${P.dir}: line 0 does not interpolate {{model}}`)
|
|
260
|
+
|
|
261
|
+
// ---- C/D. mention <-> registry ----------------------------------------
|
|
262
|
+
for (const key of ['prefix', 'text']) {
|
|
263
|
+
const body = blocks[key].join('\n')
|
|
264
|
+
const mentioned = new Set()
|
|
265
|
+
const wildcards = new Set()
|
|
266
|
+
// Uppercase is part of the token class on purpose: a drifted name such as
|
|
267
|
+
// `vibe_v4_formal_reportX` must be captured whole and judged unregistered, instead of
|
|
268
|
+
// being read as a mention of the registered `vibe_v4_formal_report`.
|
|
269
|
+
// Names that do NOT carry this preset's prefix (the shared `math_computation`) are added to the
|
|
270
|
+
// alternation from the registry, so "every registered tool must be mentioned" stays true for
|
|
271
|
+
// them too instead of forcing them into the `undocumented` snapshot.
|
|
272
|
+
const prefixRoot = P.prefix.replace(/_$/, '')
|
|
273
|
+
const prefixless = [...registered].filter((n) => !new RegExp('^' + prefixRoot + '_').test(n))
|
|
274
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
275
|
+
const re = new RegExp('\\b' + prefixRoot + '_[A-Za-z0-9_]+' + (prefixless.length ? '|\\b' + prefixless.map(escapeRe).join('\\b|\\b') + '\\b' : ''), 'g')
|
|
276
|
+
for (const m of body.matchAll(re)) {
|
|
277
|
+
if (body[m.index + m[0].length] === '*') { wildcards.add(m[0]); continue }
|
|
278
|
+
mentioned.add(m[0])
|
|
279
|
+
}
|
|
280
|
+
const stale = [...mentioned].filter((n) => !registered.has(n)).sort()
|
|
281
|
+
eq(stale, [], `${P.dir} [${key}]: persona advertises tool(s) that are not registered`)
|
|
282
|
+
for (const w of wildcards) {
|
|
283
|
+
ok([...registered].some((n) => n.startsWith(w)), `${P.dir} [${key}]: wildcard mention ${w}* matches no registered tool`)
|
|
284
|
+
}
|
|
285
|
+
const unmentioned = [...registered].filter((n) => !mentioned.has(n)).sort()
|
|
286
|
+
// ALLOW-LIST design (replaces the old "equals a reviewed snapshot" equality): a registered tool may
|
|
287
|
+
// be absent from the persona ONLY if its name is consciously allow-listed for this preset. The
|
|
288
|
+
// allow-list is empty by default (v2/v3), so a newly registered tool turns this red until it is
|
|
289
|
+
// either mentioned in the persona or deliberately added to the list with a reason.
|
|
290
|
+
const allowed = new Set(P.allowList || [])
|
|
291
|
+
const illegal = unmentioned.filter((n) => !allowed.has(n))
|
|
292
|
+
ok(illegal.length === 0, `${P.dir} [${key}]: every registered tool is mentioned in the persona, or explicitly allow-listed`,
|
|
293
|
+
'unmentioned and not allow-listed: ' + JSON.stringify(illegal))
|
|
294
|
+
const staleAllowed = [...allowed].filter((n) => !registered.has(n)).sort()
|
|
295
|
+
ok(staleAllowed.length === 0, `${P.dir} [${key}]: every allow-list entry is a real registered tool`,
|
|
296
|
+
'allow-listed but not registered: ' + JSON.stringify(staleAllowed))
|
|
297
|
+
for (const must of (P.required || [])) {
|
|
298
|
+
ok(mentioned.has(must) && !allowed.has(must), `${P.dir} [${key}]: ${must} must be MENTIONED in the persona (never allow-listed)`)
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ---- F. the slash-command surface -------------------------------------
|
|
303
|
+
// The persona enumerates the `/vN` subcommands; the plugin advertises them twice more
|
|
304
|
+
// (the command `hint` the user sees while typing, and the `usage` string returned by the
|
|
305
|
+
// unknown-subcommand path) and implements them in a third place. A `/v4` usage string once
|
|
306
|
+
// advertised a `message` subcommand that no branch implemented; the v5 persona and plan
|
|
307
|
+
// omitted `add|remove` although the handler implements them. All three surfaces must agree.
|
|
308
|
+
const region = commandRegion(src)
|
|
309
|
+
if (!ok(region !== null, `${P.dir}: no commands.register(...) block found`)) continue
|
|
310
|
+
const hinted = cmdsFrom(region.match(/hint:\s*'\[(.*)\]'/)?.[1] || '')
|
|
311
|
+
// The LAST `usage:` is the unknown-subcommand advertisement; earlier ones are inline
|
|
312
|
+
// per-branch usage hints (e.g. `/v4 message <to|all> <content>`).
|
|
313
|
+
const usageMatches = [...region.matchAll(/usage:\s*'([^']*)'/g)]
|
|
314
|
+
const used = cmdsFrom(usageMatches.length ? usageMatches[usageMatches.length - 1][1] : '')
|
|
315
|
+
const implemented = [...new Set([...region.matchAll(/cmd\s*===\s*'([a-z0-9_-]+)'/g)].map((m) => m[1]))].sort()
|
|
316
|
+
const hintOpen = /\.\.\./.test(region.match(/hint:\s*'\[(.*)\]'/)?.[1] || '')
|
|
317
|
+
ok(hinted.length > 0 && implemented.length > 0, `${P.dir}: could not extract the slash-command surface (hint ${hinted.length}, branches ${implemented.length})`)
|
|
318
|
+
eq(hinted.filter((c) => !implemented.includes(c)).sort(), [], `${P.dir}: the slash hint advertises subcommand(s) that no branch implements`)
|
|
319
|
+
if (!hintOpen) eq(implemented.filter((c) => !hinted.includes(c)).sort(), [], `${P.dir}: implemented slash subcommand(s) are missing from the hint`)
|
|
320
|
+
eq(used.sort(), implemented, `${P.dir}: the usage string and the implemented slash subcommands disagree`)
|
|
321
|
+
const personaEnum = /slash command mirrors[^(]*\(([\s\S]*?)\)/.exec(blocks.prefix.join('\n') + '\n' + blocks.text.join('\n'))
|
|
322
|
+
// An ellipsis must NOT switch this check off (that is how a dropped `paper` entry survived here),
|
|
323
|
+
// and `...` inside an argument placeholder (`set <k=v>...`) is not an abbreviation at all. So the
|
|
324
|
+
// persona is compared against the BRANCHES (the ground truth), and every implemented subcommand
|
|
325
|
+
// must be named EXPLICITLY. Only a HINT that is itself abbreviated makes the reverse direction
|
|
326
|
+
// (persona ⊆ hint) unprovable, so only that direction is relaxed then.
|
|
327
|
+
if (!personaEnum) {
|
|
328
|
+
ok(false, `${P.dir}: the persona never enumerates its /vN subcommands (list them so a human can see every one)`)
|
|
329
|
+
} else {
|
|
330
|
+
const personaCmds = cmdsFrom(personaEnum[1].replace(/^\s*\/[a-z0-9]+\s+/, ''))
|
|
331
|
+
ok(personaCmds.length > 0, `${P.dir}: no slash subcommand could be parsed from the persona's /vN list`)
|
|
332
|
+
eq(personaCmds.filter((c) => !implemented.includes(c)).sort(), [], `${P.dir}: the persona enumerates slash subcommand(s) the handler does not implement`)
|
|
333
|
+
eq(implemented.filter((c) => !personaCmds.includes(c)).sort(), [], `${P.dir}: the persona's /vN list omits subcommand(s) the handler implements — list them explicitly, do not hide behind "..."`)
|
|
334
|
+
if (!hintOpen) eq(personaCmds.slice().sort(), implemented.slice().sort(), `${P.dir}: the persona's /vN list and the command hint disagree`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- E. the Lean feature's prompt surface ----------------------------
|
|
338
|
+
const names = (s) => new RegExp('\\b' + s + '\\b')
|
|
339
|
+
for (const key of ['prefix', 'text']) {
|
|
340
|
+
const body = blocks[key].join('\n')
|
|
341
|
+
for (const tool of P.lean.tools) ok(names(tool).test(body), `${P.dir} [${key}]: Lean tool ${tool} is never named`)
|
|
342
|
+
for (const tool of P.lean.extra) ok(names(tool).test(body), `${P.dir} [${key}]: ${tool} is never named`)
|
|
343
|
+
for (const prm of LEAN_PARAMS) ok(names(prm).test(body), `${P.dir} [${key}]: Lean parameter ${prm} is never named`)
|
|
344
|
+
for (const mode of ['off', 'encourage', 'require']) ok(body.includes(`'${mode}'`), `${P.dir} [${key}]: Lean mode ${mode} is never spelled out`)
|
|
345
|
+
ok(/FIDELITY|忠实性/.test(body), `${P.dir} [${key}]: the fidelity switch of a passing Lean run is not stated`)
|
|
346
|
+
ok(body.includes('Formal/Lib') && body.includes('Formal/Proved'), `${P.dir} [${key}]: the cross-project Lean library paths are not stated`)
|
|
347
|
+
ok(body.includes('Verified/Lean'), `${P.dir} [${key}]: the archived-proof path Verified/Lean is not stated`)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ---- the shared contract must stay in step with the personas -----------
|
|
352
|
+
try {
|
|
353
|
+
const contract = readFileSync(join(ROOT, 'docs', 'formal-verification.md'), 'utf8')
|
|
354
|
+
for (const prm of LEAN_PARAMS) ok(contract.includes(prm), `docs/formal-verification.md: parameter ${prm} is not documented`)
|
|
355
|
+
for (const sec of ['off', 'encourage', 'require']) ok(new RegExp('`' + sec + '`').test(contract), `docs/formal-verification.md: mode ${sec} is not documented`)
|
|
356
|
+
ok(contract.includes('Verified/Lean'), 'docs/formal-verification.md: the archived-proof path is not documented')
|
|
357
|
+
ok(contract.includes('Formal/Lib') && contract.includes('Formal/Proved'), 'docs/formal-verification.md: the global library paths are not documented')
|
|
358
|
+
} catch (e) {
|
|
359
|
+
ok(false, `docs/formal-verification.md: cannot read the shared contract (${e.message})`)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---- G. the human-reviewable persona corpus (shipped) -------------------
|
|
363
|
+
// Assertions are not enough: the reviewer must be able to READ what the main agent receives
|
|
364
|
+
// without digging through YAML (see AUDIT-CHECKLIST §2.4). This writes the exact persona text
|
|
365
|
+
// of all four presets to `prompt-corpus-persona/` and is skipped when running against a copy
|
|
366
|
+
// (PERSONA_ROOT), so a mutation probe can never overwrite the shipped corpus.
|
|
367
|
+
{
|
|
368
|
+
const rows = PRESETS.map((P) => {
|
|
369
|
+
const yml = readFileSync(join(BASE, P.dir, 'agent.cordis.yml'), 'utf8')
|
|
370
|
+
const b = literalBlocks(yml)
|
|
371
|
+
const src = readFileSync(join(BASE, P.dir, P.js), 'utf8')
|
|
372
|
+
const registered = [...new Set([...src.matchAll(/registerTool\(\s*'([A-Za-z0-9_]+)'/g)].map((m) => m[1]))].sort()
|
|
373
|
+
const region = commandRegion(src) || ''
|
|
374
|
+
return {
|
|
375
|
+
preset: P.dir,
|
|
376
|
+
tools: registered.length,
|
|
377
|
+
prefix: (b.prefix || []).join('\n'),
|
|
378
|
+
text: (b.text || []).join('\n'),
|
|
379
|
+
slashHint: (region.match(/hint:\s*'\[(.*)\]'/) || [])[1] || '',
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
eq(rows.length, PRESETS.length, 'persona corpus: not every preset was captured')
|
|
383
|
+
ok(rows.every((r) => r.prefix.length > 0 && r.text.length > 0), 'persona corpus: a preset has an empty persona block')
|
|
384
|
+
const corpusDir = join(ROOT, 'prompt-corpus-persona')
|
|
385
|
+
if (!process.env.PERSONA_ROOT) {
|
|
386
|
+
const md = ['# 四个预设的 persona 原文(主代理实际收到的提示词)', '',
|
|
387
|
+
'> 本文件由 `audit-persona-surface.test.mjs` 生成,供人工复核:四个预设的主代理分别被告知了',
|
|
388
|
+
'> 哪些工具、哪些参数、哪些斜杠子命令。`prefix` 与 `text` 两个块**只允许第 0 行不同**。', '']
|
|
389
|
+
for (const r of rows) {
|
|
390
|
+
md.push(`## ${r.preset}`, '', `- 注册工具数:**${r.tools}**`, `- 斜杠命令 hint:\`${r.slashHint}\``, '',
|
|
391
|
+
'### config.prefix', '', '```text', r.prefix, '```', '', '### config.text', '', '```text', r.text, '```', '')
|
|
392
|
+
}
|
|
393
|
+
mkdirSync(corpusDir, { recursive: true })
|
|
394
|
+
// CONCURRENCY FIX: `audit-persona-sensitivity` spawns this suite with PERSONA_ROOT, and those child
|
|
395
|
+
// runs READ these two shipped files while a parallel `run-tests` job may be WRITING them. A plain
|
|
396
|
+
// writeFileSync let a reader observe a half-written JSON (the flake: sensitivity red only under
|
|
397
|
+
// concurrency 4, SETUP-FAIL in its control run). Write a unique temp file in the same directory and
|
|
398
|
+
// rename it into place, so a reader sees either the old or the new file, never a partial one.
|
|
399
|
+
const writeAtomic = (target, data) => {
|
|
400
|
+
const tmp = target + '.tmp-' + process.pid + '-' + Math.random().toString(36).slice(2)
|
|
401
|
+
writeFileSync(tmp, data, 'utf8')
|
|
402
|
+
renameSync(tmp, target)
|
|
403
|
+
}
|
|
404
|
+
writeAtomic(join(corpusDir, 'persona-corpus.md'), md.join('\n'))
|
|
405
|
+
writeAtomic(join(corpusDir, 'persona-corpus.json'), JSON.stringify({ presets: rows }, null, 2) + '\n')
|
|
406
|
+
}
|
|
407
|
+
// Round-trip check: the shipped corpus must name all four presets and carry line 0 of each block.
|
|
408
|
+
// Skipped under PERSONA_ROOT: there `rows` describe the mutated copy while the corpus on disk
|
|
409
|
+
// describes the real presets, so comparing them would be meaningless.
|
|
410
|
+
if (process.env.PERSONA_ROOT) {
|
|
411
|
+
ok(existsSync(join(corpusDir, 'persona-corpus.md')) && existsSync(join(corpusDir, 'persona-corpus.json')),
|
|
412
|
+
'persona corpus: the shipped corpus files are missing from the repository')
|
|
413
|
+
} else {
|
|
414
|
+
try {
|
|
415
|
+
const back = readFileSync(join(corpusDir, 'persona-corpus.md'), 'utf8')
|
|
416
|
+
for (const r of rows) {
|
|
417
|
+
ok(back.includes(`## ${r.preset}`), `persona corpus: ${r.preset} is missing from the shipped corpus`)
|
|
418
|
+
ok(back.includes(r.prefix.split('\n')[0]), `persona corpus: ${r.preset} prefix line 0 is missing from the shipped corpus`)
|
|
419
|
+
}
|
|
420
|
+
const backJson = JSON.parse(readFileSync(join(corpusDir, 'persona-corpus.json'), 'utf8'))
|
|
421
|
+
eq(backJson.presets.length, PRESETS.length, 'persona corpus (json): not every preset was captured')
|
|
422
|
+
} catch (e) {
|
|
423
|
+
ok(false, `persona corpus: cannot read back the shipped corpus (${e.message})`)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
console.log('')
|
|
429
|
+
for (const f of failures) console.log(' FAIL ' + f)
|
|
430
|
+
console.log(`\n=== PERSONA SURFACE RESULT: ${passed} passed, ${failed} failed ===`)
|
|
431
|
+
process.exit(failed === 0 ? 0 : 1)
|