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
|
@@ -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})}
|
|
@@ -4,10 +4,10 @@ import {mkdtempSync,realpathSync,mkdirSync,writeFileSync,readFileSync,readdirSyn
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {apply,storeDirFor,managedMemoryStatus,withdrawManagedSource} from '
|
|
8
|
-
import {writeFixtureMemory} from '
|
|
9
|
-
import {candidateSnapshot} from '
|
|
10
|
-
import {restorationJournalPath} from '
|
|
7
|
+
import {apply,storeDirFor,managedMemoryStatus,withdrawManagedSource} from '../index.mjs'
|
|
8
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
9
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
10
|
+
import {restorationJournalPath} from '../restoration-lock.mjs'
|
|
11
11
|
const hash=x=>createHash('sha256').update(x).digest('hex')
|
|
12
12
|
async function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'status-both-'))),home=join(root,'home'),cwd=join(root,'work'),previous=process.env.DSH_HOME;mkdirSync(home);mkdirSync(cwd);writeFileSync(join(cwd,'.dsh-root'),'');process.env.DSH_HOME=home;const f={root,home,cwd,data:join(home,'memory'),tools:[],effects:[],ctx:{}};f.ctx={systemPrompt:{context(){}},workingDirectory:{get:()=>f.cwd},tools:{register:t=>f.tools.push(t)},on(){},effect(fn){f.effects.push(fn())}};apply(f.ctx,{autoCapture:{enabled:false}});f.status=f.tools.find(t=>t.name==='memory_status');f.exec={agent:{session:{id:'status-owner'}}};try{return await run(f)}finally{for(const fn of f.effects.reverse())fn?.();if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
|
|
13
13
|
function seed(dir,summary,scope='global'){return writeFixtureMemory({dir,summary,type:'user',scope,detail:'Synthetic detail '+summary})}
|
|
@@ -40,3 +40,11 @@ test('restoration guard produces only unavailable source control for either stat
|
|
|
40
40
|
test('both metadata never turns observation discovery into cross-session access',()=>box(async f=>{
|
|
41
41
|
const before=fingerprint(f.data);await assert.rejects(f.status.execute({scope:'both',includeObservations:'yes'},f.exec),/invalid-list-option/);assert.deepEqual(fingerprint(f.data),before);await assert.rejects(f.status.execute({scope:'both',includeObservations:true},f.exec),/source-unavailable/)
|
|
42
42
|
}))
|
|
43
|
+
test('status exposes a bounded L0 lint summary',()=>box(async f=>{
|
|
44
|
+
writeFileSync(join(f.data,'MEMORY.md'),'# 索引\n\n## user\n\n- [nope] 坏类型 — user.md\n')
|
|
45
|
+
const r=await f.status.execute({scope:'global'},f.exec)
|
|
46
|
+
assert.ok(r.lint.errors>=1)
|
|
47
|
+
assert.ok(r.lint.checks.some((check)=>check.id==='I1-invalid-index-line'))
|
|
48
|
+
const text=f.status.output.render({scope:'global'},r).map((item)=>item.text).join('\n')
|
|
49
|
+
assert.match(text,/检测(L0)/)
|
|
50
|
+
}))
|
|
@@ -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
|
-
* supersede(版本替代)与来源标记。跑法:node --test ~/.dsh/plugins/memory/test-supersede.mjs
|
|
4
|
+
* supersede(版本替代)与来源标记。跑法:node --test ~/.dsh/plugins/memory/test/test-supersede.mjs
|
|
5
5
|
*
|
|
6
6
|
* 取代是「修正过期事实」的唯一正确做法:再叠一条会让新旧两条同时被注入,
|
|
7
7
|
* 模型看到矛盾信息。cc-memory 把这叫 anti-patch,MemOS 把它建模成 MemCube 的版本关系。
|
|
@@ -13,7 +13,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, rmSync, existsSync
|
|
|
13
13
|
import { join } from 'node:path'
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
|
|
16
|
-
import { storeDirFor, writeMemory } from '
|
|
16
|
+
import { storeDirFor, writeMemory } from '../index.mjs'
|
|
17
17
|
|
|
18
18
|
function sandbox() {
|
|
19
19
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-sup-')))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
const { collect } = await import('../scripts/telemetry.mjs')
|
|
8
|
+
|
|
9
|
+
const DAY = 86_400_000
|
|
10
|
+
|
|
11
|
+
function audit(now) {
|
|
12
|
+
const at = (daysAgo) => new Date(now - daysAgo * DAY).toISOString()
|
|
13
|
+
return [
|
|
14
|
+
{ op: 'capture', outcome: 'ok', inserted: 1, at: at(10) },
|
|
15
|
+
{ op: 'capture', outcome: 'ok', inserted: 0, at: at(9) },
|
|
16
|
+
{ op: 'capture', outcome: 'error', at: at(8) },
|
|
17
|
+
{ op: 'capture', outcome: 'error', at: at(7) },
|
|
18
|
+
{ op: 'capture', outcome: 'rejected', at: at(6) },
|
|
19
|
+
{ op: 'capture', outcome: 'skipped', reason: 'short-session-below-threshold', at: at(5) },
|
|
20
|
+
{ op: 'capture', outcome: 'skipped', reason: 'short-session-below-threshold', at: at(4) },
|
|
21
|
+
{ op: 'capture', outcome: 'skipped', reason: 'short-session-below-threshold', at: at(3) },
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('遥测:捕获成功率、零产出天数与短会话跳过都能算出来', () => {
|
|
26
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-telemetry-'))
|
|
27
|
+
const now = Date.parse('2026-11-08T10:00:00.000Z')
|
|
28
|
+
try {
|
|
29
|
+
writeFileSync(join(root, 'MEMORY.md'), '# 索引\n\n## project\n\n- [project] 一条 — project.md\n')
|
|
30
|
+
mkdirSync(join(root, 'projects', 'v2-x'), { recursive: true })
|
|
31
|
+
writeFileSync(join(root, 'projects', 'v2-x', 'MEMORY.md'), '# 索引\n\n## project\n\n- [project] 两条 — project.md\n')
|
|
32
|
+
writeFileSync(join(root, '.audit.jsonl'), audit(now).map((event) => JSON.stringify(event)).join('\n') + '\n')
|
|
33
|
+
const report = collect(root, now)
|
|
34
|
+
assert.equal(report.capture.total, 8)
|
|
35
|
+
assert.equal(report.capture.skipped, 3)
|
|
36
|
+
assert.equal(report.capture.successRate, 0.25)
|
|
37
|
+
assert.equal(report.output.zeroOutputDays, 10)
|
|
38
|
+
const ids = report.warnings.map((warning) => warning.id)
|
|
39
|
+
assert.ok(ids.includes('T1-low-capture-success'))
|
|
40
|
+
assert.ok(ids.includes('T2-zero-output'))
|
|
41
|
+
assert.ok(ids.includes('T3-short-sessions'))
|
|
42
|
+
assert.equal(report.stores.length, 2)
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(root, { recursive: true, force: true })
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('遥测:没有账本时不编造成功率', () => {
|
|
49
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-telemetry-empty-'))
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(join(root, 'MEMORY.md'), '# 索引\n')
|
|
52
|
+
const report = collect(root, Date.parse('2026-11-08T10:00:00.000Z'))
|
|
53
|
+
assert.equal(report.capture.total, 0)
|
|
54
|
+
assert.equal(report.capture.successRate, null)
|
|
55
|
+
assert.equal(report.output.zeroOutputDays, null)
|
|
56
|
+
assert.ok(report.warnings.some((warning) => warning.id === 'T5-no-memory'))
|
|
57
|
+
} finally {
|
|
58
|
+
rmSync(root, { recursive: true, force: true })
|
|
59
|
+
}
|
|
60
|
+
})
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import {applyWithValidationFixture as apply} from '
|
|
1
|
+
import {applyWithValidationFixture as apply} from '../fixture-host.mjs'
|
|
2
2
|
import {test} from 'node:test'
|
|
3
3
|
import assert from 'node:assert/strict'
|
|
4
4
|
import {mkdtempSync,realpathSync,mkdirSync,readFileSync,existsSync,rmSync} from 'node:fs'
|
|
5
5
|
import {join} from 'node:path'
|
|
6
6
|
import {tmpdir} from 'node:os'
|
|
7
|
-
import {indexLine, searchStore} from '
|
|
8
|
-
import {prepareMountedFixture,writeFixtureMemory} from '
|
|
9
|
-
import {candidateSnapshot,listCandidates} from '
|
|
10
|
-
import {assessCandidate} from '
|
|
11
|
-
import {normalizeCaptureDiagnostic} from '
|
|
7
|
+
import {indexLine, searchStore} from '../index.mjs'
|
|
8
|
+
import {prepareMountedFixture,writeFixtureMemory} from '../fixture-host.mjs'
|
|
9
|
+
import {candidateSnapshot,listCandidates} from '../candidates.mjs'
|
|
10
|
+
import {assessCandidate} from '../capture-evidence.mjs'
|
|
11
|
+
import {normalizeCaptureDiagnostic} from '../capture-diagnostics.mjs'
|
|
12
12
|
|
|
13
13
|
async function box(run){
|
|
14
14
|
const dir=realpathSync(mkdtempSync(join(tmpdir(),'dsh-text-admission-'))),home=join(dir,'home'),cwd=join(dir,'project'),previous=process.env.DSH_HOME
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {createToolInputTracker} from '
|
|
3
|
+
import {createToolInputTracker} from '../tool-inputs.mjs'
|
|
4
4
|
const message=(id,kind='user')=>({id,role:'user',source:{kind},content:[{type:'text',text:'Synthetic owner statement '+id}]})
|
|
5
5
|
function fixture(){const tracker=createToolInputTracker(),session={id:'synthetic-session'},agent={session};return{tracker,session,agent,pre:(turn,step,messages)=>tracker.preStep({agent,turn,step,messages}),admit:(m,seq)=>tracker.event(session,{type:'user/message',seq,data:m}),call:(callId,seq,turn=1,step=1)=>tracker.event(session,{type:'tool/call',seq,data:{callId,turn,step}}),exec:callId=>tracker.forExecution({agent,callId})}}
|
|
6
6
|
test('native call binds to the admitted owner event sequence and declares context-only basis',()=>{const f=fixture(),m=message('a');f.pre(1,1,[m]);f.admit(m,10);f.call('call',11);const proof=f.exec('call');assert.equal(proof.basis,'tool-context');assert.equal(proof.evidence[0].messageId,'synthetic-session:10');assert.match(proof.evidence[0].hash,/^[a-f0-9]{64}$/);assert.match(proof.evidence[0].quote,/owner statement/);assert.equal(JSON.stringify(proof).includes('Synthetic owner statement a'),true)})
|
|
@@ -4,12 +4,12 @@ import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync} fr
|
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
6
|
import {createHash} from 'node:crypto'
|
|
7
|
-
import {createToolInputTracker} from '
|
|
8
|
-
import {createObservationTracker,normalizeObservationCertificate,observationRoots,argumentFingerprint} from '
|
|
9
|
-
import {apply,searchStore,withdrawManagedSource,indexLine} from '
|
|
10
|
-
import {candidateSnapshot} from '
|
|
11
|
-
import {prepareSourceRetirement,validateRetiredSource} from '
|
|
12
|
-
import {writeFixtureMemory,scriptedNativeValidationLlm} from '
|
|
7
|
+
import {createToolInputTracker} from '../tool-inputs.mjs'
|
|
8
|
+
import {createObservationTracker,normalizeObservationCertificate,observationRoots,argumentFingerprint} from '../tool-observations.mjs'
|
|
9
|
+
import {apply,searchStore,withdrawManagedSource,indexLine} from '../index.mjs'
|
|
10
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
11
|
+
import {prepareSourceRetirement,validateRetiredSource} from '../source-retirement.mjs'
|
|
12
|
+
import {writeFixtureMemory,scriptedNativeValidationLlm} from '../fixture-host.mjs'
|
|
13
13
|
const hash=value=>createHash('sha256').update(value).digest('hex')
|
|
14
14
|
function fixture(){
|
|
15
15
|
const inputs=createToolInputTracker(),session={id:'native-observation-session',deriveMessages:()=>messages},agent={session},obs=createObservationTracker((session,callId)=>inputs.forExecution({agent:{session},callId})),messages=[]
|
|
@@ -5,9 +5,9 @@ import assert from 'node:assert/strict'
|
|
|
5
5
|
import {mkdtempSync,mkdirSync,rmSync,existsSync,readFileSync} from 'node:fs'
|
|
6
6
|
import {join} from 'node:path'
|
|
7
7
|
import {tmpdir} from 'node:os'
|
|
8
|
-
import {apply,storeDirFor} from '
|
|
9
|
-
import {candidateSnapshot} from '
|
|
10
|
-
import {scriptedNativeValidationLlm} from '
|
|
8
|
+
import {apply,storeDirFor} from '../index.mjs'
|
|
9
|
+
import {candidateSnapshot} from '../candidates.mjs'
|
|
10
|
+
import {scriptedNativeValidationLlm} from '../fixture-host.mjs'
|
|
11
11
|
async function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'tool-write-guard-'))),home=join(root,'home'),previous=process.env.DSH_HOME;mkdirSync(home);process.env.DSH_HOME=home;const handlers=[],tools=[],effects=[],ctx={llm:scriptedNativeValidationLlm(),systemPrompt:{context(){}},workingDirectory:{get:()=>root},tools:{register:tool=>tools.push(tool)},on:(name,handler)=>handlers.push([name,handler]),effect:fn=>{const dispose=fn();if(typeof dispose==='function')effects.push(dispose)}};apply(ctx,{autoCapture:{enabled:false}});const session={id:'synthetic-owner-session'},agent={session,options:{provider:'fixture',model:'synthetic'}},tool=tools.find(tool=>tool.name==='memory_write'),emit=(name,...args)=>{for(const [event,handler] of handlers)if(event===name)handler(...args)},input={id:'owner-input',source:{kind:'user'},role:'user',content:[{type:'text',text:'Synthetic owner statement jade9876'}]},exec={agent,callId:'native-call',rootCallId:'native-call'};const admit=()=>{emit('agent/pre-step',{agent,turn:1,step:1,messages:[input]},()=>({kind:'enter'}));emit('session/event',session,{type:'user/message',seq:10,data:input});emit('session/event',session,{type:'tool/call',seq:11,data:{turn:1,step:1,callId:'native-call'}})};try{return await run({root,dir:join(home,'memory'),tool,exec,emit,input,admit,effects,session})}finally{for(const dispose of effects)dispose();if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
|
|
12
12
|
const args={meaning:{kind:'claim'},type:'user',scope:'global',summary:'Synthetic selected jade9876',detail:'Synthetic detail jade9876'}
|
|
13
13
|
test('SDK tool rejects missing owner history, actor and forged model-supplied evidence',()=>box(async f=>{for(const exec of [f.exec,{}, {agent:{session:{id:'synthetic-other'}}}])await assert.rejects(()=>f.tool.execute({...args,evidence:[{messageId:'forged'}]},exec),/source-unavailable/);assert.equal(existsSync(join(f.dir,'MEMORY.md')),false)}))
|
|
@@ -3,9 +3,9 @@ import assert from 'node:assert/strict'
|
|
|
3
3
|
import {mkdtempSync,rmSync,readFileSync,writeFileSync,existsSync,cpSync,mkdirSync} from 'node:fs'
|
|
4
4
|
import {join} from 'node:path'
|
|
5
5
|
import {tmpdir} from 'node:os'
|
|
6
|
-
import {writeMemory,indexLine,searchStore,withdrawManagedSource,withdrawManagedSourceKey,forgetMemory,adoptCandidate,recoverMemoryPublication,storeRevision,lookupLinks} from '
|
|
7
|
-
import {candidateSnapshot,stageCandidate,withdrawSource,withdrawalPolicy,pruneCandidates,rejectCandidate} from '
|
|
8
|
-
import {filterWithdrawn} from '
|
|
6
|
+
import {writeMemory,indexLine,searchStore,withdrawManagedSource,withdrawManagedSourceKey,forgetMemory,adoptCandidate,recoverMemoryPublication,storeRevision,lookupLinks} from '../index.mjs'
|
|
7
|
+
import {candidateSnapshot,stageCandidate,withdrawSource,withdrawalPolicy,pruneCandidates,rejectCandidate} from '../candidates.mjs'
|
|
8
|
+
import {filterWithdrawn} from '../withdrawal.mjs'
|
|
9
9
|
const proof=(n=1)=>({sessionId:'synthetic-owner',basis:'tool-context',via:'memory_write',callHash:String(n).padStart(64,'a'),evidence:[{messageId:'owner-message-'+n,hash:'b'.repeat(64),quote:'Synthetic owner statement for this turn '+n}]})
|
|
10
10
|
const box=run=>{const root=mkdtempSync(join(tmpdir(),'tool-write-life-')),dir=join(root,'memory');mkdirSync(dir);try{return run(dir,root)}finally{rmSync(root,{recursive:true,force:true})}}
|
|
11
11
|
function write(dir,summary='Synthetic selected palette jade9876',options={}){const detail=options.detail??'Synthetic private detail jade9876';return writeMemory({dir,type:'user',scope:'global',summary,detail,line:indexLine('user',summary,options.expires,detail.trim().length>0),provenance:proof(options.n??1),...options})}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {writeFixtureMemory} from '
|
|
1
|
+
import {writeFixtureMemory} from '../fixture-host.mjs'
|
|
2
2
|
import {realpathSync} from 'node:fs'
|
|
3
|
-
import {applyWithOwnerFixture as apply} from '
|
|
3
|
+
import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
|
|
4
4
|
/**
|
|
5
|
-
* 可用性测试。跑法:node --test ~/.dsh/plugins/memory/test-usability.mjs
|
|
5
|
+
* 可用性测试。跑法:node --test ~/.dsh/plugins/memory/test/test-usability.mjs
|
|
6
6
|
*
|
|
7
7
|
* 这份文件验收五条指标(对应计划里的 U1–U5):
|
|
8
8
|
* U1 无死路:写入失败必须给出已存在的下一步;满索引一次 compact 恢复可写
|
|
@@ -22,7 +22,7 @@ import { join, dirname } from 'node:path'
|
|
|
22
22
|
import { tmpdir } from 'node:os'
|
|
23
23
|
import { fileURLToPath } from 'node:url'
|
|
24
24
|
|
|
25
|
-
import { countFrameTokens } from '
|
|
25
|
+
import { countFrameTokens } from '../security.mjs'
|
|
26
26
|
import {
|
|
27
27
|
|
|
28
28
|
pruneStore,
|
|
@@ -32,9 +32,9 @@ import {
|
|
|
32
32
|
statusOf,
|
|
33
33
|
storeDirFor,
|
|
34
34
|
writeMemory,
|
|
35
|
-
} from '
|
|
35
|
+
} from '../index.mjs'
|
|
36
36
|
|
|
37
|
-
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
37
|
+
const HERE = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
38
38
|
|
|
39
39
|
function sandbox() {
|
|
40
40
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-usability-')))
|
|
@@ -405,7 +405,7 @@ test('compact:超大主题文件轮转,内容不丢,之后能继续追加'
|
|
|
405
405
|
assert.equal(existsSync(join(dir, 'user.md')), false)
|
|
406
406
|
assert.equal(statSync(join(dir, report.archived[0].to)).size, Buffer.byteLength(big), '内容必须逐字保留')
|
|
407
407
|
|
|
408
|
-
const { appendTopic } = await import('
|
|
408
|
+
const { appendTopic } = await import('../index.mjs')
|
|
409
409
|
appendTopic(dir, 'user', '轮转后仍可追加')
|
|
410
410
|
assert.match(readFileSync(join(dir, 'user.md'), 'utf8'), /轮转后仍可追加/)
|
|
411
411
|
} finally {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {prepareValidationPrompt,ValidationSubmission,validateCandidatesWithLlm} from '
|
|
4
|
-
import {VALIDATION_FIELDS} from '
|
|
3
|
+
import {prepareValidationPrompt,ValidationSubmission,validateCandidatesWithLlm} from '../capture-validation.mjs'
|
|
4
|
+
import {VALIDATION_FIELDS} from '../capture-validation-contract.mjs'
|
|
5
5
|
const messages=['跨所有项目的初稿反馈先列两处问题再给办法。','这一长期约定在正式稿上不适用。','请保留这项约定,以供之后的项目使用。'].map((text,i)=>({id:'synthetic-'+i,role:'user',text,hash:String(i).repeat(64)}))
|
|
6
6
|
const candidate={type:'user',scope:'global',basis:'explicit',summary:'跨项目的初稿反馈先列两处问题再给办法',meaning:{kind:'preference',conditions:[{dimension:'task',value:'review'},{dimension:'document_kind',value:'draft'}]},evidence:messages.slice(0,2).map(({id,text})=>({messageId:id,quote:text}))}
|
|
7
7
|
function setup(){const prepared=prepareValidationPrompt(messages,[candidate]),r=prepared.records[0],check={candidateId:r.candidateId,fields:Object.fromEntries(VALIDATION_FIELDS.map(k=>[k,'supported'])),taskRestriction:'none',sourceRefs:Object.keys(r.requiredSourceRefs),evidence:[]};return{prepared,check}}
|
package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from 'node:test'
|
|
2
2
|
import assert from 'node:assert/strict'
|
|
3
|
-
import {validationTransitionContext as build} from '
|
|
3
|
+
import {validationTransitionContext as build} from '../validation-transition-context.mjs'
|
|
4
4
|
const record=(n,patch={})=>({id:String(n).padStart(64,'a'),status:'recorded',scope:'global',type:'user',summary:'Synthetic default palette '+n,indexLine:'- [user] Synthetic default palette '+n+' — 索引摘要',meaning:{kind:'preference',conditions:[],except:[]},...patch});
|
|
5
5
|
const target={summary:'Synthetic default palette new',scope:'global'};
|
|
6
6
|
test('current same-scope interpretations are data and cannot expose old quotes, bodies or other projects',()=>{
|
|
@@ -4,11 +4,11 @@ import { mkdtempSync, readFileSync, writeFileSync, readdirSync, rmSync, symlinkS
|
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
|
-
import { stageCandidate, listCandidates } from '
|
|
8
|
-
import { adoptCandidate, withdrawManagedSource, recoverMemoryPublication, storeRevision, searchStore, writeMemory, indexLine } from '
|
|
9
|
-
import { commitPublication } from '
|
|
10
|
-
import { withLock } from '
|
|
11
|
-
import { MANAGED_LOG_BYTES, MANAGED_LOG_TOTAL_BYTES, isManagedLogName } from '
|
|
7
|
+
import { stageCandidate, listCandidates } from '../candidates.mjs'
|
|
8
|
+
import { adoptCandidate, withdrawManagedSource, recoverMemoryPublication, storeRevision, searchStore, writeMemory, indexLine } from '../index.mjs'
|
|
9
|
+
import { commitPublication } from '../publication.mjs'
|
|
10
|
+
import { withLock } from '../security.mjs'
|
|
11
|
+
import { MANAGED_LOG_BYTES, MANAGED_LOG_TOTAL_BYTES, isManagedLogName } from '../managed-logs.mjs'
|
|
12
12
|
|
|
13
13
|
function box(run) {
|
|
14
14
|
const dir = mkdtempSync(join(tmpdir(), 'dsh-archive-withdrawal-'))
|
|
@@ -140,7 +140,7 @@ test('forty-one-file withdrawal survives actual SIGKILL and finishes history/arc
|
|
|
140
140
|
for (const base of ['.audit.jsonl', '.history.jsonl']) for (let suffix = 1; suffix <= 16; suffix++) writeFileSync(join(dir, `${base}.${suffix}`), readFileSync(join(dir, base)))
|
|
141
141
|
const preview = withdrawManagedSource(dir, 'archive-source', 'message-1')
|
|
142
142
|
const options = { apply: true, reviewedIndex: preview.reviewedIndex, candidateRevision: preview.candidateRevision }
|
|
143
|
-
const worker = `import {withdrawManagedSource} from ${JSON.stringify(new URL('
|
|
143
|
+
const worker = `import {withdrawManagedSource} from ${JSON.stringify(new URL('../index.mjs', import.meta.url).href)};withdrawManagedSource(process.argv[1],'archive-source','message-1',{...JSON.parse(process.argv[2]),publicationFault:event=>{if(event.phase==='file'&&event.index===34)process.kill(process.pid,'SIGKILL')}})`
|
|
144
144
|
const stopped = spawnSync(process.execPath, ['--input-type=module', '-e', worker, dir, JSON.stringify(options)], { encoding: 'utf8', timeout: 15000 })
|
|
145
145
|
assert.equal(stopped.signal, 'SIGKILL', stopped.stderr)
|
|
146
146
|
const journal=JSON.parse(readFileSync(join(dir, '.publication.json')))
|
|
@@ -4,8 +4,8 @@ import assert from 'node:assert/strict'
|
|
|
4
4
|
import { mkdtempSync, readFileSync, writeFileSync, rmSync, readdirSync, cpSync } from 'node:fs'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { tmpdir } from 'node:os'
|
|
7
|
-
import { stageCandidate, listCandidates, pruneCandidates, withdrawSource } from '
|
|
8
|
-
import { adoptCandidate, withdrawManagedSource, recoverMemoryPublication, writeMemory, storeRevision, indexLine, searchStore, lookupLinks, statusOf, forgetMemory, apply } from '
|
|
7
|
+
import { stageCandidate, listCandidates, pruneCandidates, withdrawSource } from '../candidates.mjs'
|
|
8
|
+
import { adoptCandidate, withdrawManagedSource, recoverMemoryPublication, writeMemory, storeRevision, indexLine, searchStore, lookupLinks, statusOf, forgetMemory, apply } from '../index.mjs'
|
|
9
9
|
|
|
10
10
|
function box(run) { const dir = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-withdrawal-'))); try { return run(dir) } finally { rmSync(dir, { recursive: true, force: true }) } }
|
|
11
11
|
function adopted(dir, detail = '唯一细节:只用于室内对白镜头。', source = 'source-1') {
|