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.
Files changed (98) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/COMPATIBILITY.md +8 -5
  3. package/DETECTION.md +139 -0
  4. package/DREAMING-DESIGN.md +167 -0
  5. package/README.en.md +33 -6
  6. package/README.md +38 -7
  7. package/RELEASE.md +26 -0
  8. package/SECURITY.md +7 -0
  9. package/context-history.mjs +20 -3
  10. package/detect.mjs +441 -0
  11. package/eval/bench-detect.mjs +107 -0
  12. package/eval/semantic/cases.json +387 -0
  13. package/eval/semantic-suite.mjs +156 -0
  14. package/index.mjs +82 -10
  15. package/managed-policy.mjs +3 -1
  16. package/package.json +90 -69
  17. package/scripts/demo-source-recall.mjs +123 -0
  18. package/scripts/detect.mjs +69 -0
  19. package/scripts/dream.mjs +205 -0
  20. package/scripts/probe-history.mjs +92 -0
  21. package/scripts/probe-load.mjs +1 -1
  22. package/scripts/probe-package.mjs +6 -2
  23. package/scripts/security-check.mjs +173 -0
  24. package/scripts/soak.mjs +219 -0
  25. package/scripts/telemetry.mjs +187 -0
  26. package/scripts/verify.mjs +3 -1
  27. package/{test-adversarial.mjs → test/test-adversarial.mjs} +5 -5
  28. package/{test-belief-revision.mjs → test/test-belief-revision.mjs} +7 -7
  29. package/{test-candidates.mjs → test/test-candidates.mjs} +4 -4
  30. package/{test-capture-diagnostics.mjs → test/test-capture-diagnostics.mjs} +6 -6
  31. package/{test-capture-field-lineage.mjs → test/test-capture-field-lineage.mjs} +4 -4
  32. package/{test-capture-inputs.mjs → test/test-capture-inputs.mjs} +8 -8
  33. package/{test-capture-jobs.mjs → test/test-capture-jobs.mjs} +5 -5
  34. package/{test-capture-projections.mjs → test/test-capture-projections.mjs} +12 -12
  35. package/{test-capture-revision.mjs → test/test-capture-revision.mjs} +6 -6
  36. package/{test-capture-submission.mjs → test/test-capture-submission.mjs} +3 -3
  37. package/{test-capture-validation-flow.mjs → test/test-capture-validation-flow.mjs} +5 -5
  38. package/{test-capture-validation-job.mjs → test/test-capture-validation-job.mjs} +5 -5
  39. package/{test-capture-validation-lineage.mjs → test/test-capture-validation-lineage.mjs} +2 -2
  40. package/{test-capture-validation.mjs → test/test-capture-validation.mjs} +2 -2
  41. package/{test-capture.mjs → test/test-capture.mjs} +20 -20
  42. package/{test-compaction-compat.mjs → test/test-compaction-compat.mjs} +5 -5
  43. package/{test-compat.mjs → test/test-compat.mjs} +3 -3
  44. package/{test-concurrency.mjs → test/test-concurrency.mjs} +3 -3
  45. package/{test-conflict.mjs → test/test-conflict.mjs} +2 -2
  46. package/{test-context-history.mjs → test/test-context-history.mjs} +1 -1
  47. package/{test-control-schema.mjs → test/test-control-schema.mjs} +1 -1
  48. package/test/test-detect.mjs +125 -0
  49. package/test/test-dream-plan.mjs +72 -0
  50. package/test/test-dsh-registration.mjs +111 -0
  51. package/{test-episodes.mjs → test/test-episodes.mjs} +5 -5
  52. package/{test-explicit-memory.mjs → test/test-explicit-memory.mjs} +4 -4
  53. package/{test-field-lineage.mjs → test/test-field-lineage.mjs} +4 -4
  54. package/{test-history.mjs → test/test-history.mjs} +4 -4
  55. package/{test-inline-summary.mjs → test/test-inline-summary.mjs} +5 -5
  56. package/{test-legacy-source-identity.mjs → test/test-legacy-source-identity.mjs} +2 -2
  57. package/{test-links.mjs → test/test-links.mjs} +4 -4
  58. package/{test-managed-access.mjs → test/test-managed-access.mjs} +29 -5
  59. package/{test-managed-activation.mjs → test/test-managed-activation.mjs} +5 -5
  60. package/{test-meaning.mjs → test/test-meaning.mjs} +6 -6
  61. package/{test-native-service-lifecycle.mjs → test/test-native-service-lifecycle.mjs} +1 -1
  62. package/{test-native-write-validation.mjs → test/test-native-write-validation.mjs} +4 -4
  63. package/{test-packaging.mjs → test/test-packaging.mjs} +32 -5
  64. package/{test-paired-analysis.mjs → test/test-paired-analysis.mjs} +1 -1
  65. package/{test-patch.mjs → test/test-patch.mjs} +11 -11
  66. package/{test-plugin-contract.mjs → test/test-plugin-contract.mjs} +4 -4
  67. package/{test-privacy.mjs → test/test-privacy.mjs} +3 -3
  68. package/{test-project-stores.mjs → test/test-project-stores.mjs} +5 -5
  69. package/{test-publication.mjs → test/test-publication.mjs} +4 -4
  70. package/{test-restore.mjs → test/test-restore.mjs} +11 -11
  71. package/{test-retrieval.mjs → test/test-retrieval.mjs} +2 -2
  72. package/{test-revision-admission.mjs → test/test-revision-admission.mjs} +3 -3
  73. package/{test-rule-admission.mjs → test/test-rule-admission.mjs} +8 -8
  74. package/{test-runtime-startup.mjs → test/test-runtime-startup.mjs} +2 -2
  75. package/{test-schema.mjs → test/test-schema.mjs} +2 -2
  76. package/{test-security.mjs → test/test-security.mjs} +6 -6
  77. package/test/test-semantic-suite.mjs +20 -0
  78. package/{test-service-readiness.mjs → test/test-service-readiness.mjs} +1 -1
  79. package/{test-smell.mjs → test/test-smell.mjs} +4 -4
  80. package/{test-snapshots.mjs → test/test-snapshots.mjs} +3 -3
  81. package/{test-source-completeness.mjs → test/test-source-completeness.mjs} +6 -6
  82. package/{test-source-key.mjs → test/test-source-key.mjs} +2 -2
  83. package/{test-source-retirement.mjs → test/test-source-retirement.mjs} +4 -4
  84. package/{test-status-both.mjs → test/test-status-both.mjs} +12 -4
  85. package/{test-supersede.mjs → test/test-supersede.mjs} +3 -3
  86. package/test/test-telemetry.mjs +60 -0
  87. package/{test-text-admission.mjs → test/test-text-admission.mjs} +6 -6
  88. package/{test-tool-inputs.mjs → test/test-tool-inputs.mjs} +1 -1
  89. package/{test-tool-observations.mjs → test/test-tool-observations.mjs} +6 -6
  90. package/{test-tool-write-guard.mjs → test/test-tool-write-guard.mjs} +3 -3
  91. package/{test-tool-write-lifecycle.mjs → test/test-tool-write-lifecycle.mjs} +3 -3
  92. package/{test-usability.mjs → test/test-usability.mjs} +7 -7
  93. package/{test-validation-source-refs.mjs → test/test-validation-source-refs.mjs} +2 -2
  94. package/{test-validation-transition-context.mjs → test/test-validation-transition-context.mjs} +1 -1
  95. package/{test-withdrawal-archives.mjs → test/test-withdrawal-archives.mjs} +6 -6
  96. package/{test-withdrawal.mjs → test/test-withdrawal.mjs} +2 -2
  97. package/{test.mjs → test/test.mjs} +4 -4
  98. package/version.mjs +1 -1
@@ -0,0 +1,111 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, existsSync, readFileSync, rmSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+ import { setupMemoryHistory } from '../context-history.mjs'
7
+ import { apply } from '../index.mjs'
8
+
9
+ function deferredRegistration() {
10
+ let resolve, reject, calls = 0
11
+ const pending = new Promise((yes, no) => { resolve = yes; reject = no })
12
+ // Real DSH deliberately returns a callable thenable, not a native Promise.
13
+ const registration = Object.assign(() => { calls++ }, { then: pending.then.bind(pending) })
14
+ return { registration, resolve, reject, get calls() { return calls } }
15
+ }
16
+ const header = 'Current runtime context. This snapshot supersedes earlier runtime-context snapshots.'
17
+ function oldSession() {
18
+ const sections = [{ name: 'memory:index', text: 'old synthetic memory' }]
19
+ let messages = [{ id: 'old', role: 'user', source: { kind: 'runtime-context', sections }, content: [{ type: 'text', text: `${header}\n\nold synthetic memory` }] }]
20
+ return { id: 'synthetic-session', surface: { nodes: [1] }, deriveMessages: () => messages,
21
+ append(type, message, options) { assert.equal(type, 'user/message'); assert.deepEqual(options.sourceEventSeqs, [1]); messages = [message] } }
22
+ }
23
+
24
+ test('callable asynchronous projection registration publishes no hooks before its retained handle is ready', async () => {
25
+ const deferred = deferredRegistration(), handlers = new Map(), session = oldSession()
26
+ let reads = 0
27
+ const ctx = { sessionProjections: { register: () => deferred.registration,
28
+ stateOf() { throw Error('deprecated registry reader must not be used') } },
29
+ workingDirectory: { get: () => '/synthetic' }, on: (name, fn) => handlers.set(name, fn) }
30
+ const pending = setupMemoryHistory(ctx, () => 'new synthetic memory')
31
+ assert.equal(handlers.size, 0)
32
+ deferred.resolve({ stateOf(value) { assert.equal(value, session); reads++; return { nodes: [{ id: 'old', seq: 1 }], overflow: false } } })
33
+ const history = await pending
34
+ assert.equal(history.available, true)
35
+ assert.equal(deferred.calls, 0, 'await must not invoke the legacy disposer')
36
+ assert.equal(handlers.get('agent/request')({ agent: { session } }, () => 'continued'), 'continued')
37
+ assert.equal(reads, 1)
38
+ assert.doesNotMatch(JSON.stringify(session.deriveMessages()), /old synthetic memory/)
39
+ })
40
+
41
+ test('handle-only hosts need no deprecated registry stateOf method', async () => {
42
+ const handlers = new Map()
43
+ const history = await setupMemoryHistory({ sessionProjections: { register: async () => ({ stateOf: () => ({ nodes: [], overflow: false }) }) }, on: (name, fn) => handlers.set(name, fn) }, () => '')
44
+ assert.equal(history.available, true)
45
+ assert.equal(handlers.size, 4)
46
+ })
47
+
48
+ test('rejected projection registration installs no history consumers', async () => {
49
+ const deferred = deferredRegistration(), handlers = []
50
+ const pending = setupMemoryHistory({ sessionProjections: { register: () => deferred.registration }, on: (...args) => handlers.push(args) }, () => '')
51
+ deferred.reject(Error('synthetic-registration-rejected'))
52
+ await assert.rejects(pending, /synthetic-registration-rejected/)
53
+ assert.deepEqual(handlers, [])
54
+ })
55
+
56
+ test('malformed ready handle is rejected rather than advertised as available', async () => {
57
+ await assert.rejects(setupMemoryHistory({ sessionProjections: { register: async () => ({}) } }, () => ''), /memory-context-reader-unavailable/)
58
+ })
59
+
60
+ for (const success of [true, false]) test(`apply awaits registration before tools, prompts and success marker (${success ? 'success' : 'failure'})`, async () => {
61
+ const root = mkdtempSync(join(tmpdir(), 'dsh-registration-')), previous = process.env.DSH_HOME
62
+ process.env.DSH_HOME = root
63
+ const deferred = deferredRegistration(), tools = [], prompts = [], effects = [], handlers = []
64
+ const ctx = { sessionProjections: { register: () => deferred.registration },
65
+ systemPrompt: { context: value => prompts.push(value) }, workingDirectory: { get: () => root }, tools: { register: value => tools.push(value) },
66
+ on: (...args) => handlers.push(args), effect(fn) { effects.push(fn()) } }
67
+ try {
68
+ const pending = apply(ctx, { autoCapture: { enabled: false } })
69
+ assert.equal(typeof pending?.then, 'function')
70
+ assert.deepEqual(tools, []); assert.deepEqual(prompts, []); assert.deepEqual(handlers, [])
71
+ const marker = join(root, 'memory', '.loaded.json')
72
+ assert.equal(existsSync(marker), false)
73
+ if (success) {
74
+ deferred.resolve({ stateOf: () => ({ nodes: [], overflow: false }) })
75
+ await pending
76
+ assert.equal(tools.length, 6); assert.equal(prompts.length, 2)
77
+ assert.equal(JSON.parse(readFileSync(marker)).memoryHistory.available, true)
78
+ } else {
79
+ deferred.reject(Error('synthetic-owner-disposed'))
80
+ await assert.rejects(pending, /synthetic-owner-disposed/)
81
+ assert.equal(existsSync(marker), false); assert.deepEqual(tools, []); assert.deepEqual(prompts, []); assert.deepEqual(handlers, [])
82
+ }
83
+ assert.equal(deferred.calls, 0)
84
+ } finally {
85
+ for (const dispose of effects.reverse()) dispose?.()
86
+ if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous
87
+ rmSync(root, { recursive: true, force: true })
88
+ }
89
+ })
90
+
91
+
92
+ test('a successful handle arriving after owner disposal cannot activate history or tools', async () => {
93
+ const root = mkdtempSync(join(tmpdir(), 'dsh-disposed-registration-')), previous = process.env.DSH_HOME
94
+ process.env.DSH_HOME = root
95
+ const deferred = deferredRegistration(), consumers = [], fiber = { uid: 1 }
96
+ const ctx = { fiber, sessionProjections: { register: () => deferred.registration },
97
+ systemPrompt: { context: value => consumers.push(value) }, workingDirectory: { get: () => root },
98
+ tools: { register: value => consumers.push(value) }, on: (...args) => consumers.push(args) }
99
+ try {
100
+ const pending = apply(ctx, { autoCapture: { enabled: false } })
101
+ fiber.uid = null
102
+ deferred.resolve({ stateOf: () => ({ nodes: [], overflow: false }) })
103
+ await pending
104
+ assert.deepEqual(consumers, [])
105
+ assert.equal(existsSync(join(root, 'memory', '.loaded.json')), false)
106
+ assert.equal(deferred.calls, 0)
107
+ } finally {
108
+ if (previous === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = previous
109
+ rmSync(root, { recursive: true, force: true })
110
+ }
111
+ })
@@ -4,11 +4,11 @@ import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync,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,indexLine,searchStore,forgetMemory,forgetExpired,pruneStore,withdrawManagedSource,recoverMemoryPublication,adoptCandidate,storeRevision,storeDirFor} from './index.mjs'
8
- import {candidateSnapshot,stageCandidate} from './candidates.mjs'
9
- import {writeFixtureMemory} from './fixture-host.mjs'
10
- import {episodeSnapshot,prepareEpisodes,historyRows,EPISODE_FILE,EPISODE_LIMIT} from './episodes.mjs'
11
- import {toolProposal} from './source-record.mjs'
7
+ import {apply,indexLine,searchStore,forgetMemory,forgetExpired,pruneStore,withdrawManagedSource,recoverMemoryPublication,adoptCandidate,storeRevision,storeDirFor} from '../index.mjs'
8
+ import {candidateSnapshot,stageCandidate} from '../candidates.mjs'
9
+ import {writeFixtureMemory} from '../fixture-host.mjs'
10
+ import {episodeSnapshot,prepareEpisodes,historyRows,EPISODE_FILE,EPISODE_LIMIT} from '../episodes.mjs'
11
+ import {toolProposal} from '../source-record.mjs'
12
12
 
13
13
  const digest=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex')
14
14
  async function box(run){
@@ -1,9 +1,9 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import {memoryProtocol} from './memory-protocol.mjs'
4
- import {captureSourceAliases} from './capture-source-aliases.mjs'
5
- import {extractWithLlm} from './capture-llm.mjs'
6
- import {assessCandidate} from './capture-evidence.mjs'
3
+ import {memoryProtocol} from '../memory-protocol.mjs'
4
+ import {captureSourceAliases} from '../capture-source-aliases.mjs'
5
+ import {extractWithLlm} from '../capture-llm.mjs'
6
+ import {assessCandidate} from '../capture-evidence.mjs'
7
7
 
8
8
  test('protocol reports scoped availability and remains available with no memory data', () => {
9
9
  const agent={session:{id:'owner'}}
@@ -5,10 +5,10 @@ import {createHash} from 'node:crypto'
5
5
  import {mkdtempSync,mkdirSync,rmSync,readFileSync,writeFileSync} from 'node:fs'
6
6
  import {join} from 'node:path'
7
7
  import {tmpdir} from 'node:os'
8
- import {compileValueRevision,normalizeFieldLineage} from './field-lineage.mjs'
9
- import {writeMemory,indexLine,searchStore,withdrawManagedSource,forgetMemory,recoverMemoryPublication,storeRevision} from './index.mjs'
10
- import {candidateSnapshot,stageCandidate,rejectCandidate,pruneCandidates} from './candidates.mjs'
11
- import {episodeSnapshot} from './episodes.mjs'
8
+ import {compileValueRevision,normalizeFieldLineage} from '../field-lineage.mjs'
9
+ import {writeMemory,indexLine,searchStore,withdrawManagedSource,forgetMemory,recoverMemoryPublication,storeRevision} from '../index.mjs'
10
+ import {candidateSnapshot,stageCandidate,rejectCandidate,pruneCandidates} from '../candidates.mjs'
11
+ import {episodeSnapshot} from '../episodes.mjs'
12
12
  const hash=v=>createHash('sha256').update(JSON.stringify(v)).digest('hex')
13
13
  const meaning={kind:'preference',conditions:[{dimension:'document_kind',value:'draft'}],except:[{dimension:'audience',value:'experts'}]}
14
14
  const proof=n=>({sessionId:'owner-session-'+n,basis:'tool-context',via:'memory_write',callHash:hash(['call',n]),evidence:[{messageId:'message-'+n,hash:hash(['message',n]),quote:'Synthetic source '+n+' for lasting draft palette.'}]})
@@ -1,7 +1,7 @@
1
1
  import {realpathSync} from 'node:fs'
2
- import {applyWithOwnerFixture as apply} from './fixture-host.mjs'
2
+ import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
3
3
  /**
4
- * 历史链与写入相似度闸。跑法:node --test ~/.dsh/plugins/memory/test-history.mjs
4
+ * 历史链与写入相似度闸。跑法:node --test ~/.dsh/plugins/memory/test/test-history.mjs
5
5
  *
6
6
  * 两件事各自的「为什么」:
7
7
  * · 历史链:审计回答「做过什么」,历史回答「原来是什么」。取代/删除后原文必须可回溯,
@@ -24,7 +24,7 @@ import {
24
24
  storeDirFor,
25
25
  summaryOf,
26
26
  writeMemory,
27
- } from './index.mjs'
27
+ } from '../index.mjs'
28
28
 
29
29
  function sandbox() {
30
30
  const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-hist-')))
@@ -272,7 +272,7 @@ test('工具调用遇到近似重复时,错误信息足以让模型自行修
272
272
  // ── 审计发现的两个数据安全问题(回归) ──────────────────────────────────────
273
273
 
274
274
  test('主题文件超出读取上限时拒绝追加,绝不覆盖历史', async () => {
275
- const { appendTopic } = await import('./index.mjs')
275
+ const { appendTopic } = await import('../index.mjs')
276
276
  const box = sandbox()
277
277
  try {
278
278
  const dir = join(box.cwd, 'store')
@@ -1,14 +1,14 @@
1
1
  import {realpathSync} from 'node:fs'
2
- import {applyWithOwnerFixture as apply} from './fixture-host.mjs'
2
+ import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
3
3
  import {test} from 'node:test'
4
4
  import assert from 'node:assert/strict'
5
5
  import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,existsSync} from 'node:fs'
6
6
  import {join} from 'node:path'
7
7
  import {tmpdir} from 'node:os'
8
- import {writeMemory,storeDirFor,indexLine,summaryOf,isExpired,lookupLinks,storeRevision,adoptCandidate,withdrawManagedSource,searchStore} from './index.mjs'
9
- import {stageCandidate} from './candidates.mjs'
10
- import {entryId} from './security.mjs'
11
- const legacy=JSON.parse(readFileSync(new URL('./corpus/legacy-summary-v15.json',import.meta.url)))
8
+ import {writeMemory,storeDirFor,indexLine,summaryOf,isExpired,lookupLinks,storeRevision,adoptCandidate,withdrawManagedSource,searchStore} from '../index.mjs'
9
+ import {stageCandidate} from '../candidates.mjs'
10
+ import {entryId} from '../security.mjs'
11
+ const legacy=JSON.parse(readFileSync(new URL('../corpus/legacy-summary-v15.json',import.meta.url)))
12
12
  function box(scope='global'){const root=realpathSync(mkdtempSync(join(tmpdir(),'dsh-inline-summary-'))),home=join(root,'home');mkdirSync(home,{recursive:true});const previous=process.env.DSH_HOME;process.env.DSH_HOME=home;const dir=storeDirFor(scope,root);mkdirSync(dir,{recursive:true});let mounted
13
13
  function mount(){if(mounted)return mounted;const tools=[],captured={};apply({systemPrompt:{context:provider=>captured.provider=provider},tools:{register:tool=>tools.push(tool)},workingDirectory:{get:()=>root},on(){}},{autoCapture:{enabled:false}});return mounted={tools,provider:captured.provider}}
14
14
  return{root,dir,get tools(){return mount().tools},get provider(){return mount().provider},close(){if(previous===undefined)delete process.env.DSH_HOME;else process.env.DSH_HOME=previous;rmSync(root,{recursive:true,force:true})}}
@@ -4,8 +4,8 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,realpathSync} fr
4
4
  import {join} from 'node:path'
5
5
  import {tmpdir} from 'node:os'
6
6
  import {createHash} from 'node:crypto'
7
- import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from './index.mjs'
8
- import {withdrawalSelector,discoverWithdrawalIdentity} from './source-identity.mjs'
7
+ import {initializeManagedMemory,managedMemoryStatus,writeMemory,indexLine,withdrawManagedSource,withdrawManagedSourceKey,searchStore} from '../index.mjs'
8
+ import {withdrawalSelector,discoverWithdrawalIdentity} from '../source-identity.mjs'
9
9
  const hash=value=>createHash('sha256').update(value).digest('hex'),key=(sid,msg)=>hash(JSON.stringify([sid,msg]))
10
10
  const sid='synthetic-current-owner',msg='synthetic-current-message',target=key(sid,msg)
11
11
  function box(run,{legacySid='synthetic-different-legacy-owner',auditOnly=false}={}){
@@ -1,7 +1,7 @@
1
1
  import {realpathSync} from 'node:fs'
2
- import {applyWithOwnerFixture as apply} from './fixture-host.mjs'
2
+ import {applyWithOwnerFixture as apply} from '../fixture-host.mjs'
3
3
  /**
4
- * 条目 id、关系边与多跳查询。跑法:node --test ~/.dsh/plugins/memory/test-links.mjs
4
+ * 条目 id、关系边与多跳查询。跑法:node --test ~/.dsh/plugins/memory/test/test-links.mjs
5
5
  *
6
6
  * 对应两项差距:MemOS 的「版本可查询」与 A-MEM 的「插入时建链」。
7
7
  * id 用内容寻址(sha1 前 8 位),不新增字段、不改索引格式:一行内容的 id 就是它自己。
@@ -13,8 +13,8 @@ import { mkdtempSync, mkdirSync, readFileSync, rmSync, existsSync } from 'node:f
13
13
  import { join } from 'node:path'
14
14
  import { tmpdir } from 'node:os'
15
15
 
16
- import { entryId } from './security.mjs'
17
- import { LINK_LIMIT, LINK_THRESHOLD, lookupLinks, relatedEntries, storeDirFor, writeMemory } from './index.mjs'
16
+ import { entryId } from '../security.mjs'
17
+ import { LINK_LIMIT, LINK_THRESHOLD, lookupLinks, relatedEntries, storeDirFor, writeMemory } from '../index.mjs'
18
18
 
19
19
  function sandbox() {
20
20
  const root = realpathSync(mkdtempSync(join(tmpdir(), 'dsh-mem-links-')))
@@ -4,11 +4,13 @@ import {mkdtempSync,mkdirSync,readFileSync,writeFileSync,rmSync,cpSync,readdirSy
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,lookupLinks,statusOf,storeRevision,withdrawManagedSource,recoverMemoryPublication,initializeManagedMemory,managedMemoryStatus,adoptCandidate,apply,storeDirFor} from './index.mjs'
8
- import {candidateSnapshot,listCandidates,stageCandidate,withdrawSource,withdrawalPolicy} from './candidates.mjs'
9
- import {legacyProjectSlug,inspectProjectStore,migrateProjectStore,projectStoreDir} from './project-stores.mjs'
10
- import {managedRead,managedProjectOperation} from './managed-access.mjs'
11
- import {withManagedRead} from './managed-policy.mjs'
7
+ import {once} from 'node:events'
8
+ import {Worker} from 'node:worker_threads'
9
+ import {writeMemory,indexLine,searchStore,lookupLinks,statusOf,storeRevision,withdrawManagedSource,recoverMemoryPublication,initializeManagedMemory,managedMemoryStatus,adoptCandidate,apply,storeDirFor} from '../index.mjs'
10
+ import {candidateSnapshot,listCandidates,stageCandidate,withdrawSource,withdrawalPolicy} from '../candidates.mjs'
11
+ import {legacyProjectSlug,inspectProjectStore,migrateProjectStore,projectStoreDir} from '../project-stores.mjs'
12
+ import {managedRead,managedProjectOperation} from '../managed-access.mjs'
13
+ import {withManagedRead} from '../managed-policy.mjs'
12
14
  const hash=value=>createHash('sha256').update(value).digest('hex')
13
15
  const proof=(fresh=false)=>({basis:'tool-context',via:'memory_write',sessionId:fresh?'unrelated-owner':'managed-owner',callHash:hash(fresh?'other-call':'managed-call'),evidence:[{messageId:fresh?'unrelated-root':'managed-root',hash:hash('synthetic-source'),quote:fresh?'Synthetic unrelated delivery choice cobalt7654':'Synthetic palette choice jade9876'}]})
14
16
  const box=run=>{const root=realpathSync(mkdtempSync(join(tmpdir(),'managed-access-'))),data=join(root,'memory');mkdirSync(data);try{const result=run({root,data,rows:[{name:'.',role:'global'},{name:'projects/v2-synthetic',role:'project'}],project:join(data,'projects/v2-synthetic')});if(result&&typeof result.then==='function')return result.finally(()=>rmSync(root,{recursive:true,force:true}));rmSync(root,{recursive:true,force:true});return result}catch(error){rmSync(root,{recursive:true,force:true});throw error}}
@@ -20,6 +22,28 @@ test('a read never creates a missing control database or invokes the data callba
20
22
  let read=false;assert.throws(()=>withManagedRead(f.data,'.',()=>{read=true}),/control-missing/)
21
23
  assert.equal(read,false);assert.equal(existsSync(f.data+'.control/state.sqlite'),false)
22
24
  }))
25
+ test('a managed read recovers after a brief control database lock',()=>box(async f=>{
26
+ seeded(f);initialize(f)
27
+ const worker=new Worker(`
28
+ const {DatabaseSync}=require('node:sqlite')
29
+ const {parentPort,workerData}=require('node:worker_threads')
30
+ const db=new DatabaseSync(workerData)
31
+ try{
32
+ db.exec('BEGIN IMMEDIATE')
33
+ parentPort.postMessage('locked')
34
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,220)
35
+ db.exec('ROLLBACK')
36
+ }finally{db.close()}
37
+ `,{eval:true,workerData:f.data+'.control/state.sqlite'})
38
+ const finished=once(worker,'exit')
39
+ try{
40
+ await once(worker,'message')
41
+ assert.ok(searchStore(f.data,'jade9876').hits.length>0)
42
+ }finally{
43
+ const [code]=await finished
44
+ assert.equal(code,0)
45
+ }
46
+ }))
23
47
  test('transaction identity rejection happens before fresh-source and data callbacks',()=>box(f=>{
24
48
  seeded(f);initialize(f);let fresh=false,read=false
25
49
  assert.throws(()=>withManagedRead(f.data,'.',()=>{read=true},{validateIdentity(){throw Error('synthetic-identity-rejected')},readFreshProofs(){fresh=true;return[]}}),/identity-rejected/)
@@ -6,11 +6,11 @@ import {tmpdir} from 'node:os'
6
6
  import {fileURLToPath} from 'node:url'
7
7
  import {createHash} from 'node:crypto'
8
8
  import {spawnSync} from 'node:child_process'
9
- import {writeMemory,indexLine,withdrawManagedSource,searchStore,managedMemoryStatus,initializeManagedMemory,storeRevision,adoptCandidate} from './index.mjs'
10
- import {stageCandidate} from './candidates.mjs'
11
- import {activateManagedMemory,activationStores} from './managed-activation.mjs'
12
- import {probeEnvironment} from './scripts/probe-env.mjs'
13
- const here=dirname(fileURLToPath(import.meta.url)),hash=value=>createHash('sha256').update(value).digest('hex')
9
+ import {writeMemory,indexLine,withdrawManagedSource,searchStore,managedMemoryStatus,initializeManagedMemory,storeRevision,adoptCandidate} from '../index.mjs'
10
+ import {stageCandidate} from '../candidates.mjs'
11
+ import {activateManagedMemory,activationStores} from '../managed-activation.mjs'
12
+ import {probeEnvironment} from '../scripts/probe-env.mjs'
13
+ const here=dirname(dirname(fileURLToPath(import.meta.url))),hash=value=>createHash('sha256').update(value).digest('hex')
14
14
  function box(run){const root=realpathSync(mkdtempSync(join(tmpdir(),'managed-activation-'))),data=join(root,'memory');mkdirSync(data);try{return run({root,data})}finally{rmSync(root,{recursive:true,force:true})}}
15
15
  function write(dir,scope='global',owner='old-owner',word='jade9876'){
16
16
  const summary='Synthetic activation preference '+word
@@ -1,15 +1,15 @@
1
- import {applyWithValidationFixture as apply} from './fixture-host.mjs'
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,writeFileSync,existsSync,rmSync} from 'node:fs'
5
5
  import {join} from 'node:path'
6
6
  import {tmpdir} from 'node:os'
7
7
  import {createHash} from 'node:crypto'
8
- import {normalizeMeaning,meaningForScope,normalizeContext,normalizeTask,applicability,meaningView,memoryAdvice,detailedMeanings,meaningLabel} from './meaning.mjs'
9
- import {indexLine, searchStore, writeMemory, storeRevision, withdrawManagedSource, adoptCandidate, storeDirFor} from './index.mjs'
10
- import {candidateSnapshot,stageCandidate} from './candidates.mjs'
11
- import {writeFixtureMemory,applyWithOwnerFixture} from './fixture-host.mjs'
12
- import {captureMessage,assessCandidate} from './capture-evidence.mjs'
8
+ import {normalizeMeaning,meaningForScope,normalizeContext,normalizeTask,applicability,meaningView,memoryAdvice,detailedMeanings,meaningLabel} from '../meaning.mjs'
9
+ import {indexLine, searchStore, writeMemory, storeRevision, withdrawManagedSource, adoptCandidate, storeDirFor} from '../index.mjs'
10
+ import {candidateSnapshot,stageCandidate} from '../candidates.mjs'
11
+ import {writeFixtureMemory,applyWithOwnerFixture} from '../fixture-host.mjs'
12
+ import {captureMessage,assessCandidate} from '../capture-evidence.mjs'
13
13
 
14
14
  const condition={dimension:'audience',value:'children'},exception={dimension:'delivery',value:'urgent'}
15
15
  const pref=normalizeMeaning({kind:'preference',conditions:[condition]})
@@ -5,7 +5,7 @@ import {mkdtempSync,realpathSync,mkdirSync,rmSync} from 'node:fs'
5
5
  import {tmpdir} from 'node:os'
6
6
  import {join,resolve} from 'node:path'
7
7
  import {pathToFileURL} from 'node:url'
8
- const source=process.env.DSH_NATIVE_SERVICE_SOURCE?pathToFileURL(resolve(process.env.DSH_NATIVE_SERVICE_SOURCE)+'/'):new URL('./',import.meta.url)
8
+ const source=process.env.DSH_NATIVE_SERVICE_SOURCE?pathToFileURL(resolve(process.env.DSH_NATIVE_SERVICE_SOURCE)+'/'):new URL('../',import.meta.url)
9
9
  const {apply}=await import(new URL('index.mjs',source)),{candidateSnapshot}=await import(new URL('candidates.mjs',source)),{scriptedNativeValidationLlm}=await import(new URL('fixture-host.mjs',source))
10
10
  for(const mode of ['stable-owner-fresh-proxies','owner-disappears','owner-replaced'])test('native service lifecycle: '+mode,async()=>{
11
11
  const base=realpathSync(mkdtempSync(join(tmpdir(),'native-service-generation-'))),home=join(base,'home'),old=process.env.DSH_HOME;mkdirSync(home);process.env.DSH_HOME=home
@@ -4,10 +4,10 @@ import assert from 'node:assert/strict'
4
4
  import {mkdtempSync,realpathSync,mkdirSync,rmSync,readFileSync,existsSync} from 'node:fs'
5
5
  import {tmpdir} from 'node:os'
6
6
  import {join} from 'node:path'
7
- import {apply,storeDirFor,searchStore,withdrawManagedSource} from './index.mjs'
8
- import {candidateSnapshot} from './candidates.mjs'
9
- import {VALIDATION_FIELDS} from './capture-validation-contract.mjs'
10
- import {writeFixtureMemory} from './fixture-host.mjs'
7
+ import {apply,storeDirFor,searchStore,withdrawManagedSource} from '../index.mjs'
8
+ import {candidateSnapshot} from '../candidates.mjs'
9
+ import {VALIDATION_FIELDS} from '../capture-validation-contract.mjs'
10
+ import {writeFixtureMemory} from '../fixture-host.mjs'
11
11
  const proposal={type:'user',scope:'global',summary:'长期反馈先列两处问题再给办法',meaning:{kind:'preference',conditions:[{dimension:'task',value:'review'}]}}
12
12
  const text='跨我的所有项目,长期反馈都先列两处问题再给修改办法。'
13
13
  async function box(options,run){
@@ -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 './scripts/probe-env.mjs'
15
+ import { probeEnvironment } from '../scripts/probe-env.mjs'
16
16
 
17
- import { VERSION } from './version.mjs'
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 './eval/paired-analysis.mjs'
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 './scripts/patch.mjs'
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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./index.mjs', import.meta.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('./scripts/patch.mjs',import.meta.url)),file)
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 './plugin-config.mjs'
8
- import { optionalService } from './host-services.mjs'
9
- import { extractWithLlm } from './capture-llm.mjs'
10
- import { apply,storeDirFor } from './index.mjs'
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
 
@@ -3,9 +3,9 @@ import assert from 'node:assert/strict'
3
3
  import {mkdtempSync,realpathSync,mkdirSync,readFileSync,writeFileSync,rmSync} from 'node:fs'
4
4
  import {join} from 'node:path'
5
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'
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
9
 
10
10
  test('actual background extractor receives no unrelated saved private source or summary',async()=>{
11
11
  const home=realpathSync(mkdtempSync(join(tmpdir(),'memory-private-background-'))),old=process.env.DSH_HOME;process.env.DSH_HOME=home
@@ -1,4 +1,4 @@
1
- import {prepareMountedFixture} from './fixture-host.mjs'
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 './project-stores.mjs'
11
- import { storeDirFor, writeMemory, indexLine, searchStore, apply } from './index.mjs'
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('./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')}})`
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('./scripts/projects.mjs', import.meta.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)