@evomap/evolver-core 2.0.0-beta.19 → 2.0.0-beta.22
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/assets/gep/genes.jsonl +5 -5
- package/dist/algo/bans.d.ts +10 -1
- package/dist/algo/bans.js +56 -6
- package/dist/algo/candidateAssembly.d.ts +25 -2
- package/dist/algo/candidateAssembly.js +143 -20
- package/dist/algo/capabilityCandidates.js +10 -0
- package/dist/algo/cycleEngine.d.ts +40 -5
- package/dist/algo/cycleEngine.js +277 -37
- package/dist/algo/evolutionEvent.d.ts +26 -0
- package/dist/algo/evolutionEvent.js +32 -0
- package/dist/algo/exploration.d.ts +7 -0
- package/dist/algo/exploration.js +16 -3
- package/dist/algo/geneHealth.d.ts +34 -1
- package/dist/algo/geneHealth.js +42 -0
- package/dist/algo/geneIntake.d.ts +46 -1
- package/dist/algo/geneIntake.js +121 -10
- package/dist/algo/geneSelection.d.ts +115 -6
- package/dist/algo/geneSelection.js +459 -32
- package/dist/algo/index.d.ts +4 -1
- package/dist/algo/index.js +4 -1
- package/dist/algo/kautoProjection.d.ts +41 -0
- package/dist/algo/kautoProjection.js +95 -0
- package/dist/algo/kautoValidator.d.ts +68 -0
- package/dist/algo/kautoValidator.js +256 -0
- package/dist/algo/orchestrator.d.ts +14 -1
- package/dist/algo/orchestrator.js +9 -2
- package/dist/algo/solidify.d.ts +11 -2
- package/dist/algo/solidify.js +37 -7
- package/dist/algo/ucb1.d.ts +53 -0
- package/dist/algo/ucb1.js +156 -0
- package/dist/assetrepair/hubRejection.d.ts +12 -0
- package/dist/assetrepair/hubRejection.js +109 -0
- package/dist/assetrepair/index.d.ts +2 -0
- package/dist/assetrepair/index.js +2 -0
- package/dist/assetrepair/repair.d.ts +33 -0
- package/dist/assetrepair/repair.js +155 -0
- package/dist/assetstore/assetSidecarRecords.js +5 -1
- package/dist/assetstore/assetStoreHealth.d.ts +2 -3
- package/dist/assetstore/assetStoreHealth.js +4 -10
- package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
- package/dist/assetstore/foreignJsonlSource.js +150 -0
- package/dist/assetstore/index.d.ts +3 -0
- package/dist/assetstore/index.js +3 -0
- package/dist/assetstore/learningHistory.js +3 -3
- package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
- package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
- package/dist/assetstore/localJsonl.d.ts +8 -0
- package/dist/assetstore/localJsonl.js +128 -3
- package/dist/assetstore/provenance.d.ts +38 -3
- package/dist/assetstore/provenance.js +103 -45
- package/dist/assetstore/provider.d.ts +4 -0
- package/dist/assetstore/reviewFilter.d.ts +19 -1
- package/dist/assetstore/reviewFilter.js +36 -0
- package/dist/assetstore/unionReadStore.d.ts +25 -0
- package/dist/assetstore/unionReadStore.js +119 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +4 -3
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +4 -3
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
- package/dist/benchmark/selectionFlatAbstention.js +481 -0
- package/dist/bootstrap/envFingerprint.d.ts +9 -0
- package/dist/bootstrap/envFingerprint.js +5 -0
- package/dist/bootstrap/index.d.ts +2 -1
- package/dist/bootstrap/index.js +2 -1
- package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
- package/dist/bootstrap/lifecycleBootstrap.js +433 -0
- package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
- package/dist/bootstrap/v1EnvCompat.js +48 -1
- package/dist/events/eventArchive.d.ts +2 -0
- package/dist/events/eventArchive.js +13 -3
- package/dist/events/eventSchema.d.ts +7 -7
- package/dist/events/eventStore.d.ts +2 -0
- package/dist/events/eventStore.js +5 -1
- package/dist/events/ingest.d.ts +2 -1
- package/dist/events/ingest.js +6 -0
- package/dist/exec/autoExec.d.ts +30 -3
- package/dist/exec/autoExec.js +294 -29
- package/dist/exec/autonomousCycle.d.ts +7 -0
- package/dist/exec/autonomousCycle.js +2 -0
- package/dist/exec/claudeBridge.d.ts +28 -6
- package/dist/exec/claudeBridge.js +411 -258
- package/dist/exec/executionBinding.d.ts +414 -0
- package/dist/exec/executionBinding.js +588 -0
- package/dist/exec/index.d.ts +1 -0
- package/dist/exec/index.js +1 -0
- package/dist/exec/proofOfWork.d.ts +1 -1
- package/dist/exec/proofOfWork.js +2 -2
- package/dist/exec/runnerRegistry.d.ts +5 -0
- package/dist/exec/runnerRegistry.js +54 -14
- package/dist/hub/capability.d.ts +40 -1
- package/dist/hub/conversationDistiller.d.ts +19 -0
- package/dist/hub/conversationDistiller.js +115 -37
- package/dist/hub/fake.d.ts +1 -0
- package/dist/hub/fake.js +1 -0
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/hub/recipeCompose.d.ts +27 -0
- package/dist/hub/recipeCompose.js +90 -0
- package/dist/hub/sanitize.js +119 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mailbox/ipcServer.d.ts +2 -0
- package/dist/mailbox/ipcServer.js +20 -0
- package/dist/mailbox/store.d.ts +52 -2
- package/dist/mailbox/store.js +546 -81
- package/dist/modelCompatibility.d.ts +164 -0
- package/dist/modelCompatibility.js +309 -0
- package/dist/observers/valueDigestObserver.d.ts +9 -0
- package/dist/observers/valueDigestObserver.js +35 -2
- package/dist/ops/cleanup.js +1 -1
- package/dist/ops/evolutionGraphProjection.d.ts +20 -0
- package/dist/ops/evolutionGraphProjection.js +315 -0
- package/dist/ops/index.d.ts +2 -1
- package/dist/ops/index.js +2 -1
- package/dist/ops/selfUpdate.d.ts +8 -0
- package/dist/ops/selfUpdate.js +24 -8
- package/dist/ops/valueOutreach.d.ts +3 -1
- package/dist/ops/valueOutreach.js +5 -1
- package/dist/personality/schema.d.ts +18 -18
- package/dist/schema/evolutionGraph.d.ts +784 -0
- package/dist/schema/evolutionGraph.js +187 -0
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +1 -0
- package/dist/schema/proofOfWork.d.ts +125 -6
- package/dist/schema/proofOfWork.js +102 -4
- package/dist/shadow/shadowHub.js +1 -0
- package/dist/signals/expand.d.ts +15 -1
- package/dist/signals/expand.js +148 -0
- package/dist/signals/index.d.ts +3 -1
- package/dist/signals/index.js +3 -1
- package/dist/signals/scopeVocabulary.d.ts +75 -0
- package/dist/signals/scopeVocabulary.js +91 -0
- package/dist/signals/taskDomain.d.ts +22 -0
- package/dist/signals/taskDomain.js +43 -0
- package/dist/trace/learningTrace.d.ts +7 -0
- package/dist/trace/learningTrace.js +22 -0
- package/dist/trace/trajectory.d.ts +8 -0
- package/dist/trace/trajectory.js +14 -2
- package/dist/util/fileLock.d.ts +92 -5
- package/dist/util/fileLock.js +809 -70
- package/dist/util/index.d.ts +2 -1
- package/dist/util/index.js +1 -1
- package/dist/verify/index.d.ts +2 -1
- package/dist/verify/index.js +1 -1
- package/dist/verify/sandboxRunner.d.ts +2 -0
- package/dist/verify/sandboxRunner.js +154 -10
- package/dist/verify/sandboxedValidation.d.ts +7 -2
- package/dist/verify/sandboxedValidation.js +102 -24
- package/dist/verify/validation.d.ts +24 -3
- package/dist/verify/validation.js +141 -24
- package/dist/wire/geneHints.d.ts +88 -16
- package/dist/wire/geneHints.js +124 -15
- package/dist/wire/index.d.ts +8 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/schemaGate.d.ts +21 -0
- package/dist/wire/schemaGate.js +119 -10
- package/package.json +5 -4
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type AssetKind, type AssetRecord, type AssetStoreProvider, type ConditionalPutOptions, type ConditionalPutResult, type PutResult, type SearchQuery } from './provider.js';
|
|
2
|
+
/** Signal names a record advertises, across the four key spellings the pool uses. */
|
|
3
|
+
export declare function signalsOf(a: AssetRecord): string[];
|
|
2
4
|
/**
|
|
3
5
|
* 本地 jsonl 资产库(M3-2, 移植 v1 src/gep/assetStore.js 单写锁).
|
|
4
6
|
* 每 kind 一文件(genes/capsules/events.jsonl); append-only; O_EXCL 文件锁防并发写撕裂;
|
|
@@ -9,6 +11,7 @@ export declare class LocalJsonlProvider implements AssetStoreProvider {
|
|
|
9
11
|
readonly baseDir: string;
|
|
10
12
|
private readonly index;
|
|
11
13
|
private readonly lockPath;
|
|
14
|
+
private readonly bundleJournalPath;
|
|
12
15
|
private fileState;
|
|
13
16
|
private loaded;
|
|
14
17
|
constructor(baseDir: string);
|
|
@@ -18,8 +21,12 @@ export declare class LocalJsonlProvider implements AssetStoreProvider {
|
|
|
18
21
|
private refreshUnderLock;
|
|
19
22
|
private ensureFresh;
|
|
20
23
|
private updateFileStateAfterWrite;
|
|
24
|
+
private readPendingBundle;
|
|
25
|
+
private recoverPendingBundleUnderLock;
|
|
26
|
+
private hasPendingBundle;
|
|
21
27
|
put(asset: AssetRecord): Promise<PutResult>;
|
|
22
28
|
putConditional(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
29
|
+
putBundle(assets: readonly AssetRecord[]): Promise<PutResult[]>;
|
|
23
30
|
/**
|
|
24
31
|
* 迁移专用(M8-2): 以**冻结 asset_id** 原样写入, 不经 normalizeForPut 重算/校验.
|
|
25
32
|
* 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
|
|
@@ -29,6 +36,7 @@ export declare class LocalJsonlProvider implements AssetStoreProvider {
|
|
|
29
36
|
get(assetId: string): Promise<AssetRecord | null>;
|
|
30
37
|
findByLogicalId(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
|
|
31
38
|
list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
|
|
39
|
+
listAll(kind?: AssetKind): AssetRecord[];
|
|
32
40
|
search(q: SearchQuery): Promise<AssetRecord[]>;
|
|
33
41
|
/**
|
|
34
42
|
* Opt-in log compaction: rewrite each kind's jsonl keeping ONE line per asset_id (last wins),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { unlinkSync } from 'node:fs';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
3
|
import { acquireLock, releaseLock } from '../util/fileLock.js';
|
|
3
|
-
import { appendUtf8Durable, assertAssetStoreDirectory, assertOptionalRegularFile, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, replaceUtf8Durable, } from './assetStoreStorage.js';
|
|
4
|
+
import { appendUtf8Durable, assertAssetStoreDirectory, assertOptionalRegularFile, ensureAssetStoreDirectory, fsyncDirectoryBestEffort, readUtf8Regular, regularFileFingerprint, replaceUtf8Durable, } from './assetStoreStorage.js';
|
|
4
5
|
import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
|
|
5
6
|
import { FrozenAssetIdCollisionError, frozenAssetRecordsEqual, normalizeForPut, } from './provider.js';
|
|
6
7
|
function resultLogicalId(logicalId) {
|
|
@@ -8,7 +9,12 @@ function resultLogicalId(logicalId) {
|
|
|
8
9
|
? logicalId
|
|
9
10
|
: undefined;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
+
const BUNDLE_JOURNAL_FILE = '.assetstore-bundle.json';
|
|
13
|
+
const BUNDLE_JOURNAL_SCHEMA = 'evolver.assetstore-bundle.v1';
|
|
14
|
+
const MAX_BUNDLE_ASSETS = 64;
|
|
15
|
+
const MAX_BUNDLE_JOURNAL_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
/** Signal names a record advertises, across the four key spellings the pool uses. */
|
|
17
|
+
export function signalsOf(a) {
|
|
12
18
|
const out = [];
|
|
13
19
|
for (const key of ['signals_match', 'signals', 'trigger', 'trigger_signals']) {
|
|
14
20
|
const v = a[key];
|
|
@@ -29,6 +35,7 @@ export class LocalJsonlProvider {
|
|
|
29
35
|
baseDir;
|
|
30
36
|
index = new Map();
|
|
31
37
|
lockPath;
|
|
38
|
+
bundleJournalPath;
|
|
32
39
|
fileState = new Map();
|
|
33
40
|
loaded = false;
|
|
34
41
|
// `baseDir` is public-readonly so callers that inject a store (e.g. the CLI under test) can co-locate sidecars
|
|
@@ -37,6 +44,7 @@ export class LocalJsonlProvider {
|
|
|
37
44
|
this.baseDir = baseDir;
|
|
38
45
|
ensureAssetStoreDirectory(baseDir);
|
|
39
46
|
this.lockPath = join(baseDir, '.assetstore.lock');
|
|
47
|
+
this.bundleJournalPath = join(baseDir, BUNDLE_JOURNAL_FILE);
|
|
40
48
|
}
|
|
41
49
|
captureFileState() {
|
|
42
50
|
assertAssetStoreDirectory(this.baseDir);
|
|
@@ -86,11 +94,12 @@ export class LocalJsonlProvider {
|
|
|
86
94
|
}
|
|
87
95
|
ensureFresh() {
|
|
88
96
|
const state = this.captureFileState();
|
|
89
|
-
if (!this.stateChanged(state))
|
|
97
|
+
if (!this.hasPendingBundle() && !this.stateChanged(state))
|
|
90
98
|
return;
|
|
91
99
|
assertOptionalRegularFile(this.lockPath, 'lock_file');
|
|
92
100
|
acquireLock(this.lockPath);
|
|
93
101
|
try {
|
|
102
|
+
this.recoverPendingBundleUnderLock();
|
|
94
103
|
this.refreshUnderLock();
|
|
95
104
|
}
|
|
96
105
|
finally {
|
|
@@ -101,6 +110,63 @@ export class LocalJsonlProvider {
|
|
|
101
110
|
this.fileState = this.captureFileState();
|
|
102
111
|
this.loaded = true;
|
|
103
112
|
}
|
|
113
|
+
readPendingBundle() {
|
|
114
|
+
if (assertOptionalRegularFile(this.bundleJournalPath, 'temp_file') === null)
|
|
115
|
+
return undefined;
|
|
116
|
+
const raw = readUtf8Regular(this.bundleJournalPath, MAX_BUNDLE_JOURNAL_BYTES);
|
|
117
|
+
if (raw === null)
|
|
118
|
+
return undefined;
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(raw);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error('asset store bundle journal is malformed');
|
|
125
|
+
}
|
|
126
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
127
|
+
throw new Error('asset store bundle journal is malformed');
|
|
128
|
+
}
|
|
129
|
+
const journal = parsed;
|
|
130
|
+
const assets = journal['assets'];
|
|
131
|
+
if (journal['schema'] !== BUNDLE_JOURNAL_SCHEMA || !Array.isArray(assets) || assets.length === 0 || assets.length > MAX_BUNDLE_ASSETS) {
|
|
132
|
+
throw new Error('asset store bundle journal is invalid');
|
|
133
|
+
}
|
|
134
|
+
return assets.map((asset) => {
|
|
135
|
+
if (!asset || typeof asset !== 'object' || Array.isArray(asset))
|
|
136
|
+
throw new Error('asset store bundle journal is invalid');
|
|
137
|
+
const record = asset;
|
|
138
|
+
if (typeof record.type !== 'string' || !Object.prototype.hasOwnProperty.call(LOCAL_ASSET_FILES, record.type)
|
|
139
|
+
|| typeof record.asset_id !== 'string' || record.asset_id.length === 0) {
|
|
140
|
+
throw new Error('asset store bundle journal is invalid');
|
|
141
|
+
}
|
|
142
|
+
return record;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
recoverPendingBundleUnderLock() {
|
|
146
|
+
const pending = this.readPendingBundle();
|
|
147
|
+
if (!pending)
|
|
148
|
+
return;
|
|
149
|
+
this.refreshUnderLock();
|
|
150
|
+
for (const pendingRecord of pending) {
|
|
151
|
+
const { record } = normalizeForPut(pendingRecord);
|
|
152
|
+
if (record.asset_id !== pendingRecord.asset_id)
|
|
153
|
+
throw new Error('asset store bundle journal asset_id mismatch');
|
|
154
|
+
const existing = this.index.get(record.asset_id);
|
|
155
|
+
if (existing) {
|
|
156
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
157
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
appendUtf8Durable(join(this.baseDir, LOCAL_ASSET_FILES[record.type]), `${JSON.stringify(record)}\n`);
|
|
161
|
+
this.index.set(record.asset_id, record);
|
|
162
|
+
}
|
|
163
|
+
unlinkSync(this.bundleJournalPath);
|
|
164
|
+
fsyncDirectoryBestEffort(this.baseDir);
|
|
165
|
+
this.updateFileStateAfterWrite();
|
|
166
|
+
}
|
|
167
|
+
hasPendingBundle() {
|
|
168
|
+
return assertOptionalRegularFile(this.bundleJournalPath, 'temp_file') !== null;
|
|
169
|
+
}
|
|
104
170
|
async put(asset) {
|
|
105
171
|
return this.putConditional(asset, { allowLogicalCollision: true });
|
|
106
172
|
}
|
|
@@ -112,6 +178,7 @@ export class LocalJsonlProvider {
|
|
|
112
178
|
assertOptionalRegularFile(this.lockPath, 'lock_file');
|
|
113
179
|
acquireLock(this.lockPath);
|
|
114
180
|
try {
|
|
181
|
+
this.recoverPendingBundleUnderLock();
|
|
115
182
|
// Refresh under the shared lock so another process cannot append between reload and dedupe.
|
|
116
183
|
this.refreshUnderLock();
|
|
117
184
|
const existing = this.index.get(record.asset_id);
|
|
@@ -159,6 +226,59 @@ export class LocalJsonlProvider {
|
|
|
159
226
|
} : {}),
|
|
160
227
|
};
|
|
161
228
|
}
|
|
229
|
+
async putBundle(assets) {
|
|
230
|
+
if (assets.length === 0)
|
|
231
|
+
return [];
|
|
232
|
+
if (assets.length > MAX_BUNDLE_ASSETS)
|
|
233
|
+
throw new Error('asset store bundle is too large');
|
|
234
|
+
const normalized = assets.map((asset) => normalizeForPut(asset));
|
|
235
|
+
const seen = new Map();
|
|
236
|
+
for (const normalizedRecord of normalized) {
|
|
237
|
+
const { record } = normalizedRecord;
|
|
238
|
+
const previous = seen.get(record.asset_id);
|
|
239
|
+
if (previous && !frozenAssetRecordsEqual(previous.record, record))
|
|
240
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
241
|
+
if (!previous)
|
|
242
|
+
seen.set(record.asset_id, normalizedRecord);
|
|
243
|
+
}
|
|
244
|
+
const results = [];
|
|
245
|
+
assertOptionalRegularFile(this.lockPath, 'lock_file');
|
|
246
|
+
acquireLock(this.lockPath);
|
|
247
|
+
try {
|
|
248
|
+
this.recoverPendingBundleUnderLock();
|
|
249
|
+
this.refreshUnderLock();
|
|
250
|
+
const pending = [];
|
|
251
|
+
for (const { record, verified } of seen.values()) {
|
|
252
|
+
const existing = this.index.get(record.asset_id);
|
|
253
|
+
if (existing) {
|
|
254
|
+
if (!frozenAssetRecordsEqual(existing, record))
|
|
255
|
+
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
256
|
+
results.push({ asset_id: record.asset_id, stored: false, verified });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
pending.push(record);
|
|
260
|
+
results.push({ asset_id: record.asset_id, stored: true, verified });
|
|
261
|
+
}
|
|
262
|
+
if (pending.length === 0)
|
|
263
|
+
return results;
|
|
264
|
+
const journal = `${JSON.stringify({ schema: BUNDLE_JOURNAL_SCHEMA, assets: pending })}\n`;
|
|
265
|
+
if (Buffer.byteLength(journal, 'utf8') > MAX_BUNDLE_JOURNAL_BYTES) {
|
|
266
|
+
throw new Error('asset store bundle is too large');
|
|
267
|
+
}
|
|
268
|
+
replaceUtf8Durable(this.bundleJournalPath, journal);
|
|
269
|
+
for (const record of pending) {
|
|
270
|
+
appendUtf8Durable(join(this.baseDir, LOCAL_ASSET_FILES[record.type]), `${JSON.stringify(record)}\n`);
|
|
271
|
+
this.index.set(record.asset_id, record);
|
|
272
|
+
}
|
|
273
|
+
unlinkSync(this.bundleJournalPath);
|
|
274
|
+
fsyncDirectoryBestEffort(this.baseDir);
|
|
275
|
+
this.updateFileStateAfterWrite();
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
releaseLock(this.lockPath);
|
|
279
|
+
}
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
162
282
|
/**
|
|
163
283
|
* 迁移专用(M8-2): 以**冻结 asset_id** 原样写入, 不经 normalizeForPut 重算/校验.
|
|
164
284
|
* 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
|
|
@@ -175,6 +295,7 @@ export class LocalJsonlProvider {
|
|
|
175
295
|
assertOptionalRegularFile(this.lockPath, 'lock_file');
|
|
176
296
|
acquireLock(this.lockPath);
|
|
177
297
|
try {
|
|
298
|
+
this.recoverPendingBundleUnderLock();
|
|
178
299
|
this.refreshUnderLock();
|
|
179
300
|
const existing = this.index.get(record.asset_id);
|
|
180
301
|
if (existing) {
|
|
@@ -246,6 +367,10 @@ export class LocalJsonlProvider {
|
|
|
246
367
|
}
|
|
247
368
|
return out;
|
|
248
369
|
}
|
|
370
|
+
listAll(kind) {
|
|
371
|
+
this.ensureFresh();
|
|
372
|
+
return [...this.index.values()].filter((record) => kind === undefined || record.type === kind);
|
|
373
|
+
}
|
|
249
374
|
async search(q) {
|
|
250
375
|
this.ensureFresh();
|
|
251
376
|
const out = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type AssetStoreProvider, type AssetRecord, type ConditionalPutOptions, type ConditionalPutResult, type PutResult } from './provider.js';
|
|
2
2
|
export type ProvenanceSource = 'local' | 'migrated' | 'hub';
|
|
3
3
|
export type ProvenanceDecision = 'promoted' | 'revoked';
|
|
4
|
+
export declare const UNVERIFIED_V1_IMPORT_REASON = "unverified_v1_import";
|
|
4
5
|
export interface ProvenanceRecord {
|
|
5
6
|
assetId: string;
|
|
6
7
|
source: ProvenanceSource;
|
|
@@ -11,13 +12,47 @@ export interface ProvenanceRecord {
|
|
|
11
12
|
/** Legacy promotion actor field kept for existing sidecar readers. */
|
|
12
13
|
promotedBy?: string;
|
|
13
14
|
reason?: string;
|
|
14
|
-
/** Canonical content id of the exact hash-mismatched body accepted by
|
|
15
|
+
/** Canonical content id of the exact hash-mismatched body accepted by a supported unverified ingest. */
|
|
15
16
|
frozenContentId?: string;
|
|
16
17
|
}
|
|
17
18
|
export interface ProvenanceTrustChange {
|
|
18
19
|
changed: boolean;
|
|
19
20
|
record: ProvenanceRecord;
|
|
20
21
|
}
|
|
22
|
+
export type UnverifiedIngestDisposition = {
|
|
23
|
+
readonly status: 'create';
|
|
24
|
+
readonly assetId: string;
|
|
25
|
+
readonly frozenContentId: string;
|
|
26
|
+
readonly provenanceAction: 'stage_and_finalize';
|
|
27
|
+
} | {
|
|
28
|
+
readonly status: 'already_bound';
|
|
29
|
+
readonly assetId: string;
|
|
30
|
+
readonly frozenContentId: string;
|
|
31
|
+
readonly provenanceAction: 'finalize' | 'none';
|
|
32
|
+
} | {
|
|
33
|
+
readonly status: 'preserve_trust';
|
|
34
|
+
readonly assetId: string;
|
|
35
|
+
readonly frozenContentId: string;
|
|
36
|
+
readonly provenanceAction: 'none';
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'collision';
|
|
39
|
+
readonly assetId: string;
|
|
40
|
+
readonly frozenContentId: string;
|
|
41
|
+
readonly collision: 'asset' | 'provenance';
|
|
42
|
+
readonly provenanceAction: 'none';
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
|
|
46
|
+
* target snapshot during migration planning, then apply through ingestUnverified without duplicating its
|
|
47
|
+
* trust and collision rules.
|
|
48
|
+
*/
|
|
49
|
+
export declare function planUnverifiedIngest(record: AssetRecord, existing: AssetRecord | null, currentProvenance: ProvenanceRecord | null, reason: string, source?: ProvenanceSource): UnverifiedIngestDisposition;
|
|
50
|
+
/**
|
|
51
|
+
* A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
|
|
52
|
+
* waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
|
|
53
|
+
* reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
|
|
54
|
+
*/
|
|
55
|
+
export declare function isActiveUnverifiedProvenance(record: ProvenanceRecord | null | undefined, frozenContentId: string | null | undefined): boolean;
|
|
21
56
|
export declare class ProvenanceWritePendingError extends Error {
|
|
22
57
|
readonly assetId: string;
|
|
23
58
|
readonly code = "PROVENANCE_WRITE_PENDING";
|
|
@@ -25,8 +60,8 @@ export declare class ProvenanceWritePendingError extends Error {
|
|
|
25
60
|
}
|
|
26
61
|
/**
|
|
27
62
|
* Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
|
|
28
|
-
* record = trusted:
|
|
29
|
-
* write here
|
|
63
|
+
* record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
|
|
64
|
+
* do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
|
|
30
65
|
*/
|
|
31
66
|
export declare class ProvenanceStore {
|
|
32
67
|
private readonly now;
|
|
@@ -8,6 +8,98 @@ import { assertCapsuleGeneBinding, FrozenAssetIdCollisionError, InvalidFrozenPut
|
|
|
8
8
|
import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, truncateUtf8SuffixDurable, withAssetStoreLock, } from './assetStoreStorage.js';
|
|
9
9
|
import { assertTrustSidecarHealthy, parseProvenanceRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
|
|
10
10
|
import { computeAssetId } from '../wire/index.js';
|
|
11
|
+
export const UNVERIFIED_V1_IMPORT_REASON = 'unverified_v1_import';
|
|
12
|
+
const UNVERIFIED_WRITE_PENDING_REASON = 'unverified_hub_write_pending';
|
|
13
|
+
function unverifiedStageAction(current, source, frozenContentId) {
|
|
14
|
+
if (!current)
|
|
15
|
+
return 'append';
|
|
16
|
+
const isUndecidedUntrusted = current.trusted === false
|
|
17
|
+
&& current.decision === undefined
|
|
18
|
+
&& current.decidedBy === undefined
|
|
19
|
+
&& current.promotedBy === undefined;
|
|
20
|
+
if (isActiveUnverifiedProvenance(current, frozenContentId))
|
|
21
|
+
return 'append';
|
|
22
|
+
if (isUndecidedUntrusted && current.source === source && current.reason === undefined)
|
|
23
|
+
return 'append';
|
|
24
|
+
if (isUndecidedUntrusted
|
|
25
|
+
&& current.source === source
|
|
26
|
+
&& current.reason === UNVERIFIED_WRITE_PENDING_REASON
|
|
27
|
+
&& current.frozenContentId === frozenContentId)
|
|
28
|
+
return 'reuse_pending';
|
|
29
|
+
return 'blocked';
|
|
30
|
+
}
|
|
31
|
+
function unverifiedFinalizeAction(current, source, reason, frozenContentId) {
|
|
32
|
+
if (!current)
|
|
33
|
+
return 'append';
|
|
34
|
+
if (current.trusted === true
|
|
35
|
+
|| current.decision !== undefined
|
|
36
|
+
|| current.decidedBy !== undefined
|
|
37
|
+
|| current.promotedBy !== undefined)
|
|
38
|
+
return 'preserve';
|
|
39
|
+
const sameWaiver = current.source === source && current.trusted === false && current.reason === reason;
|
|
40
|
+
if (sameWaiver && current.frozenContentId === frozenContentId)
|
|
41
|
+
return 'already_bound';
|
|
42
|
+
if (!sameWaiver && (current.reason !== UNVERIFIED_WRITE_PENDING_REASON
|
|
43
|
+
|| (current.frozenContentId !== undefined && current.frozenContentId !== frozenContentId)))
|
|
44
|
+
return 'preserve';
|
|
45
|
+
return 'append';
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
|
|
49
|
+
* target snapshot during migration planning, then apply through ingestUnverified without duplicating its
|
|
50
|
+
* trust and collision rules.
|
|
51
|
+
*/
|
|
52
|
+
export function planUnverifiedIngest(record, existing, currentProvenance, reason, source = 'hub') {
|
|
53
|
+
assertCapsuleGeneBinding(record);
|
|
54
|
+
const frozenContentId = computeAssetId(record);
|
|
55
|
+
if (!frozenContentId)
|
|
56
|
+
throw new Error('failed to compute frozen content id');
|
|
57
|
+
const base = { assetId: record.asset_id, frozenContentId };
|
|
58
|
+
if (currentProvenance && currentProvenance.assetId !== record.asset_id) {
|
|
59
|
+
return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
|
|
60
|
+
}
|
|
61
|
+
if (existing) {
|
|
62
|
+
if (!frozenAssetRecordsEqual(existing, record)) {
|
|
63
|
+
return { ...base, status: 'collision', collision: 'asset', provenanceAction: 'none' };
|
|
64
|
+
}
|
|
65
|
+
const finalize = unverifiedFinalizeAction(currentProvenance, source, reason, frozenContentId);
|
|
66
|
+
if (finalize === 'preserve') {
|
|
67
|
+
return { ...base, status: 'preserve_trust', provenanceAction: 'none' };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
...base,
|
|
71
|
+
status: 'already_bound',
|
|
72
|
+
provenanceAction: finalize === 'append' ? 'finalize' : 'none',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (unverifiedStageAction(currentProvenance, source, frozenContentId) === 'blocked') {
|
|
76
|
+
return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
|
|
77
|
+
}
|
|
78
|
+
return { ...base, status: 'create', provenanceAction: 'stage_and_finalize' };
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
|
|
82
|
+
* waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
|
|
83
|
+
* reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
|
|
84
|
+
*/
|
|
85
|
+
export function isActiveUnverifiedProvenance(record, frozenContentId) {
|
|
86
|
+
if (!record
|
|
87
|
+
|| !frozenContentId
|
|
88
|
+
|| record.trusted !== false
|
|
89
|
+
|| record.decision !== undefined
|
|
90
|
+
|| record.decidedBy !== undefined
|
|
91
|
+
|| record.promotedBy !== undefined
|
|
92
|
+
|| record.frozenContentId !== frozenContentId)
|
|
93
|
+
return false;
|
|
94
|
+
// V1 input is local legacy data rather than a network-issued identifier. Never let a malformed declared
|
|
95
|
+
// identity become healthy merely because the shallow wire gate does not enforce the schema's asset_id regex.
|
|
96
|
+
if (record.reason === UNVERIFIED_V1_IMPORT_REASON
|
|
97
|
+
&& !/^sha256:[0-9a-f]{64}$/.test(record.assetId))
|
|
98
|
+
return false;
|
|
99
|
+
return (record.source === 'hub'
|
|
100
|
+
&& (record.reason === 'unverified_hub_rewrite' || record.reason === 'unverified_hub_synthesized')) || (record.source === 'migrated'
|
|
101
|
+
&& (record.reason === 'unverified_gepx_import' || record.reason === UNVERIFIED_V1_IMPORT_REASON));
|
|
102
|
+
}
|
|
11
103
|
export class ProvenanceWritePendingError extends Error {
|
|
12
104
|
assetId;
|
|
13
105
|
code = 'PROVENANCE_WRITE_PENDING';
|
|
@@ -22,8 +114,8 @@ function immutableRecord(record) {
|
|
|
22
114
|
}
|
|
23
115
|
/**
|
|
24
116
|
* Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
|
|
25
|
-
* record = trusted:
|
|
26
|
-
* write here
|
|
117
|
+
* record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
|
|
118
|
+
* do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
|
|
27
119
|
*/
|
|
28
120
|
export class ProvenanceStore {
|
|
29
121
|
now;
|
|
@@ -127,26 +219,15 @@ export class ProvenanceStore {
|
|
|
127
219
|
return withAssetStoreLock(this.lockPath, () => {
|
|
128
220
|
this.refreshUnderLock();
|
|
129
221
|
const current = this.index.get(assetId);
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
&& current.decidedBy === undefined
|
|
133
|
-
&& current.promotedBy === undefined;
|
|
134
|
-
const isWaiver = isUndecidedUntrusted
|
|
135
|
-
&& current.frozenContentId === frozenContentId
|
|
136
|
-
&& ((current.source === 'hub'
|
|
137
|
-
&& (current.reason === 'unverified_hub_rewrite' || current.reason === 'unverified_hub_synthesized'))
|
|
138
|
-
|| (current.source === 'migrated' && current.reason === 'unverified_gepx_import'));
|
|
139
|
-
const isStaleIngestMarker = isUndecidedUntrusted
|
|
140
|
-
&& current.source === source
|
|
141
|
-
&& current.reason === undefined;
|
|
142
|
-
if (current && !isWaiver && !isStaleIngestMarker)
|
|
222
|
+
const action = unverifiedStageAction(current, source, frozenContentId);
|
|
223
|
+
if (current && action !== 'append')
|
|
143
224
|
return { record: current, appended: false };
|
|
144
225
|
return {
|
|
145
226
|
record: this.appendUnderLock({
|
|
146
227
|
assetId,
|
|
147
228
|
source,
|
|
148
229
|
trusted: false,
|
|
149
|
-
reason:
|
|
230
|
+
reason: UNVERIFIED_WRITE_PENDING_REASON,
|
|
150
231
|
frozenContentId,
|
|
151
232
|
at: new Date(this.now()).toISOString(),
|
|
152
233
|
}),
|
|
@@ -160,20 +241,9 @@ export class ProvenanceStore {
|
|
|
160
241
|
return withAssetStoreLock(this.lockPath, () => {
|
|
161
242
|
this.refreshUnderLock();
|
|
162
243
|
const current = this.index.get(assetId);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|| current.decidedBy !== undefined
|
|
167
|
-
|| current.promotedBy !== undefined)
|
|
168
|
-
return current;
|
|
169
|
-
const sameWaiver = current.source === source && current.trusted === false && current.reason === reason;
|
|
170
|
-
if (sameWaiver && current.frozenContentId === frozenContentId)
|
|
171
|
-
return current;
|
|
172
|
-
if (!sameWaiver && (current.reason !== 'unverified_hub_write_pending'
|
|
173
|
-
|| (current.frozenContentId !== undefined
|
|
174
|
-
&& current.frozenContentId !== frozenContentId)))
|
|
175
|
-
return current;
|
|
176
|
-
}
|
|
244
|
+
const action = unverifiedFinalizeAction(current, source, reason, frozenContentId);
|
|
245
|
+
if (current && action !== 'append')
|
|
246
|
+
return current;
|
|
177
247
|
return this.appendUnderLock({
|
|
178
248
|
assetId,
|
|
179
249
|
source,
|
|
@@ -221,7 +291,7 @@ export class ProvenanceStore {
|
|
|
221
291
|
if (trusted
|
|
222
292
|
&& current?.trusted === false
|
|
223
293
|
&& current.decision === undefined
|
|
224
|
-
&& current.reason ===
|
|
294
|
+
&& current.reason === UNVERIFIED_WRITE_PENDING_REASON) {
|
|
225
295
|
throw new ProvenanceWritePendingError(assetId);
|
|
226
296
|
}
|
|
227
297
|
const decision = trusted ? 'promoted' : 'revoked';
|
|
@@ -300,13 +370,7 @@ export async function ingestUnverified(store, prov, record, reason, source = 'hu
|
|
|
300
370
|
// the asset stays untrusted and health still reports its hash mismatch instead of treating it as #570.
|
|
301
371
|
const staged = prov.stageUnverifiedWrite(record.asset_id, source, frozenContentId);
|
|
302
372
|
// A missing body with an explicit trust decision cannot be replaced without reopening a promotion race.
|
|
303
|
-
if (staged.
|
|
304
|
-
|| staged.trusted
|
|
305
|
-
|| staged.decision !== undefined
|
|
306
|
-
|| staged.decidedBy !== undefined
|
|
307
|
-
|| staged.promotedBy !== undefined
|
|
308
|
-
|| staged.reason !== 'unverified_hub_write_pending'
|
|
309
|
-
|| staged.frozenContentId !== frozenContentId) {
|
|
373
|
+
if (planUnverifiedIngest(record, null, staged, reason, source).status !== 'create') {
|
|
310
374
|
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
311
375
|
}
|
|
312
376
|
const result = validateFrozenPutResult(await store.putFrozen(record), record.asset_id);
|
|
@@ -334,13 +398,7 @@ export async function ingestUnverifiedConditional(store, prov, record, reason, o
|
|
|
334
398
|
return { asset_id: record.asset_id, stored: false, verified: false, status: 'already_exists' };
|
|
335
399
|
}
|
|
336
400
|
const stage = prov.stageUnverifiedWriteTracked(record.asset_id, source, frozenContentId);
|
|
337
|
-
if (stage.record.
|
|
338
|
-
|| stage.record.trusted
|
|
339
|
-
|| stage.record.decision !== undefined
|
|
340
|
-
|| stage.record.decidedBy !== undefined
|
|
341
|
-
|| stage.record.promotedBy !== undefined
|
|
342
|
-
|| stage.record.reason !== 'unverified_hub_write_pending'
|
|
343
|
-
|| stage.record.frozenContentId !== frozenContentId) {
|
|
401
|
+
if (planUnverifiedIngest(record, null, stage.record, reason, source).status !== 'create') {
|
|
344
402
|
throw new FrozenAssetIdCollisionError(record.asset_id);
|
|
345
403
|
}
|
|
346
404
|
const result = validateConditionalPutResult(await store.putFrozenConditional(record, options), record.asset_id, options);
|
|
@@ -55,6 +55,10 @@ export interface SearchQuery {
|
|
|
55
55
|
*/
|
|
56
56
|
export interface AssetStoreProvider {
|
|
57
57
|
put(asset: AssetRecord): Promise<PutResult>;
|
|
58
|
+
/** 可选的可崩溃恢复批量写入,用于保持相关资产(例如 Gene/Capsule 对)的一致性。
|
|
59
|
+
* 需要持久化相关资产组的调用方必须先确认此能力存在,不得退回顺序单条写入。
|
|
60
|
+
* 返回结果按唯一 asset_id 各一条(同 bundle 内重复条目按幂等去重),调用方必须按 asset_id 键控消费结果,不得按索引与输入条目对齐。 */
|
|
61
|
+
putBundle?(assets: readonly AssetRecord[]): Promise<PutResult[]>;
|
|
58
62
|
/** Optional atomic capability; use supportsAtomicConditionalPut() before calling through this interface. */
|
|
59
63
|
putConditional?(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
|
|
60
64
|
get(assetId: string): Promise<AssetRecord | null>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AssetStoreProvider, AssetRecord } from './provider.js';
|
|
2
|
-
import { ReviewLedger } from './reviewLedger.js';
|
|
2
|
+
import { ReviewLedger, type ReviewRecord } from './reviewLedger.js';
|
|
3
3
|
import { ProvenanceStore } from './provenance.js';
|
|
4
4
|
/**
|
|
5
5
|
* The ReviewLedger CO-LOCATED with a store — its quarantine/approve records live in the SAME dir as the genes.
|
|
@@ -17,4 +17,22 @@ export declare function provenanceStoreForStore(store: AssetStoreProvider): Prov
|
|
|
17
17
|
* quarantined or rejected draft is withheld. A gene with NO provenance record is trusted; hub-ingested genes are
|
|
18
18
|
* withheld until promotion. This mirrors candidateAssembly's trust-first + review-first filters.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Authoritative pending-review queue: quarantined ledger records, oldest first.
|
|
22
|
+
* Callers that display a human queue MUST start here instead of `store.list`, or a
|
|
23
|
+
* draft sitting past the newest-N cutoff is invisible even though it is still blocked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function pendingReviewRecords(review: ReviewLedger, excludeAssetIds?: ReadonlySet<string>): ReviewRecord[];
|
|
26
|
+
/**
|
|
27
|
+
* Pending Gene queue with the asset store as the type/existence authority.
|
|
28
|
+
*
|
|
29
|
+
* ReviewLedger is intentionally generic because the same sidecar also records
|
|
30
|
+
* AntiGene decisions. A quarantined row can also outlive its asset after a
|
|
31
|
+
* repair or a truncated migration. Human Gene queues must therefore resolve
|
|
32
|
+
* every row through the provider instead of treating the ledger alone as a
|
|
33
|
+
* complete inventory. The lookups are deliberately sequential so a malformed
|
|
34
|
+
* or unexpectedly large ledger cannot fan out an unbounded request burst to a
|
|
35
|
+
* remote provider.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pendingGeneReviewRecords(store: AssetStoreProvider, review: ReviewLedger): Promise<ReviewRecord[]>;
|
|
20
38
|
export declare function listApprovedGenes(store: AssetStoreProvider, review: ReviewLedger, maxGenes: number, provenance?: ProvenanceStore): Promise<AssetRecord[]>;
|
|
@@ -28,6 +28,42 @@ export function provenanceStoreForStore(store) {
|
|
|
28
28
|
* quarantined or rejected draft is withheld. A gene with NO provenance record is trusted; hub-ingested genes are
|
|
29
29
|
* withheld until promotion. This mirrors candidateAssembly's trust-first + review-first filters.
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* Authoritative pending-review queue: quarantined ledger records, oldest first.
|
|
33
|
+
* Callers that display a human queue MUST start here instead of `store.list`, or a
|
|
34
|
+
* draft sitting past the newest-N cutoff is invisible even though it is still blocked.
|
|
35
|
+
*/
|
|
36
|
+
export function pendingReviewRecords(review, excludeAssetIds = new Set()) {
|
|
37
|
+
return review.records()
|
|
38
|
+
.filter((record) => record.state === 'quarantined' && !excludeAssetIds.has(record.assetId))
|
|
39
|
+
.sort((a, b) => a.at.localeCompare(b.at));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Pending Gene queue with the asset store as the type/existence authority.
|
|
43
|
+
*
|
|
44
|
+
* ReviewLedger is intentionally generic because the same sidecar also records
|
|
45
|
+
* AntiGene decisions. A quarantined row can also outlive its asset after a
|
|
46
|
+
* repair or a truncated migration. Human Gene queues must therefore resolve
|
|
47
|
+
* every row through the provider instead of treating the ledger alone as a
|
|
48
|
+
* complete inventory. The lookups are deliberately sequential so a malformed
|
|
49
|
+
* or unexpectedly large ledger cannot fan out an unbounded request burst to a
|
|
50
|
+
* remote provider.
|
|
51
|
+
*/
|
|
52
|
+
export async function pendingGeneReviewRecords(store, review) {
|
|
53
|
+
const pending = pendingReviewRecords(review);
|
|
54
|
+
// LocalJsonlProvider 可直接使用内存索引精确回答,避免 sidecar 变大后逐行等待查询。
|
|
55
|
+
if (store instanceof LocalJsonlProvider) {
|
|
56
|
+
const geneIds = new Set(store.listAll('Gene').map((asset) => asset.asset_id));
|
|
57
|
+
return pending.filter((record) => geneIds.has(record.assetId));
|
|
58
|
+
}
|
|
59
|
+
const genes = [];
|
|
60
|
+
for (const record of pending) {
|
|
61
|
+
const asset = await store.get(record.assetId);
|
|
62
|
+
if (asset?.type === 'Gene')
|
|
63
|
+
genes.push(record);
|
|
64
|
+
}
|
|
65
|
+
return genes;
|
|
66
|
+
}
|
|
31
67
|
export async function listApprovedGenes(store, review, maxGenes, provenance = provenanceStoreForStore(store)) {
|
|
32
68
|
const all = await store.list('Gene', GENE_SCAN_LIMIT);
|
|
33
69
|
const trust = provenance.snapshot();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AssetKind, AssetRecord, AssetStoreProvider, ConditionalPutOptions, ConditionalPutResult, PutResult, SearchQuery } from './provider.js';
|
|
2
|
+
export declare class UnionReadStore implements AssetStoreProvider {
|
|
3
|
+
private readonly primary;
|
|
4
|
+
private readonly readOnly;
|
|
5
|
+
readonly putConditional?: (asset: AssetRecord, options?: ConditionalPutOptions) => Promise<ConditionalPutResult>;
|
|
6
|
+
readonly putBundle?: (assets: readonly AssetRecord[]) => Promise<PutResult[]>;
|
|
7
|
+
readonly putFrozen?: (record: AssetRecord) => Promise<PutResult>;
|
|
8
|
+
readonly putFrozenConditional?: (record: AssetRecord, options?: ConditionalPutOptions) => Promise<ConditionalPutResult>;
|
|
9
|
+
readonly findByLogicalId?: (id: string, limit?: number, kind?: AssetKind) => Promise<AssetRecord[]>;
|
|
10
|
+
/**
|
|
11
|
+
* @param primary the writable pool this engine owns; every mutation lands here
|
|
12
|
+
* @param readOnly additional pools to read, in precedence order after `primary`
|
|
13
|
+
*/
|
|
14
|
+
constructor(primary: AssetStoreProvider, readOnly: readonly AssetStoreProvider[]);
|
|
15
|
+
private get sources();
|
|
16
|
+
/**
|
|
17
|
+
* Merge one read across every source: deduplicate by asset_id with earlier sources winning, then fill
|
|
18
|
+
* the result by {@link interleave} so every source gets a fair share of the cap.
|
|
19
|
+
*/
|
|
20
|
+
private merged;
|
|
21
|
+
put(asset: AssetRecord): Promise<PutResult>;
|
|
22
|
+
get(assetId: string): Promise<AssetRecord | null>;
|
|
23
|
+
list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
|
|
24
|
+
search(q: SearchQuery): Promise<AssetRecord[]>;
|
|
25
|
+
}
|