@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
@@ -3,28 +3,119 @@
3
3
  // by asset_id, NOT a field on the asset: asset_id = sha256(canonicalize(asset)), and "how we got it" metadata
4
4
  // must not enter the content hash (#30.2), or it would break content-addressing. Trust-first by construction:
5
5
  // selection defaults to trusted-only; an untrusted asset is promoted to trusted only by an explicit, logged act.
6
- import { appendFileSync, closeSync, existsSync, openSync, readFileSync, readSync, mkdirSync, statSync, truncateSync } from 'node:fs';
7
6
  import { join, dirname } from 'node:path';
8
- import { acquireLock, releaseLock } from '../util/fileLock.js';
9
- import { normalizeForPut } from './provider.js';
10
- function isErrno(error, code) {
11
- return typeof error === 'object' && error !== null && error.code === code;
7
+ import { assertCapsuleGeneBinding, FrozenAssetIdCollisionError, InvalidFrozenPutResultError, frozenAssetRecordsEqual, normalizeForPut, supportsAtomicConditionalPut, supportsAtomicFrozenConditionalPut, validateConditionalPutResult, validateFrozenPutResult, } from './provider.js';
8
+ import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, truncateUtf8SuffixDurable, withAssetStoreLock, } from './assetStoreStorage.js';
9
+ import { assertTrustSidecarHealthy, parseProvenanceRecord, parseSidecarJsonl, } from './assetSidecarRecords.js';
10
+ import { computeAssetId } from '../wire/index.js';
11
+ export const UNVERIFIED_V1_IMPORT_REASON = 'unverified_v1_import';
12
+ const UNVERIFIED_WRITE_PENDING_REASON = 'unverified_hub_write_pending';
13
+ function unverifiedStageAction(current, source, frozenContentId) {
14
+ if (!current)
15
+ return 'append';
16
+ const isUndecidedUntrusted = current.trusted === false
17
+ && current.decision === undefined
18
+ && current.decidedBy === undefined
19
+ && current.promotedBy === undefined;
20
+ if (isActiveUnverifiedProvenance(current, frozenContentId))
21
+ return 'append';
22
+ if (isUndecidedUntrusted && current.source === source && current.reason === undefined)
23
+ return 'append';
24
+ if (isUndecidedUntrusted
25
+ && current.source === source
26
+ && current.reason === UNVERIFIED_WRITE_PENDING_REASON
27
+ && current.frozenContentId === frozenContentId)
28
+ return 'reuse_pending';
29
+ return 'blocked';
12
30
  }
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}`;
31
+ function unverifiedFinalizeAction(current, source, reason, frozenContentId) {
32
+ if (!current)
33
+ return 'append';
34
+ if (current.trusted === true
35
+ || current.decision !== undefined
36
+ || current.decidedBy !== undefined
37
+ || current.promotedBy !== undefined)
38
+ return 'preserve';
39
+ const sameWaiver = current.source === source && current.trusted === false && current.reason === reason;
40
+ if (sameWaiver && current.frozenContentId === frozenContentId)
41
+ return 'already_bound';
42
+ if (!sameWaiver && (current.reason !== UNVERIFIED_WRITE_PENDING_REASON
43
+ || (current.frozenContentId !== undefined && current.frozenContentId !== frozenContentId)))
44
+ return 'preserve';
45
+ return 'append';
46
+ }
47
+ /**
48
+ * Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
49
+ * target snapshot during migration planning, then apply through ingestUnverified without duplicating its
50
+ * trust and collision rules.
51
+ */
52
+ export function planUnverifiedIngest(record, existing, currentProvenance, reason, source = 'hub') {
53
+ assertCapsuleGeneBinding(record);
54
+ const frozenContentId = computeAssetId(record);
55
+ if (!frozenContentId)
56
+ throw new Error('failed to compute frozen content id');
57
+ const base = { assetId: record.asset_id, frozenContentId };
58
+ if (currentProvenance && currentProvenance.assetId !== record.asset_id) {
59
+ return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
17
60
  }
18
- catch (error) {
19
- if (isErrno(error, 'ENOENT'))
20
- return 'missing';
21
- throw error;
61
+ if (existing) {
62
+ if (!frozenAssetRecordsEqual(existing, record)) {
63
+ return { ...base, status: 'collision', collision: 'asset', provenanceAction: 'none' };
64
+ }
65
+ const finalize = unverifiedFinalizeAction(currentProvenance, source, reason, frozenContentId);
66
+ if (finalize === 'preserve') {
67
+ return { ...base, status: 'preserve_trust', provenanceAction: 'none' };
68
+ }
69
+ return {
70
+ ...base,
71
+ status: 'already_bound',
72
+ provenanceAction: finalize === 'append' ? 'finalize' : 'none',
73
+ };
74
+ }
75
+ if (unverifiedStageAction(currentProvenance, source, frozenContentId) === 'blocked') {
76
+ return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
77
+ }
78
+ return { ...base, status: 'create', provenanceAction: 'stage_and_finalize' };
79
+ }
80
+ /**
81
+ * A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
82
+ * waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
83
+ * reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
84
+ */
85
+ export function isActiveUnverifiedProvenance(record, frozenContentId) {
86
+ if (!record
87
+ || !frozenContentId
88
+ || record.trusted !== false
89
+ || record.decision !== undefined
90
+ || record.decidedBy !== undefined
91
+ || record.promotedBy !== undefined
92
+ || record.frozenContentId !== frozenContentId)
93
+ return false;
94
+ // V1 input is local legacy data rather than a network-issued identifier. Never let a malformed declared
95
+ // identity become healthy merely because the shallow wire gate does not enforce the schema's asset_id regex.
96
+ if (record.reason === UNVERIFIED_V1_IMPORT_REASON
97
+ && !/^sha256:[0-9a-f]{64}$/.test(record.assetId))
98
+ return false;
99
+ return (record.source === 'hub'
100
+ && (record.reason === 'unverified_hub_rewrite' || record.reason === 'unverified_hub_synthesized')) || (record.source === 'migrated'
101
+ && (record.reason === 'unverified_gepx_import' || record.reason === UNVERIFIED_V1_IMPORT_REASON));
102
+ }
103
+ export class ProvenanceWritePendingError extends Error {
104
+ assetId;
105
+ code = 'PROVENANCE_WRITE_PENDING';
106
+ constructor(assetId) {
107
+ super('asset provenance write is pending');
108
+ this.assetId = assetId;
109
+ this.name = 'ProvenanceWritePendingError';
22
110
  }
23
111
  }
112
+ function immutableRecord(record) {
113
+ return Object.freeze({ ...record });
114
+ }
24
115
  /**
25
116
  * Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
26
- * record = trusted: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
27
- * write here; the sole untrusted source hub ingestion ALWAYS marks via {@link ingestUntrusted}/mark.
117
+ * record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
118
+ * do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
28
119
  */
29
120
  export class ProvenanceStore {
30
121
  now;
@@ -34,22 +125,18 @@ export class ProvenanceStore {
34
125
  fileState = null;
35
126
  constructor(baseDir, now = Date.now) {
36
127
  this.now = now;
128
+ ensureAssetStoreDirectory(baseDir);
37
129
  this.path = join(baseDir, 'provenance.jsonl');
38
130
  this.lockPath = join(baseDir, '.assetstore.lock');
39
131
  }
40
132
  rebuildIndex(state) {
41
133
  const next = new Map();
42
- if (state !== 'missing') {
43
- for (const line of readFileSync(this.path, 'utf8').split('\n')) {
44
- if (!line.trim())
45
- continue;
46
- try {
47
- const r = JSON.parse(line);
48
- if (r.assetId)
49
- next.set(r.assetId, r);
50
- }
51
- catch { /* skip corrupt line */ }
52
- }
134
+ const raw = state === 'missing' ? null : readUtf8Regular(this.path);
135
+ if (raw !== null) {
136
+ const parsed = parseSidecarJsonl(raw, parseProvenanceRecord);
137
+ assertTrustSidecarHealthy('provenance', parsed);
138
+ for (const record of parsed.records)
139
+ next.set(record.assetId, immutableRecord(record));
53
140
  }
54
141
  this.index.clear();
55
142
  for (const [assetId, record] of next)
@@ -57,82 +144,132 @@ export class ProvenanceStore {
57
144
  this.fileState = state;
58
145
  }
59
146
  refreshUnderLock() {
60
- const state = fileFingerprint(this.path);
147
+ const state = regularFileFingerprint(this.path);
61
148
  if (state !== this.fileState)
62
149
  this.rebuildIndex(state);
63
150
  }
64
151
  withFreshRead(read) {
65
- mkdirSync(dirname(this.path), { recursive: true });
66
- acquireLock(this.lockPath);
67
- try {
152
+ assertAssetStoreDirectory(dirname(this.path));
153
+ return withAssetStoreLock(this.lockPath, () => {
68
154
  this.refreshUnderLock();
69
155
  return read(this.index);
70
- }
71
- finally {
72
- releaseLock(this.lockPath);
73
- }
156
+ });
157
+ }
158
+ appendUnderLock(full) {
159
+ const stored = immutableRecord(full);
160
+ appendUtf8Durable(this.path, `${JSON.stringify(stored)}\n`);
161
+ this.index.set(stored.assetId, stored);
162
+ this.fileState = regularFileFingerprint(this.path);
163
+ return stored;
74
164
  }
75
165
  /** Record provenance for an asset_id (append-only; the JSONL history is the audit trail). */
76
166
  mark(rec) {
77
167
  const full = { ...rec, at: rec.at ?? new Date(this.now()).toISOString() };
78
- mkdirSync(dirname(this.path), { recursive: true });
79
- acquireLock(this.lockPath);
80
- try {
168
+ assertAssetStoreDirectory(dirname(this.path));
169
+ return withAssetStoreLock(this.lockPath, () => {
81
170
  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
- }
89
- return full;
171
+ return this.appendUnderLock(full);
172
+ });
173
+ }
174
+ /** Stage a verified Hub write without replacing an in-flight conservative marker. */
175
+ stageUntrustedWriteTracked(assetId, source) {
176
+ assertAssetStoreDirectory(dirname(this.path));
177
+ return withAssetStoreLock(this.lockPath, () => {
178
+ this.refreshUnderLock();
179
+ const current = this.index.get(assetId);
180
+ if (current && (current.trusted === true || current.decision !== undefined)) {
181
+ return { record: current, appended: false };
182
+ }
183
+ if (current?.trusted === false) {
184
+ return { record: current, appended: false };
185
+ }
186
+ return {
187
+ record: this.appendUnderLock({
188
+ assetId,
189
+ source,
190
+ trusted: false,
191
+ at: new Date(this.now()).toISOString(),
192
+ }),
193
+ appended: true,
194
+ };
195
+ });
196
+ }
197
+ /** Finalize a verified write unless an operator made an explicit decision during I/O. */
198
+ finalizeUntrustedWrite(assetId, source) {
199
+ assertAssetStoreDirectory(dirname(this.path));
200
+ return withAssetStoreLock(this.lockPath, () => {
201
+ this.refreshUnderLock();
202
+ const current = this.index.get(assetId);
203
+ if (current && (current.trusted === true || current.decision !== undefined))
204
+ return current;
205
+ return this.appendUnderLock({
206
+ assetId,
207
+ source,
208
+ trusted: false,
209
+ at: new Date(this.now()).toISOString(),
210
+ });
211
+ });
212
+ }
213
+ /** Stage an unverified write without overwriting an explicit trust decision or another conservative marker. */
214
+ stageUnverifiedWrite(assetId, source, frozenContentId) {
215
+ return this.stageUnverifiedWriteTracked(assetId, source, frozenContentId).record;
216
+ }
217
+ stageUnverifiedWriteTracked(assetId, source, frozenContentId) {
218
+ assertAssetStoreDirectory(dirname(this.path));
219
+ return withAssetStoreLock(this.lockPath, () => {
220
+ this.refreshUnderLock();
221
+ const current = this.index.get(assetId);
222
+ const action = unverifiedStageAction(current, source, frozenContentId);
223
+ if (current && action !== 'append')
224
+ return { record: current, appended: false };
225
+ return {
226
+ record: this.appendUnderLock({
227
+ assetId,
228
+ source,
229
+ trusted: false,
230
+ reason: UNVERIFIED_WRITE_PENDING_REASON,
231
+ frozenContentId,
232
+ at: new Date(this.now()).toISOString(),
233
+ }),
234
+ appended: true,
235
+ };
236
+ });
237
+ }
238
+ /** Atomically replace only a pending/no decision with the health-waiver reason after verified persistence. */
239
+ finalizeUnverifiedWrite(assetId, source, reason, frozenContentId) {
240
+ assertAssetStoreDirectory(dirname(this.path));
241
+ return withAssetStoreLock(this.lockPath, () => {
242
+ this.refreshUnderLock();
243
+ const current = this.index.get(assetId);
244
+ const action = unverifiedFinalizeAction(current, source, reason, frozenContentId);
245
+ if (current && action !== 'append')
246
+ return current;
247
+ return this.appendUnderLock({
248
+ assetId,
249
+ source,
250
+ trusted: false,
251
+ reason,
252
+ frozenContentId,
253
+ at: new Date(this.now()).toISOString(),
254
+ });
255
+ });
90
256
  }
91
257
  rollbackLast(rec) {
92
258
  const line = `${JSON.stringify(rec)}\n`;
93
- const lineBytes = Buffer.byteLength(line, 'utf8');
94
- let locked = false;
95
259
  try {
96
- mkdirSync(dirname(this.path), { recursive: true });
97
- acquireLock(this.lockPath);
98
- locked = true;
99
- if (!existsSync(this.path))
100
- return;
101
- const stat = statSync(this.path);
102
- if (!stat.isFile() || stat.size < lineBytes)
103
- return;
104
- const offset = stat.size - lineBytes;
105
- const buf = Buffer.alloc(lineBytes);
106
- const fd = openSync(this.path, 'r');
107
- try {
108
- readSync(fd, buf, 0, lineBytes, offset);
109
- }
110
- finally {
111
- closeSync(fd);
112
- }
113
- if (buf.toString('utf8') !== line) {
114
- this.refreshUnderLock();
115
- return;
116
- }
117
- truncateSync(this.path, offset);
118
- this.rebuildIndex(fileFingerprint(this.path));
260
+ assertAssetStoreDirectory(dirname(this.path));
261
+ withAssetStoreLock(this.lockPath, () => {
262
+ if (!truncateUtf8SuffixDurable(this.path, line)) {
263
+ this.refreshUnderLock();
264
+ return;
265
+ }
266
+ this.rebuildIndex(regularFileFingerprint(this.path));
267
+ });
119
268
  }
120
269
  catch {
121
270
  this.index.clear();
122
271
  this.fileState = null;
123
272
  }
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
- }
135
- }
136
273
  }
137
274
  get(assetId) {
138
275
  return this.withFreshRead((index) => index.get(assetId) ?? null);
@@ -145,10 +282,42 @@ export class ProvenanceStore {
145
282
  snapshot() {
146
283
  return this.withFreshRead((index) => new Map(index));
147
284
  }
285
+ /** Compare and append one trust decision under the same cross-process lock. */
286
+ changeTrust(assetId, trusted, by, reason) {
287
+ assertAssetStoreDirectory(dirname(this.path));
288
+ return withAssetStoreLock(this.lockPath, () => {
289
+ this.refreshUnderLock();
290
+ const current = this.index.get(assetId) ?? null;
291
+ if (trusted
292
+ && current?.trusted === false
293
+ && current.decision === undefined
294
+ && current.reason === UNVERIFIED_WRITE_PENDING_REASON) {
295
+ throw new ProvenanceWritePendingError(assetId);
296
+ }
297
+ const decision = trusted ? 'promoted' : 'revoked';
298
+ if (current?.trusted === trusted && current.decision === decision) {
299
+ return { changed: false, record: current };
300
+ }
301
+ const full = {
302
+ assetId,
303
+ source: current?.source ?? 'local',
304
+ trusted,
305
+ at: new Date(this.now()).toISOString(),
306
+ decision,
307
+ decidedBy: by,
308
+ ...(trusted ? { promotedBy: by } : {}),
309
+ reason,
310
+ };
311
+ return { changed: true, record: this.appendUnderLock(full) };
312
+ });
313
+ }
148
314
  /** Explicit, audited untrusted→trusted promotion. Appends a new trusted record carrying who/why. */
149
315
  promote(assetId, by, reason) {
150
- const cur = this.get(assetId);
151
- return this.mark({ assetId, source: cur?.source ?? 'hub', trusted: true, promotedBy: by, reason });
316
+ return this.changeTrust(assetId, true, by, reason).record;
317
+ }
318
+ /** Explicit, audited trusted-to-untrusted revocation. A record-less asset is local by default. */
319
+ revoke(assetId, by, reason) {
320
+ return this.changeTrust(assetId, false, by, reason).record;
152
321
  }
153
322
  }
154
323
  /**
@@ -166,4 +335,119 @@ export async function ingestUntrusted(store, prov, record, source = 'hub') {
166
335
  if (!result.stored)
167
336
  prov.rollbackLast(mark);
168
337
  return result;
338
+ }
339
+ /**
340
+ * Hub → local-pool landing for an asset whose content does NOT hash to its declared asset_id. The hub
341
+ * demonstrably rewrites delivered payloads (injected `validation`, wholesale `payload_backfill_reason`
342
+ * synthesis — evolver-v2#570), which breaks {@link ingestUntrusted}'s normalizeForPut self-consistency check
343
+ * even though the loop's own in-run reuse already consumes hub content without re-verifying it (adapter
344
+ * `hubReuse.ts`). Rather than hard-reject a save the operator explicitly asked for, freeze the asset under its
345
+ * declared (network) asset_id via `putFrozen` and mark it untrusted in the sidecar with an explicit reason.
346
+ * Trust-first still holds (#30.1): selection defaults to trusted-only, so an unverified asset never silently
347
+ * enters the reasoning pool — it lands where the operator put it and stays flagged until an explicit,
348
+ * audited promotion. `reason` records WHY verification was waived (e.g. hub rewrite vs synthesized payload).
349
+ */
350
+ export async function ingestUnverified(store, prov, record, reason, source = 'hub') {
351
+ if (typeof store.putFrozen !== 'function') {
352
+ // Only a content-addressed local pool receives hub reuse writes; a provider that cannot freeze a
353
+ // hash-inconsistent record cannot preserve the network id, so fail loudly rather than silently restamp it.
354
+ throw new Error('ingestUnverified requires a store that implements putFrozen');
355
+ }
356
+ // putFrozen bypasses normalizeForPut, so re-assert the M3-4 Capsule↔gene binding here — a hash-mismatched
357
+ // Capsule with an empty gene must still fail closed on the frozen path, exactly as it does on the verified one.
358
+ assertCapsuleGeneBinding(record);
359
+ const frozenContentId = computeAssetId(record);
360
+ if (!frozenContentId)
361
+ throw new Error('failed to compute frozen content id');
362
+ const existing = await store.get(record.asset_id);
363
+ if (existing) {
364
+ if (!frozenAssetRecordsEqual(existing, record))
365
+ throw new FrozenAssetIdCollisionError(record.asset_id);
366
+ prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
367
+ return { asset_id: record.asset_id, stored: false, verified: false };
368
+ }
369
+ // Stage a non-waiver record before the body write. If the provider throws or lies about persistence,
370
+ // the asset stays untrusted and health still reports its hash mismatch instead of treating it as #570.
371
+ const staged = prov.stageUnverifiedWrite(record.asset_id, source, frozenContentId);
372
+ // A missing body with an explicit trust decision cannot be replaced without reopening a promotion race.
373
+ if (planUnverifiedIngest(record, null, staged, reason, source).status !== 'create') {
374
+ throw new FrozenAssetIdCollisionError(record.asset_id);
375
+ }
376
+ const result = validateFrozenPutResult(await store.putFrozen(record), record.asset_id);
377
+ const persisted = await store.get(record.asset_id);
378
+ if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
379
+ throw new FrozenAssetIdCollisionError(record.asset_id);
380
+ }
381
+ prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
382
+ return result;
383
+ }
384
+ /** Atomic frozen variant used by reuse so the logical-id check and append share one provider lock. */
385
+ export async function ingestUnverifiedConditional(store, prov, record, reason, options, source = 'hub') {
386
+ if (!supportsAtomicFrozenConditionalPut(store)) {
387
+ throw new Error('asset store does not support conditional frozen writes');
388
+ }
389
+ assertCapsuleGeneBinding(record);
390
+ const frozenContentId = computeAssetId(record);
391
+ if (!frozenContentId)
392
+ throw new Error('failed to compute frozen content id');
393
+ const existing = await store.get(record.asset_id);
394
+ if (existing) {
395
+ if (!frozenAssetRecordsEqual(existing, record))
396
+ throw new FrozenAssetIdCollisionError(record.asset_id);
397
+ prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
398
+ return { asset_id: record.asset_id, stored: false, verified: false, status: 'already_exists' };
399
+ }
400
+ const stage = prov.stageUnverifiedWriteTracked(record.asset_id, source, frozenContentId);
401
+ if (planUnverifiedIngest(record, null, stage.record, reason, source).status !== 'create') {
402
+ throw new FrozenAssetIdCollisionError(record.asset_id);
403
+ }
404
+ const result = validateConditionalPutResult(await store.putFrozenConditional(record, options), record.asset_id, options);
405
+ if (result.verified !== false)
406
+ throw new InvalidFrozenPutResultError();
407
+ if (!result.stored) {
408
+ // The pending row may be shared with a concurrent writer. Keep it until a persisted body is finalized.
409
+ if (result.status === 'already_exists') {
410
+ const persisted = await store.get(record.asset_id);
411
+ if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
412
+ throw new FrozenAssetIdCollisionError(record.asset_id);
413
+ }
414
+ prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
415
+ }
416
+ return result;
417
+ }
418
+ const persisted = await store.get(record.asset_id);
419
+ if (!persisted || !frozenAssetRecordsEqual(persisted, record)) {
420
+ throw new FrozenAssetIdCollisionError(record.asset_id);
421
+ }
422
+ prov.finalizeUnverifiedWrite(record.asset_id, source, reason, frozenContentId);
423
+ return result;
424
+ }
425
+ /**
426
+ * Conditional variant used by Hub sync to reject a logical-id collision without ever allowing a Hub record
427
+ * to become implicitly trusted. Providers that cannot make the condition atomically are rejected here.
428
+ */
429
+ export async function ingestUntrustedConditional(store, prov, record, options, source = 'hub') {
430
+ if (!supportsAtomicConditionalPut(store)) {
431
+ throw new Error('asset store does not support conditional writes');
432
+ }
433
+ const normalized = normalizeForPut(record);
434
+ const stage = prov.stageUntrustedWriteTracked(normalized.record.asset_id, source);
435
+ const result = validateConditionalPutResult(await store.putConditional(record, options), normalized.record.asset_id, options);
436
+ if (result.status === 'logical_collision') {
437
+ if (stage.appended)
438
+ prov.rollbackLast(stage.record);
439
+ return result;
440
+ }
441
+ if (!result.stored) {
442
+ const persisted = await store.get(normalized.record.asset_id);
443
+ if (!persisted || !frozenAssetRecordsEqual(persisted, normalized.record)) {
444
+ throw new FrozenAssetIdCollisionError(normalized.record.asset_id);
445
+ }
446
+ if (stage.appended)
447
+ prov.rollbackLast(stage.record);
448
+ }
449
+ if (!stage.appended) {
450
+ prov.finalizeUntrustedWrite(normalized.record.asset_id, source);
451
+ }
452
+ return result;
169
453
  }
@@ -10,12 +10,43 @@ export interface PutResult {
10
10
  stored: boolean;
11
11
  verified: boolean;
12
12
  }
13
+ export declare class FrozenAssetIdCollisionError extends Error {
14
+ readonly assetId: string;
15
+ readonly code = "FROZEN_ASSET_ID_COLLISION";
16
+ constructor(assetId: string);
17
+ }
18
+ export declare class InvalidFrozenPutResultError extends Error {
19
+ readonly code = "INVALID_FROZEN_PUT_RESULT";
20
+ constructor();
21
+ }
22
+ export declare function frozenAssetRecordsEqual(left: AssetRecord, right: AssetRecord): boolean;
23
+ export declare function validateFrozenPutResult(value: unknown, expectedAssetId: string): PutResult;
24
+ export type ConditionalPutStatus = 'stored' | 'already_exists' | 'logical_collision';
25
+ export interface ConditionalPutOptions {
26
+ /** Only explicit force-like callers may keep multiple content versions for the same type + logical id. */
27
+ allowLogicalCollision?: boolean;
28
+ }
29
+ export interface ConditionalPutResult extends PutResult {
30
+ status: ConditionalPutStatus;
31
+ logicalId?: string;
32
+ collisionWithAssetId?: string;
33
+ }
34
+ export type InvalidConditionalPutResultReason = 'malformed_result' | 'asset_id_mismatch' | 'inconsistent_status' | 'invalid_collision' | 'collision_bypass';
35
+ export declare class InvalidConditionalPutResultError extends Error {
36
+ readonly reason: InvalidConditionalPutResultReason;
37
+ readonly code = "INVALID_CONDITIONAL_PUT_RESULT";
38
+ constructor(reason: InvalidConditionalPutResultReason);
39
+ }
40
+ /** Validate an injected provider response before callers treat it as an explicit write/no-write decision. */
41
+ export declare function validateConditionalPutResult(value: unknown, expectedAssetId: string, options?: ConditionalPutOptions): ConditionalPutResult;
13
42
  export interface SearchQuery {
14
43
  kind?: AssetKind;
15
44
  signalsAny?: string[];
16
45
  category?: string;
17
46
  gene?: string;
18
47
  text?: string;
48
+ /** Task-domain scope (hub taxonomy slug, e.g. "software_engineering"). Local providers may ignore it; the hub applies it as a recall fence. */
49
+ domain?: string;
19
50
  limit?: number;
20
51
  }
21
52
  /**
@@ -24,10 +55,36 @@ export interface SearchQuery {
24
55
  */
25
56
  export interface AssetStoreProvider {
26
57
  put(asset: AssetRecord): Promise<PutResult>;
58
+ /** 可选的可崩溃恢复批量写入,用于保持相关资产(例如 Gene/Capsule 对)的一致性。
59
+ * 需要持久化相关资产组的调用方必须先确认此能力存在,不得退回顺序单条写入。
60
+ * 返回结果按唯一 asset_id 各一条(同 bundle 内重复条目按幂等去重),调用方必须按 asset_id 键控消费结果,不得按索引与输入条目对齐。 */
61
+ putBundle?(assets: readonly AssetRecord[]): Promise<PutResult[]>;
62
+ /** Optional atomic capability; use supportsAtomicConditionalPut() before calling through this interface. */
63
+ putConditional?(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
27
64
  get(assetId: string): Promise<AssetRecord | null>;
65
+ /** Optional direct lookup for non-content-addressed logical ids. Callers must handle 0, 1, or multiple matches. */
66
+ findByLogicalId?(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
67
+ /**
68
+ * Optional frozen write: store the record under its OWN declared asset_id without recomputing or
69
+ * normalizing it (bypasses {@link normalizeForPut}'s self-consistency check). Only providers backing a
70
+ * content-addressed local pool implement it. Callers that must preserve a hash-inconsistent asset —
71
+ * v1 migration import, and unverified hub reuse of a hub-rewritten payload (see `ingestUnverified`) —
72
+ * feature-detect it rather than assuming it exists.
73
+ */
74
+ putFrozen?(record: AssetRecord): Promise<PutResult>;
75
+ /** Atomic frozen variant that also enforces the type + logical-id condition under the provider write lock. */
76
+ putFrozenConditional?(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
28
77
  search(query: SearchQuery): Promise<AssetRecord[]>;
29
78
  list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
30
79
  }
80
+ export type AtomicConditionalPutProvider = AssetStoreProvider & {
81
+ putConditional(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
82
+ };
83
+ export declare function supportsAtomicConditionalPut(provider: AssetStoreProvider): provider is AtomicConditionalPutProvider;
84
+ export type AtomicFrozenConditionalPutProvider = AssetStoreProvider & {
85
+ putFrozenConditional(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
86
+ };
87
+ export declare function supportsAtomicFrozenConditionalPut(provider: AssetStoreProvider): provider is AtomicFrozenConditionalPutProvider;
31
88
  export declare class AssetIdMismatchError extends Error {
32
89
  readonly claimed: string;
33
90
  readonly actual: string;
@@ -36,6 +93,12 @@ export declare class AssetIdMismatchError extends Error {
36
93
  export declare class CapsuleGeneBindingError extends Error {
37
94
  constructor();
38
95
  }
96
+ /**
97
+ * M3-4 强绑定校验: Capsule.gene 必须非空(否则一条无来源基因的经验会污染选择池).
98
+ * 抽成独立 helper 以便 normalizeForPut(校验落库路径)与 ingestUnverified(冻结落库路径,
99
+ * 绕过 normalizeForPut)共用同一条不变量,不让降级路径把绑定校验漏掉.
100
+ */
101
+ export declare function assertCapsuleGeneBinding(asset: AssetRecord): void;
39
102
  /**
40
103
  * 落库前规范化(共享给各 provider): 计算/校验 asset_id + 强绑定校验.
41
104
  * - 缺 asset_id → 计算填入(verified=false 表示非入参自带).