@evomap/evolver-core 2.0.0-beta.0 → 2.0.0-beta.2

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 (75) hide show
  1. package/assets/gep/genes.jsonl +5 -0
  2. package/dist/algo/candidateAssembly.js +17 -11
  3. package/dist/algo/capabilityCandidates.d.ts +2 -0
  4. package/dist/algo/capabilityCandidates.js +5 -0
  5. package/dist/algo/conversationSniffer.d.ts +18 -0
  6. package/dist/algo/conversationSniffer.js +132 -0
  7. package/dist/algo/cycleEngine.js +2 -0
  8. package/dist/algo/cycleFailureClassifier.js +13 -5
  9. package/dist/algo/geneIntake.d.ts +14 -4
  10. package/dist/algo/geneIntake.js +37 -9
  11. package/dist/algo/geneSelection.d.ts +6 -0
  12. package/dist/algo/geneSelection.js +19 -11
  13. package/dist/algo/index.d.ts +1 -0
  14. package/dist/algo/index.js +1 -0
  15. package/dist/assetstore/assetSyncLedger.d.ts +28 -0
  16. package/dist/assetstore/assetSyncLedger.js +86 -0
  17. package/dist/assetstore/index.d.ts +3 -1
  18. package/dist/assetstore/index.js +3 -1
  19. package/dist/assetstore/localJsonl.d.ts +9 -2
  20. package/dist/assetstore/localJsonl.js +75 -23
  21. package/dist/assetstore/pendingSignals.js +3 -1
  22. package/dist/assetstore/provenance.d.ts +7 -2
  23. package/dist/assetstore/provenance.js +85 -25
  24. package/dist/assetstore/reviewFilter.js +2 -1
  25. package/dist/assetstore/reviewLedger.d.ts +6 -0
  26. package/dist/assetstore/reviewLedger.js +9 -0
  27. package/dist/assetstore/seedGenes.d.ts +3 -0
  28. package/dist/assetstore/seedGenes.js +134 -0
  29. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  30. package/dist/benchmark/antiGeneBenchmark.js +11 -1
  31. package/dist/benchmark/antiGeneImpact.d.ts +16 -0
  32. package/dist/benchmark/antiGeneImpact.js +34 -0
  33. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  34. package/dist/benchmark/antiGeneRollout.js +13 -1
  35. package/dist/events/eventArchive.d.ts +65 -0
  36. package/dist/events/eventArchive.js +343 -0
  37. package/dist/events/eventStore.js +2 -12
  38. package/dist/events/public.d.ts +3 -1
  39. package/dist/events/public.js +2 -1
  40. package/dist/events/retention.d.ts +24 -1
  41. package/dist/events/retention.js +133 -37
  42. package/dist/exec/autoExec.js +3 -0
  43. package/dist/exec/claudeBridge.js +23 -0
  44. package/dist/exec/runnerRegistry.d.ts +8 -6
  45. package/dist/exec/runnerRegistry.js +71 -18
  46. package/dist/exec/selfPrObfuscation.d.ts +2 -1
  47. package/dist/exec/selfPrObfuscation.js +19 -6
  48. package/dist/hub/agentDirectory.d.ts +90 -0
  49. package/dist/hub/agentDirectory.js +104 -0
  50. package/dist/hub/bindings.js +13 -3
  51. package/dist/hub/capability.d.ts +14 -2
  52. package/dist/hub/fake.d.ts +4 -2
  53. package/dist/hub/fake.js +3 -2
  54. package/dist/hub/index.d.ts +1 -0
  55. package/dist/hub/index.js +1 -0
  56. package/dist/mailbox/catalog.js +3 -0
  57. package/dist/mailbox/ipcServer.js +2 -2
  58. package/dist/mailbox/store.d.ts +14 -0
  59. package/dist/mailbox/store.js +58 -6
  60. package/dist/material/consumer.js +9 -6
  61. package/dist/material/index.d.ts +1 -0
  62. package/dist/material/index.js +1 -0
  63. package/dist/material/materialArchive.d.ts +81 -0
  64. package/dist/material/materialArchive.js +466 -0
  65. package/dist/material/materialStore.d.ts +1 -0
  66. package/dist/material/materialStore.js +6 -13
  67. package/dist/schema/common.d.ts +1 -1
  68. package/dist/schema/common.js +1 -1
  69. package/dist/schema/material.d.ts +5 -5
  70. package/dist/trace/trajectoryExport.js +2 -2
  71. package/dist/wire/geneHints.d.ts +42 -1
  72. package/dist/wire/geneHints.js +52 -1
  73. package/dist/wire/index.d.ts +14 -2
  74. package/dist/wire/index.js +1 -1
  75. package/package.json +2 -1
@@ -0,0 +1,86 @@
1
+ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ export class AssetSyncLedger {
4
+ now;
5
+ path;
6
+ index = new Map();
7
+ loaded = false;
8
+ constructor(baseDir, now = Date.now) {
9
+ this.now = now;
10
+ this.path = join(baseDir, 'asset-sync.jsonl');
11
+ }
12
+ append(rec) {
13
+ this.load();
14
+ const full = {
15
+ ...rec,
16
+ syncedAt: rec.syncedAt ?? new Date(this.now()).toISOString(),
17
+ };
18
+ mkdirSync(dirname(this.path), { recursive: true });
19
+ appendFileSync(this.path, `${JSON.stringify(full)}\n`);
20
+ this.index.set(full.assetId, full);
21
+ return full;
22
+ }
23
+ get(assetId) {
24
+ this.load();
25
+ return this.index.get(assetId) ?? null;
26
+ }
27
+ list() {
28
+ this.load();
29
+ return [...this.index.values()];
30
+ }
31
+ load() {
32
+ if (this.loaded)
33
+ return;
34
+ if (existsSync(this.path)) {
35
+ for (const line of readFileSync(this.path, 'utf8').split('\n')) {
36
+ if (!line.trim())
37
+ continue;
38
+ try {
39
+ const rec = parseRecord(JSON.parse(line));
40
+ if (rec)
41
+ this.index.set(rec.assetId, rec);
42
+ }
43
+ catch {
44
+ // Skip corrupt audit lines; the asset store remains the source of content truth.
45
+ }
46
+ }
47
+ }
48
+ this.loaded = true;
49
+ }
50
+ }
51
+ function parseRecord(value) {
52
+ const assetId = stringField(value, 'assetId');
53
+ const type = stringField(value, 'type');
54
+ const source = stringField(value, 'source');
55
+ const scope = stringField(value, 'scope');
56
+ const syncedAt = stringField(value, 'syncedAt');
57
+ const remoteAssetId = stringField(value, 'remoteAssetId');
58
+ if (!assetId ||
59
+ (type !== 'Gene' && type !== 'Capsule') ||
60
+ source !== 'hub' ||
61
+ (scope !== 'purchased' && scope !== 'published') ||
62
+ !syncedAt ||
63
+ !remoteAssetId) {
64
+ return null;
65
+ }
66
+ const logicalId = stringField(value, 'logicalId');
67
+ const status = stringField(value, 'status');
68
+ const forced = value['forced'] === true;
69
+ const collisionWithAssetId = stringField(value, 'collisionWithAssetId');
70
+ return {
71
+ assetId,
72
+ type,
73
+ source,
74
+ scope,
75
+ syncedAt,
76
+ remoteAssetId,
77
+ ...(logicalId ? { logicalId } : {}),
78
+ ...(status ? { status } : {}),
79
+ ...(forced ? { forced: true } : {}),
80
+ ...(collisionWithAssetId ? { collisionWithAssetId } : {}),
81
+ };
82
+ }
83
+ function stringField(value, key) {
84
+ const raw = value[key];
85
+ return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
86
+ }
@@ -3,6 +3,8 @@ export * from './localJsonl.js';
3
3
  export * from './remoteStub.js';
4
4
  export * from './learningHistory.js';
5
5
  export * from './provenance.js';
6
+ export * from './assetSyncLedger.js';
6
7
  export * from './reviewLedger.js';
7
8
  export * from './reviewFilter.js';
8
- export * from './pendingSignals.js';
9
+ export * from './pendingSignals.js';
10
+ export * from './seedGenes.js';
@@ -3,6 +3,8 @@ export * from './localJsonl.js';
3
3
  export * from './remoteStub.js';
4
4
  export * from './learningHistory.js';
5
5
  export * from './provenance.js';
6
+ export * from './assetSyncLedger.js';
6
7
  export * from './reviewLedger.js';
7
8
  export * from './reviewFilter.js';
8
- export * from './pendingSignals.js';
9
+ export * from './pendingSignals.js';
10
+ export * from './seedGenes.js';
@@ -2,15 +2,22 @@ import { type AssetKind, type AssetRecord, type AssetStoreProvider, type PutResu
2
2
  /**
3
3
  * 本地 jsonl 资产库(M3-2, 移植 v1 src/gep/assetStore.js 单写锁).
4
4
  * 每 kind 一文件(genes/capsules/events.jsonl); append-only; O_EXCL 文件锁防并发写撕裂;
5
- * 内存索引(asset_id→record)供 get/search; 同 asset_id 去重(内容寻址天然幂等).
5
+ * 内存索引(asset_id→record)供 get/search; 文件指纹变化时在共享锁内重建索引,保证多个
6
+ * CLI/daemon 进程之间可见; 写入也在锁内刷新后再按 asset_id 去重(内容寻址天然幂等).
6
7
  */
7
8
  export declare class LocalJsonlProvider implements AssetStoreProvider {
8
9
  readonly baseDir: string;
9
10
  private readonly index;
10
11
  private readonly lockPath;
12
+ private fileState;
11
13
  private loaded;
12
14
  constructor(baseDir: string);
13
- private ensureLoaded;
15
+ private captureFileState;
16
+ private stateChanged;
17
+ private rebuildIndex;
18
+ private refreshUnderLock;
19
+ private ensureFresh;
20
+ private updateFileStateAfterWrite;
14
21
  put(asset: AssetRecord): Promise<PutResult>;
15
22
  /**
16
23
  * 迁移专用(M8-2): 以**冻结 asset_id** 原样写入, 不经 normalizeForPut 重算/校验.
@@ -1,8 +1,22 @@
1
- import { appendFileSync, existsSync, readFileSync, writeFileSync, renameSync, mkdirSync } from 'node:fs';
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, renameSync, mkdirSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { acquireLock, releaseLock } from '../util/fileLock.js';
4
4
  import { normalizeForPut, } from './provider.js';
5
5
  const FILES = { Gene: 'genes.jsonl', Capsule: 'capsules.jsonl', EvolutionEvent: 'events.jsonl', AntiGene: 'anti-genes.jsonl' };
6
+ function isErrno(error, code) {
7
+ return typeof error === 'object' && error !== null && error.code === code;
8
+ }
9
+ function fileFingerprint(path) {
10
+ try {
11
+ const stat = statSync(path, { bigint: true });
12
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
13
+ }
14
+ catch (error) {
15
+ if (isErrno(error, 'ENOENT'))
16
+ return 'missing';
17
+ throw error;
18
+ }
19
+ }
6
20
  function signalsOf(a) {
7
21
  const out = [];
8
22
  for (const key of ['signals_match', 'signals', 'trigger', 'trigger_signals']) {
@@ -17,12 +31,14 @@ function signalsOf(a) {
17
31
  /**
18
32
  * 本地 jsonl 资产库(M3-2, 移植 v1 src/gep/assetStore.js 单写锁).
19
33
  * 每 kind 一文件(genes/capsules/events.jsonl); append-only; O_EXCL 文件锁防并发写撕裂;
20
- * 内存索引(asset_id→record)供 get/search; 同 asset_id 去重(内容寻址天然幂等).
34
+ * 内存索引(asset_id→record)供 get/search; 文件指纹变化时在共享锁内重建索引,保证多个
35
+ * CLI/daemon 进程之间可见; 写入也在锁内刷新后再按 asset_id 去重(内容寻址天然幂等).
21
36
  */
22
37
  export class LocalJsonlProvider {
23
38
  baseDir;
24
39
  index = new Map();
25
40
  lockPath;
41
+ fileState = new Map();
26
42
  loaded = false;
27
43
  // `baseDir` is public-readonly so callers that inject a store (e.g. the CLI under test) can co-locate sidecars
28
44
  // — the ReviewLedger/ProvenanceStore — in the SAME directory, instead of defaulting to the real ~/.evomap.
@@ -31,9 +47,24 @@ export class LocalJsonlProvider {
31
47
  mkdirSync(baseDir, { recursive: true });
32
48
  this.lockPath = join(baseDir, '.assetstore.lock');
33
49
  }
34
- ensureLoaded() {
35
- if (this.loaded)
36
- return;
50
+ captureFileState() {
51
+ const state = new Map();
52
+ for (const [kind, file] of Object.entries(FILES)) {
53
+ state.set(kind, fileFingerprint(join(this.baseDir, file)));
54
+ }
55
+ return state;
56
+ }
57
+ stateChanged(next) {
58
+ if (!this.loaded || next.size !== this.fileState.size)
59
+ return true;
60
+ for (const [kind, fingerprint] of next) {
61
+ if (this.fileState.get(kind) !== fingerprint)
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+ rebuildIndex(state) {
67
+ const next = new Map();
37
68
  for (const file of Object.values(FILES)) {
38
69
  const p = join(this.baseDir, file);
39
70
  if (!existsSync(p))
@@ -44,29 +75,50 @@ export class LocalJsonlProvider {
44
75
  try {
45
76
  const r = JSON.parse(line);
46
77
  if (r.asset_id)
47
- this.index.set(r.asset_id, r);
78
+ next.set(r.asset_id, r);
48
79
  }
49
80
  catch { /* skip 坏行 */ }
50
81
  }
51
82
  }
83
+ this.index.clear();
84
+ for (const [assetId, record] of next)
85
+ this.index.set(assetId, record);
86
+ this.fileState = state;
87
+ this.loaded = true;
88
+ }
89
+ refreshUnderLock() {
90
+ const state = this.captureFileState();
91
+ if (this.stateChanged(state))
92
+ this.rebuildIndex(state);
93
+ }
94
+ ensureFresh() {
95
+ const state = this.captureFileState();
96
+ if (!this.stateChanged(state))
97
+ return;
98
+ acquireLock(this.lockPath);
99
+ try {
100
+ this.refreshUnderLock();
101
+ }
102
+ finally {
103
+ releaseLock(this.lockPath);
104
+ }
105
+ }
106
+ updateFileStateAfterWrite() {
107
+ this.fileState = this.captureFileState();
52
108
  this.loaded = true;
53
109
  }
54
110
  async put(asset) {
55
- this.ensureLoaded();
56
111
  const { record, verified } = normalizeForPut(asset);
57
- if (this.index.has(record.asset_id))
58
- return { asset_id: record.asset_id, stored: false, verified };
59
112
  const file = join(this.baseDir, FILES[record.type]);
60
113
  acquireLock(this.lockPath);
61
114
  try {
62
- // 锁内复检(另一进程可能刚写)
63
- if (!this.index.has(record.asset_id)) {
64
- appendFileSync(file, `${JSON.stringify(record)}\n`);
65
- this.index.set(record.asset_id, record);
66
- }
67
- else {
115
+ // Refresh under the shared lock so another process cannot append between reload and dedupe.
116
+ this.refreshUnderLock();
117
+ if (this.index.has(record.asset_id))
68
118
  return { asset_id: record.asset_id, stored: false, verified };
69
- }
119
+ appendFileSync(file, `${JSON.stringify(record)}\n`);
120
+ this.index.set(record.asset_id, record);
121
+ this.updateFileStateAfterWrite();
70
122
  }
71
123
  finally {
72
124
  releaseLock(this.lockPath);
@@ -78,18 +130,17 @@ export class LocalJsonlProvider {
78
130
  * 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
79
131
  */
80
132
  async putFrozen(record) {
81
- this.ensureLoaded();
82
133
  if (!record.asset_id)
83
134
  throw new Error('putFrozen 需 record 自带冻结 asset_id');
84
- if (this.index.has(record.asset_id))
85
- return { asset_id: record.asset_id, stored: false, verified: false };
86
135
  const file = join(this.baseDir, FILES[record.type]);
87
136
  acquireLock(this.lockPath);
88
137
  try {
138
+ this.refreshUnderLock();
89
139
  if (this.index.has(record.asset_id))
90
140
  return { asset_id: record.asset_id, stored: false, verified: false };
91
141
  appendFileSync(file, `${JSON.stringify(record)}\n`);
92
142
  this.index.set(record.asset_id, record);
143
+ this.updateFileStateAfterWrite();
93
144
  }
94
145
  finally {
95
146
  releaseLock(this.lockPath);
@@ -97,11 +148,11 @@ export class LocalJsonlProvider {
97
148
  return { asset_id: record.asset_id, stored: true, verified: false };
98
149
  }
99
150
  async get(assetId) {
100
- this.ensureLoaded();
151
+ this.ensureFresh();
101
152
  return this.index.get(assetId) ?? null;
102
153
  }
103
154
  async list(kind, limit = 1000) {
104
- this.ensureLoaded();
155
+ this.ensureFresh();
105
156
  const out = [];
106
157
  for (const r of this.index.values()) {
107
158
  if (!kind || r.type === kind)
@@ -112,7 +163,7 @@ export class LocalJsonlProvider {
112
163
  return out;
113
164
  }
114
165
  async search(q) {
115
- this.ensureLoaded();
166
+ this.ensureFresh();
116
167
  const out = [];
117
168
  for (const r of this.index.values()) {
118
169
  if (q.kind && r.type !== q.kind)
@@ -148,9 +199,9 @@ export class LocalJsonlProvider {
148
199
  * the write lock. Returns kept/removed line counts.
149
200
  */
150
201
  async compact() {
151
- this.ensureLoaded();
152
202
  acquireLock(this.lockPath);
153
203
  try {
204
+ this.refreshUnderLock();
154
205
  let kept = 0, removed = 0;
155
206
  for (const [kind, file] of Object.entries(FILES)) {
156
207
  const p = join(this.baseDir, file);
@@ -173,6 +224,7 @@ export class LocalJsonlProvider {
173
224
  kept += byId.size;
174
225
  removed += lines.length - byId.size;
175
226
  }
227
+ this.rebuildIndex(this.captureFileState());
176
228
  return { kept, removed };
177
229
  }
178
230
  finally {
@@ -1,5 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
- import { dirname, join, resolve } from 'node:path';
2
+ import { basename, dirname, join, resolve } from 'node:path';
3
3
  import { acquireLock, releaseLock } from '../util/fileLock.js';
4
4
  import { LocalJsonlProvider } from './localJsonl.js';
5
5
  const MAX_SIGNAL_LENGTH = 200;
@@ -33,6 +33,8 @@ function findRepoRoot(start) {
33
33
  while (current) {
34
34
  if (existsSync(join(current, '.git')))
35
35
  return current;
36
+ if (basename(current) === 'node_modules')
37
+ return null;
36
38
  const parent = dirname(current);
37
39
  if (parent === current)
38
40
  return null;
@@ -16,10 +16,13 @@ export interface ProvenanceRecord {
16
16
  export declare class ProvenanceStore {
17
17
  private readonly now;
18
18
  private readonly path;
19
+ private readonly lockPath;
19
20
  private readonly index;
20
- private loaded;
21
+ private fileState;
21
22
  constructor(baseDir: string, now?: () => number);
22
- private load;
23
+ private rebuildIndex;
24
+ private refreshUnderLock;
25
+ private withFreshRead;
23
26
  /** Record provenance for an asset_id (append-only; the JSONL history is the audit trail). */
24
27
  mark(rec: Omit<ProvenanceRecord, 'at'> & {
25
28
  at?: string;
@@ -28,6 +31,8 @@ export declare class ProvenanceStore {
28
31
  get(assetId: string): ProvenanceRecord | null;
29
32
  /** No record → trusted (local default); a record → its trusted flag. */
30
33
  isTrusted(assetId: string): boolean;
34
+ /** One linearizable trust snapshot for bounded batch readers. */
35
+ snapshot(): ReadonlyMap<string, ProvenanceRecord>;
31
36
  /** Explicit, audited untrusted→trusted promotion. Appends a new trusted record carrying who/why. */
32
37
  promote(assetId: string, by: string, reason: string): ProvenanceRecord;
33
38
  }
@@ -5,7 +5,22 @@
5
5
  // selection defaults to trusted-only; an untrusted asset is promoted to trusted only by an explicit, logged act.
6
6
  import { appendFileSync, closeSync, existsSync, openSync, readFileSync, readSync, mkdirSync, statSync, truncateSync } from 'node:fs';
7
7
  import { join, dirname } from 'node:path';
8
+ import { acquireLock, releaseLock } from '../util/fileLock.js';
8
9
  import { normalizeForPut } from './provider.js';
10
+ function isErrno(error, code) {
11
+ return typeof error === 'object' && error !== null && error.code === code;
12
+ }
13
+ function fileFingerprint(path) {
14
+ try {
15
+ const stat = statSync(path, { bigint: true });
16
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
17
+ }
18
+ catch (error) {
19
+ if (isErrno(error, 'ENOENT'))
20
+ return 'missing';
21
+ throw error;
22
+ }
23
+ }
9
24
  /**
10
25
  * Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
11
26
  * record = trusted: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
@@ -14,42 +29,73 @@ import { normalizeForPut } from './provider.js';
14
29
  export class ProvenanceStore {
15
30
  now;
16
31
  path;
32
+ lockPath;
17
33
  index = new Map();
18
- loaded = false;
34
+ fileState = null;
19
35
  constructor(baseDir, now = Date.now) {
20
36
  this.now = now;
21
37
  this.path = join(baseDir, 'provenance.jsonl');
38
+ this.lockPath = join(baseDir, '.assetstore.lock');
22
39
  }
23
- load() {
24
- if (this.loaded)
25
- return;
26
- if (existsSync(this.path)) {
40
+ rebuildIndex(state) {
41
+ const next = new Map();
42
+ if (state !== 'missing') {
27
43
  for (const line of readFileSync(this.path, 'utf8').split('\n')) {
28
44
  if (!line.trim())
29
45
  continue;
30
46
  try {
31
47
  const r = JSON.parse(line);
32
48
  if (r.assetId)
33
- this.index.set(r.assetId, r);
49
+ next.set(r.assetId, r);
34
50
  }
35
51
  catch { /* skip corrupt line */ }
36
52
  }
37
53
  }
38
- this.loaded = true;
54
+ this.index.clear();
55
+ for (const [assetId, record] of next)
56
+ this.index.set(assetId, record);
57
+ this.fileState = state;
58
+ }
59
+ refreshUnderLock() {
60
+ const state = fileFingerprint(this.path);
61
+ if (state !== this.fileState)
62
+ this.rebuildIndex(state);
63
+ }
64
+ withFreshRead(read) {
65
+ mkdirSync(dirname(this.path), { recursive: true });
66
+ acquireLock(this.lockPath);
67
+ try {
68
+ this.refreshUnderLock();
69
+ return read(this.index);
70
+ }
71
+ finally {
72
+ releaseLock(this.lockPath);
73
+ }
39
74
  }
40
75
  /** Record provenance for an asset_id (append-only; the JSONL history is the audit trail). */
41
76
  mark(rec) {
42
- this.load();
43
77
  const full = { ...rec, at: rec.at ?? new Date(this.now()).toISOString() };
44
78
  mkdirSync(dirname(this.path), { recursive: true });
45
- appendFileSync(this.path, `${JSON.stringify(full)}\n`);
46
- this.index.set(full.assetId, full);
79
+ acquireLock(this.lockPath);
80
+ try {
81
+ this.refreshUnderLock();
82
+ appendFileSync(this.path, `${JSON.stringify(full)}\n`);
83
+ this.index.set(full.assetId, full);
84
+ this.fileState = fileFingerprint(this.path);
85
+ }
86
+ finally {
87
+ releaseLock(this.lockPath);
88
+ }
47
89
  return full;
48
90
  }
49
91
  rollbackLast(rec) {
50
92
  const line = `${JSON.stringify(rec)}\n`;
51
93
  const lineBytes = Buffer.byteLength(line, 'utf8');
94
+ let locked = false;
52
95
  try {
96
+ mkdirSync(dirname(this.path), { recursive: true });
97
+ acquireLock(this.lockPath);
98
+ locked = true;
53
99
  if (!existsSync(this.path))
54
100
  return;
55
101
  const stat = statSync(this.path);
@@ -64,26 +110,40 @@ export class ProvenanceStore {
64
110
  finally {
65
111
  closeSync(fd);
66
112
  }
67
- if (buf.toString('utf8') !== line)
113
+ if (buf.toString('utf8') !== line) {
114
+ this.refreshUnderLock();
68
115
  return;
116
+ }
69
117
  truncateSync(this.path, offset);
70
- this.index.clear();
71
- this.loaded = false;
118
+ this.rebuildIndex(fileFingerprint(this.path));
72
119
  }
73
120
  catch {
74
121
  this.index.clear();
75
- this.loaded = false;
122
+ this.fileState = null;
123
+ }
124
+ finally {
125
+ if (locked) {
126
+ try {
127
+ releaseLock(this.lockPath);
128
+ }
129
+ catch {
130
+ // Rollback is best-effort and must not mask the store failure that triggered it.
131
+ this.index.clear();
132
+ this.fileState = null;
133
+ }
134
+ }
76
135
  }
77
136
  }
78
137
  get(assetId) {
79
- this.load();
80
- return this.index.get(assetId) ?? null;
138
+ return this.withFreshRead((index) => index.get(assetId) ?? null);
81
139
  }
82
140
  /** No record → trusted (local default); a record → its trusted flag. */
83
141
  isTrusted(assetId) {
84
- this.load();
85
- const r = this.index.get(assetId);
86
- return r ? r.trusted : true;
142
+ return this.withFreshRead((index) => index.get(assetId)?.trusted ?? true);
143
+ }
144
+ /** One linearizable trust snapshot for bounded batch readers. */
145
+ snapshot() {
146
+ return this.withFreshRead((index) => new Map(index));
87
147
  }
88
148
  /** Explicit, audited untrusted→trusted promotion. Appends a new trusted record carrying who/why. */
89
149
  promote(assetId, by, reason) {
@@ -99,11 +159,11 @@ export class ProvenanceStore {
99
159
  export async function ingestUntrusted(store, prov, record, source = 'hub') {
100
160
  const normalized = normalizeForPut(record);
101
161
  const mark = prov.mark({ assetId: normalized.record.asset_id, source, trusted: false });
102
- try {
103
- return await store.put(record);
104
- }
105
- catch (err) {
162
+ const result = await store.put(record);
163
+ // A thrown write has an ambiguous outcome: the asset may have reached disk before the acknowledgement was
164
+ // lost. Keep the untrusted marker in that case so a persisted Hub asset can never fall through the default
165
+ // no-record => trusted policy. Only an explicit no-write result is safe to roll back.
166
+ if (!result.stored)
106
167
  prov.rollbackLast(mark);
107
- throw err;
108
- }
168
+ return result;
109
169
  }
@@ -30,9 +30,10 @@ export function provenanceStoreForStore(store) {
30
30
  */
31
31
  export async function listApprovedGenes(store, review, maxGenes, provenance = provenanceStoreForStore(store)) {
32
32
  const all = await store.list('Gene', GENE_SCAN_LIMIT);
33
+ const trust = provenance.snapshot();
33
34
  const approved = [];
34
35
  for (const g of all) {
35
- if (!provenance.isTrusted(String(g.asset_id)))
36
+ if (trust.get(String(g.asset_id))?.trusted === false)
36
37
  continue; // hub-untrusted → withhold until promoted
37
38
  if (!review.isApproved(String(g.asset_id)))
38
39
  continue; // quarantined/rejected draft → withhold
@@ -52,4 +52,10 @@ export declare class ReviewLedger {
52
52
  records(): ReviewRecord[];
53
53
  /** No record → approved (default eligible); a record → approved only when its state is 'approved'. */
54
54
  isApproved(assetId: string): boolean;
55
+ /**
56
+ * True only when a human approval record exists. Safety-sensitive consumers such as AntiGene warning
57
+ * injection must fail closed when review state is absent; unlike cycle/migrate-authored Genes, an unreviewed
58
+ * negative-memory asset must never inherit the ledger's backward-compatible "no record = eligible" default.
59
+ */
60
+ isExplicitlyApproved(assetId: string): boolean;
55
61
  }
@@ -118,4 +118,13 @@ export class ReviewLedger {
118
118
  const r = this.index.get(assetId);
119
119
  return r ? r.state === 'approved' : true;
120
120
  }
121
+ /**
122
+ * True only when a human approval record exists. Safety-sensitive consumers such as AntiGene warning
123
+ * injection must fail closed when review state is absent; unlike cycle/migrate-authored Genes, an unreviewed
124
+ * negative-memory asset must never inherit the ledger's backward-compatible "no record = eligible" default.
125
+ */
126
+ isExplicitlyApproved(assetId) {
127
+ this.load();
128
+ return this.index.get(assetId)?.state === 'approved';
129
+ }
121
130
  }
@@ -0,0 +1,3 @@
1
+ import { type Gene } from '../wire/index.js';
2
+ export declare const BUNDLED_HARNESS_SEED_GENES: readonly Gene[];
3
+ export declare function bundledHarnessSeedGenesJsonl(): string;