solopup-memory 0.21.0-beta.2 → 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.
Files changed (99) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/COMPATIBILITY.md +8 -5
  3. package/DETECTION.md +139 -0
  4. package/DREAMING-DESIGN.md +167 -0
  5. package/README.en.md +35 -6
  6. package/README.md +44 -7
  7. package/RELEASE.md +36 -0
  8. package/SECURITY.md +11 -0
  9. package/context-history.mjs +20 -3
  10. package/detect.mjs +441 -0
  11. package/eval/bench-detect.mjs +107 -0
  12. package/eval/semantic/cases.json +387 -0
  13. package/eval/semantic-suite.mjs +156 -0
  14. package/index.mjs +117 -27
  15. package/managed-policy.mjs +3 -1
  16. package/package.json +91 -68
  17. package/privacy.mjs +25 -0
  18. package/scripts/demo-source-recall.mjs +123 -0
  19. package/scripts/detect.mjs +69 -0
  20. package/scripts/dream.mjs +205 -0
  21. package/scripts/probe-history.mjs +92 -0
  22. package/scripts/probe-load.mjs +1 -1
  23. package/scripts/probe-package.mjs +6 -2
  24. package/scripts/security-check.mjs +173 -0
  25. package/scripts/soak.mjs +219 -0
  26. package/scripts/telemetry.mjs +187 -0
  27. package/scripts/verify.mjs +3 -1
  28. package/{test-adversarial.mjs → test/test-adversarial.mjs} +5 -5
  29. package/{test-belief-revision.mjs → test/test-belief-revision.mjs} +7 -7
  30. package/{test-candidates.mjs → test/test-candidates.mjs} +4 -4
  31. package/{test-capture-diagnostics.mjs → test/test-capture-diagnostics.mjs} +6 -6
  32. package/{test-capture-field-lineage.mjs → test/test-capture-field-lineage.mjs} +4 -4
  33. package/{test-capture-inputs.mjs → test/test-capture-inputs.mjs} +8 -8
  34. package/{test-capture-jobs.mjs → test/test-capture-jobs.mjs} +5 -5
  35. package/{test-capture-projections.mjs → test/test-capture-projections.mjs} +12 -12
  36. package/{test-capture-revision.mjs → test/test-capture-revision.mjs} +6 -6
  37. package/{test-capture-submission.mjs → test/test-capture-submission.mjs} +3 -3
  38. package/{test-capture-validation-flow.mjs → test/test-capture-validation-flow.mjs} +5 -5
  39. package/{test-capture-validation-job.mjs → test/test-capture-validation-job.mjs} +5 -5
  40. package/{test-capture-validation-lineage.mjs → test/test-capture-validation-lineage.mjs} +2 -2
  41. package/{test-capture-validation.mjs → test/test-capture-validation.mjs} +2 -2
  42. package/{test-capture.mjs → test/test-capture.mjs} +20 -20
  43. package/{test-compaction-compat.mjs → test/test-compaction-compat.mjs} +5 -5
  44. package/{test-compat.mjs → test/test-compat.mjs} +3 -3
  45. package/{test-concurrency.mjs → test/test-concurrency.mjs} +3 -3
  46. package/{test-conflict.mjs → test/test-conflict.mjs} +2 -2
  47. package/{test-context-history.mjs → test/test-context-history.mjs} +1 -1
  48. package/{test-control-schema.mjs → test/test-control-schema.mjs} +1 -1
  49. package/test/test-detect.mjs +125 -0
  50. package/test/test-dream-plan.mjs +72 -0
  51. package/test/test-dsh-registration.mjs +111 -0
  52. package/{test-episodes.mjs → test/test-episodes.mjs} +5 -5
  53. package/{test-explicit-memory.mjs → test/test-explicit-memory.mjs} +4 -4
  54. package/{test-field-lineage.mjs → test/test-field-lineage.mjs} +4 -4
  55. package/{test-history.mjs → test/test-history.mjs} +4 -4
  56. package/{test-inline-summary.mjs → test/test-inline-summary.mjs} +5 -5
  57. package/{test-legacy-source-identity.mjs → test/test-legacy-source-identity.mjs} +2 -2
  58. package/{test-links.mjs → test/test-links.mjs} +4 -4
  59. package/{test-managed-access.mjs → test/test-managed-access.mjs} +29 -5
  60. package/{test-managed-activation.mjs → test/test-managed-activation.mjs} +5 -5
  61. package/{test-meaning.mjs → test/test-meaning.mjs} +6 -6
  62. package/{test-native-service-lifecycle.mjs → test/test-native-service-lifecycle.mjs} +1 -1
  63. package/{test-native-write-validation.mjs → test/test-native-write-validation.mjs} +10 -4
  64. package/{test-packaging.mjs → test/test-packaging.mjs} +32 -5
  65. package/{test-paired-analysis.mjs → test/test-paired-analysis.mjs} +1 -1
  66. package/{test-patch.mjs → test/test-patch.mjs} +11 -11
  67. package/{test-plugin-contract.mjs → test/test-plugin-contract.mjs} +4 -4
  68. package/test/test-privacy.mjs +65 -0
  69. package/{test-project-stores.mjs → test/test-project-stores.mjs} +5 -5
  70. package/{test-publication.mjs → test/test-publication.mjs} +4 -4
  71. package/{test-restore.mjs → test/test-restore.mjs} +11 -11
  72. package/{test-retrieval.mjs → test/test-retrieval.mjs} +2 -2
  73. package/{test-revision-admission.mjs → test/test-revision-admission.mjs} +3 -3
  74. package/{test-rule-admission.mjs → test/test-rule-admission.mjs} +8 -8
  75. package/{test-runtime-startup.mjs → test/test-runtime-startup.mjs} +2 -2
  76. package/{test-schema.mjs → test/test-schema.mjs} +2 -2
  77. package/{test-security.mjs → test/test-security.mjs} +6 -6
  78. package/test/test-semantic-suite.mjs +20 -0
  79. package/{test-service-readiness.mjs → test/test-service-readiness.mjs} +1 -1
  80. package/{test-smell.mjs → test/test-smell.mjs} +4 -4
  81. package/{test-snapshots.mjs → test/test-snapshots.mjs} +3 -3
  82. package/{test-source-completeness.mjs → test/test-source-completeness.mjs} +6 -6
  83. package/{test-source-key.mjs → test/test-source-key.mjs} +2 -2
  84. package/{test-source-retirement.mjs → test/test-source-retirement.mjs} +4 -4
  85. package/{test-status-both.mjs → test/test-status-both.mjs} +12 -4
  86. package/{test-supersede.mjs → test/test-supersede.mjs} +3 -3
  87. package/test/test-telemetry.mjs +60 -0
  88. package/{test-text-admission.mjs → test/test-text-admission.mjs} +6 -6
  89. package/{test-tool-inputs.mjs → test/test-tool-inputs.mjs} +1 -1
  90. package/{test-tool-observations.mjs → test/test-tool-observations.mjs} +6 -6
  91. package/{test-tool-write-guard.mjs → test/test-tool-write-guard.mjs} +3 -3
  92. package/{test-tool-write-lifecycle.mjs → test/test-tool-write-lifecycle.mjs} +3 -3
  93. package/{test-usability.mjs → test/test-usability.mjs} +7 -7
  94. package/{test-validation-source-refs.mjs → test/test-validation-source-refs.mjs} +2 -2
  95. package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs} +1 -1
  96. package/{test-withdrawal-archives.mjs → test/test-withdrawal-archives.mjs} +6 -6
  97. package/{test-withdrawal.mjs → test/test-withdrawal.mjs} +2 -2
  98. package/{test.mjs → test/test.mjs} +4 -4
  99. 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
+ }