@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.
- package/assets/gep/genes.jsonl +5 -0
- package/dist/algo/candidateAssembly.js +17 -11
- package/dist/algo/capabilityCandidates.d.ts +2 -0
- package/dist/algo/capabilityCandidates.js +5 -0
- package/dist/algo/conversationSniffer.d.ts +18 -0
- package/dist/algo/conversationSniffer.js +132 -0
- package/dist/algo/cycleEngine.js +2 -0
- package/dist/algo/cycleFailureClassifier.js +13 -5
- package/dist/algo/geneIntake.d.ts +14 -4
- package/dist/algo/geneIntake.js +37 -9
- package/dist/algo/geneSelection.d.ts +6 -0
- package/dist/algo/geneSelection.js +19 -11
- package/dist/algo/index.d.ts +1 -0
- package/dist/algo/index.js +1 -0
- package/dist/assetstore/assetSyncLedger.d.ts +28 -0
- package/dist/assetstore/assetSyncLedger.js +86 -0
- package/dist/assetstore/index.d.ts +3 -1
- package/dist/assetstore/index.js +3 -1
- package/dist/assetstore/localJsonl.d.ts +9 -2
- package/dist/assetstore/localJsonl.js +75 -23
- package/dist/assetstore/pendingSignals.js +3 -1
- package/dist/assetstore/provenance.d.ts +7 -2
- package/dist/assetstore/provenance.js +85 -25
- package/dist/assetstore/reviewFilter.js +2 -1
- package/dist/assetstore/reviewLedger.d.ts +6 -0
- package/dist/assetstore/reviewLedger.js +9 -0
- package/dist/assetstore/seedGenes.d.ts +3 -0
- package/dist/assetstore/seedGenes.js +134 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +11 -1
- package/dist/benchmark/antiGeneImpact.d.ts +16 -0
- package/dist/benchmark/antiGeneImpact.js +34 -0
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +13 -1
- package/dist/events/eventArchive.d.ts +65 -0
- package/dist/events/eventArchive.js +343 -0
- package/dist/events/eventStore.js +2 -12
- package/dist/events/public.d.ts +3 -1
- package/dist/events/public.js +2 -1
- package/dist/events/retention.d.ts +24 -1
- package/dist/events/retention.js +133 -37
- package/dist/exec/autoExec.js +3 -0
- package/dist/exec/claudeBridge.js +23 -0
- package/dist/exec/runnerRegistry.d.ts +8 -6
- package/dist/exec/runnerRegistry.js +71 -18
- package/dist/exec/selfPrObfuscation.d.ts +2 -1
- package/dist/exec/selfPrObfuscation.js +19 -6
- package/dist/hub/agentDirectory.d.ts +90 -0
- package/dist/hub/agentDirectory.js +104 -0
- package/dist/hub/bindings.js +13 -3
- package/dist/hub/capability.d.ts +14 -2
- package/dist/hub/fake.d.ts +4 -2
- package/dist/hub/fake.js +3 -2
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/mailbox/catalog.js +3 -0
- package/dist/mailbox/ipcServer.js +2 -2
- package/dist/mailbox/store.d.ts +14 -0
- package/dist/mailbox/store.js +58 -6
- package/dist/material/consumer.js +9 -6
- package/dist/material/index.d.ts +1 -0
- package/dist/material/index.js +1 -0
- package/dist/material/materialArchive.d.ts +81 -0
- package/dist/material/materialArchive.js +466 -0
- package/dist/material/materialStore.d.ts +1 -0
- package/dist/material/materialStore.js +6 -13
- package/dist/schema/common.d.ts +1 -1
- package/dist/schema/common.js +1 -1
- package/dist/schema/material.d.ts +5 -5
- package/dist/trace/trajectoryExport.js +2 -2
- package/dist/wire/geneHints.d.ts +42 -1
- package/dist/wire/geneHints.js +52 -1
- package/dist/wire/index.d.ts +14 -2
- package/dist/wire/index.js +1 -1
- 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';
|
package/dist/assetstore/index.js
CHANGED
|
@@ -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;
|
|
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
|
|
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;
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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.
|
|
151
|
+
this.ensureFresh();
|
|
101
152
|
return this.index.get(assetId) ?? null;
|
|
102
153
|
}
|
|
103
154
|
async list(kind, limit = 1000) {
|
|
104
|
-
this.
|
|
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.
|
|
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
|
|
21
|
+
private fileState;
|
|
21
22
|
constructor(baseDir: string, now?: () => number);
|
|
22
|
-
private
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
49
|
+
next.set(r.assetId, r);
|
|
34
50
|
}
|
|
35
51
|
catch { /* skip corrupt line */ }
|
|
36
52
|
}
|
|
37
53
|
}
|
|
38
|
-
this.
|
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
71
|
-
this.loaded = false;
|
|
118
|
+
this.rebuildIndex(fileFingerprint(this.path));
|
|
72
119
|
}
|
|
73
120
|
catch {
|
|
74
121
|
this.index.clear();
|
|
75
|
-
this.
|
|
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.
|
|
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.
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
}
|