solopup-memory 0.21.0-beta.3 → 0.22.0-beta.3
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/CHANGELOG.md +30 -0
- package/COMPATIBILITY.md +8 -5
- package/DETECTION.md +139 -0
- package/DREAMING-DESIGN.md +167 -0
- package/README.en.md +33 -6
- package/README.md +38 -7
- package/RELEASE.md +26 -0
- package/SECURITY.md +7 -0
- package/context-history.mjs +20 -3
- package/detect.mjs +441 -0
- package/eval/bench-detect.mjs +107 -0
- package/eval/semantic/cases.json +387 -0
- package/eval/semantic-suite.mjs +156 -0
- package/index.mjs +82 -10
- package/managed-policy.mjs +3 -1
- package/package.json +90 -69
- package/scripts/demo-source-recall.mjs +123 -0
- package/scripts/detect.mjs +69 -0
- package/scripts/dream.mjs +205 -0
- package/scripts/probe-history.mjs +92 -0
- package/scripts/probe-load.mjs +1 -1
- package/scripts/probe-package.mjs +6 -2
- package/scripts/security-check.mjs +173 -0
- package/scripts/soak.mjs +219 -0
- package/scripts/telemetry.mjs +187 -0
- package/scripts/verify.mjs +3 -1
- package/{test-adversarial.mjs → test/test-adversarial.mjs} +5 -5
- package/{test-belief-revision.mjs → test/test-belief-revision.mjs} +7 -7
- package/{test-candidates.mjs → test/test-candidates.mjs} +4 -4
- package/{test-capture-diagnostics.mjs → test/test-capture-diagnostics.mjs} +6 -6
- package/{test-capture-field-lineage.mjs → test/test-capture-field-lineage.mjs} +4 -4
- package/{test-capture-inputs.mjs → test/test-capture-inputs.mjs} +8 -8
- package/{test-capture-jobs.mjs → test/test-capture-jobs.mjs} +5 -5
- package/{test-capture-projections.mjs → test/test-capture-projections.mjs} +12 -12
- package/{test-capture-revision.mjs → test/test-capture-revision.mjs} +6 -6
- package/{test-capture-submission.mjs → test/test-capture-submission.mjs} +3 -3
- package/{test-capture-validation-flow.mjs → test/test-capture-validation-flow.mjs} +5 -5
- package/{test-capture-validation-job.mjs → test/test-capture-validation-job.mjs} +5 -5
- package/{test-capture-validation-lineage.mjs → test/test-capture-validation-lineage.mjs} +2 -2
- package/{test-capture-validation.mjs → test/test-capture-validation.mjs} +2 -2
- package/{test-capture.mjs → test/test-capture.mjs} +20 -20
- package/{test-compaction-compat.mjs → test/test-compaction-compat.mjs} +5 -5
- package/{test-compat.mjs → test/test-compat.mjs} +3 -3
- package/{test-concurrency.mjs → test/test-concurrency.mjs} +3 -3
- package/{test-conflict.mjs → test/test-conflict.mjs} +2 -2
- package/{test-context-history.mjs → test/test-context-history.mjs} +1 -1
- package/{test-control-schema.mjs → test/test-control-schema.mjs} +1 -1
- package/test/test-detect.mjs +125 -0
- package/test/test-dream-plan.mjs +72 -0
- package/test/test-dsh-registration.mjs +111 -0
- package/{test-episodes.mjs → test/test-episodes.mjs} +5 -5
- package/{test-explicit-memory.mjs → test/test-explicit-memory.mjs} +4 -4
- package/{test-field-lineage.mjs → test/test-field-lineage.mjs} +4 -4
- package/{test-history.mjs → test/test-history.mjs} +4 -4
- package/{test-inline-summary.mjs → test/test-inline-summary.mjs} +5 -5
- package/{test-legacy-source-identity.mjs → test/test-legacy-source-identity.mjs} +2 -2
- package/{test-links.mjs → test/test-links.mjs} +4 -4
- package/{test-managed-access.mjs → test/test-managed-access.mjs} +29 -5
- package/{test-managed-activation.mjs → test/test-managed-activation.mjs} +5 -5
- package/{test-meaning.mjs → test/test-meaning.mjs} +6 -6
- package/{test-native-service-lifecycle.mjs → test/test-native-service-lifecycle.mjs} +1 -1
- package/{test-native-write-validation.mjs → test/test-native-write-validation.mjs} +4 -4
- package/{test-packaging.mjs → test/test-packaging.mjs} +32 -5
- package/{test-paired-analysis.mjs → test/test-paired-analysis.mjs} +1 -1
- package/{test-patch.mjs → test/test-patch.mjs} +11 -11
- package/{test-plugin-contract.mjs → test/test-plugin-contract.mjs} +4 -4
- package/{test-privacy.mjs → test/test-privacy.mjs} +3 -3
- package/{test-project-stores.mjs → test/test-project-stores.mjs} +5 -5
- package/{test-publication.mjs → test/test-publication.mjs} +4 -4
- package/{test-restore.mjs → test/test-restore.mjs} +11 -11
- package/{test-retrieval.mjs → test/test-retrieval.mjs} +2 -2
- package/{test-revision-admission.mjs → test/test-revision-admission.mjs} +3 -3
- package/{test-rule-admission.mjs → test/test-rule-admission.mjs} +8 -8
- package/{test-runtime-startup.mjs → test/test-runtime-startup.mjs} +2 -2
- package/{test-schema.mjs → test/test-schema.mjs} +2 -2
- package/{test-security.mjs → test/test-security.mjs} +6 -6
- package/test/test-semantic-suite.mjs +20 -0
- package/{test-service-readiness.mjs → test/test-service-readiness.mjs} +1 -1
- package/{test-smell.mjs → test/test-smell.mjs} +4 -4
- package/{test-snapshots.mjs → test/test-snapshots.mjs} +3 -3
- package/{test-source-completeness.mjs → test/test-source-completeness.mjs} +6 -6
- package/{test-source-key.mjs → test/test-source-key.mjs} +2 -2
- package/{test-source-retirement.mjs → test/test-source-retirement.mjs} +4 -4
- package/{test-status-both.mjs → test/test-status-both.mjs} +12 -4
- package/{test-supersede.mjs → test/test-supersede.mjs} +3 -3
- package/test/test-telemetry.mjs +60 -0
- package/{test-text-admission.mjs → test/test-text-admission.mjs} +6 -6
- package/{test-tool-inputs.mjs → test/test-tool-inputs.mjs} +1 -1
- package/{test-tool-observations.mjs → test/test-tool-observations.mjs} +6 -6
- package/{test-tool-write-guard.mjs → test/test-tool-write-guard.mjs} +3 -3
- package/{test-tool-write-lifecycle.mjs → test/test-tool-write-lifecycle.mjs} +3 -3
- package/{test-usability.mjs → test/test-usability.mjs} +7 -7
- package/{test-validation-source-refs.mjs → test/test-validation-source-refs.mjs} +2 -2
- package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs} +1 -1
- package/{test-withdrawal-archives.mjs → test/test-withdrawal-archives.mjs} +6 -6
- package/{test-withdrawal.mjs → test/test-withdrawal.mjs} +2 -2
- package/{test.mjs → test/test.mjs} +4 -4
- package/version.mjs +1 -1
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "identity-claim",
|
|
4
|
+
"tags": [
|
|
5
|
+
"write",
|
|
6
|
+
"meaning"
|
|
7
|
+
],
|
|
8
|
+
"title": "用户自述长期身份:存成 claim,不是已核实事实",
|
|
9
|
+
"messages": [
|
|
10
|
+
{
|
|
11
|
+
"id": "m1",
|
|
12
|
+
"role": "user",
|
|
13
|
+
"text": "我是做电视剧剧本的,长期负责剧本结构。"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"gold": {
|
|
17
|
+
"type": "user",
|
|
18
|
+
"scope": "global",
|
|
19
|
+
"basis": "explicit",
|
|
20
|
+
"summary": "长期做电视剧剧本,负责剧本结构",
|
|
21
|
+
"meaning": {
|
|
22
|
+
"kind": "claim"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"expect": {
|
|
26
|
+
"status": "eligible",
|
|
27
|
+
"meaningMatches": true,
|
|
28
|
+
"search": [
|
|
29
|
+
{
|
|
30
|
+
"query": "电视剧剧本",
|
|
31
|
+
"hits": 1
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "conditional-preference",
|
|
38
|
+
"tags": [
|
|
39
|
+
"write",
|
|
40
|
+
"conditions"
|
|
41
|
+
],
|
|
42
|
+
"title": "有听众条件的偏好:条件必须落库,并在检索时过滤",
|
|
43
|
+
"messages": [
|
|
44
|
+
{
|
|
45
|
+
"id": "m1",
|
|
46
|
+
"role": "user",
|
|
47
|
+
"text": "给儿童讲解的时候先举例,其他场合不用这样。"
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"gold": {
|
|
51
|
+
"type": "user",
|
|
52
|
+
"scope": "global",
|
|
53
|
+
"basis": "explicit",
|
|
54
|
+
"summary": "给儿童讲解时先举例",
|
|
55
|
+
"meaning": {
|
|
56
|
+
"kind": "preference",
|
|
57
|
+
"conditions": [
|
|
58
|
+
{
|
|
59
|
+
"dimension": "audience",
|
|
60
|
+
"value": "children"
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"expect": {
|
|
66
|
+
"status": "eligible",
|
|
67
|
+
"meaningMatches": true,
|
|
68
|
+
"search": [
|
|
69
|
+
{
|
|
70
|
+
"query": "举例",
|
|
71
|
+
"context": {
|
|
72
|
+
"audience": "children"
|
|
73
|
+
},
|
|
74
|
+
"hits": 1
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"query": "举例",
|
|
78
|
+
"context": {
|
|
79
|
+
"audience": "adult"
|
|
80
|
+
},
|
|
81
|
+
"hits": 0
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"id": "project-decision-detail",
|
|
88
|
+
"tags": [
|
|
89
|
+
"write",
|
|
90
|
+
"project"
|
|
91
|
+
],
|
|
92
|
+
"title": "项目决定 + 正文细节:索引摘要与主题正文都要在",
|
|
93
|
+
"messages": [
|
|
94
|
+
{
|
|
95
|
+
"id": "m1",
|
|
96
|
+
"role": "user",
|
|
97
|
+
"text": "这个项目就按 26 集、每集 20 分钟来做。"
|
|
98
|
+
}
|
|
99
|
+
],
|
|
100
|
+
"gold": {
|
|
101
|
+
"type": "project",
|
|
102
|
+
"scope": "project",
|
|
103
|
+
"basis": "explicit",
|
|
104
|
+
"summary": "项目固定 26 集、每集 20 分钟",
|
|
105
|
+
"detail": "硬格式:每集 20 分钟;不是 12 集 × 45 分钟。",
|
|
106
|
+
"meaning": {
|
|
107
|
+
"kind": "decision"
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
"expect": {
|
|
111
|
+
"status": "eligible",
|
|
112
|
+
"meaningMatches": true,
|
|
113
|
+
"detailContains": [
|
|
114
|
+
"每集 20 分钟"
|
|
115
|
+
],
|
|
116
|
+
"search": [
|
|
117
|
+
{
|
|
118
|
+
"query": "26 集",
|
|
119
|
+
"hits": 1
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "inferred-stays-pending",
|
|
126
|
+
"tags": [
|
|
127
|
+
"gate",
|
|
128
|
+
"attribution"
|
|
129
|
+
],
|
|
130
|
+
"title": "推断出来的偏好只能进待审,不能直接发布",
|
|
131
|
+
"messages": [
|
|
132
|
+
{
|
|
133
|
+
"id": "m1",
|
|
134
|
+
"role": "user",
|
|
135
|
+
"text": "我最近晚上写东西比较多。"
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
"gold": {
|
|
139
|
+
"type": "user",
|
|
140
|
+
"scope": "global",
|
|
141
|
+
"basis": "inferred",
|
|
142
|
+
"summary": "可能偏好夜间写作",
|
|
143
|
+
"meaning": {
|
|
144
|
+
"kind": "belief",
|
|
145
|
+
"alternatives": [
|
|
146
|
+
"只是最近任务较轻"
|
|
147
|
+
]
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"expect": {
|
|
151
|
+
"status": "candidate",
|
|
152
|
+
"reason": "inference-needs-review",
|
|
153
|
+
"published": false
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"id": "quote-must-be-verbatim",
|
|
158
|
+
"tags": [
|
|
159
|
+
"gate",
|
|
160
|
+
"provenance"
|
|
161
|
+
],
|
|
162
|
+
"title": "引文不是用户原话逐字子串:整条拒绝",
|
|
163
|
+
"messages": [
|
|
164
|
+
{
|
|
165
|
+
"id": "m1",
|
|
166
|
+
"role": "user",
|
|
167
|
+
"text": "我负责剪辑和声音。"
|
|
168
|
+
}
|
|
169
|
+
],
|
|
170
|
+
"gold": {
|
|
171
|
+
"type": "user",
|
|
172
|
+
"scope": "global",
|
|
173
|
+
"basis": "explicit",
|
|
174
|
+
"summary": "负责剪辑与声音",
|
|
175
|
+
"meaning": {
|
|
176
|
+
"kind": "claim"
|
|
177
|
+
},
|
|
178
|
+
"evidence": [
|
|
179
|
+
{
|
|
180
|
+
"messageId": "m1",
|
|
181
|
+
"quote": "我负责剪辑、声音和配乐。"
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
"expect": {
|
|
186
|
+
"status": "rejected",
|
|
187
|
+
"reason": "invalid-user-evidence",
|
|
188
|
+
"published": false
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
"id": "project-decision-cannot-be-global",
|
|
193
|
+
"tags": [
|
|
194
|
+
"gate",
|
|
195
|
+
"scope"
|
|
196
|
+
],
|
|
197
|
+
"title": "项目约定写到 global:拒绝(scope 闸)",
|
|
198
|
+
"messages": [
|
|
199
|
+
{
|
|
200
|
+
"id": "m1",
|
|
201
|
+
"role": "user",
|
|
202
|
+
"text": "这个项目以后都用双栏格式。"
|
|
203
|
+
}
|
|
204
|
+
],
|
|
205
|
+
"gold": {
|
|
206
|
+
"type": "project",
|
|
207
|
+
"scope": "global",
|
|
208
|
+
"basis": "explicit",
|
|
209
|
+
"summary": "项目使用双栏格式",
|
|
210
|
+
"meaning": {
|
|
211
|
+
"kind": "preference"
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
"expect": {
|
|
215
|
+
"status": "rejected",
|
|
216
|
+
"reason": "project-global",
|
|
217
|
+
"published": false
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "missing-basis",
|
|
222
|
+
"tags": [
|
|
223
|
+
"gate",
|
|
224
|
+
"attribution"
|
|
225
|
+
],
|
|
226
|
+
"title": "没有 basis 的候选:拒绝",
|
|
227
|
+
"messages": [
|
|
228
|
+
{
|
|
229
|
+
"id": "m1",
|
|
230
|
+
"role": "user",
|
|
231
|
+
"text": "我习惯先看完所有素材再动笔。"
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"gold": {
|
|
235
|
+
"type": "user",
|
|
236
|
+
"scope": "global",
|
|
237
|
+
"summary": "先看完素材再动笔",
|
|
238
|
+
"meaning": {
|
|
239
|
+
"kind": "preference"
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
"expect": {
|
|
243
|
+
"status": "rejected",
|
|
244
|
+
"reason": "missing-basis",
|
|
245
|
+
"published": false
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"id": "supersede-old-rule",
|
|
250
|
+
"tags": [
|
|
251
|
+
"write",
|
|
252
|
+
"supersede"
|
|
253
|
+
],
|
|
254
|
+
"title": "更正旧规则:旧行消失、新行生效、历史留痕",
|
|
255
|
+
"messages": [
|
|
256
|
+
{
|
|
257
|
+
"id": "m1",
|
|
258
|
+
"role": "user",
|
|
259
|
+
"text": "改稿别直接给结论,先说理由再说结论。"
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
"setup": [
|
|
263
|
+
{
|
|
264
|
+
"type": "feedback",
|
|
265
|
+
"line": "- [feedback] 改稿先给结论 — feedback.md"
|
|
266
|
+
}
|
|
267
|
+
],
|
|
268
|
+
"supersedes": "- [feedback] 改稿先给结论 — feedback.md",
|
|
269
|
+
"gold": {
|
|
270
|
+
"type": "feedback",
|
|
271
|
+
"scope": "project",
|
|
272
|
+
"basis": "explicit",
|
|
273
|
+
"summary": "改稿先说理由再说结论",
|
|
274
|
+
"meaning": {
|
|
275
|
+
"kind": "preference"
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
"expect": {
|
|
279
|
+
"status": "eligible",
|
|
280
|
+
"meaningMatches": true,
|
|
281
|
+
"indexContains": [
|
|
282
|
+
"改稿先说理由再说结论"
|
|
283
|
+
],
|
|
284
|
+
"indexExcludes": [
|
|
285
|
+
"- [feedback] 改稿先给结论 — feedback.md"
|
|
286
|
+
],
|
|
287
|
+
"superseded": true,
|
|
288
|
+
"search": [
|
|
289
|
+
{
|
|
290
|
+
"query": "改稿理由",
|
|
291
|
+
"hits": 1
|
|
292
|
+
}
|
|
293
|
+
]
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
"id": "condition-in-detail",
|
|
298
|
+
"tags": [
|
|
299
|
+
"write",
|
|
300
|
+
"conditions"
|
|
301
|
+
],
|
|
302
|
+
"title": "任务条件写进摘要与正文,检索按条件过滤",
|
|
303
|
+
"messages": [
|
|
304
|
+
{
|
|
305
|
+
"id": "m1",
|
|
306
|
+
"role": "user",
|
|
307
|
+
"text": "审稿的时候只看结构,不看错别字;其他场合不看结构。"
|
|
308
|
+
}
|
|
309
|
+
],
|
|
310
|
+
"gold": {
|
|
311
|
+
"type": "feedback",
|
|
312
|
+
"scope": "project",
|
|
313
|
+
"basis": "explicit",
|
|
314
|
+
"summary": "审稿时只看结构,不看错别字",
|
|
315
|
+
"detail": "审稿任务适用;其他任务不适用。",
|
|
316
|
+
"meaning": {
|
|
317
|
+
"kind": "preference",
|
|
318
|
+
"conditions": [
|
|
319
|
+
{
|
|
320
|
+
"dimension": "task",
|
|
321
|
+
"value": "review"
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
"expect": {
|
|
327
|
+
"status": "eligible",
|
|
328
|
+
"meaningMatches": true,
|
|
329
|
+
"detailContains": [
|
|
330
|
+
"审稿任务适用"
|
|
331
|
+
],
|
|
332
|
+
"search": [
|
|
333
|
+
{
|
|
334
|
+
"query": "只看结构",
|
|
335
|
+
"context": {
|
|
336
|
+
"task": "review"
|
|
337
|
+
},
|
|
338
|
+
"hits": 1
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"query": "只看结构",
|
|
342
|
+
"context": {
|
|
343
|
+
"task": "draft"
|
|
344
|
+
},
|
|
345
|
+
"hits": 0
|
|
346
|
+
}
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"id": "expired-not-retrievable",
|
|
352
|
+
"tags": [
|
|
353
|
+
"lifecycle",
|
|
354
|
+
"expiry"
|
|
355
|
+
],
|
|
356
|
+
"title": "过期条目保留在索引,但检索不返回",
|
|
357
|
+
"messages": [
|
|
358
|
+
{
|
|
359
|
+
"id": "m1",
|
|
360
|
+
"role": "user",
|
|
361
|
+
"text": "下周的内部活动只对内部开放。"
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
"gold": {
|
|
365
|
+
"type": "reference",
|
|
366
|
+
"scope": "global",
|
|
367
|
+
"basis": "explicit",
|
|
368
|
+
"summary": "内部活动只对内部开放",
|
|
369
|
+
"expires": "2000-01-01",
|
|
370
|
+
"meaning": {
|
|
371
|
+
"kind": "reference"
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
"expect": {
|
|
375
|
+
"status": "eligible",
|
|
376
|
+
"indexContains": [
|
|
377
|
+
"(exp:2000-01-01)"
|
|
378
|
+
],
|
|
379
|
+
"search": [
|
|
380
|
+
{
|
|
381
|
+
"query": "内部活动",
|
|
382
|
+
"hits": 0
|
|
383
|
+
}
|
|
384
|
+
]
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
]
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* L2 语义套件的**管道保真**层(确定性,不调用模型)。
|
|
4
|
+
*
|
|
5
|
+
* 它回答的问题是:假设抽取器给出了正确的候选(或故意错误的候选),
|
|
6
|
+
* 后续的机械闸门、写入、投影、检索会不会把它弄丢、弄错、放宽。
|
|
7
|
+
*
|
|
8
|
+
* 它**不回答**:真实模型会不会抽出正确候选。那需要 --live 主机适配器,
|
|
9
|
+
* 目前只能在真实 DSH 宿主里录一次基线再回放;不得把本套件通过写成语义验收通过。
|
|
10
|
+
*
|
|
11
|
+
* node eval/semantic-suite.mjs # 人类可读
|
|
12
|
+
* node eval/semantic-suite.mjs --json # 机器可读
|
|
13
|
+
* node eval/semantic-suite.mjs --verbose # 逐例输出
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync } from 'node:fs'
|
|
18
|
+
import { dirname, join } from 'node:path'
|
|
19
|
+
import { tmpdir } from 'node:os'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
import { assessCandidate } from '../capture-evidence.mjs'
|
|
23
|
+
import { normalizeMeaning } from '../meaning.mjs'
|
|
24
|
+
import { normalizeText } from '../security.mjs'
|
|
25
|
+
import { candidateSnapshot } from '../candidates.mjs'
|
|
26
|
+
import { indexLine, searchStore, writeMemory } from '../index.mjs'
|
|
27
|
+
|
|
28
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
29
|
+
export const CASES = JSON.parse(readFileSync(join(HERE, 'semantic', 'cases.json'), 'utf8'))
|
|
30
|
+
const hash = (text) => createHash('sha256').update(text).digest('hex')
|
|
31
|
+
|
|
32
|
+
function assert(condition, message) {
|
|
33
|
+
if (!condition) throw new Error(message)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runCase(testCase, room) {
|
|
37
|
+
const dir = join(room, testCase.gold.scope === 'project' ? 'project' : 'global')
|
|
38
|
+
mkdirSync(dir, { recursive: true })
|
|
39
|
+
|
|
40
|
+
for (const setup of testCase.setup ?? []) {
|
|
41
|
+
writeMemory({ type: setup.type, dir, line: setup.line })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const messages = testCase.messages.map((message) => {
|
|
45
|
+
const text = normalizeText(message.text).trim()
|
|
46
|
+
return { id: message.id, role: message.role ?? 'user', text, hash: hash(text) }
|
|
47
|
+
})
|
|
48
|
+
const evidence = testCase.gold.evidence ?? [{ messageId: messages[0].id, quote: messages[0].text.slice(0, Math.min(60, messages[0].text.length)) }]
|
|
49
|
+
const candidate = { ...testCase.gold, evidence }
|
|
50
|
+
const expect = testCase.expect ?? {}
|
|
51
|
+
const hasProject = candidate.scope === 'project'
|
|
52
|
+
const assessment = assessCandidate(candidate, messages, hasProject)
|
|
53
|
+
|
|
54
|
+
if (expect.status !== 'eligible') {
|
|
55
|
+
assert(assessment.status === expect.status, `期望 ${expect.status},实际 ${assessment.status}`)
|
|
56
|
+
if (expect.reason !== undefined) assert(assessment.reason === expect.reason, `期望原因 ${expect.reason},实际 ${assessment.reason}`)
|
|
57
|
+
assert(assessment.status !== 'eligible', '非 eligible 的用例不应该被写入')
|
|
58
|
+
return { assertions: 1 }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
assert(assessment.status === 'eligible', `期望 eligible,实际 ${assessment.status}${assessment.reason ? `(${assessment.reason})` : ''}`)
|
|
62
|
+
const detail = typeof candidate.detail === 'string' && candidate.detail.trim().length > 0 ? candidate.detail : undefined
|
|
63
|
+
const line = indexLine(candidate.type, candidate.summary, candidate.expires, detail !== undefined)
|
|
64
|
+
const written = writeMemory({
|
|
65
|
+
type: candidate.type,
|
|
66
|
+
dir,
|
|
67
|
+
line,
|
|
68
|
+
summary: candidate.summary,
|
|
69
|
+
scope: candidate.scope,
|
|
70
|
+
detail,
|
|
71
|
+
meaning: candidate.meaning,
|
|
72
|
+
expires: candidate.expires,
|
|
73
|
+
supersedes: testCase.supersedes,
|
|
74
|
+
provenance: { basis: 'tool-context', sessionId: `semantic-suite:${testCase.id}`, callHash: hash(testCase.id), evidence: assessment.evidence },
|
|
75
|
+
})
|
|
76
|
+
let assertions = 1
|
|
77
|
+
|
|
78
|
+
assert(written.indexLine === line, `写入行不一致:${written.indexLine}`)
|
|
79
|
+
const indexText = readFileSync(join(dir, 'MEMORY.md'), 'utf8')
|
|
80
|
+
for (const needle of expect.indexContains ?? []) assert(indexText.includes(needle), `索引缺少:${needle}`)
|
|
81
|
+
for (const needle of expect.indexExcludes ?? []) assert(!indexText.includes(needle), `索引不应包含:${needle}`)
|
|
82
|
+
if (expect.superseded === true) assert(written.superseded !== undefined, '期望返回被取代的旧行')
|
|
83
|
+
if (expect.detailContains) {
|
|
84
|
+
const topic = readFileSync(join(dir, `${candidate.type}.md`), 'utf8')
|
|
85
|
+
for (const needle of expect.detailContains) assert(topic.includes(needle), `正文缺少:${needle}`)
|
|
86
|
+
}
|
|
87
|
+
if (expect.meaningMatches === true) {
|
|
88
|
+
const snapshot = candidateSnapshot(dir, candidate.scope)
|
|
89
|
+
const record = snapshot.records.find((row) => row.indexLine === written.indexLine)
|
|
90
|
+
assert(record !== undefined, '账本里找不到刚写入的记录')
|
|
91
|
+
assert(JSON.stringify(record.meaning) === JSON.stringify(normalizeMeaning(candidate.meaning)), '认识层/条件在写入后发生变化')
|
|
92
|
+
}
|
|
93
|
+
for (const probe of expect.search ?? []) {
|
|
94
|
+
const hits = searchStore(dir, probe.query, {
|
|
95
|
+
...(probe.context === undefined ? {} : { context: probe.context }),
|
|
96
|
+
...(probe.task === undefined ? {} : { task: probe.task }),
|
|
97
|
+
}).hits
|
|
98
|
+
assert(hits.length === probe.hits, `检索「${probe.query}」期望 ${probe.hits} 条,实际 ${hits.length} 条`)
|
|
99
|
+
assertions += 1
|
|
100
|
+
}
|
|
101
|
+
return { assertions }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function runSuite(cases = CASES) {
|
|
105
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-semantic-suite-'))
|
|
106
|
+
const previousHome = process.env.DSH_HOME
|
|
107
|
+
process.env.DSH_HOME = root
|
|
108
|
+
const results = []
|
|
109
|
+
const startedAt = Date.now()
|
|
110
|
+
try {
|
|
111
|
+
for (const testCase of cases) {
|
|
112
|
+
const room = join(root, testCase.id)
|
|
113
|
+
mkdirSync(room, { recursive: true })
|
|
114
|
+
const begin = Date.now()
|
|
115
|
+
try {
|
|
116
|
+
const outcome = runCase(testCase, room)
|
|
117
|
+
results.push({ id: testCase.id, tags: testCase.tags ?? [], ok: true, assertions: outcome.assertions, ms: Date.now() - begin })
|
|
118
|
+
} catch (error) {
|
|
119
|
+
results.push({ id: testCase.id, tags: testCase.tags ?? [], ok: false, error: error.message, ms: Date.now() - begin })
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
if (previousHome === undefined) delete process.env.DSH_HOME
|
|
124
|
+
else process.env.DSH_HOME = previousHome
|
|
125
|
+
rmSync(root, { recursive: true, force: true })
|
|
126
|
+
}
|
|
127
|
+
const failed = results.filter((row) => !row.ok)
|
|
128
|
+
return {
|
|
129
|
+
kind: 'semantic-pipeline',
|
|
130
|
+
mode: 'pipeline',
|
|
131
|
+
semanticTruthEvaluated: false,
|
|
132
|
+
cases: results.length,
|
|
133
|
+
passed: results.length - failed.length,
|
|
134
|
+
failed: failed.length,
|
|
135
|
+
assertions: results.reduce((sum, row) => sum + (row.assertions ?? 0), 0),
|
|
136
|
+
ms: Date.now() - startedAt,
|
|
137
|
+
results,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`) {
|
|
142
|
+
const argv = process.argv.slice(2)
|
|
143
|
+
const report = runSuite()
|
|
144
|
+
if (argv.includes('--json')) process.stdout.write(JSON.stringify(report, null, 2) + '\n')
|
|
145
|
+
else {
|
|
146
|
+
const lines = [
|
|
147
|
+
`L2 语义套件(管道保真层)· ${report.cases} 例 · 通过 ${report.passed} · 失败 ${report.failed} · ${report.assertions} 项断言 · ${report.ms}ms`,
|
|
148
|
+
'说明:通过只表示“管道不会弄丢/弄错已给出的候选”,不表示真实模型的抽取质量。',
|
|
149
|
+
]
|
|
150
|
+
for (const row of report.results) {
|
|
151
|
+
if (!row.ok || argv.includes('--verbose')) lines.push(` ${row.ok ? '✔' : '✖'} ${row.id}${row.ok ? `(${row.assertions} 项)` : `:${row.error}`}`)
|
|
152
|
+
}
|
|
153
|
+
process.stdout.write(lines.join('\n') + '\n')
|
|
154
|
+
}
|
|
155
|
+
if (report.failed > 0) process.exit(1)
|
|
156
|
+
}
|