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.
- package/CHANGELOG.md +37 -0
- package/COMPATIBILITY.md +8 -5
- package/DETECTION.md +139 -0
- package/DREAMING-DESIGN.md +167 -0
- package/README.en.md +35 -6
- package/README.md +44 -7
- package/RELEASE.md +36 -0
- package/SECURITY.md +11 -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 +117 -27
- package/managed-policy.mjs +3 -1
- package/package.json +91 -68
- package/privacy.mjs +25 -0
- 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} +10 -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/test-privacy.mjs +65 -0
- 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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 打包完整性。跑法:node --test ~/.dsh/plugins/memory/test-packaging.mjs
|
|
2
|
+
* 打包完整性。跑法:node --test ~/.dsh/plugins/memory/test/test-packaging.mjs
|
|
3
3
|
*
|
|
4
4
|
* 发布前最容易漏的四件事:版本两处不一致、manifest 字段缺失、files 漏了运行时文件、
|
|
5
5
|
* 文档里指向不存在的文件。这些都是机械可查的,不该靠人记得。
|
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
|
|
8
8
|
import { test } from 'node:test'
|
|
9
9
|
import assert from 'node:assert/strict'
|
|
10
|
-
import { existsSync, readFileSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs'
|
|
10
|
+
import { existsSync, readFileSync, readdirSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs'
|
|
11
11
|
import { join, dirname } from 'node:path'
|
|
12
12
|
import { fileURLToPath } from 'node:url'
|
|
13
13
|
import { tmpdir } from 'node:os'
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
|
-
import { probeEnvironment } from '
|
|
15
|
+
import { probeEnvironment } from '../scripts/probe-env.mjs'
|
|
16
16
|
|
|
17
|
-
import { VERSION } from '
|
|
17
|
+
import { VERSION } from '../version.mjs'
|
|
18
18
|
|
|
19
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
19
|
+
const HERE = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
20
20
|
const pkg = JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8'))
|
|
21
21
|
|
|
22
22
|
test('版本单点:package.json 与 version.mjs 一致', () => {
|
|
@@ -72,6 +72,15 @@ test('npm scripts 指向真实存在的测试文件', () => {
|
|
|
72
72
|
for (const file of files) assert.ok(existsSync(join(HERE, file)), `脚本引用了不存在的 ${file}`)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
+
test('测试集中在 test/,命令与发布清单完整覆盖同一集合', () => {
|
|
76
|
+
const isSuite = name => /^test(?:-[a-z0-9-]+)?\.mjs$/.test(name)
|
|
77
|
+
assert.deepEqual(readdirSync(HERE).filter(isSuite), [], '根目录不应再有测试入口')
|
|
78
|
+
const suites = readdirSync(join(HERE, 'test')).filter(isSuite).map(name => `test/${name}`).sort()
|
|
79
|
+
const declared = pkg.scripts.test.split(/\s+/).filter(token => token.endsWith('.mjs')).sort()
|
|
80
|
+
assert.deepEqual(declared, suites, 'npm test 应覆盖全部测试且没有重复项')
|
|
81
|
+
for (const suite of suites) assert.ok(pkg.files.includes(suite), `files 漏了 ${suite}`)
|
|
82
|
+
})
|
|
83
|
+
|
|
75
84
|
test('LICENSE 与 package.json 的 license 字段一致', () => {
|
|
76
85
|
assert.equal(pkg.license, 'MIT')
|
|
77
86
|
assert.match(readFileSync(join(HERE, 'LICENSE'), 'utf8'), /MIT License/)
|
|
@@ -100,3 +109,21 @@ test('真实探针子进程看不到合成凭据或 NODE_OPTIONS,home/tmp 均
|
|
|
100
109
|
assert.doesNotMatch(child.stdout,/synthetic-key/)
|
|
101
110
|
} finally {rmSync(root,{recursive:true,force:true})}
|
|
102
111
|
})
|
|
112
|
+
|
|
113
|
+
// This executes from both the source tree and the extracted npm archive.
|
|
114
|
+
test('随包示例执行真实处理器,保留来源并隔离不同项目(非真实模型验收)', () => {
|
|
115
|
+
const child = spawnSync(process.execPath, [join(HERE, 'scripts/demo-source-recall.mjs'), '--json'], { encoding: 'utf8', timeout: 30000 })
|
|
116
|
+
assert.equal(child.status, 0, child.stderr)
|
|
117
|
+
const receipt = JSON.parse(child.stdout)
|
|
118
|
+
assert.equal(receipt.kind, 'synthetic-scripted-host-mechanics')
|
|
119
|
+
assert.equal(receipt.realDshHost, false)
|
|
120
|
+
assert.equal(receipt.realModel, false)
|
|
121
|
+
assert.equal(receipt.networkRequests, 0)
|
|
122
|
+
assert.equal(receipt.isolatedTemporaryHome, true)
|
|
123
|
+
assert.equal(receipt.separateProcesses, 3)
|
|
124
|
+
assert.equal(receipt.phases.write.result.status, 'recorded')
|
|
125
|
+
assert.equal(receipt.phases.recall.current.hits.length, 1)
|
|
126
|
+
assert.equal(receipt.phases.recall.history.hits.length, 1)
|
|
127
|
+
assert.equal(receipt.phases['other-project'].current.hits.length, 0)
|
|
128
|
+
assert.equal(receipt.phases['other-project'].history.hits.length, 0)
|
|
129
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { analyzePairs, signTail } from '
|
|
3
|
+
import { analyzePairs, signTail } from '../eval/paired-analysis.mjs'
|
|
4
4
|
|
|
5
5
|
test('配对精确检验:已知二项尾概率', () => {
|
|
6
6
|
assert.equal(signTail(0, 0), 1)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 安装/卸载脚本。跑法:node --test ~/.dsh/plugins/memory/test-patch.mjs
|
|
2
|
+
* 安装/卸载脚本。跑法:node --test ~/.dsh/plugins/memory/test/test-patch.mjs
|
|
3
3
|
*
|
|
4
4
|
* 这类脚本最容易在"用户的文件"上出事:删多了、重复插、覆盖掉别人的配置。
|
|
5
5
|
* 所以测试重点不是"能不能装上",而是"只动自己那几行"。
|
|
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os'
|
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
14
14
|
import { spawnSync } from 'node:child_process'
|
|
15
15
|
|
|
16
|
-
import { run } from '
|
|
16
|
+
import { run } from '../scripts/patch.mjs'
|
|
17
17
|
|
|
18
18
|
const OTHER = `# 用户自己的配置
|
|
19
19
|
- id: locale
|
|
@@ -57,7 +57,7 @@ test('插件路径不存在时拒绝安装,且不改动文件', () => {
|
|
|
57
57
|
test('安装:真实路径下写入块,且原有条目一行不少', () => {
|
|
58
58
|
const box = sandbox()
|
|
59
59
|
try {
|
|
60
|
-
const plugin = fileURLToPath(new URL('
|
|
60
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
61
61
|
const result = run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
62
62
|
assert.equal(result.changed, true)
|
|
63
63
|
|
|
@@ -79,7 +79,7 @@ test('安装:真实路径下写入块,且原有条目一行不少', () => {
|
|
|
79
79
|
test('幂等:装两次不产生第二个块', () => {
|
|
80
80
|
const box = sandbox()
|
|
81
81
|
try {
|
|
82
|
-
const plugin = fileURLToPath(new URL('
|
|
82
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
83
83
|
run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
84
84
|
const first = readFileSync(box.file, 'utf8')
|
|
85
85
|
|
|
@@ -95,7 +95,7 @@ test('幂等:装两次不产生第二个块', () => {
|
|
|
95
95
|
test('卸载:只删自己的块,用户配置逐字保留', () => {
|
|
96
96
|
const box = sandbox()
|
|
97
97
|
try {
|
|
98
|
-
const plugin = fileURLToPath(new URL('
|
|
98
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
99
99
|
run(['install', '--profile', box.profile, '--plugin', plugin])
|
|
100
100
|
const result = run(['uninstall', '--profile', box.profile])
|
|
101
101
|
assert.equal(result.changed, true)
|
|
@@ -124,7 +124,7 @@ test('卸载:没装过时不动文件', () => {
|
|
|
124
124
|
test('dry-run 不落盘', () => {
|
|
125
125
|
const box = sandbox()
|
|
126
126
|
try {
|
|
127
|
-
const plugin = fileURLToPath(new URL('
|
|
127
|
+
const plugin = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
128
128
|
const before = readFileSync(box.file, 'utf8')
|
|
129
129
|
const result = run(['install', '--profile', box.profile, '--plugin', plugin, '--dry-run'])
|
|
130
130
|
assert.equal(result.dryRun, true)
|
|
@@ -138,7 +138,7 @@ test('dry-run 不落盘', () => {
|
|
|
138
138
|
test('标记不成对时拒绝操作(不猜)', () => {
|
|
139
139
|
const box = sandbox(`${OTHER}# >>> dsh-memory\n- insert:\n`)
|
|
140
140
|
try {
|
|
141
|
-
assert.throws(() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
141
|
+
assert.throws(() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))]), /标记不成对/)
|
|
142
142
|
assert.throws(() => run(['uninstall', '--profile', box.profile]), /标记不成对/)
|
|
143
143
|
} finally {
|
|
144
144
|
box.cleanup()
|
|
@@ -158,7 +158,7 @@ test('已有未托管的 memory 条目时拒绝安装(不制造副本)', ()
|
|
|
158
158
|
try {
|
|
159
159
|
const before = readFileSync(box.file, 'utf8')
|
|
160
160
|
assert.throws(
|
|
161
|
-
() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
161
|
+
() => run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))]),
|
|
162
162
|
/未托管的 memory 条目/,
|
|
163
163
|
)
|
|
164
164
|
assert.equal(readFileSync(box.file, 'utf8'), before, '拒绝时不应改动文件')
|
|
@@ -171,7 +171,7 @@ test('全新空列表配置可安装,卸载逐字恢复注释与空白', () =>
|
|
|
171
171
|
const original = '# clean profile\n [] # intentional empty\n\n'
|
|
172
172
|
const box = sandbox(original)
|
|
173
173
|
try {
|
|
174
|
-
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
174
|
+
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))])
|
|
175
175
|
const installed = readFileSync(box.file, 'utf8')
|
|
176
176
|
assert.doesNotMatch(installed, /^\s*\[\]/m)
|
|
177
177
|
assert.match(installed, /^- insert:/m)
|
|
@@ -183,7 +183,7 @@ test('全新空列表配置可安装,卸载逐字恢复注释与空白', () =>
|
|
|
183
183
|
test('恢复空列表时保留托管块以外的新注释,损坏恢复标记则拒绝', () => {
|
|
184
184
|
const box = sandbox('[]\n')
|
|
185
185
|
try {
|
|
186
|
-
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('
|
|
186
|
+
run(['install', '--profile', box.profile, '--plugin', fileURLToPath(new URL('../index.mjs', import.meta.url))])
|
|
187
187
|
const installed = readFileSync(box.file, 'utf8')
|
|
188
188
|
writeFileSync(box.file, '# newer outside comment\n' + installed)
|
|
189
189
|
run(['uninstall', '--profile', box.profile])
|
|
@@ -212,7 +212,7 @@ test('插件路径中的空格、中文与井号使用有效 file URL', () => {
|
|
|
212
212
|
test('default plugin path installs correctly from a Unicode directory with spaces and URL metacharacters',()=>{
|
|
213
213
|
const box=sandbox()
|
|
214
214
|
try{
|
|
215
|
-
const local=join(box.root,'插件 空间#'),scripts=join(local,'scripts');mkdirSync(scripts,{recursive:true});writeFileSync(join(local,'index.mjs'),'export function apply() {}\n');const file=join(scripts,'patch.mjs');copyFileSync(fileURLToPath(new URL('
|
|
215
|
+
const local=join(box.root,'插件 空间#'),scripts=join(local,'scripts');mkdirSync(scripts,{recursive:true});writeFileSync(join(local,'index.mjs'),'export function apply() {}\n');const file=join(scripts,'patch.mjs');copyFileSync(fileURLToPath(new URL('../scripts/patch.mjs',import.meta.url)),file)
|
|
216
216
|
const result=spawnSync(process.execPath,[file,'install','--profile',box.profile],{encoding:'utf8'});assert.equal(result.status,0,result.stderr);const body=readFileSync(box.file,'utf8'),href=JSON.parse(body.split('\n').filter(line=>line.trim().startsWith('name:')).at(-1).split('name:')[1].trim());assert.equal(realpathSync(fileURLToPath(href)),realpathSync(join(local,'index.mjs')));assert.ok(href.includes('%20')&&href.includes('%23'));assert.ok(body.includes('id: mcp-godot'))
|
|
217
217
|
}finally{box.cleanup()}
|
|
218
218
|
})
|
|
@@ -4,10 +4,10 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync } from 'node:fs'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { join } from 'node:path'
|
|
7
|
-
import { Config, normalizePluginConfig, DEFAULT_AUTO_CAPTURE } from '
|
|
8
|
-
import { optionalService } from '
|
|
9
|
-
import { extractWithLlm } from '
|
|
10
|
-
import { apply,storeDirFor } from '
|
|
7
|
+
import { Config, normalizePluginConfig, DEFAULT_AUTO_CAPTURE } from '../plugin-config.mjs'
|
|
8
|
+
import { optionalService } from '../host-services.mjs'
|
|
9
|
+
import { extractWithLlm } from '../capture-llm.mjs'
|
|
10
|
+
import { apply,storeDirFor } from '../index.mjs'
|
|
11
11
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
12
12
|
async function until(check) { for (let i = 0; i < 200; i++) { if (check()) return; await sleep(5) } throw Error('fixture deadline') }
|
|
13
13
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync} from 'node:fs'
|
|
4
|
+
import {join} from 'node:path'
|
|
5
|
+
import {tmpdir} from 'node:os'
|
|
6
|
+
import {apply,searchStore,lookupLinks} from '../index.mjs'
|
|
7
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
8
|
+
import {memoryPrivacy,privateMemory,publicMemoryRecords,publicMemoryIndex} from '../privacy.mjs'
|
|
9
|
+
|
|
10
|
+
test('actual background extractor receives no unrelated saved private source or summary',async()=>{
|
|
11
|
+
const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-private-background-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
|
|
12
|
+
const cwd=join(home,'project'),handlers=[],effects=[];mkdirSync(cwd);let requestText,finished=false
|
|
13
|
+
const llm={async prepareCall(config){return{config,async*stream(request){requestText=JSON.stringify(request);yield{type:'text-delta',text:'[]'};yield{type:'finish',reason:{kind:'stop'}};finished=true}}}}
|
|
14
|
+
try{
|
|
15
|
+
apply({llm,systemPrompt:{context(){}},workingDirectory:{get:()=>cwd},tools:{register(){}},on:(name,fn)=>handlers.push([name,fn]),effect:fn=>effects.push(fn())},
|
|
16
|
+
{autoCapture:{everyTurns:1,provider:'fixture',model:'fixture'}})
|
|
17
|
+
writeFixtureMemory({dir:join(home,'memory'),type:'user',scope:'global',summary:'个人档案:出生于1993年4月16日,女性',meaning:{kind:'claim'}})
|
|
18
|
+
const session={id:'synthetic-unrelated-work'},emit=e=>{for(const [n,f]of handlers)if(n==='session/event')f(session,e)}
|
|
19
|
+
emit({type:'user/message',seq:1,data:{source:{kind:'user'},content:[{type:'text',text:'本项目以后交付字幕时固定先检查断句。'}]}})
|
|
20
|
+
emit({type:'step/end',seq:2})
|
|
21
|
+
for(let i=0;i<100&&!finished;i++)await new Promise(r=>setTimeout(r,5))
|
|
22
|
+
assert.equal(finished,true)
|
|
23
|
+
assert.doesNotMatch(requestText,/1993|4月16|个人档案:/)
|
|
24
|
+
}finally{for(const f of effects.reverse())if(typeof f==='function')f();if(old===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=old;rmSync(home,{recursive:true,force:true})}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('known personal categories default to on-demand, including evidence-only private data',()=>{
|
|
28
|
+
for(const text of ['生日是1993年4月16日','个人档案:女性','病史与用药资料','亲密关系与伴侣','家庭住址','[private:personal] opaque','birthday is April 16'])assert.equal(privateMemory(text),true)
|
|
29
|
+
assert.equal(privateMemory({summary:'工作反馈先给结论',evidence:[{quote:'我的生日是1993年4月16日,我希望先给结论。'}]}),true)
|
|
30
|
+
assert.equal(memoryPrivacy('字幕交付先检查中英断句').exposure,'automatic')
|
|
31
|
+
assert.equal(privateMemory('工具失败的原生记录仅用于诊断'),false)
|
|
32
|
+
})
|
|
33
|
+
test('background projections and pointers omit private evidence without changing originals',()=>{
|
|
34
|
+
const secret={indexLine:'- [user] 个人档案:出生于1993年4月16日 — user.md',summary:'个人档案:出生于1993年4月16日',evidence:[]}
|
|
35
|
+
const mixed={indexLine:'- [user] 回答先给结论 — user.md',summary:'回答先给结论',evidence:[{quote:'我的生日是1993年4月16日,回答先给结论'}]}
|
|
36
|
+
const safe={indexLine:'- [feedback] 字幕保留断句 — feedback.md',summary:'字幕保留断句',evidence:[]}
|
|
37
|
+
const records=[secret,mixed,safe],before=JSON.stringify(records)
|
|
38
|
+
assert.deepEqual(publicMemoryRecords(records),[safe])
|
|
39
|
+
assert.deepEqual(publicMemoryIndex(records.map(r=>r.indexLine),records),[safe.indexLine])
|
|
40
|
+
assert.equal(JSON.stringify(records),before)
|
|
41
|
+
})
|
|
42
|
+
test('real plugin render and both current/history search hide private records by default',()=>{
|
|
43
|
+
const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-privacy-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
|
|
44
|
+
const cwd=join(home,'project'),providers=[],tools=[];mkdirSync(cwd)
|
|
45
|
+
try{
|
|
46
|
+
apply({systemPrompt:{context:p=>providers.push(p)},workingDirectory:{get:()=>cwd},tools:{register:t=>tools.push(t),schemas:()=>[{name:'memory_write'}]},on(){},effect(){}},{autoCapture:{enabled:false}})
|
|
47
|
+
const dir=join(home,'memory'),secret='固定个人档案:出生于1993年4月16日,女性。'
|
|
48
|
+
const privateWrite=writeFixtureMemory({dir,type:'user',scope:'global',summary:secret,detail:'私人资料分行\n1993-04-16\n女性',meaning:{kind:'claim'}})
|
|
49
|
+
writeFixtureMemory({dir,type:'feedback',scope:'global',summary:'字幕验收先看断句',meaning:{kind:'preference'}})
|
|
50
|
+
const before=readFileSync(join(dir,'MEMORY.md'),'utf8')
|
|
51
|
+
const rendered=providers.find(p=>p.name==='memory:index').text({agent:{session:{}}})
|
|
52
|
+
assert.doesNotMatch(rendered,/1993|女性|固定个人档案/)
|
|
53
|
+
assert.match(rendered,/字幕验收先看断句/)
|
|
54
|
+
assert.match(providers.find(p=>p.name==='memory:protocol').text({agent:{session:{}}}),/includePrivate=true/)
|
|
55
|
+
assert.equal(searchStore(dir,'1993',{mode:'exact'}).hits.length,0)
|
|
56
|
+
assert.ok(searchStore(dir,'1993',{mode:'exact',includePrivate:true}).hits.length>0)
|
|
57
|
+
assert.equal(lookupLinks(dir,{id:privateWrite.id}).found,false)
|
|
58
|
+
assert.equal(lookupLinks(dir,{match:'个人档案'}).found,false)
|
|
59
|
+
assert.equal(searchStore(dir,'1993',{mode:'exact',view:'history',scope:'global'}).hits.length,0)
|
|
60
|
+
assert.ok(searchStore(dir,'1993',{mode:'exact',view:'history',scope:'global',includePrivate:true}).hits.length>0)
|
|
61
|
+
assert.throws(()=>searchStore(dir,'1993',{includePrivate:'true'}),/private-option/)
|
|
62
|
+
assert.equal(readFileSync(join(dir,'MEMORY.md'),'utf8'),before)
|
|
63
|
+
assert.ok(tools.find(t=>t.name==='memory_search').parameters.properties.includePrivate)
|
|
64
|
+
}finally{if(old===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=old;rmSync(home,{recursive:true,force:true})}
|
|
65
|
+
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {prepareMountedFixture} from '
|
|
1
|
+
import {prepareMountedFixture} from '../fixture-host.mjs'
|
|
2
2
|
import {realpathSync} from 'node:fs'
|
|
3
3
|
import { test } from 'node:test'
|
|
4
4
|
import assert from 'node:assert/strict'
|
|
@@ -7,8 +7,8 @@ import { spawnSync } from 'node:child_process'
|
|
|
7
7
|
import { join } from 'node:path'
|
|
8
8
|
import { tmpdir } from 'node:os'
|
|
9
9
|
import { fileURLToPath } from 'node:url'
|
|
10
|
-
import { projectStoreDir, projectIdentity, legacyProjectSlug, inspectProjectStore, migrateProjectStore, recoverProjectStore } from '
|
|
11
|
-
import { storeDirFor, writeMemory, indexLine, searchStore, apply } from '
|
|
10
|
+
import { projectStoreDir, projectIdentity, legacyProjectSlug, inspectProjectStore, migrateProjectStore, recoverProjectStore } from '../project-stores.mjs'
|
|
11
|
+
import { storeDirFor, writeMemory, indexLine, searchStore, apply } from '../index.mjs'
|
|
12
12
|
|
|
13
13
|
function box(run) {
|
|
14
14
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-project-store-'))), store = join(root, 'memory'), a = join(root, 'A B'), b = join(root, 'A-B')
|
|
@@ -160,7 +160,7 @@ test('a live loaded host blocks applying migration while read-only inspection re
|
|
|
160
160
|
}))
|
|
161
161
|
test('actual SIGKILL after directory publication is recoverable and never assigns the copy to a colliding project', () => box(box => {
|
|
162
162
|
const dir = legacy(box), before = contents(dir), options = reviewed(box)
|
|
163
|
-
const worker = `import {migrateProjectStore} from ${JSON.stringify(new URL('
|
|
163
|
+
const worker = `import {migrateProjectStore} from ${JSON.stringify(new URL('../project-stores.mjs', import.meta.url).href)};migrateProjectStore(process.argv[1],process.argv[2],{...JSON.parse(process.argv[3]),fault:event=>{if(event.phase==='renamed')process.kill(process.pid,'SIGKILL')}})`
|
|
164
164
|
const result = spawnSync(process.execPath, ['--input-type=module', '-e', worker, box.store, box.a, JSON.stringify(options)], { encoding: 'utf8', timeout: 15000 })
|
|
165
165
|
assert.equal(result.signal, 'SIGKILL', result.stderr)
|
|
166
166
|
assert.throws(() => storeDirFor('project', box.a), /migration-needs-recovery/)
|
|
@@ -204,7 +204,7 @@ test('source edits after completed migration are diagnosed and never silently me
|
|
|
204
204
|
}))
|
|
205
205
|
test('local CLI previews and applies with both review hashes without package installation', () => box(box => {
|
|
206
206
|
legacy(box)
|
|
207
|
-
const cli = fileURLToPath(new URL('
|
|
207
|
+
const cli = fileURLToPath(new URL('../scripts/projects.mjs', import.meta.url))
|
|
208
208
|
const run = args => spawnSync(process.execPath, [cli, ...args, '--cwd', box.a], { env: process.env, encoding: 'utf8', timeout: 10000 })
|
|
209
209
|
const preview = run(['migrate'])
|
|
210
210
|
assert.equal(preview.status, 0, preview.stderr)
|
|
@@ -6,11 +6,11 @@ import { join } from 'node:path'
|
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import { stageCandidate, listCandidates, rejectCandidate, withdrawSource, pruneCandidates } from '
|
|
10
|
-
import { adoptCandidate, recoverMemoryPublication, writeMemory, storeRevision, indexLine, searchStore, apply } from '
|
|
11
|
-
import { commitPublication } from '
|
|
9
|
+
import { stageCandidate, listCandidates, rejectCandidate, withdrawSource, pruneCandidates } from '../candidates.mjs'
|
|
10
|
+
import { adoptCandidate, recoverMemoryPublication, writeMemory, storeRevision, indexLine, searchStore, apply } from '../index.mjs'
|
|
11
|
+
import { commitPublication } from '../publication.mjs'
|
|
12
12
|
|
|
13
|
-
const modulePath = fileURLToPath(new URL('
|
|
13
|
+
const modulePath = fileURLToPath(new URL('../index.mjs', import.meta.url))
|
|
14
14
|
function fixture(dir) {
|
|
15
15
|
return { type: 'user', scope: 'project', summary: '本项目偏好柔和低反差的光线', detail: '仅限本项目的夜间室内对白场景。',
|
|
16
16
|
sessionId: 'review-session', indexRevision: storeRevision(dir),
|
|
@@ -6,17 +6,17 @@ import {tmpdir} from 'node:os'
|
|
|
6
6
|
import {spawnSync} from 'node:child_process'
|
|
7
7
|
import {fileURLToPath} from 'node:url'
|
|
8
8
|
import {createHash} from 'node:crypto'
|
|
9
|
-
import {createMemorySnapshot,snapshotIO} from '
|
|
10
|
-
import {restoreMemorySnapshot,reviewMemoryRestore,resumeMemoryRestore} from '
|
|
11
|
-
import {restorationPending,restorationJournalPath} from '
|
|
12
|
-
import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,searchStore,withdrawManagedSource,storeDirFor,apply} from '
|
|
13
|
-
import {controlStatus} from '
|
|
14
|
-
import {claimCaptureJob,finishCaptureJob,listCaptureJobs} from '
|
|
15
|
-
import {captureInputDescriptor,enqueueCaptureInput,listCaptureInputs,bindCaptureInput} from '
|
|
16
|
-
import {compileValueRevision} from '
|
|
17
|
-
import {candidateSnapshot} from '
|
|
18
|
-
import {episodeSnapshot} from '
|
|
19
|
-
const here=dirname(fileURLToPath(import.meta.url)),hash=b=>createHash('sha256').update(b).digest('hex')
|
|
9
|
+
import {createMemorySnapshot,snapshotIO} from '../snapshots.mjs'
|
|
10
|
+
import {restoreMemorySnapshot,reviewMemoryRestore,resumeMemoryRestore} from '../restore.mjs'
|
|
11
|
+
import {restorationPending,restorationJournalPath} from '../restoration-lock.mjs'
|
|
12
|
+
import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,searchStore,withdrawManagedSource,storeDirFor,apply} from '../index.mjs'
|
|
13
|
+
import {controlStatus} from '../managed-policy.mjs'
|
|
14
|
+
import {claimCaptureJob,finishCaptureJob,listCaptureJobs} from '../capture-jobs.mjs'
|
|
15
|
+
import {captureInputDescriptor,enqueueCaptureInput,listCaptureInputs,bindCaptureInput} from '../capture-inputs.mjs'
|
|
16
|
+
import {compileValueRevision} from '../field-lineage.mjs'
|
|
17
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
18
|
+
import {episodeSnapshot} from '../episodes.mjs'
|
|
19
|
+
const here=dirname(dirname(fileURLToPath(import.meta.url))),hash=b=>createHash('sha256').update(b).digest('hex')
|
|
20
20
|
async function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'memory-restore-'))),home=join(root,'home'),data=join(home,'memory'),snapshot=join(root,'snapshot'),cwd=join(root,'project'),previous=process.env.DSH_HOME;mkdirSync(home);mkdirSync(data);mkdirSync(cwd);writeFileSync(join(cwd,'.dsh-root'),'');process.env.DSH_HOME=home;const p=initializeManagedMemory(data);initializeManagedMemory(data,undefined,{apply:true,reviewed:p.review});try{return await run({root,home,data,snapshot,cwd})}finally{if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
|
|
21
21
|
function seed(dir,word,owner='owner-'+word,scope='global'){const summary='Synthetic lasting preference '+word;return writeMemory({dir,scope,type:'user',summary,detail:summary,line:indexLine('user',summary,undefined,true),provenance:{basis:'tool-context',sessionId:owner,callHash:hash(owner+word),evidence:[{messageId:'root',hash:hash('quote '+word),quote:summary}]}})}
|
|
22
22
|
function revoke(data,owner){const p=withdrawManagedSource(data,owner,'root');return withdrawManagedSource(data,owner,'root',{apply:true,controlReview:p.controlReview})}
|
|
@@ -3,8 +3,8 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
|
-
import { queryTokens, rankLines, foldEnglishInflection } from '
|
|
7
|
-
import { searchStore } from '
|
|
6
|
+
import { queryTokens, rankLines, foldEnglishInflection } from '../retrieval.mjs'
|
|
7
|
+
import { searchStore } from '../index.mjs'
|
|
8
8
|
|
|
9
9
|
test('词元:中文双字、拉丁大小写与标识符', () => {
|
|
10
10
|
const tokens = queryTokens('中英字幕 E01-Final')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {isRevisionAdmissionError} from '
|
|
4
|
-
import {compileValueRevision,normalizeFieldLineage} from '
|
|
5
|
-
import {captureRevision} from '
|
|
3
|
+
import {isRevisionAdmissionError} from '../revision-admission.mjs'
|
|
4
|
+
import {compileValueRevision,normalizeFieldLineage} from '../field-lineage.mjs'
|
|
5
|
+
import {captureRevision} from '../capture-revision.mjs'
|
|
6
6
|
const caught=fn=>{try{fn()}catch(error){return error}assert.fail('expected a real guard refusal')}
|
|
7
7
|
const parent={id:'a'.repeat(64),status:'recorded',scope:'global',meaning:{kind:'preference',conditions:[{dimension:'document_kind',value:'draft'}]},sources:['b'.repeat(64),'c'.repeat(64)]}
|
|
8
8
|
test('real value-change and target-identity guards are preparation refusals',()=>{
|
|
@@ -4,13 +4,13 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import {mkdtempSync,realpathSync,readFileSync,writeFileSync,existsSync,rmSync} from 'node:fs'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
6
|
import {tmpdir} from 'node:os'
|
|
7
|
-
import {assertMeaningSatisfiable,normalizeMeaning,applicability} from '
|
|
8
|
-
import {assessCandidate} from '
|
|
9
|
-
import {writeFixtureMemory} from '
|
|
10
|
-
import {candidateSnapshot,stageCandidate} from '
|
|
11
|
-
import {adoptCandidate,storeRevision} from '
|
|
12
|
-
import {prepareValidationPrompt,ValidationSubmission} from '
|
|
13
|
-
import {VALIDATION_FIELDS} from '
|
|
7
|
+
import {assertMeaningSatisfiable,normalizeMeaning,applicability} from '../meaning.mjs'
|
|
8
|
+
import {assessCandidate} from '../capture-evidence.mjs'
|
|
9
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
10
|
+
import {candidateSnapshot,stageCandidate} from '../candidates.mjs'
|
|
11
|
+
import {adoptCandidate,storeRevision} from '../index.mjs'
|
|
12
|
+
import {prepareValidationPrompt,ValidationSubmission} from '../capture-validation.mjs'
|
|
13
|
+
import {VALIDATION_FIELDS} from '../capture-validation-contract.mjs'
|
|
14
14
|
const contradictory={kind:'preference',conditions:[{dimension:'document_kind',value:'exploratory_draft'}],except:[{dimension:'document_kind',value:'exploratory_draft'}]}
|
|
15
15
|
const valid={...contradictory,except:[{dimension:'document_kind',value:'final'}]}
|
|
16
16
|
const owner={id:'synthetic-owner',role:'user',hash:'a'.repeat(64),text:'长期处理探索性草稿时先确认试验目标。'}
|
|
@@ -38,7 +38,7 @@ test('reviewed adoption cannot publish an impossible pending candidate or change
|
|
|
38
38
|
assert.equal(readFileSync(join(dir,'.candidates.json'),'utf8'),before);assert.equal(existsSync(join(dir,'MEMORY.md')),false)
|
|
39
39
|
}))
|
|
40
40
|
test('old effective self-excluding records retain their identities and remain readable without rewriting',()=>box(dir=>{
|
|
41
|
-
const fixture=JSON.parse(readFileSync(new URL('
|
|
41
|
+
const fixture=JSON.parse(readFileSync(new URL('../corpus/legacy-self-excluding-dev36.json',import.meta.url)))
|
|
42
42
|
for(const[name,content]of Object.entries(fixture.files))writeFileSync(join(dir,name),content)
|
|
43
43
|
const records=candidateSnapshot(dir,'global').records;assert.equal(records.length,1);assert.equal(records[0].status,'recorded')
|
|
44
44
|
assert.deepEqual(normalizeMeaning(records[0].meaning),normalizeMeaning(contradictory))
|
|
@@ -4,8 +4,8 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,realpathSync,exi
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {apply,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,storeDirFor,searchStore} from '
|
|
8
|
-
import {projectIdentity} from '
|
|
7
|
+
import {apply,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,storeDirFor,searchStore} from '../index.mjs'
|
|
8
|
+
import {projectIdentity} from '../project-stores.mjs'
|
|
9
9
|
const hash=value=>createHash('sha256').update(value).digest('hex')
|
|
10
10
|
async function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'memory-startup-'))),home=join(root,'home'),cwd=join(root,'work'),data=join(home,'memory'),previous=process.env.DSH_HOME;mkdirSync(home);mkdirSync(cwd);process.env.DSH_HOME=home;const effects=[]
|
|
11
11
|
try{return await run({root,home,cwd,data,effects})}finally{for(const effect of effects.reverse())effect?.();if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {realpathSync} from 'node:fs'
|
|
2
|
-
import {applyWithOwnerFixture as apply} from '
|
|
2
|
+
import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
|
|
3
3
|
/**
|
|
4
|
-
* 工具 schema 的静态校验。跑法:node --test ~/.dsh/plugins/memory/test-schema.mjs
|
|
4
|
+
* 工具 schema 的静态校验。跑法:node --test ~/.dsh/plugins/memory/test/test-schema.mjs
|
|
5
5
|
*
|
|
6
6
|
* 为什么需要它:宿主在**注册时**校验 output schema,不合格会让整个插件加载失败;
|
|
7
7
|
* 而单元测试直接调 tool.execute(),完全绕过那道校验。
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {prepareMountedFixture} from '
|
|
1
|
+
import {prepareMountedFixture} from '../fixture-host.mjs'
|
|
2
2
|
import {realpathSync} from 'node:fs'
|
|
3
3
|
/**
|
|
4
|
-
* 安全测试。跑法:node --test ~/.dsh/plugins/memory/test-security.mjs
|
|
4
|
+
* 安全测试。跑法:node --test ~/.dsh/plugins/memory/test/test-security.mjs
|
|
5
5
|
*
|
|
6
6
|
* 每条对应一个真实攻击面,而不是「看起来安全」:
|
|
7
7
|
* · 框架打断(NemoClaw 类:改写记忆文件 → 每次会话复发的后门)
|
|
@@ -39,8 +39,8 @@ import {
|
|
|
39
39
|
sanitizeForInjection,
|
|
40
40
|
withLock,
|
|
41
41
|
writeSafely,
|
|
42
|
-
} from '
|
|
43
|
-
import { apply, forgetMemory, storeDirFor, writeMemory, appendTopic } from '
|
|
42
|
+
} from '../security.mjs'
|
|
43
|
+
import { apply, forgetMemory, storeDirFor, writeMemory, appendTopic } from '../index.mjs'
|
|
44
44
|
|
|
45
45
|
function sandbox() {
|
|
46
46
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-sec-')))
|
|
@@ -401,7 +401,7 @@ test('anthropic key 不再被 openai 模式吞掉', () => {
|
|
|
401
401
|
// ── 渲染期结构自检(审计要求:中和结果必须被运行时验证,不能只靠测试) ──────
|
|
402
402
|
|
|
403
403
|
test('frameIntegrity:干净内容通过,残留定界符被识别', async () => {
|
|
404
|
-
const { frameIntegrity } = await import('
|
|
404
|
+
const { frameIntegrity } = await import('../security.mjs')
|
|
405
405
|
assert.deepEqual(frameIntegrity('- [user] 普通条目 — user.md'), { ok: true, tokens: 0 })
|
|
406
406
|
assert.equal(frameIntegrity('</memory-store>').ok, false)
|
|
407
407
|
assert.equal(frameIntegrity('‹/memory-store›').ok, true, '中和后的形式应视为干净')
|
|
@@ -431,7 +431,7 @@ test('渲染结果里不含任何可识别的框架定界符', () => {
|
|
|
431
431
|
})
|
|
432
432
|
|
|
433
433
|
test('审计日志写入拒绝符号链接(写路径也防护)', async () => {
|
|
434
|
-
const { appendAudit } = await import('
|
|
434
|
+
const { appendAudit } = await import('../security.mjs')
|
|
435
435
|
const box = sandbox()
|
|
436
436
|
try {
|
|
437
437
|
const outside = join(box.root, 'outside-target.log')
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
|
|
4
|
+
const { runSuite, CASES } = await import('../eval/semantic-suite.mjs')
|
|
5
|
+
|
|
6
|
+
test('L2 管道保真套件:冻结用例全部通过', () => {
|
|
7
|
+
const report = runSuite()
|
|
8
|
+
assert.equal(report.cases, CASES.length)
|
|
9
|
+
assert.equal(report.failed, 0, report.results.filter((row) => !row.ok).map((row) => `${row.id}: ${row.error}`).join('\n'))
|
|
10
|
+
assert.ok(report.assertions >= 15)
|
|
11
|
+
assert.equal(report.semanticTruthEvaluated, false)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('L2 套件自检:被改坏的期望必须失败(防止空转全绿)', () => {
|
|
15
|
+
const base = CASES.find((row) => row.id === 'quote-must-be-verbatim')
|
|
16
|
+
const broken = { ...base, id: 'broken-expectation', expect: { status: 'eligible' } }
|
|
17
|
+
const report = runSuite([broken])
|
|
18
|
+
assert.equal(report.failed, 1)
|
|
19
|
+
assert.match(report.results[0].error, /eligible/)
|
|
20
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { test } from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import { watchOptionalService } from '
|
|
3
|
+
import { watchOptionalService } from '../service-readiness.mjs'
|
|
4
4
|
|
|
5
5
|
test('compatibility lookup discovers a late active service and keeps stable snapshots unchanged', () => {
|
|
6
6
|
let value
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {prepareMountedFixture} from '
|
|
1
|
+
import {prepareMountedFixture} from '../fixture-host.mjs'
|
|
2
2
|
import {realpathSync} from 'node:fs'
|
|
3
3
|
/**
|
|
4
|
-
* 指令气味检测(R5)。跑法:node --test ~/.dsh/plugins/memory/test-smell.mjs
|
|
4
|
+
* 指令气味检测(R5)。跑法:node --test ~/.dsh/plugins/memory/test/test-smell.mjs
|
|
5
5
|
*
|
|
6
6
|
* 这是 L2 组合污染的**缓解**,不是防护:MemPoison 已经说明写时过滤压不住组合型攻击。
|
|
7
7
|
* 做法是给「像指令」的条目打 ⚠,让模型和人都能看见,而不是假装拦住了。
|
|
@@ -13,8 +13,8 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
|
|
16
|
-
import { SMELL_THRESHOLD, instructionSmell, isSmelly } from '
|
|
17
|
-
import { apply, storeDirFor, writeMemory } from '
|
|
16
|
+
import { SMELL_THRESHOLD, instructionSmell, isSmelly } from '../security.mjs'
|
|
17
|
+
import { apply, storeDirFor, writeMemory } from '../index.mjs'
|
|
18
18
|
|
|
19
19
|
const captured_text = (cwd) => {
|
|
20
20
|
const captured = mount(cwd)
|
|
@@ -5,10 +5,10 @@ import {join,dirname} from 'node:path'
|
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {spawnSync} from 'node:child_process'
|
|
7
7
|
import {fileURLToPath} from 'node:url'
|
|
8
|
-
import {createMemorySnapshot,verifyMemorySnapshot,SNAPSHOT_LIMITS} from '
|
|
9
|
-
import {initializeManagedMemory,writeMemory,indexLine,withdrawManagedSource,managedMemoryStatus} from '
|
|
8
|
+
import {createMemorySnapshot,verifyMemorySnapshot,SNAPSHOT_LIMITS} from '../snapshots.mjs'
|
|
9
|
+
import {initializeManagedMemory,writeMemory,indexLine,withdrawManagedSource,managedMemoryStatus} from '../index.mjs'
|
|
10
10
|
import {createHash} from 'node:crypto'
|
|
11
|
-
const hash=b=>createHash('sha256').update(b).digest('hex'),here=dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const hash=b=>createHash('sha256').update(b).digest('hex'),here=dirname(dirname(fileURLToPath(import.meta.url)))
|
|
12
12
|
function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'memory-snapshot-'))),data=join(root,'memory'),snapshot=join(root,'backup');mkdirSync(data);try{return run({root,data,snapshot})}finally{rmSync(root,{recursive:true,force:true})}}
|
|
13
13
|
function text(data){writeFileSync(join(data,'MEMORY.md'),'- [user] Synthetic preference cyan1234\n',{mode:0o600});mkdirSync(join(data,'projects/empty'),{recursive:true})}
|
|
14
14
|
function tree(root){const rows=[];const walk=(p,r='')=>{for(const name of readdirSync(p).sort()){const f=join(p,name),n=r?r+'/'+name:name,s=lstatSync(f);if(s.isDirectory()){rows.push([n,'dir']);walk(f,n)}else rows.push([n,hash(readFileSync(f))])}};walk(root);return rows}
|
|
@@ -3,12 +3,12 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,rmSync} from 'node:fs'
|
|
4
4
|
import {tmpdir} from 'node:os'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
|
-
import {captureMessage,assessCandidate} from '
|
|
7
|
-
import {captureInputDescriptor,reconstructCaptureInput,listCaptureInputs} from '
|
|
8
|
-
import {extractWithLlm} from '
|
|
9
|
-
import {apply} from '
|
|
10
|
-
import {candidateSnapshot} from '
|
|
11
|
-
import {captureJobStatus,listCaptureJobs} from '
|
|
6
|
+
import {captureMessage,assessCandidate} from '../capture-evidence.mjs'
|
|
7
|
+
import {captureInputDescriptor,reconstructCaptureInput,listCaptureInputs} from '../capture-inputs.mjs'
|
|
8
|
+
import {extractWithLlm} from '../capture-llm.mjs'
|
|
9
|
+
import {apply} from '../index.mjs'
|
|
10
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
11
|
+
import {captureJobStatus,listCaptureJobs} from '../capture-jobs.mjs'
|
|
12
12
|
const prefix='甲'.repeat(2000),long=prefix+'但上述约定只在初稿反馈时适用,正式稿不采用。'
|
|
13
13
|
const event=text=>({seq:0,type:'user/message',data:{source:{kind:'user'},content:[{type:'text',text}]}})
|
|
14
14
|
const shortMessage=()=>({...captureMessage(event(prefix),'synthetic:0'),seq:0}),longMessage=()=>({...captureMessage(event(long),'synthetic:0'),seq:0})
|
|
@@ -4,8 +4,8 @@ import {mkdtempSync,rmSync,readFileSync,writeFileSync,existsSync} from 'node:fs'
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {stageCandidate,candidateSnapshot,sourceWithdrawalReviewKey} from '
|
|
8
|
-
import {writeMemory,indexLine,storeRevision,adoptCandidate,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from '
|
|
7
|
+
import {stageCandidate,candidateSnapshot,sourceWithdrawalReviewKey} from '../candidates.mjs'
|
|
8
|
+
import {writeMemory,indexLine,storeRevision,adoptCandidate,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from '../index.mjs'
|
|
9
9
|
const key=(session,message)=>createHash('sha256').update(JSON.stringify([session,message])).digest('hex')
|
|
10
10
|
function box(run){const dir=mkdtempSync(join(tmpdir(),'dsh-source-key-'));try{return run(dir)}finally{rmSync(dir,{recursive:true,force:true})}}
|
|
11
11
|
function adopted(dir){const id=stageCandidate(dir,{type:'user',scope:'global',summary:'Synthetic palette jade9876',detail:'Synthetic dated detail jade9876',sessionId:'owner-session',indexRevision:storeRevision(dir),evidence:[{messageId:'owner-message',hash:'b'.repeat(64),quote:'Synthetic independently supplied palette jade9876'}]}).id,review=adoptCandidate(dir,id,{scope:'global'});adoptCandidate(dir,id,{scope:'global',apply:true,reviewedIndex:review.reviewedIndex,candidateRevision:review.candidateRevision});return id}
|
|
@@ -4,10 +4,10 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync} from 'node:fs'
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {writeMemory,indexLine,searchStore,withdrawManagedSource,storeRevision,recoverMemoryPublication,forgetMemory,adoptCandidate} from '
|
|
8
|
-
import {candidateSnapshot,stageCandidate,withdrawSource,withdrawalPolicy,sourceLedgerStatus} from '
|
|
9
|
-
import {prepareSourceRetirement,validateRetiredSource,SOURCE_LEDGER_BYTES} from '
|
|
10
|
-
import {commitPublication} from '
|
|
7
|
+
import {writeMemory,indexLine,searchStore,withdrawManagedSource,storeRevision,recoverMemoryPublication,forgetMemory,adoptCandidate} from '../index.mjs'
|
|
8
|
+
import {candidateSnapshot,stageCandidate,withdrawSource,withdrawalPolicy,sourceLedgerStatus} from '../candidates.mjs'
|
|
9
|
+
import {prepareSourceRetirement,validateRetiredSource,SOURCE_LEDGER_BYTES} from '../source-retirement.mjs'
|
|
10
|
+
import {commitPublication} from '../publication.mjs'
|
|
11
11
|
const box=run=>{const root=mkdtempSync(join(tmpdir(),'source-retire-'));mkdirSync(join(root,'memory'));try{return run(join(root,'memory'))}finally{rmSync(root,{recursive:true,force:true})}}
|
|
12
12
|
const hash=value=>createHash('sha256').update(value).digest('hex')
|
|
13
13
|
function write(dir,n,{supersedes,publicationFault,unique=false}={}){const value='palette'+String(n).padStart(4,'0'),summary=unique?'Synthetic '+hash('entry-'+n).slice(0,48)+' '+value:'Synthetic retained source '+value,detail='Synthetic private detail '+value;return writeMemory({dir,scope:'global',type:'user',summary,detail,supersedes,line:indexLine('user',summary,undefined,true),provenance:{sessionId:'synthetic-retirement-owner',basis:'tool-context',via:'memory_write',callHash:hash('call-'+n),evidence:[{messageId:'owner-'+n,hash:hash('message-'+n),quote:'Synthetic owner chose '+value}]},publicationFault})}
|