@evomap/evolver-core 2.0.0-beta.9 → 2.0.0

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 (142) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/cycleEngine.d.ts +23 -2
  5. package/dist/algo/cycleEngine.js +236 -36
  6. package/dist/algo/exploration.d.ts +7 -0
  7. package/dist/algo/exploration.js +16 -3
  8. package/dist/algo/geneHealth.d.ts +2 -2
  9. package/dist/algo/geneHealth.js +5 -4
  10. package/dist/algo/geneSelection.d.ts +63 -5
  11. package/dist/algo/geneSelection.js +330 -30
  12. package/dist/algo/index.d.ts +2 -0
  13. package/dist/algo/index.js +2 -0
  14. package/dist/algo/orchestrator.d.ts +8 -1
  15. package/dist/algo/orchestrator.js +15 -3
  16. package/dist/algo/publishEligibility.d.ts +34 -0
  17. package/dist/algo/publishEligibility.js +52 -0
  18. package/dist/algo/ucb1.d.ts +53 -0
  19. package/dist/algo/ucb1.js +156 -0
  20. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  21. package/dist/assetstore/assetSidecarRecords.js +238 -0
  22. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  23. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  24. package/dist/assetstore/assetStoreHealth.js +56 -14
  25. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  26. package/dist/assetstore/assetStoreStorage.js +27 -9
  27. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  28. package/dist/assetstore/assetSyncLedger.js +683 -4
  29. package/dist/assetstore/index.d.ts +1 -0
  30. package/dist/assetstore/index.js +1 -0
  31. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  33. package/dist/assetstore/localJsonl.d.ts +4 -2
  34. package/dist/assetstore/localJsonl.js +89 -9
  35. package/dist/assetstore/provenance.d.ts +80 -4
  36. package/dist/assetstore/provenance.js +313 -5
  37. package/dist/assetstore/provider.d.ts +58 -1
  38. package/dist/assetstore/provider.js +97 -6
  39. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  40. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  41. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  42. package/dist/benchmark/antiGeneRollout.js +4 -3
  43. package/dist/benchmark/index.d.ts +2 -1
  44. package/dist/benchmark/index.js +2 -1
  45. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  46. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  47. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  48. package/dist/bootstrap/envFingerprint.js +5 -0
  49. package/dist/bootstrap/index.d.ts +2 -1
  50. package/dist/bootstrap/index.js +2 -1
  51. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  52. package/dist/bootstrap/v1EnvCompat.js +280 -0
  53. package/dist/events/eventArchive.d.ts +2 -0
  54. package/dist/events/eventArchive.js +13 -3
  55. package/dist/events/eventSchema.d.ts +7 -7
  56. package/dist/events/eventStore.d.ts +2 -0
  57. package/dist/events/eventStore.js +5 -1
  58. package/dist/events/ingest.d.ts +1 -0
  59. package/dist/events/ingest.js +1 -0
  60. package/dist/events/paths.d.ts +3 -1
  61. package/dist/events/paths.js +4 -0
  62. package/dist/events/public.d.ts +2 -2
  63. package/dist/events/public.js +2 -2
  64. package/dist/events/reports.d.ts +2 -0
  65. package/dist/events/reports.js +4 -0
  66. package/dist/exec/autoExec.d.ts +43 -2
  67. package/dist/exec/autoExec.js +76 -9
  68. package/dist/exec/autonomousCycle.d.ts +22 -4
  69. package/dist/exec/autonomousCycle.js +64 -13
  70. package/dist/exec/claudeBridge.d.ts +32 -7
  71. package/dist/exec/claudeBridge.js +281 -29
  72. package/dist/exec/prompt.js +5 -1
  73. package/dist/exec/runnerRegistry.d.ts +68 -26
  74. package/dist/exec/runnerRegistry.js +307 -72
  75. package/dist/exec/selfPr.js +1 -7
  76. package/dist/feedback/envelope.d.ts +61 -0
  77. package/dist/feedback/envelope.js +168 -0
  78. package/dist/feedback/index.d.ts +1 -0
  79. package/dist/feedback/index.js +1 -0
  80. package/dist/hooks/hooks.js +1 -0
  81. package/dist/hub/assetCallLog.d.ts +35 -1
  82. package/dist/hub/assetCallLog.js +124 -1
  83. package/dist/hub/bindings.d.ts +8 -1
  84. package/dist/hub/bindings.js +17 -6
  85. package/dist/hub/capability.d.ts +90 -3
  86. package/dist/hub/fake.d.ts +2 -2
  87. package/dist/hub/fake.js +1 -1
  88. package/dist/hub/questionGenerator.d.ts +5 -1
  89. package/dist/hub/questionGenerator.js +8 -6
  90. package/dist/index.d.ts +3 -1
  91. package/dist/index.js +4 -1
  92. package/dist/mailbox/dispatch.d.ts +1 -1
  93. package/dist/mailbox/dispatch.js +22 -6
  94. package/dist/mailbox/envelope.d.ts +7 -1
  95. package/dist/mailbox/envelope.js +9 -2
  96. package/dist/mailbox/ipcServer.d.ts +10 -2
  97. package/dist/mailbox/ipcServer.js +163 -13
  98. package/dist/mailbox/store.d.ts +89 -3
  99. package/dist/mailbox/store.js +895 -41
  100. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  101. package/dist/ops/evolutionGraphProjection.js +315 -0
  102. package/dist/ops/index.d.ts +2 -1
  103. package/dist/ops/index.js +2 -1
  104. package/dist/ops/selfUpdate.d.ts +8 -0
  105. package/dist/ops/selfUpdate.js +24 -8
  106. package/dist/personality/schema.d.ts +16 -16
  107. package/dist/schema/evolutionGraph.d.ts +784 -0
  108. package/dist/schema/evolutionGraph.js +187 -0
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/signal.d.ts +3 -3
  112. package/dist/schema/signal.js +1 -1
  113. package/dist/signals/curriculum.d.ts +55 -0
  114. package/dist/signals/curriculum.js +202 -0
  115. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  116. package/dist/signals/expand.d.ts +15 -1
  117. package/dist/signals/expand.js +169 -1
  118. package/dist/signals/extractor.d.ts +2 -2
  119. package/dist/signals/extractor.js +31 -6
  120. package/dist/signals/index.d.ts +3 -1
  121. package/dist/signals/index.js +3 -1
  122. package/dist/signals/metaSignals.d.ts +4 -0
  123. package/dist/signals/metaSignals.js +42 -0
  124. package/dist/signals/signalGate.js +1 -1
  125. package/dist/signals/taskDomain.d.ts +22 -0
  126. package/dist/signals/taskDomain.js +43 -0
  127. package/dist/strategy/constraintAblation.js +115 -369
  128. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  129. package/dist/strategy/constraintAblationPredicates.js +339 -0
  130. package/dist/trace/index.d.ts +3 -1
  131. package/dist/trace/index.js +3 -1
  132. package/dist/trace/learningTrace.d.ts +209 -0
  133. package/dist/trace/learningTrace.js +276 -0
  134. package/dist/trace/proxyTurns.d.ts +31 -0
  135. package/dist/trace/proxyTurns.js +137 -0
  136. package/dist/verify/sandboxRunner.d.ts +28 -0
  137. package/dist/verify/sandboxRunner.js +218 -19
  138. package/dist/verify/sandboxedValidation.d.ts +9 -0
  139. package/dist/verify/sandboxedValidation.js +113 -13
  140. package/dist/verify/validation.d.ts +11 -1
  141. package/dist/verify/validation.js +31 -0
  142. package/package.json +5 -2
@@ -1,6 +1,7 @@
1
- import { type AssetStoreProvider, type AssetRecord, type PutResult } from './provider.js';
1
+ import { type AssetStoreProvider, type AssetRecord, type ConditionalPutOptions, type ConditionalPutResult, type PutResult } from './provider.js';
2
2
  export type ProvenanceSource = 'local' | 'migrated' | 'hub';
3
3
  export type ProvenanceDecision = 'promoted' | 'revoked';
4
+ export declare const UNVERIFIED_V1_IMPORT_REASON = "unverified_v1_import";
4
5
  export interface ProvenanceRecord {
5
6
  assetId: string;
6
7
  source: ProvenanceSource;
@@ -11,15 +12,56 @@ export interface ProvenanceRecord {
11
12
  /** Legacy promotion actor field kept for existing sidecar readers. */
12
13
  promotedBy?: string;
13
14
  reason?: string;
15
+ /** Canonical content id of the exact hash-mismatched body accepted by a supported unverified ingest. */
16
+ frozenContentId?: string;
14
17
  }
15
18
  export interface ProvenanceTrustChange {
16
19
  changed: boolean;
17
20
  record: ProvenanceRecord;
18
21
  }
22
+ export type UnverifiedIngestDisposition = {
23
+ readonly status: 'create';
24
+ readonly assetId: string;
25
+ readonly frozenContentId: string;
26
+ readonly provenanceAction: 'stage_and_finalize';
27
+ } | {
28
+ readonly status: 'already_bound';
29
+ readonly assetId: string;
30
+ readonly frozenContentId: string;
31
+ readonly provenanceAction: 'finalize' | 'none';
32
+ } | {
33
+ readonly status: 'preserve_trust';
34
+ readonly assetId: string;
35
+ readonly frozenContentId: string;
36
+ readonly provenanceAction: 'none';
37
+ } | {
38
+ readonly status: 'collision';
39
+ readonly assetId: string;
40
+ readonly frozenContentId: string;
41
+ readonly collision: 'asset' | 'provenance';
42
+ readonly provenanceAction: 'none';
43
+ };
44
+ /**
45
+ * Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
46
+ * target snapshot during migration planning, then apply through ingestUnverified without duplicating its
47
+ * trust and collision rules.
48
+ */
49
+ export declare function planUnverifiedIngest(record: AssetRecord, existing: AssetRecord | null, currentProvenance: ProvenanceRecord | null, reason: string, source?: ProvenanceSource): UnverifiedIngestDisposition;
50
+ /**
51
+ * A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
52
+ * waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
53
+ * reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
54
+ */
55
+ export declare function isActiveUnverifiedProvenance(record: ProvenanceRecord | null | undefined, frozenContentId: string | null | undefined): boolean;
56
+ export declare class ProvenanceWritePendingError extends Error {
57
+ readonly assetId: string;
58
+ readonly code = "PROVENANCE_WRITE_PENDING";
59
+ constructor(assetId: string);
60
+ }
19
61
  /**
20
62
  * Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
21
- * record = trusted: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
22
- * write here; the sole untrusted source hub ingestion ALWAYS marks via {@link ingestUntrusted}/mark.
63
+ * record = trusted: verified local writers (cycleEngine self-produce and self-consistent v1 migration rows)
64
+ * do not write here. Every untrusted or hash-mismatched ingest path MUST mark before persistence.
23
65
  */
24
66
  export declare class ProvenanceStore {
25
67
  private readonly now;
@@ -36,6 +78,21 @@ export declare class ProvenanceStore {
36
78
  mark(rec: Omit<ProvenanceRecord, 'at'> & {
37
79
  at?: string;
38
80
  }): ProvenanceRecord;
81
+ /** Stage a verified Hub write without replacing an in-flight conservative marker. */
82
+ stageUntrustedWriteTracked(assetId: string, source: ProvenanceSource): {
83
+ record: ProvenanceRecord;
84
+ appended: boolean;
85
+ };
86
+ /** Finalize a verified write unless an operator made an explicit decision during I/O. */
87
+ finalizeUntrustedWrite(assetId: string, source: ProvenanceSource): ProvenanceRecord;
88
+ /** Stage an unverified write without overwriting an explicit trust decision or another conservative marker. */
89
+ stageUnverifiedWrite(assetId: string, source: ProvenanceSource, frozenContentId: string): ProvenanceRecord;
90
+ stageUnverifiedWriteTracked(assetId: string, source: ProvenanceSource, frozenContentId: string): {
91
+ record: ProvenanceRecord;
92
+ appended: boolean;
93
+ };
94
+ /** Atomically replace only a pending/no decision with the health-waiver reason after verified persistence. */
95
+ finalizeUnverifiedWrite(assetId: string, source: ProvenanceSource, reason: string, frozenContentId: string): ProvenanceRecord;
39
96
  rollbackLast(rec: ProvenanceRecord): void;
40
97
  get(assetId: string): ProvenanceRecord | null;
41
98
  /** No record → trusted (local default); a record → its trusted flag. */
@@ -54,4 +111,23 @@ export declare class ProvenanceStore {
54
111
  * remote-supplied asset_id is never trusted) and mark it untrusted in the sidecar. This is the ONLY path that
55
112
  * should bring hub-fetched assets into the local pool — trust-first from the first byte (#30.1).
56
113
  */
57
- export declare function ingestUntrusted(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, source?: ProvenanceSource): Promise<PutResult>;
114
+ export declare function ingestUntrusted(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, source?: ProvenanceSource): Promise<PutResult>;
115
+ /**
116
+ * Hub → local-pool landing for an asset whose content does NOT hash to its declared asset_id. The hub
117
+ * demonstrably rewrites delivered payloads (injected `validation`, wholesale `payload_backfill_reason`
118
+ * synthesis — evolver-v2#570), which breaks {@link ingestUntrusted}'s normalizeForPut self-consistency check
119
+ * even though the loop's own in-run reuse already consumes hub content without re-verifying it (adapter
120
+ * `hubReuse.ts`). Rather than hard-reject a save the operator explicitly asked for, freeze the asset under its
121
+ * declared (network) asset_id via `putFrozen` and mark it untrusted in the sidecar with an explicit reason.
122
+ * Trust-first still holds (#30.1): selection defaults to trusted-only, so an unverified asset never silently
123
+ * enters the reasoning pool — it lands where the operator put it and stays flagged until an explicit,
124
+ * audited promotion. `reason` records WHY verification was waived (e.g. hub rewrite vs synthesized payload).
125
+ */
126
+ export declare function ingestUnverified(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, source?: ProvenanceSource): Promise<PutResult>;
127
+ /** Atomic frozen variant used by reuse so the logical-id check and append share one provider lock. */
128
+ export declare function ingestUnverifiedConditional(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, reason: string, options?: ConditionalPutOptions, source?: ProvenanceSource): Promise<ConditionalPutResult>;
129
+ /**
130
+ * Conditional variant used by Hub sync to reject a logical-id collision without ever allowing a Hub record
131
+ * to become implicitly trusted. Providers that cannot make the condition atomically are rejected here.
132
+ */
133
+ export declare function ingestUntrustedConditional(store: AssetStoreProvider, prov: ProvenanceStore, record: AssetRecord, options?: ConditionalPutOptions, source?: ProvenanceSource): Promise<ConditionalPutResult>;
@@ -4,16 +4,118 @@
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
6
  import { join, dirname } from 'node:path';
7
- import { normalizeForPut } from './provider.js';
7
+ import { assertCapsuleGeneBinding, FrozenAssetIdCollisionError, InvalidFrozenPutResultError, frozenAssetRecordsEqual, normalizeForPut, supportsAtomicConditionalPut, supportsAtomicFrozenConditionalPut, validateConditionalPutResult, validateFrozenPutResult, } from './provider.js';
8
8
  import { appendUtf8Durable, assertAssetStoreDirectory, ensureAssetStoreDirectory, readUtf8Regular, regularFileFingerprint, truncateUtf8SuffixDurable, withAssetStoreLock, } from './assetStoreStorage.js';
9
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';
30
+ }
31
+ function unverifiedFinalizeAction(current, source, reason, frozenContentId) {
32
+ if (!current)
33
+ return 'append';
34
+ if (current.trusted === true
35
+ || current.decision !== undefined
36
+ || current.decidedBy !== undefined
37
+ || current.promotedBy !== undefined)
38
+ return 'preserve';
39
+ const sameWaiver = current.source === source && current.trusted === false && current.reason === reason;
40
+ if (sameWaiver && current.frozenContentId === frozenContentId)
41
+ return 'already_bound';
42
+ if (!sameWaiver && (current.reason !== UNVERIFIED_WRITE_PENDING_REASON
43
+ || (current.frozenContentId !== undefined && current.frozenContentId !== frozenContentId)))
44
+ return 'preserve';
45
+ return 'append';
46
+ }
47
+ /**
48
+ * Resolve the exact no-I/O disposition used by {@link ingestUnverified}. Callers may inspect a read-only
49
+ * target snapshot during migration planning, then apply through ingestUnverified without duplicating its
50
+ * trust and collision rules.
51
+ */
52
+ export function planUnverifiedIngest(record, existing, currentProvenance, reason, source = 'hub') {
53
+ assertCapsuleGeneBinding(record);
54
+ const frozenContentId = computeAssetId(record);
55
+ if (!frozenContentId)
56
+ throw new Error('failed to compute frozen content id');
57
+ const base = { assetId: record.asset_id, frozenContentId };
58
+ if (currentProvenance && currentProvenance.assetId !== record.asset_id) {
59
+ return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
60
+ }
61
+ if (existing) {
62
+ if (!frozenAssetRecordsEqual(existing, record)) {
63
+ return { ...base, status: 'collision', collision: 'asset', provenanceAction: 'none' };
64
+ }
65
+ const finalize = unverifiedFinalizeAction(currentProvenance, source, reason, frozenContentId);
66
+ if (finalize === 'preserve') {
67
+ return { ...base, status: 'preserve_trust', provenanceAction: 'none' };
68
+ }
69
+ return {
70
+ ...base,
71
+ status: 'already_bound',
72
+ provenanceAction: finalize === 'append' ? 'finalize' : 'none',
73
+ };
74
+ }
75
+ if (unverifiedStageAction(currentProvenance, source, frozenContentId) === 'blocked') {
76
+ return { ...base, status: 'collision', collision: 'provenance', provenanceAction: 'none' };
77
+ }
78
+ return { ...base, status: 'create', provenanceAction: 'stage_and_finalize' };
79
+ }
80
+ /**
81
+ * A frozen hash mismatch is benign only while its latest provenance record is an undecided, content-bound
82
+ * waiver created by a supported ingest path. Keep the storage/health classification centralized; downstream
83
+ * reuse and sync apply their own stricter allowlists before an unverified asset may leave quarantine.
84
+ */
85
+ export function isActiveUnverifiedProvenance(record, frozenContentId) {
86
+ if (!record
87
+ || !frozenContentId
88
+ || record.trusted !== false
89
+ || record.decision !== undefined
90
+ || record.decidedBy !== undefined
91
+ || record.promotedBy !== undefined
92
+ || record.frozenContentId !== frozenContentId)
93
+ return false;
94
+ // V1 input is local legacy data rather than a network-issued identifier. Never let a malformed declared
95
+ // identity become healthy merely because the shallow wire gate does not enforce the schema's asset_id regex.
96
+ if (record.reason === UNVERIFIED_V1_IMPORT_REASON
97
+ && !/^sha256:[0-9a-f]{64}$/.test(record.assetId))
98
+ return false;
99
+ return (record.source === 'hub'
100
+ && (record.reason === 'unverified_hub_rewrite' || record.reason === 'unverified_hub_synthesized')) || (record.source === 'migrated'
101
+ && (record.reason === 'unverified_gepx_import' || record.reason === UNVERIFIED_V1_IMPORT_REASON));
102
+ }
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';
110
+ }
111
+ }
10
112
  function immutableRecord(record) {
11
113
  return Object.freeze({ ...record });
12
114
  }
13
115
  /**
14
116
  * Append-only JSONL sidecar (last-write-wins) at <baseDir>/provenance.jsonl. Default for an asset with NO
15
- * record = trusted: the only local writers (cycleEngine self-produce, v1 migration) are trusted and never
16
- * 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.
17
119
  */
18
120
  export class ProvenanceStore {
19
121
  now;
@@ -69,6 +171,89 @@ export class ProvenanceStore {
69
171
  return this.appendUnderLock(full);
70
172
  });
71
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
+ });
256
+ }
72
257
  rollbackLast(rec) {
73
258
  const line = `${JSON.stringify(rec)}\n`;
74
259
  try {
@@ -103,14 +288,22 @@ export class ProvenanceStore {
103
288
  return withAssetStoreLock(this.lockPath, () => {
104
289
  this.refreshUnderLock();
105
290
  const current = this.index.get(assetId) ?? null;
106
- if (current?.trusted === trusted)
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) {
107
299
  return { changed: false, record: current };
300
+ }
108
301
  const full = {
109
302
  assetId,
110
303
  source: current?.source ?? 'local',
111
304
  trusted,
112
305
  at: new Date(this.now()).toISOString(),
113
- decision: trusted ? 'promoted' : 'revoked',
306
+ decision,
114
307
  decidedBy: by,
115
308
  ...(trusted ? { promotedBy: by } : {}),
116
309
  reason,
@@ -142,4 +335,119 @@ export async function ingestUntrusted(store, prov, record, source = 'hub') {
142
335
  if (!result.stored)
143
336
  prov.rollbackLast(mark);
144
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;
145
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,12 +55,32 @@ export interface SearchQuery {
24
55
  */
25
56
  export interface AssetStoreProvider {
26
57
  put(asset: AssetRecord): Promise<PutResult>;
58
+ /** Optional atomic capability; use supportsAtomicConditionalPut() before calling through this interface. */
59
+ putConditional?(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
27
60
  get(assetId: string): Promise<AssetRecord | null>;
28
61
  /** Optional direct lookup for non-content-addressed logical ids. Callers must handle 0, 1, or multiple matches. */
29
- findByLogicalId?(id: string, limit?: number): Promise<AssetRecord[]>;
62
+ findByLogicalId?(id: string, limit?: number, kind?: AssetKind): Promise<AssetRecord[]>;
63
+ /**
64
+ * Optional frozen write: store the record under its OWN declared asset_id without recomputing or
65
+ * normalizing it (bypasses {@link normalizeForPut}'s self-consistency check). Only providers backing a
66
+ * content-addressed local pool implement it. Callers that must preserve a hash-inconsistent asset —
67
+ * v1 migration import, and unverified hub reuse of a hub-rewritten payload (see `ingestUnverified`) —
68
+ * feature-detect it rather than assuming it exists.
69
+ */
70
+ putFrozen?(record: AssetRecord): Promise<PutResult>;
71
+ /** Atomic frozen variant that also enforces the type + logical-id condition under the provider write lock. */
72
+ putFrozenConditional?(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
30
73
  search(query: SearchQuery): Promise<AssetRecord[]>;
31
74
  list(kind?: AssetKind, limit?: number): Promise<AssetRecord[]>;
32
75
  }
76
+ export type AtomicConditionalPutProvider = AssetStoreProvider & {
77
+ putConditional(asset: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
78
+ };
79
+ export declare function supportsAtomicConditionalPut(provider: AssetStoreProvider): provider is AtomicConditionalPutProvider;
80
+ export type AtomicFrozenConditionalPutProvider = AssetStoreProvider & {
81
+ putFrozenConditional(record: AssetRecord, options?: ConditionalPutOptions): Promise<ConditionalPutResult>;
82
+ };
83
+ export declare function supportsAtomicFrozenConditionalPut(provider: AssetStoreProvider): provider is AtomicFrozenConditionalPutProvider;
33
84
  export declare class AssetIdMismatchError extends Error {
34
85
  readonly claimed: string;
35
86
  readonly actual: string;
@@ -38,6 +89,12 @@ export declare class AssetIdMismatchError extends Error {
38
89
  export declare class CapsuleGeneBindingError extends Error {
39
90
  constructor();
40
91
  }
92
+ /**
93
+ * M3-4 强绑定校验: Capsule.gene 必须非空(否则一条无来源基因的经验会污染选择池).
94
+ * 抽成独立 helper 以便 normalizeForPut(校验落库路径)与 ingestUnverified(冻结落库路径,
95
+ * 绕过 normalizeForPut)共用同一条不变量,不让降级路径把绑定校验漏掉.
96
+ */
97
+ export declare function assertCapsuleGeneBinding(asset: AssetRecord): void;
41
98
  /**
42
99
  * 落库前规范化(共享给各 provider): 计算/校验 asset_id + 强绑定校验.
43
100
  * - 缺 asset_id → 计算填入(verified=false 表示非入参自带).