@evomap/evolver-core 2.0.0-beta.2 → 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 (240) 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 +171 -26
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/conversationSniffer.js +25 -8
  8. package/dist/algo/cycleEngine.d.ts +63 -5
  9. package/dist/algo/cycleEngine.js +320 -42
  10. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  11. package/dist/algo/evolutionEvent.d.ts +26 -0
  12. package/dist/algo/evolutionEvent.js +32 -0
  13. package/dist/algo/exploration.d.ts +7 -0
  14. package/dist/algo/exploration.js +16 -3
  15. package/dist/algo/geneHealth.d.ts +36 -3
  16. package/dist/algo/geneHealth.js +47 -4
  17. package/dist/algo/geneIntake.d.ts +46 -1
  18. package/dist/algo/geneIntake.js +121 -10
  19. package/dist/algo/geneSelection.d.ts +126 -6
  20. package/dist/algo/geneSelection.js +479 -36
  21. package/dist/algo/index.d.ts +6 -1
  22. package/dist/algo/index.js +6 -1
  23. package/dist/algo/kautoProjection.d.ts +41 -0
  24. package/dist/algo/kautoProjection.js +95 -0
  25. package/dist/algo/kautoValidator.d.ts +68 -0
  26. package/dist/algo/kautoValidator.js +256 -0
  27. package/dist/algo/memoryGraph.d.ts +62 -0
  28. package/dist/algo/memoryGraph.js +86 -0
  29. package/dist/algo/orchestrator.d.ts +17 -1
  30. package/dist/algo/orchestrator.js +30 -4
  31. package/dist/algo/publishEligibility.d.ts +34 -0
  32. package/dist/algo/publishEligibility.js +52 -0
  33. package/dist/algo/solidify.d.ts +11 -2
  34. package/dist/algo/solidify.js +37 -7
  35. package/dist/algo/ucb1.d.ts +53 -0
  36. package/dist/algo/ucb1.js +156 -0
  37. package/dist/assetrepair/hubRejection.d.ts +12 -0
  38. package/dist/assetrepair/hubRejection.js +109 -0
  39. package/dist/assetrepair/index.d.ts +2 -0
  40. package/dist/assetrepair/index.js +2 -0
  41. package/dist/assetrepair/repair.d.ts +33 -0
  42. package/dist/assetrepair/repair.js +155 -0
  43. package/dist/assetstore/assetSidecarRecords.d.ts +28 -0
  44. package/dist/assetstore/assetSidecarRecords.js +384 -0
  45. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  46. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  47. package/dist/assetstore/assetStoreHealth.d.ts +81 -0
  48. package/dist/assetstore/assetStoreHealth.js +319 -0
  49. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  50. package/dist/assetstore/assetStoreLayout.js +6 -0
  51. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  52. package/dist/assetstore/assetStoreStorage.js +336 -0
  53. package/dist/assetstore/assetSyncLedger.d.ts +91 -1
  54. package/dist/assetstore/assetSyncLedger.js +718 -59
  55. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  56. package/dist/assetstore/foreignJsonlSource.js +150 -0
  57. package/dist/assetstore/index.d.ts +5 -0
  58. package/dist/assetstore/index.js +5 -0
  59. package/dist/assetstore/learningHistory.js +3 -3
  60. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  61. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  62. package/dist/assetstore/localJsonl.d.ts +12 -1
  63. package/dist/assetstore/localJsonl.js +251 -42
  64. package/dist/assetstore/provenance.d.ts +93 -4
  65. package/dist/assetstore/provenance.js +368 -84
  66. package/dist/assetstore/provider.d.ts +63 -0
  67. package/dist/assetstore/provider.js +97 -6
  68. package/dist/assetstore/reviewFilter.d.ts +19 -1
  69. package/dist/assetstore/reviewFilter.js +39 -1
  70. package/dist/assetstore/reviewLedger.d.ts +8 -2
  71. package/dist/assetstore/reviewLedger.js +71 -45
  72. package/dist/assetstore/unionReadStore.d.ts +25 -0
  73. package/dist/assetstore/unionReadStore.js +119 -0
  74. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  75. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  76. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  77. package/dist/benchmark/antiGeneRollout.js +4 -3
  78. package/dist/benchmark/index.d.ts +3 -1
  79. package/dist/benchmark/index.js +3 -1
  80. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  81. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  82. package/dist/benchmark/triggerShift.d.ts +62 -0
  83. package/dist/benchmark/triggerShift.js +106 -0
  84. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  85. package/dist/bootstrap/envFingerprint.js +5 -0
  86. package/dist/bootstrap/index.d.ts +3 -1
  87. package/dist/bootstrap/index.js +3 -1
  88. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  89. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  90. package/dist/bootstrap/v1EnvCompat.d.ts +113 -0
  91. package/dist/bootstrap/v1EnvCompat.js +303 -0
  92. package/dist/events/eventArchive.d.ts +2 -0
  93. package/dist/events/eventArchive.js +13 -3
  94. package/dist/events/eventSchema.d.ts +7 -7
  95. package/dist/events/eventStore.d.ts +2 -0
  96. package/dist/events/eventStore.js +5 -1
  97. package/dist/events/ingest.d.ts +2 -1
  98. package/dist/events/ingest.js +15 -0
  99. package/dist/events/paths.d.ts +12 -10
  100. package/dist/events/paths.js +24 -20
  101. package/dist/events/public.d.ts +2 -2
  102. package/dist/events/public.js +2 -2
  103. package/dist/events/reports.d.ts +2 -0
  104. package/dist/events/reports.js +4 -0
  105. package/dist/exec/autoExec.d.ts +68 -4
  106. package/dist/exec/autoExec.js +392 -33
  107. package/dist/exec/autonomousCycle.d.ts +31 -4
  108. package/dist/exec/autonomousCycle.js +71 -13
  109. package/dist/exec/claudeBridge.d.ts +80 -15
  110. package/dist/exec/claudeBridge.js +845 -76
  111. package/dist/exec/executionBinding.d.ts +414 -0
  112. package/dist/exec/executionBinding.js +588 -0
  113. package/dist/exec/index.d.ts +1 -0
  114. package/dist/exec/index.js +1 -0
  115. package/dist/exec/openPrRegistry.d.ts +8 -2
  116. package/dist/exec/openPrRegistry.js +32 -22
  117. package/dist/exec/prompt.js +14 -1
  118. package/dist/exec/proofOfWork.d.ts +1 -1
  119. package/dist/exec/proofOfWork.js +2 -2
  120. package/dist/exec/runnerRegistry.d.ts +153 -36
  121. package/dist/exec/runnerRegistry.js +848 -65
  122. package/dist/exec/selfPr.js +1 -7
  123. package/dist/feedback/envelope.d.ts +61 -0
  124. package/dist/feedback/envelope.js +168 -0
  125. package/dist/feedback/index.d.ts +1 -0
  126. package/dist/feedback/index.js +1 -0
  127. package/dist/hooks/hooks.js +1 -0
  128. package/dist/hub/assetCallLog.d.ts +35 -1
  129. package/dist/hub/assetCallLog.js +124 -1
  130. package/dist/hub/bindings.d.ts +8 -1
  131. package/dist/hub/bindings.js +29 -8
  132. package/dist/hub/capability.d.ts +130 -4
  133. package/dist/hub/conversationDistiller.d.ts +19 -0
  134. package/dist/hub/conversationDistiller.js +115 -37
  135. package/dist/hub/fake.d.ts +3 -2
  136. package/dist/hub/fake.js +2 -1
  137. package/dist/hub/index.d.ts +1 -0
  138. package/dist/hub/index.js +1 -0
  139. package/dist/hub/questionGenerator.d.ts +5 -1
  140. package/dist/hub/questionGenerator.js +8 -6
  141. package/dist/hub/recipeCompose.d.ts +27 -0
  142. package/dist/hub/recipeCompose.js +90 -0
  143. package/dist/hub/sanitize.js +122 -7
  144. package/dist/index.d.ts +6 -1
  145. package/dist/index.js +7 -1
  146. package/dist/issueReporter/index.d.ts +156 -0
  147. package/dist/issueReporter/index.js +1688 -0
  148. package/dist/mailbox/dispatch.d.ts +1 -1
  149. package/dist/mailbox/dispatch.js +22 -6
  150. package/dist/mailbox/envelope.d.ts +7 -1
  151. package/dist/mailbox/envelope.js +9 -2
  152. package/dist/mailbox/ipcServer.d.ts +12 -2
  153. package/dist/mailbox/ipcServer.js +183 -13
  154. package/dist/mailbox/store.d.ts +89 -3
  155. package/dist/mailbox/store.js +895 -41
  156. package/dist/modelCompatibility.d.ts +164 -0
  157. package/dist/modelCompatibility.js +309 -0
  158. package/dist/observers/valueDigestObserver.d.ts +9 -0
  159. package/dist/observers/valueDigestObserver.js +35 -2
  160. package/dist/ops/cleanup.js +1 -1
  161. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  162. package/dist/ops/evolutionGraphProjection.js +315 -0
  163. package/dist/ops/index.d.ts +2 -1
  164. package/dist/ops/index.js +2 -1
  165. package/dist/ops/savingsCore.js +1 -2
  166. package/dist/ops/selfUpdate.d.ts +18 -1
  167. package/dist/ops/selfUpdate.js +88 -23
  168. package/dist/ops/valueOutreach.d.ts +3 -1
  169. package/dist/ops/valueOutreach.js +5 -1
  170. package/dist/personality/schema.d.ts +24 -24
  171. package/dist/schema/evolutionGraph.d.ts +784 -0
  172. package/dist/schema/evolutionGraph.js +187 -0
  173. package/dist/schema/index.d.ts +1 -0
  174. package/dist/schema/index.js +1 -0
  175. package/dist/schema/proofOfWork.d.ts +125 -6
  176. package/dist/schema/proofOfWork.js +102 -4
  177. package/dist/schema/signal.d.ts +3 -3
  178. package/dist/schema/signal.js +1 -1
  179. package/dist/shadow/shadowHub.js +1 -0
  180. package/dist/signals/curriculum.d.ts +55 -0
  181. package/dist/signals/curriculum.js +202 -0
  182. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  183. package/dist/signals/expand.d.ts +15 -1
  184. package/dist/signals/expand.js +169 -1
  185. package/dist/signals/extractor.d.ts +2 -2
  186. package/dist/signals/extractor.js +31 -6
  187. package/dist/signals/index.d.ts +4 -1
  188. package/dist/signals/index.js +4 -1
  189. package/dist/signals/metaSignals.d.ts +4 -0
  190. package/dist/signals/metaSignals.js +42 -0
  191. package/dist/signals/scopeVocabulary.d.ts +75 -0
  192. package/dist/signals/scopeVocabulary.js +91 -0
  193. package/dist/signals/signalGate.js +1 -1
  194. package/dist/signals/taskDomain.d.ts +22 -0
  195. package/dist/signals/taskDomain.js +43 -0
  196. package/dist/strategy/constraintAblation.d.ts +64 -0
  197. package/dist/strategy/constraintAblation.js +2820 -0
  198. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  199. package/dist/strategy/constraintAblationPredicates.js +339 -0
  200. package/dist/strategy/index.d.ts +2 -1
  201. package/dist/strategy/index.js +2 -1
  202. package/dist/trace/index.d.ts +3 -1
  203. package/dist/trace/index.js +3 -1
  204. package/dist/trace/learningTrace.d.ts +216 -0
  205. package/dist/trace/learningTrace.js +298 -0
  206. package/dist/trace/proxyTurns.d.ts +31 -0
  207. package/dist/trace/proxyTurns.js +137 -0
  208. package/dist/trace/trajectory.d.ts +8 -0
  209. package/dist/trace/trajectory.js +14 -2
  210. package/dist/util/fetchPort.d.ts +1 -0
  211. package/dist/util/fetchPort.js +11 -0
  212. package/dist/util/fileLock.d.ts +113 -7
  213. package/dist/util/fileLock.js +1035 -80
  214. package/dist/util/index.d.ts +3 -1
  215. package/dist/util/index.js +2 -1
  216. package/dist/verify/index.d.ts +2 -1
  217. package/dist/verify/index.js +1 -1
  218. package/dist/verify/sandboxRunner.d.ts +30 -0
  219. package/dist/verify/sandboxRunner.js +370 -27
  220. package/dist/verify/sandboxedValidation.d.ts +16 -2
  221. package/dist/verify/sandboxedValidation.js +200 -22
  222. package/dist/verify/validation.d.ts +35 -4
  223. package/dist/verify/validation.js +164 -16
  224. package/dist/wire/geneHints.d.ts +88 -16
  225. package/dist/wire/geneHints.js +124 -15
  226. package/dist/wire/index.d.ts +8 -3
  227. package/dist/wire/index.js +2 -2
  228. package/dist/wire/schemaGate.d.ts +21 -0
  229. package/dist/wire/schemaGate.js +119 -10
  230. package/dist/workflow/dsl.d.ts +24 -3
  231. package/dist/workflow/dsl.js +4 -0
  232. package/dist/workflow/engine.d.ts +5 -1
  233. package/dist/workflow/engine.js +3 -0
  234. package/dist/workflow/index.d.ts +3 -1
  235. package/dist/workflow/index.js +3 -1
  236. package/dist/workflow/runtime.d.ts +110 -0
  237. package/dist/workflow/runtime.js +1298 -0
  238. package/dist/workflow/stateStore.d.ts +172 -0
  239. package/dist/workflow/stateStore.js +1044 -0
  240. package/package.json +12 -4
@@ -0,0 +1,48 @@
1
+ import type { AssetKind, AssetRecord, AssetStoreProvider, PutResult, SearchQuery } from './provider.js';
2
+ /** Thrown by every mutating method: a foreign pool is readable, never writable. */
3
+ export declare class ForeignStoreReadOnlyError extends Error {
4
+ readonly baseDir: string;
5
+ readonly kind?: AssetKind | undefined;
6
+ readonly code = "FOREIGN_STORE_READ_ONLY";
7
+ constructor(baseDir: string, kind?: AssetKind | undefined);
8
+ }
9
+ /**
10
+ * EvoX's asset directory: `EVOX_ASSETS_DIR`, else `~/.evox/agent/evolver/assets`. Returns the path
11
+ * whether or not it exists; {@link ForeignJsonlSource} treats a missing directory as an empty pool.
12
+ *
13
+ * The name is engine-scoped on purpose, matching the EVOLVER_HOME / EVOMAP_HOME split. It deliberately
14
+ * does NOT honour `GEP_ASSETS_DIR`: in THIS repo that variable relocates evolver's OWN pool
15
+ * (`pendingSignals.legacyV1BaseDir`, README.npm.md), so a user who moved their own store per our own
16
+ * documentation would silently get a union of the primary with itself and never read EvoX at all.
17
+ */
18
+ export declare function evoxAssetDir(env?: NodeJS.ProcessEnv): string;
19
+ /**
20
+ * Read-only provider over a foreign asset directory. Re-reads on every call rather than caching: the
21
+ * owning engine appends to these files behind our back, and a stale cache would silently freeze a gene's
22
+ * evidence at whatever it was when this process started.
23
+ */
24
+ export declare class ForeignJsonlSource implements AssetStoreProvider {
25
+ readonly baseDir: string;
26
+ private readonly reportedUnreadable;
27
+ constructor(baseDir: string);
28
+ /**
29
+ * Read one foreign file, degrading it to "empty" on ANY failure.
30
+ *
31
+ * `readUtf8Regular` returns null only for a missing file and THROWS for a symlink, a non-regular file,
32
+ * EACCES, ELOOP or ENOTDIR. This read runs inside gene selection (aggregateLearningHistory →
33
+ * candidateAssembly), where an exception would abort the host's whole cycle — a pool we do not own, and
34
+ * whose contents are optional extra evidence, must never have that power. Degrading is per file so one
35
+ * unreadable capsules.jsonl does not also hide genes.jsonl.
36
+ *
37
+ * Warned once per path per instance: reads are re-run on every call by design, and a per-call warning
38
+ * would bury the cycle's real output.
39
+ */
40
+ private readOrDegrade;
41
+ private read;
42
+ put(asset: AssetRecord): Promise<PutResult>;
43
+ get(assetId: string): Promise<AssetRecord | null>;
44
+ /** Filters by logical id BEFORE the cap, so `limit` bounds matches returned, never records scanned. */
45
+ findByLogicalId(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
46
+ list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
47
+ search(q: SearchQuery): Promise<AssetRecord[]>;
48
+ }
@@ -0,0 +1,150 @@
1
+ // Read-only view over ANOTHER evolution engine's local asset pool (#195 follow-up).
2
+ //
3
+ // Why this exists: gene health (algo/geneHealth.ts) is derived from capsule outcomes via
4
+ // aggregateLearningHistory, which can only see what the injected provider sees. EvoX runs write their
5
+ // capsules to EvoX's own pool (`~/.evox/agent/evolver/assets`), not to `~/.evomap`, so every EvoX-produced
6
+ // gene aggregates to an empty view here and scores 0 no matter how its runs actually went. Pointing a
7
+ // LocalJsonlProvider at that directory is NOT an option: its constructor creates the directory and its
8
+ // reads take a lock file inside it, i.e. it would WRITE into a store this engine does not own.
9
+ //
10
+ // The boundary is symmetric and deliberate. EvoX blocks its own agent from writing into `~/.evomap`
11
+ // (`evox-agent-core::foreign_store_guard`) while explicitly leaving reads unguarded — "inspecting a
12
+ // foreign store is legitimate diagnosis; mutating it is not". This module is the mirror of that rule:
13
+ // every write path throws, nothing is created, no lock is taken, and the foreign directory is left
14
+ // byte-identical.
15
+ //
16
+ // The on-disk layout is shared (one jsonl per kind, content-addressed records), so reading is just
17
+ // parsing — no format adapter and no second wire contract.
18
+ import { homedir } from 'node:os';
19
+ import { join, resolve } from 'node:path';
20
+ import { readUtf8Regular } from './assetStoreStorage.js';
21
+ import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
22
+ import { signalsOf } from './localJsonl.js';
23
+ /** Thrown by every mutating method: a foreign pool is readable, never writable. */
24
+ export class ForeignStoreReadOnlyError extends Error {
25
+ baseDir;
26
+ kind;
27
+ code = 'FOREIGN_STORE_READ_ONLY';
28
+ constructor(baseDir, kind) {
29
+ super(`refusing to write ${kind ?? 'an asset'} into a foreign evolution store: ${baseDir}`);
30
+ this.baseDir = baseDir;
31
+ this.kind = kind;
32
+ this.name = 'ForeignStoreReadOnlyError';
33
+ }
34
+ }
35
+ /**
36
+ * EvoX's asset directory: `EVOX_ASSETS_DIR`, else `~/.evox/agent/evolver/assets`. Returns the path
37
+ * whether or not it exists; {@link ForeignJsonlSource} treats a missing directory as an empty pool.
38
+ *
39
+ * The name is engine-scoped on purpose, matching the EVOLVER_HOME / EVOMAP_HOME split. It deliberately
40
+ * does NOT honour `GEP_ASSETS_DIR`: in THIS repo that variable relocates evolver's OWN pool
41
+ * (`pendingSignals.legacyV1BaseDir`, README.npm.md), so a user who moved their own store per our own
42
+ * documentation would silently get a union of the primary with itself and never read EvoX at all.
43
+ */
44
+ export function evoxAssetDir(env = process.env) {
45
+ const override = env['EVOX_ASSETS_DIR']?.trim();
46
+ if (override)
47
+ return resolve(override);
48
+ return join(homedir(), '.evox', 'agent', 'evolver', 'assets');
49
+ }
50
+ function parseJsonlRecords(raw) {
51
+ const out = [];
52
+ for (const line of raw.split('\n')) {
53
+ if (!line.trim())
54
+ continue;
55
+ try {
56
+ const record = JSON.parse(line);
57
+ if (record.asset_id)
58
+ out.push(record);
59
+ }
60
+ catch { /* skip corrupt line, same tolerance LocalJsonlProvider applies */ }
61
+ }
62
+ return out;
63
+ }
64
+ function matchesQuery(record, q) {
65
+ if (q.kind && record.type !== q.kind)
66
+ return false;
67
+ if (q.gene && record.gene !== q.gene)
68
+ return false;
69
+ if (q.category) {
70
+ const category = record.category
71
+ ?? record.intent;
72
+ if (category !== q.category)
73
+ return false;
74
+ }
75
+ if (q.text && !String(record.summary ?? '').includes(q.text))
76
+ return false;
77
+ if (q.signalsAny && q.signalsAny.length > 0) {
78
+ const advertised = new Set(signalsOf(record).map((signal) => signal.trim().toLowerCase()));
79
+ if (!q.signalsAny.some((signal) => advertised.has(signal.trim().toLowerCase())))
80
+ return false;
81
+ }
82
+ return true;
83
+ }
84
+ /**
85
+ * Read-only provider over a foreign asset directory. Re-reads on every call rather than caching: the
86
+ * owning engine appends to these files behind our back, and a stale cache would silently freeze a gene's
87
+ * evidence at whatever it was when this process started.
88
+ */
89
+ export class ForeignJsonlSource {
90
+ baseDir;
91
+ reportedUnreadable = new Set();
92
+ constructor(baseDir) {
93
+ this.baseDir = baseDir;
94
+ }
95
+ /**
96
+ * Read one foreign file, degrading it to "empty" on ANY failure.
97
+ *
98
+ * `readUtf8Regular` returns null only for a missing file and THROWS for a symlink, a non-regular file,
99
+ * EACCES, ELOOP or ENOTDIR. This read runs inside gene selection (aggregateLearningHistory →
100
+ * candidateAssembly), where an exception would abort the host's whole cycle — a pool we do not own, and
101
+ * whose contents are optional extra evidence, must never have that power. Degrading is per file so one
102
+ * unreadable capsules.jsonl does not also hide genes.jsonl.
103
+ *
104
+ * Warned once per path per instance: reads are re-run on every call by design, and a per-call warning
105
+ * would bury the cycle's real output.
106
+ */
107
+ readOrDegrade(path) {
108
+ try {
109
+ return readUtf8Regular(path);
110
+ }
111
+ catch (error) {
112
+ if (!this.reportedUnreadable.has(path)) {
113
+ this.reportedUnreadable.add(path);
114
+ const message = error instanceof Error ? error.message : String(error);
115
+ console.warn(`[ForeignAssetPool] Failed to read ${path} (non-fatal, treated as empty): ${message}`);
116
+ }
117
+ return null;
118
+ }
119
+ }
120
+ read(kind) {
121
+ const files = kind ? [LOCAL_ASSET_FILES[kind]] : Object.values(LOCAL_ASSET_FILES);
122
+ const byAssetId = new Map();
123
+ for (const file of files) {
124
+ const raw = this.readOrDegrade(join(this.baseDir, file));
125
+ if (raw === null)
126
+ continue;
127
+ for (const record of parseJsonlRecords(raw))
128
+ byAssetId.set(record.asset_id, record);
129
+ }
130
+ return [...byAssetId.values()];
131
+ }
132
+ async put(asset) {
133
+ throw new ForeignStoreReadOnlyError(this.baseDir, asset.type);
134
+ }
135
+ async get(assetId) {
136
+ return this.read().find((record) => record.asset_id === assetId) ?? null;
137
+ }
138
+ /** Filters by logical id BEFORE the cap, so `limit` bounds matches returned, never records scanned. */
139
+ async findByLogicalId(id, limit = 1000, kind) {
140
+ return this.read(kind)
141
+ .filter((record) => record['id'] === id && (kind === undefined || record.type === kind))
142
+ .slice(0, Math.max(1, limit));
143
+ }
144
+ async list(kind, limit = 1000) {
145
+ return this.read(kind).slice(0, limit);
146
+ }
147
+ async search(q) {
148
+ return this.read(q.kind).filter((record) => matchesQuery(record, q)).slice(0, q.limit ?? 1000);
149
+ }
150
+ }
@@ -1,5 +1,10 @@
1
1
  export * from './provider.js';
2
2
  export * from './localJsonl.js';
3
+ export * from './foreignJsonlSource.js';
4
+ export * from './unionReadStore.js';
5
+ export * from './localAssetStoreSnapshot.js';
6
+ export * from './assetStoreHealth.js';
7
+ export * from './assetSidecarRecovery.js';
3
8
  export * from './remoteStub.js';
4
9
  export * from './learningHistory.js';
5
10
  export * from './provenance.js';
@@ -1,5 +1,10 @@
1
1
  export * from './provider.js';
2
2
  export * from './localJsonl.js';
3
+ export * from './foreignJsonlSource.js';
4
+ export * from './unionReadStore.js';
5
+ export * from './localAssetStoreSnapshot.js';
6
+ export * from './assetStoreHealth.js';
7
+ export * from './assetSidecarRecovery.js';
3
8
  export * from './remoteStub.js';
4
9
  export * from './learningHistory.js';
5
10
  export * from './provenance.js';
@@ -1,4 +1,4 @@
1
- import { proofOfWork } from '../schema/proofOfWork.js';
1
+ import { normalizeProofOfWork, proofOfWork } from '../schema/proofOfWork.js';
2
2
  import { proofIndicatesOutput } from '../algo/solidify.js';
3
3
  /** 扫该 gene 关联的 Capsule, 派生学习视图(只读, 不落库). */
4
4
  export async function aggregateLearningHistory(provider, geneId, recentN = 10) {
@@ -14,7 +14,7 @@ export async function aggregateLearningHistory(provider, geneId, recentN = 10) {
14
14
  // signal solidify uses to mark it `suppressed_observationally`) is INERT, not a real success. Counting it
15
15
  // would inflate successRate for a gene that only ever does nothing. Inert is neutral, not a failure: it is
16
16
  // excluded from BOTH sides of successRate, so it neither builds nor erodes confidence.
17
- const proof = c.proof_of_work;
17
+ const proof = normalizeProofOfWork(c.proof_of_work);
18
18
  if (proofIndicatesOutput(proof))
19
19
  success += 1;
20
20
  else
@@ -34,5 +34,5 @@ export async function aggregateLearningHistory(provider, geneId, recentN = 10) {
34
34
  }
35
35
  /** M3-5: 校验 agent 提交的 proof_of_work(zod). 返回规范化值或抛. */
36
36
  export function validateProofOfWork(input) {
37
- return proofOfWork.parse(input);
37
+ return proofOfWork.parse(normalizeProofOfWork(input) ?? input);
38
38
  }
@@ -0,0 +1,51 @@
1
+ import { type AssetRecord } from './provider.js';
2
+ import type { ProvenanceRecord } from './provenance.js';
3
+ export type CorruptLocalAssetStoreReason = 'invalid_utf8' | 'unterminated' | 'invalid_json' | 'invalid_record';
4
+ export declare class CorruptLocalAssetStoreError extends Error {
5
+ readonly reason: CorruptLocalAssetStoreReason;
6
+ readonly file?: string | undefined;
7
+ readonly row?: number | undefined;
8
+ readonly code = "CORRUPT_LOCAL_ASSET_STORE";
9
+ constructor(reason: CorruptLocalAssetStoreReason, file?: string | undefined, row?: number | undefined);
10
+ }
11
+ export type LocalAssetStoreSnapshotLimitReason = 'file_bytes' | 'total_bytes' | 'records';
12
+ export declare class LocalAssetStoreSnapshotLimitError extends Error {
13
+ readonly reason: LocalAssetStoreSnapshotLimitReason;
14
+ readonly file?: string | undefined;
15
+ readonly code = "LOCAL_ASSET_STORE_SNAPSHOT_LIMIT";
16
+ constructor(reason: LocalAssetStoreSnapshotLimitReason, file?: string | undefined);
17
+ }
18
+ export type LocalAssetStoreSnapshotChangeReason = 'changed' | 'lock_present' | 'invalid_snapshot';
19
+ export declare class LocalAssetStoreSnapshotChangedError extends Error {
20
+ readonly reason: LocalAssetStoreSnapshotChangeReason;
21
+ readonly code = "LOCAL_ASSET_STORE_SNAPSHOT_CHANGED";
22
+ constructor(reason: LocalAssetStoreSnapshotChangeReason);
23
+ }
24
+ export interface ReadLocalAssetStoreSnapshotOptions {
25
+ maxFileBytes?: number;
26
+ maxTotalBytes?: number;
27
+ maxRecords?: number;
28
+ }
29
+ export interface ReadLocalAssetStoreSnapshotDeps {
30
+ /** Test seam invoked after every tracked file has been read but before the stable-read verification. */
31
+ beforeVerify?: () => void;
32
+ }
33
+ export interface LocalAssetStoreSnapshot {
34
+ readonly exists: boolean;
35
+ readonly fingerprint: string;
36
+ readonly assets: ReadonlyMap<string, AssetRecord>;
37
+ readonly provenance: ReadonlyMap<string, ProvenanceRecord>;
38
+ readonly stats: {
39
+ readonly bytes: number;
40
+ readonly assetRows: number;
41
+ readonly provenanceRows: number;
42
+ readonly uniqueAssets: number;
43
+ };
44
+ }
45
+ /**
46
+ * Read a bounded, strict target-store snapshot without creating the directory, a lock, or any other file.
47
+ * Writers are detected through the lock/root/file fingerprints; callers must revalidate before apply.
48
+ */
49
+ export declare function readLocalAssetStoreSnapshot(baseDir: string, options?: ReadLocalAssetStoreSnapshotOptions, deps?: ReadLocalAssetStoreSnapshotDeps): LocalAssetStoreSnapshot;
50
+ /** Fail closed when the target store changed between planning and apply. Performs reads only. */
51
+ export declare function assertLocalAssetStoreSnapshotCurrent(snapshot: LocalAssetStoreSnapshot): void;
@@ -0,0 +1,329 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstatSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { LOCAL_ASSET_FILES } from './assetStoreLayout.js';
5
+ import { AssetStoreReadLimitError, readRegularBuffer, UnsafeAssetStorePathError, } from './assetStoreStorage.js';
6
+ import { parseProvenanceRecord, CorruptAssetSidecarError, } from './assetSidecarRecords.js';
7
+ import { frozenAssetRecordsEqual, FrozenAssetIdCollisionError } from './provider.js';
8
+ const PROVENANCE_FILE = 'provenance.jsonl';
9
+ const LOCK_FILE = '.assetstore.lock';
10
+ const DEFAULT_MAX_FILE_BYTES = 64 * 1024 * 1024;
11
+ const DEFAULT_MAX_TOTAL_BYTES = 128 * 1024 * 1024;
12
+ const DEFAULT_MAX_RECORDS = 100_000;
13
+ export class CorruptLocalAssetStoreError extends Error {
14
+ reason;
15
+ file;
16
+ row;
17
+ code = 'CORRUPT_LOCAL_ASSET_STORE';
18
+ constructor(reason, file, row) {
19
+ super(`corrupt local asset store: ${reason}${file ? `/${file}` : ''}${row ? `/${row}` : ''}`);
20
+ this.reason = reason;
21
+ this.file = file;
22
+ this.row = row;
23
+ this.name = 'CorruptLocalAssetStoreError';
24
+ }
25
+ }
26
+ export class LocalAssetStoreSnapshotLimitError extends Error {
27
+ reason;
28
+ file;
29
+ code = 'LOCAL_ASSET_STORE_SNAPSHOT_LIMIT';
30
+ constructor(reason, file) {
31
+ super(`local asset store snapshot limit exceeded: ${reason}${file ? `/${file}` : ''}`);
32
+ this.reason = reason;
33
+ this.file = file;
34
+ this.name = 'LocalAssetStoreSnapshotLimitError';
35
+ }
36
+ }
37
+ export class LocalAssetStoreSnapshotChangedError extends Error {
38
+ reason;
39
+ code = 'LOCAL_ASSET_STORE_SNAPSHOT_CHANGED';
40
+ constructor(reason) {
41
+ super(`local asset store snapshot changed: ${reason}`);
42
+ this.reason = reason;
43
+ this.name = 'LocalAssetStoreSnapshotChangedError';
44
+ }
45
+ }
46
+ class SnapshotMap {
47
+ #values;
48
+ constructor(entries) {
49
+ this.#values = new Map(entries);
50
+ Object.freeze(this);
51
+ }
52
+ get size() { return this.#values.size; }
53
+ get(key) { return this.#values.get(key); }
54
+ has(key) { return this.#values.has(key); }
55
+ entries() { return this.#values.entries(); }
56
+ keys() { return this.#values.keys(); }
57
+ values() { return this.#values.values(); }
58
+ [Symbol.iterator]() { return this.#values[Symbol.iterator](); }
59
+ forEach(callbackfn, thisArg) {
60
+ for (const [key, value] of this.#values)
61
+ callbackfn.call(thisArg, value, key, this);
62
+ }
63
+ }
64
+ const metadataBySnapshot = new WeakMap();
65
+ function isErrno(error, code) {
66
+ return typeof error === 'object' && error !== null && error.code === code;
67
+ }
68
+ function boundedPositiveInt(value, fallback) {
69
+ return value === undefined || !Number.isFinite(value)
70
+ ? fallback
71
+ : Math.max(1, Math.floor(value));
72
+ }
73
+ function statFingerprint(stat) {
74
+ return `${stat.dev}:${stat.ino}:${stat.mode}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
75
+ }
76
+ function directoryFingerprint(path) {
77
+ try {
78
+ const stat = lstatSync(path, { bigint: true });
79
+ if (stat.isSymbolicLink())
80
+ throw new UnsafeAssetStorePathError('base_directory', 'symlink');
81
+ if (!stat.isDirectory())
82
+ throw new UnsafeAssetStorePathError('base_directory', 'not_directory');
83
+ return statFingerprint(stat);
84
+ }
85
+ catch (error) {
86
+ if (isErrno(error, 'ENOENT'))
87
+ return 'missing';
88
+ throw error;
89
+ }
90
+ }
91
+ function optionalFileFingerprint(path, role) {
92
+ try {
93
+ const stat = lstatSync(path, { bigint: true });
94
+ if (stat.isSymbolicLink())
95
+ throw new UnsafeAssetStorePathError(role, 'symlink');
96
+ if (!stat.isFile())
97
+ throw new UnsafeAssetStorePathError(role, 'not_regular_file');
98
+ return statFingerprint(stat);
99
+ }
100
+ catch (error) {
101
+ if (isErrno(error, 'ENOENT'))
102
+ return 'missing';
103
+ throw error;
104
+ }
105
+ }
106
+ function captureState(baseDir) {
107
+ const root = directoryFingerprint(baseDir);
108
+ const files = new Map();
109
+ if (root === 'missing') {
110
+ for (const kind of Object.keys(LOCAL_ASSET_FILES))
111
+ files.set(kind, 'missing');
112
+ files.set('provenance', 'missing');
113
+ return { root, lock: 'missing', files };
114
+ }
115
+ for (const [kind, file] of Object.entries(LOCAL_ASSET_FILES)) {
116
+ files.set(kind, optionalFileFingerprint(join(baseDir, file), 'asset_file'));
117
+ }
118
+ files.set('provenance', optionalFileFingerprint(join(baseDir, PROVENANCE_FILE), 'asset_file'));
119
+ return {
120
+ root,
121
+ lock: optionalFileFingerprint(join(baseDir, LOCK_FILE), 'lock_file'),
122
+ files,
123
+ };
124
+ }
125
+ function recaptureState(baseDir) {
126
+ try {
127
+ return captureState(baseDir);
128
+ }
129
+ catch (error) {
130
+ if (error instanceof UnsafeAssetStorePathError) {
131
+ throw new LocalAssetStoreSnapshotChangedError('changed');
132
+ }
133
+ throw error;
134
+ }
135
+ }
136
+ function statesEqual(left, right) {
137
+ if (left.root !== right.root || left.lock !== right.lock || left.files.size !== right.files.size)
138
+ return false;
139
+ for (const [file, fingerprint] of left.files) {
140
+ if (right.files.get(file) !== fingerprint)
141
+ return false;
142
+ }
143
+ return true;
144
+ }
145
+ function snapshotFingerprint(baseDir, state) {
146
+ const fileState = [...state.files.entries()];
147
+ const digest = createHash('sha256')
148
+ .update(JSON.stringify([baseDir, state.root, state.lock, fileState]))
149
+ .digest('hex');
150
+ return `sha256:${digest}`;
151
+ }
152
+ function decodeStrict(bytes, file) {
153
+ try {
154
+ return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
155
+ }
156
+ catch {
157
+ throw new CorruptLocalAssetStoreError('invalid_utf8', file);
158
+ }
159
+ }
160
+ function assertTerminated(raw, file) {
161
+ if (raw.length > 0 && !raw.endsWith('\n'))
162
+ throw new CorruptLocalAssetStoreError('unterminated', file);
163
+ }
164
+ function immutableJson(value) {
165
+ if (!value || typeof value !== 'object')
166
+ return value;
167
+ const pending = [value];
168
+ const seen = new Set();
169
+ while (pending.length > 0) {
170
+ const current = pending.pop();
171
+ if (seen.has(current))
172
+ continue;
173
+ seen.add(current);
174
+ for (const item of Object.values(current)) {
175
+ if (item && typeof item === 'object')
176
+ pending.push(item);
177
+ }
178
+ Object.freeze(current);
179
+ }
180
+ return value;
181
+ }
182
+ function parseAssetFile(bytes, kind, file, assets, consumeRecord) {
183
+ const raw = decodeStrict(bytes, file);
184
+ assertTerminated(raw, file);
185
+ let row = 0;
186
+ for (const line of raw.split('\n')) {
187
+ if (!line.trim())
188
+ continue;
189
+ row += 1;
190
+ consumeRecord();
191
+ let value;
192
+ try {
193
+ value = JSON.parse(line);
194
+ }
195
+ catch {
196
+ throw new CorruptLocalAssetStoreError('invalid_json', file, row);
197
+ }
198
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
199
+ throw new CorruptLocalAssetStoreError('invalid_record', file, row);
200
+ }
201
+ const candidate = value;
202
+ if (candidate['type'] !== kind
203
+ || typeof candidate['asset_id'] !== 'string'
204
+ || !candidate['asset_id']
205
+ || candidate['asset_id'] !== candidate['asset_id'].trim()) {
206
+ throw new CorruptLocalAssetStoreError('invalid_record', file, row);
207
+ }
208
+ const record = candidate;
209
+ const existing = assets.get(record.asset_id);
210
+ if (existing && !frozenAssetRecordsEqual(existing, record)) {
211
+ throw new FrozenAssetIdCollisionError(record.asset_id);
212
+ }
213
+ if (!existing)
214
+ assets.set(record.asset_id, immutableJson(record));
215
+ }
216
+ return row;
217
+ }
218
+ function parseProvenanceFile(bytes, consumeRecord) {
219
+ const raw = decodeStrict(bytes, PROVENANCE_FILE);
220
+ const records = new Map();
221
+ let rows = 0;
222
+ if (raw.length > 0 && !raw.endsWith('\n')) {
223
+ throw new CorruptAssetSidecarError('provenance', 'unterminated');
224
+ }
225
+ for (const line of raw.split('\n')) {
226
+ if (!line.trim())
227
+ continue;
228
+ rows += 1;
229
+ consumeRecord();
230
+ let record;
231
+ try {
232
+ record = parseProvenanceRecord(JSON.parse(line));
233
+ }
234
+ catch {
235
+ record = null;
236
+ }
237
+ if (!record)
238
+ throw new CorruptAssetSidecarError('provenance', 'invalid_row');
239
+ records.set(record.assetId, immutableJson(record));
240
+ }
241
+ return { records: new SnapshotMap(records), rows };
242
+ }
243
+ function readTrackedFile(path, file, maxFileBytes) {
244
+ try {
245
+ return readRegularBuffer(path, maxFileBytes) ?? Buffer.alloc(0);
246
+ }
247
+ catch (error) {
248
+ if (error instanceof AssetStoreReadLimitError) {
249
+ throw new LocalAssetStoreSnapshotLimitError('file_bytes', file);
250
+ }
251
+ if (isErrno(error, 'ENOENT') || isErrno(error, 'ELOOP') || isErrno(error, 'EISDIR')) {
252
+ throw new LocalAssetStoreSnapshotChangedError('changed');
253
+ }
254
+ if (error instanceof UnsafeAssetStorePathError && error.reason === 'path_changed') {
255
+ throw new LocalAssetStoreSnapshotChangedError('changed');
256
+ }
257
+ throw error;
258
+ }
259
+ }
260
+ /**
261
+ * Read a bounded, strict target-store snapshot without creating the directory, a lock, or any other file.
262
+ * Writers are detected through the lock/root/file fingerprints; callers must revalidate before apply.
263
+ */
264
+ export function readLocalAssetStoreSnapshot(baseDir, options = {}, deps = {}) {
265
+ const resolvedBaseDir = resolve(baseDir);
266
+ const maxFileBytes = boundedPositiveInt(options.maxFileBytes, DEFAULT_MAX_FILE_BYTES);
267
+ const maxTotalBytes = boundedPositiveInt(options.maxTotalBytes, DEFAULT_MAX_TOTAL_BYTES);
268
+ const maxRecords = boundedPositiveInt(options.maxRecords, DEFAULT_MAX_RECORDS);
269
+ const before = captureState(resolvedBaseDir);
270
+ if (before.lock !== 'missing')
271
+ throw new LocalAssetStoreSnapshotChangedError('lock_present');
272
+ const buffers = new Map();
273
+ let totalBytes = 0;
274
+ if (before.root !== 'missing') {
275
+ for (const [kind, file] of Object.entries(LOCAL_ASSET_FILES)) {
276
+ const bytes = readTrackedFile(join(resolvedBaseDir, file), file, maxFileBytes);
277
+ totalBytes += bytes.length;
278
+ if (totalBytes > maxTotalBytes)
279
+ throw new LocalAssetStoreSnapshotLimitError('total_bytes');
280
+ buffers.set(kind, bytes);
281
+ }
282
+ const provenanceBytes = readTrackedFile(join(resolvedBaseDir, PROVENANCE_FILE), PROVENANCE_FILE, maxFileBytes);
283
+ totalBytes += provenanceBytes.length;
284
+ if (totalBytes > maxTotalBytes)
285
+ throw new LocalAssetStoreSnapshotLimitError('total_bytes');
286
+ buffers.set('provenance', provenanceBytes);
287
+ }
288
+ deps.beforeVerify?.();
289
+ const after = recaptureState(resolvedBaseDir);
290
+ if (after.lock !== 'missing' || !statesEqual(before, after)) {
291
+ throw new LocalAssetStoreSnapshotChangedError(after.lock !== 'missing' ? 'lock_present' : 'changed');
292
+ }
293
+ let records = 0;
294
+ const consumeRecords = (count) => {
295
+ records += count;
296
+ if (records > maxRecords)
297
+ throw new LocalAssetStoreSnapshotLimitError('records');
298
+ };
299
+ const assets = new Map();
300
+ let assetRows = 0;
301
+ for (const [kind, file] of Object.entries(LOCAL_ASSET_FILES)) {
302
+ assetRows += parseAssetFile(buffers.get(kind) ?? Buffer.alloc(0), kind, file, assets, () => consumeRecords(1));
303
+ }
304
+ const parsedProvenance = parseProvenanceFile(buffers.get('provenance') ?? Buffer.alloc(0), () => consumeRecords(1));
305
+ const snapshot = Object.freeze({
306
+ exists: before.root !== 'missing',
307
+ fingerprint: snapshotFingerprint(resolvedBaseDir, before),
308
+ assets: new SnapshotMap(assets),
309
+ provenance: parsedProvenance.records,
310
+ stats: Object.freeze({
311
+ bytes: totalBytes,
312
+ assetRows,
313
+ provenanceRows: parsedProvenance.rows,
314
+ uniqueAssets: assets.size,
315
+ }),
316
+ });
317
+ metadataBySnapshot.set(snapshot, { baseDir: resolvedBaseDir, state: before });
318
+ return snapshot;
319
+ }
320
+ /** Fail closed when the target store changed between planning and apply. Performs reads only. */
321
+ export function assertLocalAssetStoreSnapshotCurrent(snapshot) {
322
+ const metadata = metadataBySnapshot.get(snapshot);
323
+ if (!metadata)
324
+ throw new LocalAssetStoreSnapshotChangedError('invalid_snapshot');
325
+ const current = recaptureState(metadata.baseDir);
326
+ if (current.lock !== 'missing' || !statesEqual(metadata.state, current)) {
327
+ throw new LocalAssetStoreSnapshotChangedError(current.lock !== 'missing' ? 'lock_present' : 'changed');
328
+ }
329
+ }
@@ -1,4 +1,6 @@
1
- import { type AssetKind, type AssetRecord, type AssetStoreProvider, type PutResult, type SearchQuery } from './provider.js';
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,14 +21,22 @@ 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>;
28
+ putConditional(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
29
+ putBundle(assets: readonly AssetRecord[]): Promise<PutResult[]>;
22
30
  /**
23
31
  * 迁移专用(M8-2): 以**冻结 asset_id** 原样写入, 不经 normalizeForPut 重算/校验.
24
32
  * 仅 v1→v2 导入用(硬化 A6 存量冻结); 普通写一律走 put(). record 必须自带 asset_id.
25
33
  */
26
34
  putFrozen(record: AssetRecord): Promise<PutResult>;
35
+ putFrozenConditional(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
27
36
  get(assetId: string): Promise<AssetRecord | null>;
37
+ findByLogicalId(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
28
38
  list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
39
+ listAll(kind?: AssetKind): AssetRecord[];
29
40
  search(q: SearchQuery): Promise<AssetRecord[]>;
30
41
  /**
31
42
  * Opt-in log compaction: rewrite each kind's jsonl keeping ONE line per asset_id (last wins),