@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.
Files changed (160) hide show
  1. package/assets/gep/genes.jsonl +5 -5
  2. package/dist/algo/bans.d.ts +10 -1
  3. package/dist/algo/bans.js +56 -6
  4. package/dist/algo/candidateAssembly.d.ts +25 -2
  5. package/dist/algo/candidateAssembly.js +143 -20
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/cycleEngine.d.ts +40 -5
  8. package/dist/algo/cycleEngine.js +277 -37
  9. package/dist/algo/evolutionEvent.d.ts +26 -0
  10. package/dist/algo/evolutionEvent.js +32 -0
  11. package/dist/algo/exploration.d.ts +7 -0
  12. package/dist/algo/exploration.js +16 -3
  13. package/dist/algo/geneHealth.d.ts +34 -1
  14. package/dist/algo/geneHealth.js +42 -0
  15. package/dist/algo/geneIntake.d.ts +46 -1
  16. package/dist/algo/geneIntake.js +121 -10
  17. package/dist/algo/geneSelection.d.ts +115 -6
  18. package/dist/algo/geneSelection.js +459 -32
  19. package/dist/algo/index.d.ts +4 -1
  20. package/dist/algo/index.js +4 -1
  21. package/dist/algo/kautoProjection.d.ts +41 -0
  22. package/dist/algo/kautoProjection.js +95 -0
  23. package/dist/algo/kautoValidator.d.ts +68 -0
  24. package/dist/algo/kautoValidator.js +256 -0
  25. package/dist/algo/orchestrator.d.ts +14 -1
  26. package/dist/algo/orchestrator.js +9 -2
  27. package/dist/algo/solidify.d.ts +11 -2
  28. package/dist/algo/solidify.js +37 -7
  29. package/dist/algo/ucb1.d.ts +53 -0
  30. package/dist/algo/ucb1.js +156 -0
  31. package/dist/assetrepair/hubRejection.d.ts +12 -0
  32. package/dist/assetrepair/hubRejection.js +109 -0
  33. package/dist/assetrepair/index.d.ts +2 -0
  34. package/dist/assetrepair/index.js +2 -0
  35. package/dist/assetrepair/repair.d.ts +33 -0
  36. package/dist/assetrepair/repair.js +155 -0
  37. package/dist/assetstore/assetSidecarRecords.js +5 -1
  38. package/dist/assetstore/assetStoreHealth.d.ts +2 -3
  39. package/dist/assetstore/assetStoreHealth.js +4 -10
  40. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  41. package/dist/assetstore/foreignJsonlSource.js +150 -0
  42. package/dist/assetstore/index.d.ts +3 -0
  43. package/dist/assetstore/index.js +3 -0
  44. package/dist/assetstore/learningHistory.js +3 -3
  45. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  46. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  47. package/dist/assetstore/localJsonl.d.ts +8 -0
  48. package/dist/assetstore/localJsonl.js +128 -3
  49. package/dist/assetstore/provenance.d.ts +38 -3
  50. package/dist/assetstore/provenance.js +103 -45
  51. package/dist/assetstore/provider.d.ts +4 -0
  52. package/dist/assetstore/reviewFilter.d.ts +19 -1
  53. package/dist/assetstore/reviewFilter.js +36 -0
  54. package/dist/assetstore/unionReadStore.d.ts +25 -0
  55. package/dist/assetstore/unionReadStore.js +119 -0
  56. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  57. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  58. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  59. package/dist/benchmark/antiGeneRollout.js +4 -3
  60. package/dist/benchmark/index.d.ts +2 -1
  61. package/dist/benchmark/index.js +2 -1
  62. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  63. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  64. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  65. package/dist/bootstrap/envFingerprint.js +5 -0
  66. package/dist/bootstrap/index.d.ts +2 -1
  67. package/dist/bootstrap/index.js +2 -1
  68. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  69. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  70. package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
  71. package/dist/bootstrap/v1EnvCompat.js +48 -1
  72. package/dist/events/eventArchive.d.ts +2 -0
  73. package/dist/events/eventArchive.js +13 -3
  74. package/dist/events/eventSchema.d.ts +7 -7
  75. package/dist/events/eventStore.d.ts +2 -0
  76. package/dist/events/eventStore.js +5 -1
  77. package/dist/events/ingest.d.ts +2 -1
  78. package/dist/events/ingest.js +6 -0
  79. package/dist/exec/autoExec.d.ts +30 -3
  80. package/dist/exec/autoExec.js +294 -29
  81. package/dist/exec/autonomousCycle.d.ts +7 -0
  82. package/dist/exec/autonomousCycle.js +2 -0
  83. package/dist/exec/claudeBridge.d.ts +28 -6
  84. package/dist/exec/claudeBridge.js +411 -258
  85. package/dist/exec/executionBinding.d.ts +414 -0
  86. package/dist/exec/executionBinding.js +588 -0
  87. package/dist/exec/index.d.ts +1 -0
  88. package/dist/exec/index.js +1 -0
  89. package/dist/exec/proofOfWork.d.ts +1 -1
  90. package/dist/exec/proofOfWork.js +2 -2
  91. package/dist/exec/runnerRegistry.d.ts +5 -0
  92. package/dist/exec/runnerRegistry.js +54 -14
  93. package/dist/hub/capability.d.ts +40 -1
  94. package/dist/hub/conversationDistiller.d.ts +19 -0
  95. package/dist/hub/conversationDistiller.js +115 -37
  96. package/dist/hub/fake.d.ts +1 -0
  97. package/dist/hub/fake.js +1 -0
  98. package/dist/hub/index.d.ts +1 -0
  99. package/dist/hub/index.js +1 -0
  100. package/dist/hub/recipeCompose.d.ts +27 -0
  101. package/dist/hub/recipeCompose.js +90 -0
  102. package/dist/hub/sanitize.js +119 -5
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +2 -0
  105. package/dist/mailbox/ipcServer.d.ts +2 -0
  106. package/dist/mailbox/ipcServer.js +20 -0
  107. package/dist/mailbox/store.d.ts +52 -2
  108. package/dist/mailbox/store.js +546 -81
  109. package/dist/modelCompatibility.d.ts +164 -0
  110. package/dist/modelCompatibility.js +309 -0
  111. package/dist/observers/valueDigestObserver.d.ts +9 -0
  112. package/dist/observers/valueDigestObserver.js +35 -2
  113. package/dist/ops/cleanup.js +1 -1
  114. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  115. package/dist/ops/evolutionGraphProjection.js +315 -0
  116. package/dist/ops/index.d.ts +2 -1
  117. package/dist/ops/index.js +2 -1
  118. package/dist/ops/selfUpdate.d.ts +8 -0
  119. package/dist/ops/selfUpdate.js +24 -8
  120. package/dist/ops/valueOutreach.d.ts +3 -1
  121. package/dist/ops/valueOutreach.js +5 -1
  122. package/dist/personality/schema.d.ts +18 -18
  123. package/dist/schema/evolutionGraph.d.ts +784 -0
  124. package/dist/schema/evolutionGraph.js +187 -0
  125. package/dist/schema/index.d.ts +1 -0
  126. package/dist/schema/index.js +1 -0
  127. package/dist/schema/proofOfWork.d.ts +125 -6
  128. package/dist/schema/proofOfWork.js +102 -4
  129. package/dist/shadow/shadowHub.js +1 -0
  130. package/dist/signals/expand.d.ts +15 -1
  131. package/dist/signals/expand.js +148 -0
  132. package/dist/signals/index.d.ts +3 -1
  133. package/dist/signals/index.js +3 -1
  134. package/dist/signals/scopeVocabulary.d.ts +75 -0
  135. package/dist/signals/scopeVocabulary.js +91 -0
  136. package/dist/signals/taskDomain.d.ts +22 -0
  137. package/dist/signals/taskDomain.js +43 -0
  138. package/dist/trace/learningTrace.d.ts +7 -0
  139. package/dist/trace/learningTrace.js +22 -0
  140. package/dist/trace/trajectory.d.ts +8 -0
  141. package/dist/trace/trajectory.js +14 -2
  142. package/dist/util/fileLock.d.ts +92 -5
  143. package/dist/util/fileLock.js +809 -70
  144. package/dist/util/index.d.ts +2 -1
  145. package/dist/util/index.js +1 -1
  146. package/dist/verify/index.d.ts +2 -1
  147. package/dist/verify/index.js +1 -1
  148. package/dist/verify/sandboxRunner.d.ts +2 -0
  149. package/dist/verify/sandboxRunner.js +154 -10
  150. package/dist/verify/sandboxedValidation.d.ts +7 -2
  151. package/dist/verify/sandboxedValidation.js +102 -24
  152. package/dist/verify/validation.d.ts +24 -3
  153. package/dist/verify/validation.js +141 -24
  154. package/dist/wire/geneHints.d.ts +88 -16
  155. package/dist/wire/geneHints.js +124 -15
  156. package/dist/wire/index.d.ts +8 -3
  157. package/dist/wire/index.js +2 -2
  158. package/dist/wire/schemaGate.d.ts +21 -0
  159. package/dist/wire/schemaGate.js +119 -10
  160. 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
- function signalsOf(a) {
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 an unverified Hub ingest. */
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: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
29
- * write here; the sole untrusted source hub ingestion ALWAYS marks via {@link ingestUntrusted}/mark.
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: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
26
- * write here; the sole untrusted source hub ingestion ALWAYS marks via {@link ingestUntrusted}/mark.
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 isUndecidedUntrusted = current?.trusted === false
131
- && current.decision === undefined
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: 'unverified_hub_write_pending',
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
- if (current) {
164
- if (current.trusted === true
165
- || current.decision !== undefined
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 === 'unverified_hub_write_pending') {
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.source !== source
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.source !== source
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
+ }