dsh-memu-memory 1.1.3 → 1.2.0

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 (173) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.md +31 -2
  3. package/README.zh.md +30 -2
  4. package/docs/active-memory-ab.md +385 -0
  5. package/docs/binding-lifecycle.md +99 -0
  6. package/docs/contributing.md +1 -1
  7. package/docs/emotion-model-spec.md +281 -0
  8. package/docs/host-api.md +2 -2
  9. package/docs/memory-semantics.md +12 -10
  10. package/docs/recall-benchmark.md +238 -0
  11. package/docs/troubleshooting.md +395 -395
  12. package/lib/active-memory.js +165 -0
  13. package/lib/activity-log.js +1 -1
  14. package/lib/binding-diagnostics.js +8 -3
  15. package/lib/client.js +33 -11
  16. package/lib/consolidation-ledger.js +347 -0
  17. package/lib/consolidation-receipt.js +284 -0
  18. package/lib/consolidation-snapshot.js +274 -0
  19. package/lib/consolidation-worker.js +115 -0
  20. package/lib/cycle.js +40 -6
  21. package/lib/inject.js +104 -76
  22. package/lib/local-embedding-manager.js +24 -6
  23. package/lib/memory.js +221 -19
  24. package/lib/mood.js +268 -39
  25. package/lib/preset-scope.js +1 -1
  26. package/lib/private.js +335 -40
  27. package/lib/prompts/private-consolidation.coding.md +1 -0
  28. package/lib/prompts/private-consolidation.md +1 -0
  29. package/lib/prompts/private-consolidation.mixed.md +1 -0
  30. package/lib/prompts/private-consolidation.rp.md +1 -0
  31. package/lib/prompts.js +4 -4
  32. package/lib/remote-summary.js +51 -25
  33. package/lib/rpc.js +9 -1
  34. package/lib/state.js +29 -12
  35. package/lib/tools.js +49 -2
  36. package/lib/vector-profile.js +27 -0
  37. package/package.json +2 -1
  38. package/scripts/acceptance-local.mjs +2 -1
  39. package/scripts/active-memory-ab-live.mjs +655 -0
  40. package/scripts/active-memory-ab.mjs +1302 -0
  41. package/scripts/chain-matrix.mjs +11 -1
  42. package/scripts/check-all.mjs +10 -1
  43. package/tests/activation.test.mjs +89 -0
  44. package/tests/active-memory-ab.test.mjs +745 -0
  45. package/tests/active-memory.test.mjs +129 -0
  46. package/tests/activity-log.test.mjs +126 -0
  47. package/tests/activity-wiring.test.mjs +103 -0
  48. package/tests/appendix-b.test.mjs +212 -0
  49. package/tests/appendix-b2.test.mjs +166 -0
  50. package/tests/audit-regression.test.mjs +272 -0
  51. package/tests/audit-safety.test.mjs +227 -0
  52. package/tests/backend-compensation.test.mjs +272 -0
  53. package/tests/backup.test.mjs +793 -0
  54. package/tests/binding-diagnostics.test.mjs +720 -0
  55. package/tests/browser.test.mjs +366 -0
  56. package/tests/client-render.test.mjs +892 -0
  57. package/tests/coalesce.test.mjs +113 -0
  58. package/tests/config.test.mjs +159 -0
  59. package/tests/consolidation-credential.test.mjs +320 -0
  60. package/tests/consolidation-fork.test.mjs +357 -0
  61. package/tests/consolidation-ledger.test.mjs +298 -0
  62. package/tests/consolidation-snapshot.test.mjs +321 -0
  63. package/tests/consolidation-value.test.mjs +89 -0
  64. package/tests/context-scope.test.mjs +40 -0
  65. package/tests/cue.test.mjs +44 -0
  66. package/tests/curator-probe.test.mjs +433 -0
  67. package/tests/curator.test.mjs +150 -0
  68. package/tests/cycle.test.mjs +411 -0
  69. package/tests/disclosure.test.mjs +231 -0
  70. package/tests/doc-drift.test.mjs +222 -0
  71. package/tests/embedding-catalog.test.mjs +266 -0
  72. package/tests/embedding-download.test.mjs +391 -0
  73. package/tests/embedding-runtime.test.mjs +389 -0
  74. package/tests/emotion-chain.test.mjs +84 -0
  75. package/tests/emotion-contract.test.mjs +445 -0
  76. package/tests/engine.test.mjs +35 -0
  77. package/tests/fixtures/active-memory-ab/README.md +56 -0
  78. package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
  79. package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
  80. package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
  81. package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
  82. package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
  83. package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
  84. package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
  85. package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
  86. package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
  87. package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
  88. package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
  89. package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
  90. package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
  91. package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
  92. package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
  93. package/tests/fixtures/active-memory-ab/manifest.json +199 -0
  94. package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
  95. package/tests/fixtures/recall-bench/02-exact.json +153 -0
  96. package/tests/fixtures/recall-bench/03-negation.json +173 -0
  97. package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
  98. package/tests/fixtures/recall-bench/05-conflict.json +194 -0
  99. package/tests/fixtures/recall-bench/06-boundary.json +273 -0
  100. package/tests/fixtures/recall-bench/07-secret.json +191 -0
  101. package/tests/fixtures/recall-bench/08-coding.json +164 -0
  102. package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
  103. package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
  104. package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
  105. package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
  106. package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
  107. package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
  108. package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
  109. package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
  110. package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
  111. package/tests/fixtures/repetition-eval/02-en.json +97 -0
  112. package/tests/fixtures/repetition-eval/03-code.json +91 -0
  113. package/tests/fixtures/repetition-eval/04-names.json +116 -0
  114. package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
  115. package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
  116. package/tests/graph-view.test.mjs +187 -0
  117. package/tests/graph.test.mjs +77 -0
  118. package/tests/hook-order.test.mjs +72 -0
  119. package/tests/humanlike.test.mjs +232 -0
  120. package/tests/hybrid-recall.test.mjs +92 -0
  121. package/tests/inbound.test.mjs +393 -0
  122. package/tests/inject-guide.test.mjs +364 -0
  123. package/tests/inject.test.mjs +222 -0
  124. package/tests/injection-budget-settings.test.mjs +35 -0
  125. package/tests/injection-budget.test.mjs +64 -0
  126. package/tests/injection-chain.test.mjs +191 -0
  127. package/tests/loader-register.mjs +3 -0
  128. package/tests/loader.mjs +12 -0
  129. package/tests/local-embedding-entry.test.mjs +95 -0
  130. package/tests/local-embedding-manager.test.mjs +476 -0
  131. package/tests/local-embedding-protocol.test.mjs +95 -0
  132. package/tests/local-embedding-worker.test.mjs +346 -0
  133. package/tests/managed-memory.test.mjs +536 -0
  134. package/tests/managed-runtime.test.mjs +40 -0
  135. package/tests/media.test.mjs +132 -0
  136. package/tests/migration-payload.test.mjs +42 -0
  137. package/tests/mood.test.mjs +109 -0
  138. package/tests/multiprocess.test.mjs +29 -0
  139. package/tests/new-session-gate.test.mjs +361 -0
  140. package/tests/package-shipping.test.mjs +84 -0
  141. package/tests/preset-scope.test.mjs +212 -0
  142. package/tests/prompts.test.mjs +250 -0
  143. package/tests/react-snapshot.test.mjs +23 -0
  144. package/tests/recall-bench.test.mjs +306 -0
  145. package/tests/remote.test.mjs +140 -0
  146. package/tests/rename.test.mjs +30 -0
  147. package/tests/repetition-detector-snapshot.test.mjs +231 -0
  148. package/tests/repetition-eval-live.test.mjs +177 -0
  149. package/tests/repetition-eval.test.mjs +373 -0
  150. package/tests/salient-date.test.mjs +142 -0
  151. package/tests/settings-beat.test.mjs +221 -0
  152. package/tests/short-term.test.mjs +166 -0
  153. package/tests/sidebar-beat.test.mjs +208 -0
  154. package/tests/sidebar-refresh.test.mjs +123 -0
  155. package/tests/sidebar.test.mjs +70 -0
  156. package/tests/snippet.test.mjs +47 -0
  157. package/tests/source-kind.test.mjs +52 -0
  158. package/tests/state.test.mjs +150 -0
  159. package/tests/stats.test.mjs +112 -0
  160. package/tests/storage.test.mjs +125 -0
  161. package/tests/stubs/dsh-llm.js +5 -0
  162. package/tests/stubs/dsh-session.js +7 -0
  163. package/tests/stubs/dsh-tools.js +101 -0
  164. package/tests/tool-schema.test.mjs +73 -0
  165. package/tests/tools.test.mjs +184 -0
  166. package/tests/ui-defaults.test.mjs +57 -0
  167. package/tests/update-fields.test.mjs +138 -0
  168. package/tests/vector-migration.test.mjs +733 -0
  169. package/tests/vector-profile.test.mjs +425 -0
  170. package/tests/visibility-migration.test.mjs +152 -0
  171. package/tests/webui-auth.test.mjs +454 -0
  172. package/tests/webui.test.mjs +164 -0
  173. package/tests/windows-env.test.mjs +89 -0
@@ -0,0 +1,222 @@
1
+ import test, { after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { execFileSync, spawnSync } from 'node:child_process'
4
+ import fs from 'node:fs'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ import { fileURLToPath, pathToFileURL } from 'node:url'
8
+
9
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
10
+ const REPO = path.resolve(HERE, '..')
11
+ const GATE = path.join(REPO, 'scripts', 'check-doc-drift.mjs')
12
+ const gate = await import(pathToFileURL(GATE).href)
13
+
14
+ const tempRoots = []
15
+ after(() => {
16
+ for (const root of tempRoots) fs.rmSync(root, { recursive: true, force: true })
17
+ tempRoots.length = 0
18
+ })
19
+
20
+ function tempDir() {
21
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'doc-drift-test-'))
22
+ tempRoots.push(root)
23
+ return root
24
+ }
25
+
26
+ function write(root, rel, body) {
27
+ const abs = path.join(root, rel)
28
+ fs.mkdirSync(path.dirname(abs), { recursive: true })
29
+ fs.writeFileSync(abs, body)
30
+ }
31
+
32
+ const MARKER = (value) => '<!-- rpc-method-count: ' + value + ' -->'
33
+
34
+ // Three real dispatch cases plus decoys: a nested switch case (depth 3), two
35
+ // commented-out case labels, and a case in a completely different switch.
36
+ const RPC_FIXTURE = "export async function dispatch(ctx, endpoint, payload = {}) {\n"
37
+ + " switch (endpoint) {\n"
38
+ + " case 'alpha': {\n"
39
+ + " // case 'commentedOut': not a dispatch method\n"
40
+ + " /* case 'blockCommented': not one either */\n"
41
+ + " switch (endpoint) {\n"
42
+ + " case 'nestedIgnored': return 1\n"
43
+ + " default: return 2\n"
44
+ + " }\n"
45
+ + " }\n"
46
+ + " case 'beta': return 3\n"
47
+ + " case 'gamma': return 4\n"
48
+ + " default: return null\n"
49
+ + " }\n"
50
+ + "}\n"
51
+ + "\n"
52
+ + "function otherSwitch(kind) {\n"
53
+ + " switch (kind) {\n"
54
+ + " case 'notCounted': return 1\n"
55
+ + " default: return 0\n"
56
+ + " }\n"
57
+ + "}\n"
58
+
59
+ function makeFixture(options = {}) {
60
+ const root = tempDir()
61
+ write(root, 'lib/rpc.js', RPC_FIXTURE)
62
+ if (options.marker !== null) {
63
+ const value = options.marker === undefined ? 3 : options.marker
64
+ write(root, 'README.md', '# Fixture\n\n' + MARKER(value) + '\n')
65
+ }
66
+ write(root, 'tests/loader-register.mjs', "export const registered = true\n")
67
+ write(root, 'tests/loader.mjs', "export const loaded = true\n")
68
+ write(root, 'tests/stubs/dsh-llm.js', "export const llm = true\n")
69
+ write(root, 'tests/stubs/dsh-session.js', "export const session = true\n")
70
+ write(root, 'tests/stubs/dsh-tools.js', "export const tools = true\n")
71
+ write(root, 'tests/sample.test.mjs', "import test from 'node:test'\ntest('fixture', () => {})\n")
72
+ for (const [rel, body] of Object.entries(options.docs || {})) write(root, rel, body)
73
+ return root
74
+ }
75
+
76
+ function initGit(root) {
77
+ execFileSync('git', ['init', '-q'], { cwd: root, stdio: 'pipe' })
78
+ execFileSync('git', ['add', '-A'], { cwd: root, stdio: 'pipe' })
79
+ }
80
+
81
+ let gitAvailable = true
82
+ try {
83
+ execFileSync('git', ['--version'], { stdio: 'pipe' })
84
+ } catch {
85
+ gitAvailable = false
86
+ }
87
+
88
+ const detailText = (result) => (result.details || []).join('\n')
89
+
90
+ test('extractDispatchMethods counts only top-level cases of dispatch()', () => {
91
+ assert.deepEqual(gate.extractDispatchMethods(RPC_FIXTURE), ['alpha', 'beta', 'gamma'])
92
+ })
93
+
94
+ test('doc discovery covers README*.md, AGENTS.md and docs/*.md only', () => {
95
+ const root = makeFixture({ docs: { 'AGENTS.md': '# agents\n', 'docs/notes.md': '# notes\n', 'other.md': '# ignored\n' } })
96
+ assert.deepEqual(gate.docFiles(root), ['AGENTS.md', 'README.md', 'docs/notes.md'])
97
+ })
98
+
99
+ test('missing rpc-method-count marker fails with expected value', () => {
100
+ const root = makeFixture({ marker: null })
101
+ write(root, 'README.md', '# Fixture\n\nNo marker here.\n')
102
+ const result = gate.checkRpcMethodCount(root)
103
+ assert.equal(result.status, 'fail')
104
+ assert.match(result.summary, /no rpc-method-count marker/)
105
+ assert.match(detailText(result), /marker: \(none\)/)
106
+ assert.match(detailText(result), /expected: 3 methods/)
107
+ assert.match(detailText(result), /README\.md/)
108
+ })
109
+
110
+ test('drifted rpc-method-count marker fails and names file, marker and expected', () => {
111
+ const root = makeFixture({ marker: 4 })
112
+ const result = gate.checkRpcMethodCount(root)
113
+ assert.equal(result.status, 'fail')
114
+ const text = detailText(result)
115
+ assert.match(text, /README\.md: marker 4 != expected 3/)
116
+ assert.match(text, /expected: 3 methods/)
117
+ })
118
+
119
+ test('conflicting markers fail even when one value matches', () => {
120
+ const root = makeFixture({ marker: 3, docs: { 'docs/other.md': MARKER(4) + '\n' } })
121
+ const result = gate.checkRpcMethodCount(root)
122
+ assert.equal(result.status, 'fail')
123
+ const text = detailText(result)
124
+ assert.match(text, /README\.md: marker 3/)
125
+ assert.match(text, /docs\/other\.md: marker 4 != expected 3/)
126
+ assert.match(text, /markers disagree: 3, 4/)
127
+ })
128
+
129
+ test('matching marker passes', () => {
130
+ const root = makeFixture({ marker: 3 })
131
+ const result = gate.checkRpcMethodCount(root)
132
+ assert.equal(result.status, 'ok')
133
+ assert.match(result.summary, /3 RPC methods/)
134
+ })
135
+
136
+ test('agreeing markers in several docs pass', () => {
137
+ const root = makeFixture({ marker: 3, docs: { 'docs/a.md': MARKER(3) + '\n', 'docs/b.md': MARKER(3) + '\n' } })
138
+ assert.equal(gate.checkRpcMethodCount(root).status, 'ok')
139
+ })
140
+
141
+ test('no .git means test tracking is skipped, not failed', () => {
142
+ const root = makeFixture()
143
+ const result = gate.checkTestTracking(root)
144
+ assert.equal(result.status, 'skipped')
145
+ assert.match(result.summary, /not applicable/)
146
+ assert.match(result.summary, /npm package install directory/)
147
+ assert.notEqual(result.status, 'fail')
148
+ })
149
+
150
+ test('temp git repo with all tests staged passes test tracking', (t) => {
151
+ if (!gitAvailable) return t.skip('git not available')
152
+ const root = makeFixture()
153
+ initGit(root)
154
+ const result = gate.checkTestTracking(root)
155
+ assert.equal(result.status, 'ok')
156
+ assert.match(result.summary, /3 test \.mjs file\(s\) tracked and staged/)
157
+ })
158
+
159
+ test('untracked test file on disk in a git repo fails test tracking', (t) => {
160
+ if (!gitAvailable) return t.skip('git not available')
161
+ const root = makeFixture()
162
+ initGit(root)
163
+ write(root, 'tests/leaked.test.mjs', "import test from 'node:test'\ntest('leak', () => {})\n")
164
+ const result = gate.checkTestTracking(root)
165
+ assert.equal(result.status, 'fail')
166
+ const text = detailText(result)
167
+ assert.match(text, /untracked\/unstaged: tests\/leaked\.test\.mjs/)
168
+ assert.doesNotMatch(text, /tests\/sample\.test\.mjs/)
169
+ assert.match(result.summary, /1 of 4 test \.mjs file\(s\) are not tracked or staged/)
170
+ })
171
+
172
+ test('loader and stub completeness passes when every required file is non-empty', () => {
173
+ const root = makeFixture()
174
+ const result = gate.checkLoaderStubs(root)
175
+ assert.equal(result.status, 'ok')
176
+ assert.match(result.summary, /5 required loader\/stub file\(s\) present/)
177
+ })
178
+
179
+ test('missing stub fails and is named', () => {
180
+ const root = makeFixture()
181
+ fs.rmSync(path.join(root, 'tests/stubs/dsh-tools.js'))
182
+ const result = gate.checkLoaderStubs(root)
183
+ assert.equal(result.status, 'fail')
184
+ assert.match(detailText(result), /tests\/stubs\/dsh-tools\.js: missing/)
185
+ })
186
+
187
+ test('empty loader fails and reports zero bytes', () => {
188
+ const root = makeFixture()
189
+ write(root, 'tests/loader.mjs', '')
190
+ const result = gate.checkLoaderStubs(root)
191
+ assert.equal(result.status, 'fail')
192
+ assert.match(detailText(result), /tests\/loader\.mjs: empty/)
193
+ })
194
+
195
+ test('suite without any *.test.mjs fails loader/stub completeness', () => {
196
+ const root = makeFixture()
197
+ fs.rmSync(path.join(root, 'tests/sample.test.mjs'))
198
+ const result = gate.checkLoaderStubs(root)
199
+ assert.equal(result.status, 'fail')
200
+ assert.match(detailText(result), /tests\/: no \*\.test\.mjs file/)
201
+ })
202
+
203
+ test('runChecks returns the three gates in order', () => {
204
+ const root = makeFixture()
205
+ assert.deepEqual(gate.runChecks(root).map((entry) => entry.label), ['rpc-method-count', 'test-tracking', 'loader-stubs'])
206
+ })
207
+
208
+ test('CLI exits 1 and prints FAIL/skip when a gate fails', () => {
209
+ const root = makeFixture({ marker: null })
210
+ const result = spawnSync(process.execPath, [GATE, '--root', root], { encoding: 'utf8' })
211
+ assert.equal(result.status, 1)
212
+ assert.match(result.stdout, /FAIL rpc-method-count/)
213
+ assert.match(result.stdout, /skip test-tracking/)
214
+ assert.match(result.stdout, /1 doc-drift check\(s\) failed: rpc-method-count/)
215
+ })
216
+
217
+ test('CLI exits 0 when all gates pass', () => {
218
+ const root = makeFixture()
219
+ const result = spawnSync(process.execPath, [GATE, '--root', root], { encoding: 'utf8' })
220
+ assert.equal(result.status, 0)
221
+ assert.match(result.stdout, /all doc-drift checks passed/)
222
+ })
@@ -0,0 +1,266 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Buffer } from 'node:buffer'
4
+ import {
5
+ FILE_ROLES,
6
+ PROVIDER_ORDER,
7
+ getEmbeddingCatalog,
8
+ getEmbeddingModel,
9
+ profileFingerprint,
10
+ readOnnxGraphHeader,
11
+ resolveEmbeddingSources,
12
+ toProfileDescriptor,
13
+ validateEmbeddingManifest,
14
+ } from '../lib/embedding-catalog.js'
15
+
16
+ const BGE = 'bge-small-zh-v1.5-int8'
17
+ const BGE_REVISION = '313386ad7ffad360a4870fd1910f8ecaf1a05151'
18
+
19
+ function clone(value) {
20
+ return JSON.parse(JSON.stringify(value))
21
+ }
22
+
23
+ // 合成一个最小 ONNX 图头,用来验证解析器与"manifest 声称 vs 制品实际"的对照逻辑。
24
+ function varint(value) {
25
+ const bytes = []
26
+ let rest = value
27
+ while (rest > 127) { bytes.push((rest & 0x7f) | 0x80); rest = Math.floor(rest / 128) }
28
+ bytes.push(rest)
29
+ return Buffer.from(bytes)
30
+ }
31
+ const tag = (field, wire) => varint((field << 3) | wire)
32
+ const bytesField = (field, buffer) => Buffer.concat([tag(field, 2), varint(buffer.length), buffer])
33
+ const textField = (field, text) => bytesField(field, Buffer.from(text, 'utf8'))
34
+ const intField = (field, value) => Buffer.concat([tag(field, 0), varint(value)])
35
+
36
+ function dimension(dim) {
37
+ return typeof dim === 'number' ? intField(1, dim) : textField(2, String(dim))
38
+ }
39
+
40
+ function valueInfo(name, elemType, shape) {
41
+ const shapeMessage = Buffer.concat(shape.map(dim => bytesField(1, dimension(dim))))
42
+ const tensorType = Buffer.concat([intField(1, elemType), bytesField(2, shapeMessage)])
43
+ return Buffer.concat([textField(1, name), bytesField(2, bytesField(1, tensorType))])
44
+ }
45
+
46
+ function tinyOnnxGraph({ outputDim = 8, initializer = Buffer.from('weights-fixture') } = {}) {
47
+ const initializerMessage = Buffer.concat([intField(1, 1), intField(2, 1), textField(8, 'w'), bytesField(9, initializer)])
48
+ const graph = Buffer.concat([
49
+ bytesField(5, initializerMessage),
50
+ bytesField(11, valueInfo('input_ids', 7, ['batch_size', 'sequence_length'])),
51
+ bytesField(11, valueInfo('attention_mask', 7, ['batch_size', 'sequence_length'])),
52
+ bytesField(11, valueInfo('token_type_ids', 7, ['batch_size', 'sequence_length'])),
53
+ bytesField(12, valueInfo('last_hidden_state', 1, ['batch_size', 'sequence_length', outputDim])),
54
+ ])
55
+ const opset = Buffer.concat([textField(1, ''), intField(2, 11)])
56
+ return Buffer.concat([intField(1, 6), textField(2, 'onnx.quantize'), textField(3, '0.1.0'), bytesField(7, graph), bytesField(8, opset)])
57
+ }
58
+
59
+ test('目录是冻结的只读快照,未知 id 返回 null 而不是默认模型', () => {
60
+ const catalog = getEmbeddingCatalog()
61
+ assert.ok(Object.isFrozen(catalog))
62
+ assert.ok(Object.isFrozen(catalog[0]))
63
+ assert.throws(() => { catalog.push({ id: 'x' }) }, TypeError)
64
+ assert.deepEqual(catalog.map(entry => entry.id), [BGE, 'all-minilm-l6-v2-int8', 'multilingual-e5-small'])
65
+ assert.equal(getEmbeddingModel('nope'), null)
66
+ assert.equal(getEmbeddingModel(undefined), null)
67
+ assert.equal(getEmbeddingModel('../../etc/passwd'), null)
68
+ })
69
+
70
+ test('首个 BGE 条目带着已核实的固定 commit、逐文件字节数与 SHA-256', () => {
71
+ const entry = getEmbeddingModel(BGE)
72
+ assert.equal(entry.availability, 'available')
73
+ assert.match(entry.sources.modelscope.revision, /^[0-9a-f]{40}$/)
74
+ assert.equal(entry.sources.modelscope.revision, BGE_REVISION)
75
+ assert.equal(entry.sources.modelscope.repo, 'Xenova/bge-small-zh-v1.5')
76
+ assert.equal(entry.dimension, 512)
77
+ assert.equal(entry.maxTokens, 512)
78
+ assert.equal(entry.pooling, 'cls')
79
+ assert.equal(entry.normalization, 'l2')
80
+ assert.equal(entry.graph.opset, 11)
81
+ assert.equal(entry.graph.outputs[0].name, 'last_hidden_state')
82
+ assert.equal(entry.files.length, 9)
83
+ assert.equal(entry.totalBytes, 24562636)
84
+ for (const file of entry.files) {
85
+ assert.match(file.sha256, /^[0-9a-f]{64}$/, file.path + ' sha256')
86
+ assert.ok(Number.isInteger(file.bytes) && file.bytes > 0, file.path + ' bytes')
87
+ assert.ok(FILE_ROLES.includes(file.role), file.path + ' role')
88
+ }
89
+ const graph = entry.files.find(file => file.role === 'graph')
90
+ assert.equal(graph.bytes, 24010842)
91
+ assert.equal(graph.sha256, '15b717c382bcb518ba457b93ea6850ede7f4f1cd8937454aa06972366cd19bcc')
92
+ assert.equal(entry.verification.status, 'verified')
93
+ assert.equal(entry.verification.method, 'sha256+onnx-header')
94
+ assert.ok(!Number.isNaN(Date.parse(entry.verification.checkedAt)))
95
+ assert.equal(entry.license.base.id, 'mit')
96
+ })
97
+
98
+ test('已核实的 BGE manifest 通过校验,但把未核实项如实标成警告', () => {
99
+ const report = validateEmbeddingManifest(getEmbeddingModel(BGE))
100
+ assert.deepEqual(report.errors, [])
101
+ assert.equal(report.ok, true)
102
+ const codes = report.warnings.map(item => item.code)
103
+ assert.ok(codes.includes('LICENSE_DERIVATIVE_UNDECLARED'), 'ONNX 导出许可未单独声明必须可见')
104
+ assert.ok(!codes.includes('RUNTIME_VERSION_UNPINNED'), '隔离真实 ORT smoke 已固定运行时版本')
105
+ assert.equal(codes.filter(code => code === 'SOURCE_REVISION_UNPINNED').length, 2)
106
+ })
107
+
108
+ test('未核实模型是 pending:没有文件、没有哈希、理由齐全', () => {
109
+ for (const id of ['all-minilm-l6-v2-int8', 'multilingual-e5-small']) {
110
+ const entry = getEmbeddingModel(id)
111
+ assert.equal(entry.availability, 'pending')
112
+ assert.deepEqual(entry.files, [])
113
+ assert.equal(entry.totalBytes, 0)
114
+ assert.ok(entry.pendingReasons.length >= 2)
115
+ const report = validateEmbeddingManifest(entry)
116
+ assert.equal(report.ok, true, id + ' pending 条目本身应结构合法')
117
+ assert.equal(report.errors.length, 0)
118
+ }
119
+ })
120
+
121
+ test('manifest 校验会拒绝伪造/缺失的制品元数据', () => {
122
+ const broken = clone(getEmbeddingModel(BGE))
123
+ broken.files[0].sha256 = 'not-a-hash'
124
+ broken.files[1].bytes = '439125'
125
+ broken.files[2].path = '../escape.json'
126
+ broken.files[3].path = 'C:/windows/system32/config'
127
+ broken.totalBytes = 1
128
+ const report = validateEmbeddingManifest(broken)
129
+ assert.equal(report.ok, false)
130
+ const codes = new Set(report.errors.map(item => item.code))
131
+ for (const code of ['FILE_SHA256_INVALID', 'FILE_BYTES_INVALID', 'FILE_PATH_UNSAFE', 'TOTAL_BYTES_MISMATCH']) {
132
+ assert.ok(codes.has(code), 'expect ' + code)
133
+ }
134
+ })
135
+
136
+ test('available 但制品未核实、没有固定 commit 的条目不可下载', () => {
137
+ const lying = clone(getEmbeddingModel(BGE))
138
+ lying.verification = { status: 'unverified', method: null, checkedAt: null }
139
+ lying.sources = {
140
+ modelscope: { repo: 'Xenova/bge-small-zh-v1.5', revision: null },
141
+ 'hf-mirror': { repo: 'Xenova/bge-small-zh-v1.5', revision: null },
142
+ huggingface: { repo: 'Xenova/bge-small-zh-v1.5', revision: null },
143
+ }
144
+ const report = validateEmbeddingManifest(lying)
145
+ assert.equal(report.ok, false)
146
+ const codes = report.errors.map(item => item.code)
147
+ assert.ok(codes.includes('VERIFICATION_MISSING'))
148
+ assert.ok(codes.includes('SOURCE_NOT_PINNED'))
149
+ })
150
+
151
+ test('指纹随向量空间字段变化,篡改指纹会被拒绝', () => {
152
+ const entry = getEmbeddingModel(BGE)
153
+ assert.match(entry.profileFingerprint, /^[0-9a-f]{64}$/)
154
+ assert.equal(profileFingerprint(entry), entry.profileFingerprint)
155
+ const otherPooling = clone(entry)
156
+ otherPooling.pooling = 'mean'
157
+ assert.notEqual(profileFingerprint(otherPooling), entry.profileFingerprint)
158
+ const otherPrefix = clone(entry)
159
+ otherPrefix.prefixes = { query: 'query: ', document: 'passage: ' }
160
+ assert.notEqual(profileFingerprint(otherPrefix), entry.profileFingerprint)
161
+ const otherGraph = clone(entry)
162
+ otherGraph.files.find(file => file.role === 'graph').sha256 = 'f'.repeat(64)
163
+ assert.notEqual(profileFingerprint(otherGraph), entry.profileFingerprint)
164
+ const tampered = clone(entry)
165
+ tampered.profileFingerprint = '0'.repeat(64)
166
+ assert.ok(validateEmbeddingManifest(tampered).errors.some(item => item.code === 'FINGERPRINT_MISMATCH'))
167
+ })
168
+
169
+ test('来源解析默认 modelscope→hf-mirror→huggingface,只为显式固定的 revision 生成 URL', () => {
170
+ const resolved = resolveEmbeddingSources(BGE)
171
+ assert.deepEqual(resolved.order, [...PROVIDER_ORDER])
172
+ assert.deepEqual(resolved.order, ['modelscope', 'hf-mirror', 'huggingface'])
173
+ assert.equal(resolved.sources.length, 1)
174
+ const primary = resolved.primary
175
+ assert.equal(primary.provider, 'modelscope')
176
+ assert.equal(primary.revision, BGE_REVISION)
177
+ assert.equal(primary.files.length, 9)
178
+ const config = primary.files.find(file => file.path === 'config.json')
179
+ assert.equal(config.url, 'https://modelscope.cn/api/v1/models/Xenova/bge-small-zh-v1.5/repo?Revision=' + BGE_REVISION + '&FilePath=config.json')
180
+ const graph = primary.files.find(file => file.path === 'onnx/model_quantized.onnx')
181
+ assert.match(graph.url, /FilePath=onnx%2Fmodel_quantized\.onnx$/)
182
+ // 见证制品来自 BAAI 仓库的固定 revision,而不是冒充 BGE 导出仓库。
183
+ const pooling = primary.files.find(file => file.path === '1_Pooling/config.json')
184
+ assert.match(pooling.url, /BAAI\/bge-small-zh-v1\.5/)
185
+ assert.match(pooling.url, /Revision=8399f11f8da998fe932df2684586c92024219d05/)
186
+ // 没有固定 commit 的来源绝不生成 URL。
187
+ assert.deepEqual(resolved.skipped.map(item => item.provider), ['hf-mirror', 'huggingface'])
188
+ assert.ok(resolved.skipped.every(item => item.reason === 'revision-unpinned'))
189
+ const serialized = JSON.stringify(resolved)
190
+ assert.ok(!serialized.includes('hf-mirror.com'))
191
+ assert.ok(!serialized.includes('huggingface.co'))
192
+ })
193
+
194
+ test('来源顺序可覆盖,三个来源都固定时按顺序给出各自的 URL 形状', () => {
195
+ const pinned = clone(getEmbeddingModel(BGE))
196
+ pinned.files = pinned.files.filter(file => !file.origin)
197
+ const revision = 'b'.repeat(40)
198
+ pinned.sources = {
199
+ modelscope: { repo: 'Xenova/bge-small-zh-v1.5', revision },
200
+ 'hf-mirror': { repo: 'Xenova/bge-small-zh-v1.5', revision },
201
+ huggingface: { repo: 'Xenova/bge-small-zh-v1.5', revision },
202
+ }
203
+ const resolved = resolveEmbeddingSources(pinned)
204
+ assert.deepEqual(resolved.sources.map(source => source.provider), ['modelscope', 'hf-mirror', 'huggingface'])
205
+ assert.deepEqual(resolved.skipped, [])
206
+ assert.equal(resolved.sources[1].files[0].url, 'https://hf-mirror.com/Xenova/bge-small-zh-v1.5/resolve/' + revision + '/config.json')
207
+ assert.equal(resolved.sources[2].files[0].url, 'https://huggingface.co/Xenova/bge-small-zh-v1.5/resolve/' + revision + '/config.json')
208
+ const onlyHf = resolveEmbeddingSources(pinned, { providers: ['huggingface'] })
209
+ assert.deepEqual(onlyHf.sources.map(source => source.provider), ['huggingface'])
210
+ assert.equal(onlyHf.order.length, 1)
211
+ })
212
+
213
+ test('文件 origin 与来源不一致时该来源不可用,不会拼错 URL', () => {
214
+ const entry = clone(getEmbeddingModel(BGE))
215
+ entry.sources['hf-mirror'] = { repo: 'Xenova/bge-small-zh-v1.5', revision: 'c'.repeat(40) }
216
+ const resolved = resolveEmbeddingSources(entry)
217
+ const skipped = resolved.skipped.find(item => item.provider === 'hf-mirror')
218
+ assert.equal(skipped.reason, 'origin-provider-mismatch')
219
+ assert.equal(skipped.path, '1_Pooling/config.json')
220
+ assert.equal(resolved.sources.length, 1)
221
+ })
222
+
223
+ test('ONNX 图头解析读得出输入输出/dtype/形状/opset 与自包含性', () => {
224
+ const header = readOnnxGraphHeader(tinyOnnxGraph())
225
+ assert.equal(header.irVersion, 6)
226
+ assert.deepEqual(header.opsets, [{ domain: '', version: 11 }])
227
+ assert.deepEqual(header.inputs.map(io => io.name), ['input_ids', 'attention_mask', 'token_type_ids'])
228
+ assert.ok(header.inputs.every(io => io.dtype === 'int64'))
229
+ assert.deepEqual(header.outputs, [{ name: 'last_hidden_state', dtype: 'float32', shape: ['batch_size', 'sequence_length', 8] }])
230
+ assert.equal(header.initializerCount, 1)
231
+ assert.equal(header.externalDataEntries, 0)
232
+ assert.equal(header.selfContained, true)
233
+ })
234
+
235
+ test('ONNX 图头解析拒绝非图字节,也不猜维度', () => {
236
+ assert.throws(() => readOnnxGraphHeader(Buffer.from('definitely not an onnx graph')), error => error.code === 'EMBEDDING_ONNX_INVALID')
237
+ assert.throws(() => readOnnxGraphHeader(Buffer.alloc(0)), error => error.code === 'EMBEDDING_ONNX_INVALID')
238
+ assert.throws(() => readOnnxGraphHeader('string-is-not-a-buffer'), error => error.code === 'EMBEDDING_ONNX_INVALID')
239
+ })
240
+
241
+ test('图输出维度与 manifest 声明不一致时校验失败', () => {
242
+ const entry = clone(getEmbeddingModel(BGE))
243
+ entry.graph.outputs[0].shape = ['batch_size', 'sequence_length', 384]
244
+ const report = validateEmbeddingManifest(entry)
245
+ assert.equal(report.ok, false)
246
+ assert.ok(report.errors.some(item => item.code === 'GRAPH_DIMENSION_MISMATCH'))
247
+ })
248
+
249
+ test("目录条目能映射成向量 profile 描述符,指纹下传不丢字段", () => {
250
+ const entry = getEmbeddingModel(BGE)
251
+ const descriptor = toProfileDescriptor(entry)
252
+ // 这些字段是 lib/vector-profile.js 计算 profile 指纹时读取的集合:
253
+ // 两边必须来自同一份事实,否则安装与迁移会拿两套不可比的 hash 互相判等。
254
+ for (const key of ["id", "model", "repo", "revision", "runtime", "tokenizer", "preprocessing", "pooling", "normalization", "prefixes", "maxTokens", "dimension", "protocol"]) {
255
+ assert.ok(key in descriptor, "descriptor needs " + key)
256
+ }
257
+ assert.equal(descriptor.id, entry.id)
258
+ assert.equal(descriptor.repo, "Xenova/bge-small-zh-v1.5")
259
+ assert.equal(descriptor.revision, BGE_REVISION)
260
+ assert.equal(descriptor.tokenizer, entry.files.find(file => file.role === "tokenizer").sha256)
261
+ assert.equal(descriptor.dimension, 512)
262
+ assert.equal(descriptor.pooling, "cls")
263
+ assert.equal(descriptor.protocol, "dsh-local-embedding-v1")
264
+ assert.equal(descriptor.fingerprint, entry.profileFingerprint)
265
+ assert.equal(descriptor.preprocessing.maxTokens, 512)
266
+ })