@evomap/evolver-core 2.0.0-beta.19 → 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 (160) 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 +143 -20
  6. package/dist/algo/capabilityCandidates.js +10 -0
  7. package/dist/algo/cycleEngine.d.ts +40 -5
  8. package/dist/algo/cycleEngine.js +277 -37
  9. package/dist/algo/evolutionEvent.d.ts +26 -0
  10. package/dist/algo/evolutionEvent.js +32 -0
  11. package/dist/algo/exploration.d.ts +7 -0
  12. package/dist/algo/exploration.js +16 -3
  13. package/dist/algo/geneHealth.d.ts +34 -1
  14. package/dist/algo/geneHealth.js +42 -0
  15. package/dist/algo/geneIntake.d.ts +46 -1
  16. package/dist/algo/geneIntake.js +121 -10
  17. package/dist/algo/geneSelection.d.ts +115 -6
  18. package/dist/algo/geneSelection.js +459 -32
  19. package/dist/algo/index.d.ts +4 -1
  20. package/dist/algo/index.js +4 -1
  21. package/dist/algo/kautoProjection.d.ts +41 -0
  22. package/dist/algo/kautoProjection.js +95 -0
  23. package/dist/algo/kautoValidator.d.ts +68 -0
  24. package/dist/algo/kautoValidator.js +256 -0
  25. package/dist/algo/orchestrator.d.ts +14 -1
  26. package/dist/algo/orchestrator.js +9 -2
  27. package/dist/algo/solidify.d.ts +11 -2
  28. package/dist/algo/solidify.js +37 -7
  29. package/dist/algo/ucb1.d.ts +53 -0
  30. package/dist/algo/ucb1.js +156 -0
  31. package/dist/assetrepair/hubRejection.d.ts +12 -0
  32. package/dist/assetrepair/hubRejection.js +109 -0
  33. package/dist/assetrepair/index.d.ts +2 -0
  34. package/dist/assetrepair/index.js +2 -0
  35. package/dist/assetrepair/repair.d.ts +33 -0
  36. package/dist/assetrepair/repair.js +155 -0
  37. package/dist/assetstore/assetSidecarRecords.js +5 -1
  38. package/dist/assetstore/assetStoreHealth.d.ts +2 -3
  39. package/dist/assetstore/assetStoreHealth.js +4 -10
  40. package/dist/assetstore/foreignJsonlSource.d.ts +48 -0
  41. package/dist/assetstore/foreignJsonlSource.js +150 -0
  42. package/dist/assetstore/index.d.ts +3 -0
  43. package/dist/assetstore/index.js +3 -0
  44. package/dist/assetstore/learningHistory.js +3 -3
  45. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  46. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  47. package/dist/assetstore/localJsonl.d.ts +8 -0
  48. package/dist/assetstore/localJsonl.js +128 -3
  49. package/dist/assetstore/provenance.d.ts +38 -3
  50. package/dist/assetstore/provenance.js +103 -45
  51. package/dist/assetstore/provider.d.ts +4 -0
  52. package/dist/assetstore/reviewFilter.d.ts +19 -1
  53. package/dist/assetstore/reviewFilter.js +36 -0
  54. package/dist/assetstore/unionReadStore.d.ts +25 -0
  55. package/dist/assetstore/unionReadStore.js +119 -0
  56. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  57. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  58. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  59. package/dist/benchmark/antiGeneRollout.js +4 -3
  60. package/dist/benchmark/index.d.ts +2 -1
  61. package/dist/benchmark/index.js +2 -1
  62. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  63. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  64. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  65. package/dist/bootstrap/envFingerprint.js +5 -0
  66. package/dist/bootstrap/index.d.ts +2 -1
  67. package/dist/bootstrap/index.js +2 -1
  68. package/dist/bootstrap/lifecycleBootstrap.d.ts +111 -0
  69. package/dist/bootstrap/lifecycleBootstrap.js +433 -0
  70. package/dist/bootstrap/v1EnvCompat.d.ts +3 -0
  71. package/dist/bootstrap/v1EnvCompat.js +48 -1
  72. package/dist/events/eventArchive.d.ts +2 -0
  73. package/dist/events/eventArchive.js +13 -3
  74. package/dist/events/eventSchema.d.ts +7 -7
  75. package/dist/events/eventStore.d.ts +2 -0
  76. package/dist/events/eventStore.js +5 -1
  77. package/dist/events/ingest.d.ts +2 -1
  78. package/dist/events/ingest.js +6 -0
  79. package/dist/exec/autoExec.d.ts +30 -3
  80. package/dist/exec/autoExec.js +294 -29
  81. package/dist/exec/autonomousCycle.d.ts +7 -0
  82. package/dist/exec/autonomousCycle.js +2 -0
  83. package/dist/exec/claudeBridge.d.ts +28 -6
  84. package/dist/exec/claudeBridge.js +411 -258
  85. package/dist/exec/executionBinding.d.ts +414 -0
  86. package/dist/exec/executionBinding.js +588 -0
  87. package/dist/exec/index.d.ts +1 -0
  88. package/dist/exec/index.js +1 -0
  89. package/dist/exec/proofOfWork.d.ts +1 -1
  90. package/dist/exec/proofOfWork.js +2 -2
  91. package/dist/exec/runnerRegistry.d.ts +5 -0
  92. package/dist/exec/runnerRegistry.js +54 -14
  93. package/dist/hub/capability.d.ts +40 -1
  94. package/dist/hub/conversationDistiller.d.ts +19 -0
  95. package/dist/hub/conversationDistiller.js +115 -37
  96. package/dist/hub/fake.d.ts +1 -0
  97. package/dist/hub/fake.js +1 -0
  98. package/dist/hub/index.d.ts +1 -0
  99. package/dist/hub/index.js +1 -0
  100. package/dist/hub/recipeCompose.d.ts +27 -0
  101. package/dist/hub/recipeCompose.js +90 -0
  102. package/dist/hub/sanitize.js +119 -5
  103. package/dist/index.d.ts +2 -0
  104. package/dist/index.js +2 -0
  105. package/dist/mailbox/ipcServer.d.ts +2 -0
  106. package/dist/mailbox/ipcServer.js +20 -0
  107. package/dist/mailbox/store.d.ts +52 -2
  108. package/dist/mailbox/store.js +546 -81
  109. package/dist/modelCompatibility.d.ts +164 -0
  110. package/dist/modelCompatibility.js +309 -0
  111. package/dist/observers/valueDigestObserver.d.ts +9 -0
  112. package/dist/observers/valueDigestObserver.js +35 -2
  113. package/dist/ops/cleanup.js +1 -1
  114. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  115. package/dist/ops/evolutionGraphProjection.js +315 -0
  116. package/dist/ops/index.d.ts +2 -1
  117. package/dist/ops/index.js +2 -1
  118. package/dist/ops/selfUpdate.d.ts +8 -0
  119. package/dist/ops/selfUpdate.js +24 -8
  120. package/dist/ops/valueOutreach.d.ts +3 -1
  121. package/dist/ops/valueOutreach.js +5 -1
  122. package/dist/personality/schema.d.ts +18 -18
  123. package/dist/schema/evolutionGraph.d.ts +784 -0
  124. package/dist/schema/evolutionGraph.js +187 -0
  125. package/dist/schema/index.d.ts +1 -0
  126. package/dist/schema/index.js +1 -0
  127. package/dist/schema/proofOfWork.d.ts +125 -6
  128. package/dist/schema/proofOfWork.js +102 -4
  129. package/dist/shadow/shadowHub.js +1 -0
  130. package/dist/signals/expand.d.ts +15 -1
  131. package/dist/signals/expand.js +148 -0
  132. package/dist/signals/index.d.ts +3 -1
  133. package/dist/signals/index.js +3 -1
  134. package/dist/signals/scopeVocabulary.d.ts +75 -0
  135. package/dist/signals/scopeVocabulary.js +91 -0
  136. package/dist/signals/taskDomain.d.ts +22 -0
  137. package/dist/signals/taskDomain.js +43 -0
  138. package/dist/trace/learningTrace.d.ts +7 -0
  139. package/dist/trace/learningTrace.js +22 -0
  140. package/dist/trace/trajectory.d.ts +8 -0
  141. package/dist/trace/trajectory.js +14 -2
  142. package/dist/util/fileLock.d.ts +92 -5
  143. package/dist/util/fileLock.js +809 -70
  144. package/dist/util/index.d.ts +2 -1
  145. package/dist/util/index.js +1 -1
  146. package/dist/verify/index.d.ts +2 -1
  147. package/dist/verify/index.js +1 -1
  148. package/dist/verify/sandboxRunner.d.ts +2 -0
  149. package/dist/verify/sandboxRunner.js +154 -10
  150. package/dist/verify/sandboxedValidation.d.ts +7 -2
  151. package/dist/verify/sandboxedValidation.js +102 -24
  152. package/dist/verify/validation.d.ts +24 -3
  153. package/dist/verify/validation.js +141 -24
  154. package/dist/wire/geneHints.d.ts +88 -16
  155. package/dist/wire/geneHints.js +124 -15
  156. package/dist/wire/index.d.ts +8 -3
  157. package/dist/wire/index.js +2 -2
  158. package/dist/wire/schemaGate.d.ts +21 -0
  159. package/dist/wire/schemaGate.js +119 -10
  160. package/package.json +5 -4
@@ -1,4 +1,5 @@
1
- import { canTransition } from '../cycle/stateMachine.js';
1
+ import { LineTooLargeError } from '../events/eventStore.js';
2
+ import { TERMINAL, canTransition, stageForEventType } from '../cycle/stateMachine.js';
2
3
  import {} from './geneSelection.js';
3
4
  import { detectPlateau } from './exploration.js';
4
5
  import { epigeneticPenaltyForIds } from './epigenetics.js';
@@ -11,11 +12,126 @@ import { applySelectForRun, applyStatsUpdate, applyForcePivot } from '../persona
11
12
  import { solidify } from './solidify.js';
12
13
  import { buildEvolutionEvent } from './evolutionEvent.js';
13
14
  import { cycleRecordsFromEvents } from '../signals/cycleHistoryFromEvents.js';
15
+ import { withoutTaskDomainSignals } from '../signals/taskDomain.js';
14
16
  import { computeMetaSignals, deriveCycleHistory } from '../signals/metaSignals.js';
15
17
  import { capabilityGapsFromSignals, curriculumOutcomesFromEvents, generateCurriculumSignals, normalizeCapabilityGaps, } from '../signals/curriculum.js';
16
18
  import { resolveStrategy } from './strategyPresets.js';
17
19
  import { classifyCycleFailure, } from './cycleFailureClassifier.js';
18
20
  import { isDistilledGeneId } from './geneIntake.js';
21
+ import { deriveUcb1History } from './ucb1.js';
22
+ function persistedTerminalAfterLatestSelection(ingestor, cycleId, attemptStartedSeq) {
23
+ const events = ingestor.readAll();
24
+ let selectedSeq = -1;
25
+ for (let index = events.length - 1; index >= 0; index -= 1) {
26
+ const event = events[index];
27
+ if (event.seq <= attemptStartedSeq)
28
+ break;
29
+ if (event.type === 'decision.gene_selected' && event.payload['cycleId'] === cycleId) {
30
+ selectedSeq = event.seq;
31
+ break;
32
+ }
33
+ }
34
+ if (selectedSeq < 0)
35
+ return null;
36
+ for (let index = events.length - 1; index >= 0; index -= 1) {
37
+ const event = events[index];
38
+ if (event.seq <= selectedSeq)
39
+ break;
40
+ if (event.payload['cycleId'] !== cycleId)
41
+ continue;
42
+ const terminal = stageForEventType(event.type);
43
+ if (terminal && TERMINAL.has(terminal))
44
+ return terminal;
45
+ }
46
+ return null;
47
+ }
48
+ function selectionPolicyEventProjection(trace) {
49
+ return {
50
+ requested: trace.requested,
51
+ effective: trace.effective,
52
+ version: trace.selectionPolicyVersion,
53
+ rewardVersion: trace.rewardPolicyVersion,
54
+ ...(trace.arm ? {
55
+ arm: {
56
+ id: trace.arm.armId,
57
+ pulls: trace.arm.pulls,
58
+ completed: trace.arm.completedPulls,
59
+ total: trace.arm.totalPulls,
60
+ mean: trace.arm.meanReward,
61
+ index: trace.arm.index,
62
+ },
63
+ } : {}),
64
+ ...(trace.shadowDisagrees !== undefined ? { disagrees: trace.shadowDisagrees } : {}),
65
+ ...(trace.fallbackReason ? { fallback: trace.fallbackReason } : {}),
66
+ };
67
+ }
68
+ function minimalSelectionPolicyEventProjection(trace) {
69
+ return {
70
+ requested: trace.requested,
71
+ effective: trace.effective,
72
+ version: trace.selectionPolicyVersion,
73
+ rewardVersion: trace.rewardPolicyVersion,
74
+ };
75
+ }
76
+ function selectionGuardEventProjection(trace) {
77
+ return {
78
+ mode: trace.mode,
79
+ version: trace.version,
80
+ status: trace.status,
81
+ ...(trace.reason ? { reason: trace.reason } : {}),
82
+ ...(trace.maxMatch !== undefined ? { maxMatch: trace.maxMatch } : {}),
83
+ ...(trace.matchSpread !== undefined ? { spread: trace.matchSpread } : {}),
84
+ };
85
+ }
86
+ function minimalSelectionGuardEventProjection(trace) {
87
+ return {
88
+ mode: trace.mode,
89
+ version: trace.version,
90
+ status: trace.status,
91
+ ...(trace.reason ? { reason: trace.reason } : {}),
92
+ };
93
+ }
94
+ function selectionCandidateEventProjection(candidate) {
95
+ // matchScore and scoring diagnostics stay off decision.gene_selected payloads so
96
+ // UCB1/plateau telemetry retains the 4096B root-event line budget.
97
+ const { matchScore: _matchScore, scoreBase: _scoreBase, kautoContribution: _kautoContribution, ...persisted } = candidate;
98
+ return persisted;
99
+ }
100
+ async function ingestGeneSelectedWithTraceBudget(ingestor, event, policyTrace, guardTrace) {
101
+ const guard = guardTrace ? selectionGuardEventProjection(guardTrace) : undefined;
102
+ const minimalGuard = guardTrace ? minimalSelectionGuardEventProjection(guardTrace) : undefined;
103
+ const projections = [];
104
+ if (policyTrace) {
105
+ projections.push({ selectionPolicy: selectionPolicyEventProjection(policyTrace), ...(guard ? { selectionGuard: guard } : {}) });
106
+ projections.push({ selectionPolicy: minimalSelectionPolicyEventProjection(policyTrace), ...(guard ? { selectionGuard: guard } : {}) });
107
+ if (minimalGuard) {
108
+ projections.push({ selectionPolicy: minimalSelectionPolicyEventProjection(policyTrace), selectionGuard: minimalGuard });
109
+ projections.push({ selectionGuard: minimalGuard });
110
+ }
111
+ projections.push({ selectionPolicy: minimalSelectionPolicyEventProjection(policyTrace) });
112
+ }
113
+ else if (guard) {
114
+ projections.push({ selectionGuard: guard });
115
+ if (minimalGuard)
116
+ projections.push({ selectionGuard: minimalGuard });
117
+ }
118
+ projections.push({});
119
+ for (let index = 0; index < projections.length; index += 1) {
120
+ const projection = projections[index];
121
+ try {
122
+ await ingestor.ingest({
123
+ type: event.type,
124
+ human: event.human,
125
+ payload: { ...event.payload, ...projection },
126
+ });
127
+ return;
128
+ }
129
+ catch (error) {
130
+ if (!(error instanceof LineTooLargeError) || index === projections.length - 1)
131
+ throw error;
132
+ }
133
+ }
134
+ }
19
135
  /** Read the trailing run of cycle outcomes (newest last) from the event log, for plateau detection. */
20
136
  function recentCycleOutcomes(ingestor, window) {
21
137
  let events;
@@ -107,7 +223,7 @@ function categoryForStrategy(category, strategy) {
107
223
  * Reading from the same append-only log makes this fully replayable. ad-hoc (innovate) cycles have no gene to
108
224
  * credit and are skipped.
109
225
  */
110
- function recentConfidenceObservations(ingestor, window) {
226
+ function recentConfidenceObservations(ingestor, window, opts = {}) {
111
227
  let events;
112
228
  try {
113
229
  events = ingestor.tail(window);
@@ -147,8 +263,9 @@ function recentConfidenceObservations(ingestor, window) {
147
263
  // Older events predate the producedValue tag; when it is absent we treat the cycle as a real success, so we
148
264
  // never retroactively reclassify history into a surprise ban.
149
265
  const status = e.type === 'cycle.failed' ? 'failed' : p?.producedValue === false ? 'inert' : 'success';
266
+ const fingerprintSignals = opts.ignoreTaskDomain ? withoutTaskDomainSignals(signals) : signals;
150
267
  out.push({
151
- signalFingerprint: signalFingerprint(signals),
268
+ signalFingerprint: signalFingerprint(fingerprintSignals),
152
269
  geneId: gene,
153
270
  status,
154
271
  at: Number.isFinite(at) ? at : 0,
@@ -234,6 +351,18 @@ export class CycleEngine {
234
351
  const executionMetadata = (exec) => ({
235
352
  ...(exec.failureKind !== undefined ? { failureKind: exec.failureKind } : {}),
236
353
  ...(exec.exitCode !== undefined ? { exitCode: exec.exitCode } : {}),
354
+ ...(exec.bindingCorrelation ? { bindingDigest: exec.bindingCorrelation.bindingDigest, runId: exec.bindingCorrelation.runId } : {}),
355
+ ...(exec.executionTerminal ? { terminalDisposition: exec.executionTerminal.disposition } : {}),
356
+ ...(exec.hubLifecycle ? { hubLifecycle: exec.hubLifecycle } : {}),
357
+ ...(exec.provenance ? {
358
+ provenance: {
359
+ geneIds: exec.provenance.gene_ids,
360
+ capsuleIds: exec.provenance.capsule_ids,
361
+ resultAssetRefs: exec.provenance.result_asset_refs,
362
+ proofRefs: exec.provenance.proof_refs,
363
+ terminalDisposition: exec.provenance.terminal_disposition,
364
+ },
365
+ } : {}),
237
366
  });
238
367
  const strategy = resolveStrategy({ name: input.strategyName, signals: cycleSignals, cycleCount: historyCycleCount(ingestor, 1000, input.cycleId) });
239
368
  const cycleCategory = categoryForStrategy(input.category, strategy);
@@ -243,7 +372,7 @@ export class CycleEngine {
243
372
  throw new Error(`非法 cycle 迁移 ${stage}→${to}`);
244
373
  stage = to;
245
374
  };
246
- await ingestor.ingest({ type: 'cycle.started', human: { title: `cycle ${input.cycleId} 启动` }, payload: { cycleId: input.cycleId, problemId: input.problem.id } });
375
+ const cycleStarted = await ingestor.ingest({ type: 'cycle.started', human: { title: `cycle ${input.cycleId} 启动` }, payload: { cycleId: input.cycleId, problemId: input.problem.id } });
247
376
  advance('started');
248
377
  await ingestor.ingest({
249
378
  type: 'cycle.signals_collected',
@@ -295,8 +424,24 @@ export class CycleEngine {
295
424
  return;
296
425
  await applyStatsUpdate({ store: this.deps.personality, ingestor, cycleId: input.cycleId }, { personality: personalityForRun, outcome, score });
297
426
  };
427
+ let ucb1History;
428
+ if (plateau.active && input.selectionPolicy && input.selectionPolicy !== 'engine-health') {
429
+ try {
430
+ ucb1History = deriveUcb1History(ingestor.readAllStrict());
431
+ }
432
+ catch {
433
+ // Active UCB1 fails safe to legacy drift; shadow records missing_history in its compact policy trace.
434
+ ucb1History = undefined;
435
+ }
436
+ }
298
437
  const exploration = plateau.active
299
- ? { plateau, driftEnabled: true, totalAttempts: input.candidates.reduce((s, c) => s + (c.view?.total ?? 0), 0) }
438
+ ? {
439
+ plateau,
440
+ driftEnabled: true,
441
+ totalAttempts: input.candidates.reduce((s, c) => s + (c.view?.total ?? 0), 0),
442
+ ...(input.selectionPolicy ? { policy: input.selectionPolicy } : {}),
443
+ ...(ucb1History ? { ucb1History } : {}),
444
+ }
300
445
  : undefined;
301
446
  // Epigenetic: penalize candidates that fail in THIS environment class (derived from per-env outcome history).
302
447
  const geneOutcomes = recentGeneOutcomes(ingestor, 200);
@@ -304,7 +449,7 @@ export class CycleEngine {
304
449
  // signal fingerprint. Derived from the same event log, applied as a soft fourth factor in selection.
305
450
  const confidenceObs = recentConfidenceObservations(ingestor, 200);
306
451
  const confidenceEdges = deriveConfidenceEdges(confidenceObs);
307
- const currentFingerprint = signalFingerprint(baseSignals);
452
+ const currentConfidenceFingerprint = signalFingerprint(baseSignals);
308
453
  // #195: drop genes stuck producing only inert (zero-work) cycles on THIS signal. A sole-matching do-nothing
309
454
  // gene is otherwise re-selected every cycle (drift can't diversify a single candidate) and the failure-streak
310
455
  // ban never fires because nothing "fails" — so it dominates --loop forever while producing no artifacts.
@@ -314,46 +459,85 @@ export class CycleEngine {
314
459
  // cycles evict its inert run (or the earlier real success that exempts it) and the ban would flap. Recomputing
315
460
  // it over INERT_BAN_OBSERVATION_WINDOW events (mirroring v1's memory-graph read horizon) keeps the trailing run
316
461
  // and the successCount exemption alive across intervening cycles. tail() reads the full log either way (no I/O cost).
317
- const inertBanObs = recentConfidenceObservations(ingestor, INERT_BAN_OBSERVATION_WINDOW);
318
- const inertBanned = inertBannedGeneIds(deriveConfidenceEdges(inertBanObs), inertBanObs, currentFingerprint);
462
+ const inertBanObs = recentConfidenceObservations(ingestor, INERT_BAN_OBSERVATION_WINDOW, { ignoreTaskDomain: true });
463
+ const inertBanFingerprint = signalFingerprint(withoutTaskDomainSignals(baseSignals));
464
+ const inertBanned = inertBannedGeneIds(deriveConfidenceEdges(inertBanObs), inertBanObs, inertBanFingerprint);
319
465
  const candidates = input.candidates
320
466
  .filter((c) => !isCandidateInSet(c, inertBanned))
321
467
  .map((c) => {
322
468
  const epi = epigeneticPenaltyForIds(candidateIds(c), envKey, geneOutcomes);
323
- const conf = confidenceFor(confidenceEdges, currentFingerprint, c.geneId, now);
469
+ const conf = confidenceFor(confidenceEdges, currentConfidenceFingerprint, c.geneId, now);
324
470
  const withEpi = epi > 0 ? { ...c, epigeneticPenalty: epi } : c;
325
471
  return conf > 0 ? { ...withEpi, confidence: conf } : withEpi;
326
472
  });
327
473
  // #97 distilled-gene fallback pool: attach the env-scoped epigenetic penalty (so selection can hard-skip a
328
474
  // suppressed fallback, v1 parity) and drop any inert-banned id (composes with #195). Selection uses these only
329
- // when no normal candidate clears the floor.
475
+ // when normal selection has no reusable positive choice.
330
476
  const distilledFallback = (input.distilledFallback ?? [])
331
477
  .filter((c) => !isCandidateInSet(c, inertBanned))
332
478
  .map((c) => {
333
479
  const epi = epigeneticPenaltyForIds(candidateIds(c), envKey, geneOutcomes);
334
480
  return epi > 0 ? { ...c, epigeneticPenalty: epi } : c;
335
481
  });
336
- const decision = (await this.deps.selection.run({ signals: selectionSignals, candidates, floor: input.selectionFloor, ...(input.forcedGeneId !== undefined ? { forcedGeneId: input.forcedGeneId } : {}), ...(exploration ? { exploration } : {}), ...(distilledFallback.length > 0 ? { distilledFallback } : {}), ...(input.antiWarnings && input.antiWarnings.length > 0 ? { antiWarnings: input.antiWarnings } : {}), ...(input.memoryEvidence && input.memoryEvidence.length > 0 ? { memoryEvidence: input.memoryEvidence } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
337
- await ingestor.ingest({
482
+ const semanticCorpus = input.semanticCorpus?.filter((c) => !isCandidateInSet(c, inertBanned));
483
+ const decision = (await this.deps.selection.run({ signals: selectionSignals, candidates, ...(semanticCorpus ? { semanticCorpus } : {}), ...(input.disableSemanticIdf ? { disableSemanticIdf: true } : {}), floor: input.selectionFloor, ...(input.selectionGuard ? { selectionGuard: input.selectionGuard } : {}), ...(input.forcedGeneId !== undefined ? { forcedGeneId: input.forcedGeneId } : {}), ...(exploration ? { exploration } : {}), ...(distilledFallback.length > 0 ? { distilledFallback } : {}), ...(input.antiWarnings && input.antiWarnings.length > 0 ? { antiWarnings: input.antiWarnings } : {}), ...(input.memoryEvidence && input.memoryEvidence.length > 0 ? { memoryEvidence: input.memoryEvidence } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
484
+ const geneId = decision.selectedGeneId ?? 'ad-hoc';
485
+ const runPostSelectionStep = async (phase, step) => {
486
+ try {
487
+ return await step();
488
+ }
489
+ catch (error) {
490
+ let persistedTerminal = null;
491
+ try {
492
+ persistedTerminal = persistedTerminalAfterLatestSelection(ingestor, input.cycleId, cycleStarted.seq);
493
+ }
494
+ catch {
495
+ // Readback is best-effort; compensation below remains the fallback when the log is unavailable.
496
+ }
497
+ if (persistedTerminal && !TERMINAL.has(stage) && canTransition(stage, persistedTerminal)) {
498
+ advance(persistedTerminal);
499
+ }
500
+ if (!persistedTerminal && !TERMINAL.has(stage)) {
501
+ try {
502
+ await ingestor.ingest({
503
+ type: 'cycle.failed',
504
+ human: { title: `cycle ${input.cycleId} selection 后异常` },
505
+ payload: { cycleId: input.cycleId, reason: 'post_selection_error', phase, gene: geneId, env: envKey },
506
+ });
507
+ advance('failed');
508
+ try {
509
+ await recordPersonalityOutcome('failed', null);
510
+ }
511
+ catch {
512
+ // Personality is soft state; never let its writeback replace the original failure.
513
+ }
514
+ }
515
+ catch {
516
+ // The event store itself may be unavailable. Preserve and rethrow the original step failure.
517
+ }
518
+ }
519
+ throw error;
520
+ }
521
+ };
522
+ await runPostSelectionStep('decision_event', () => ingestGeneSelectedWithTraceBudget(ingestor, {
338
523
  type: 'decision.gene_selected',
339
524
  human: { title: `选 gene ${decision.selectedGeneId ?? '(innovate)'}`, why: decision.candidates.map((c) => `${c.geneId}:${c.score.toFixed(3)}`).join(', ') || '无候选→innovate' },
340
- payload: { cycleId: input.cycleId, selectedGeneId: decision.selectedGeneId, ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}), ...(decision.selectedReason ? { selectedReason: decision.selectedReason } : {}), candidates: decision.candidates, ...(decision.antiWarnings && decision.antiWarnings.length > 0 ? { antiWarnings: decision.antiWarnings } : {}), ...(decision.memoryEvidence && decision.memoryEvidence.length > 0 ? { memoryEvidence: decision.memoryEvidence } : {}), weightsVersion: decision.weightsVersion, strategy: decision.strategyName, strategyPreset: strategy.name, ...(plateau.active ? { plateau } : {}), ...(inertBanned.size > 0 ? { inertBanned: [...inertBanned] } : {}) },
341
- });
525
+ payload: { cycleId: input.cycleId, selectedGeneId: decision.selectedGeneId, ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}), ...(decision.selectedReason ? { selectedReason: decision.selectedReason } : {}), candidates: decision.candidates.map(selectionCandidateEventProjection), ...(decision.antiWarnings && decision.antiWarnings.length > 0 ? { antiWarnings: decision.antiWarnings } : {}), ...(decision.memoryEvidence && decision.memoryEvidence.length > 0 ? { memoryEvidence: decision.memoryEvidence } : {}), weightsVersion: decision.weightsVersion, ...(decision.semanticProfileVersion ? { semanticProfileVersion: decision.semanticProfileVersion } : {}), ...(decision.semanticDocumentCount !== undefined ? { semanticDocumentCount: decision.semanticDocumentCount } : {}), strategy: decision.strategyName, strategyPreset: strategy.name, ...(plateau.active ? { plateau } : {}), ...(inertBanned.size > 0 ? { inertBanned: [...inertBanned] } : {}) },
526
+ }, decision.selectionPolicy, decision.selectionGuard));
342
527
  advance('gene_selected');
343
- const geneId = decision.selectedGeneId ?? 'ad-hoc';
344
528
  // 变异
345
- const builtMutation = buildMutation({ decision, category: cycleCategory, signals: cycleSignals, target: input.target, expectedEffect: input.expectedEffect });
529
+ const builtMutation = await runPostSelectionStep('mutation_build', () => buildMutation({ decision, category: cycleCategory, signals: cycleSignals, target: input.target, expectedEffect: input.expectedEffect }));
346
530
  // 人格风险闸(用途②): 高危人格禁 innovate / 未达阈值的 high-risk 降级. 未注入人格 ⇒ 原样放行.
347
531
  // 用本轮 applySelectForRun 选出的状态(personalityForRun), 而非再读一次 store.currentState() ——
348
532
  // 保证"喂 prompt 的风格 / 过风险闸 / 回写统计"三者用的是同一个当轮人格.
349
533
  let mutation = builtMutation;
350
534
  if (this.deps.personality && personalityForRun) {
351
- await emitPersonalitySelected(ingestor, { cycleId: input.cycleId, personality: personalityForRun, known: personalityKnown });
352
- const gate = gatePersonalityRisk(builtMutation, personalityForRun);
535
+ await runPostSelectionStep('personality_selected', () => emitPersonalitySelected(ingestor, { cycleId: input.cycleId, personality: personalityForRun, known: personalityKnown }));
536
+ const gate = await runPostSelectionStep('personality_risk_gate', () => gatePersonalityRisk(builtMutation, personalityForRun));
353
537
  mutation = gate.mutation;
354
- await emitPersonalityRiskGated(ingestor, { cycleId: input.cycleId, personality: personalityForRun, gate });
538
+ await runPostSelectionStep('personality_risk_gate', () => emitPersonalityRiskGated(ingestor, { cycleId: input.cycleId, personality: personalityForRun, gate }));
355
539
  }
356
- await ingestor.ingest({ type: 'mutation.built', human: { title: `变异 ${mutation.category} → ${mutation.target}` }, payload: { cycleId: input.cycleId, mutationId: mutation.id, risk: mutation.risk_level, category: mutation.category, strategyPreset: strategy.name } });
540
+ await runPostSelectionStep('mutation_event', () => ingestor.ingest({ type: 'mutation.built', human: { title: `变异 ${mutation.category} → ${mutation.target}` }, payload: { cycleId: input.cycleId, mutationId: mutation.id, risk: mutation.risk_level, category: mutation.category, strategyPreset: strategy.name } }));
357
541
  advance('mutation_built');
358
542
  // 执行(agent/runtime)
359
543
  let exec;
@@ -364,7 +548,7 @@ export class CycleEngine {
364
548
  const msg = e instanceof Error ? e.message : String(e);
365
549
  // execute threw before returning a transcript; the error text IS the host context (a provider error that
366
550
  // surfaces as a thrown exception still classifies as host_provider_error). Pass it as the sessionLog.
367
- await ingestor.ingest({ type: 'cycle.failed', human: { title: `cycle ${input.cycleId} 执行抛错` }, payload: { cycleId: input.cycleId, error: msg, gene: geneId, env: envKey, ...failureClassPayload({ geneId }, msg) } });
551
+ await runPostSelectionStep('terminal_event', () => ingestor.ingest({ type: 'cycle.failed', human: { title: `cycle ${input.cycleId} 执行抛错` }, payload: { cycleId: input.cycleId, error: msg, gene: geneId, env: envKey, ...failureClassPayload({ geneId }, msg) } }));
368
552
  advance('failed');
369
553
  await recordPersonalityOutcome('failed', null);
370
554
  return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, reasons: [...reasons, `执行异常: ${msg}`] };
@@ -374,6 +558,9 @@ export class CycleEngine {
374
558
  try {
375
559
  permit = await input.solidifyPermit({
376
560
  cycleId: input.cycleId,
561
+ ...(input.executionBinding ? { bindingDigest: input.executionBinding.binding_digest, runId: input.executionBinding.correlation.run_id } : {}),
562
+ ...(exec.executionTerminal ? { executionTerminal: exec.executionTerminal } : {}),
563
+ ...(exec.provenance ? { provenance: exec.provenance } : {}),
377
564
  geneId,
378
565
  signals: cycleSignals,
379
566
  mutation,
@@ -388,7 +575,15 @@ export class CycleEngine {
388
575
  }
389
576
  if (!permit.ok) {
390
577
  const reason = `solidify permit denied: ${permit.reason}`;
391
- await ingestor.ingest({
578
+ exec = {
579
+ ...exec,
580
+ outcome: { ...exec.outcome, status: 'failed', reason },
581
+ executionTerminal: { status: 'failed', disposition: 'denied' },
582
+ provenance: exec.provenance
583
+ ? { ...exec.provenance, permit, terminal_disposition: 'denied' }
584
+ : undefined,
585
+ };
586
+ await runPostSelectionStep('terminal_event', () => ingestor.ingest({
392
587
  type: 'cycle.failed',
393
588
  human: { title: `cycle ${input.cycleId} permit denied` },
394
589
  payload: {
@@ -399,30 +594,60 @@ export class CycleEngine {
399
594
  ...executionMetadata(exec),
400
595
  ...failureClassPayload({ geneId }),
401
596
  },
402
- });
597
+ }));
403
598
  advance('failed');
404
599
  await recordPersonalityOutcome('failed', null);
405
- return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, ...executionMetadata(exec), reasons: [...reasons, reason] };
600
+ return {
601
+ cycleId: input.cycleId,
602
+ triggered: true,
603
+ finalStage: stage,
604
+ producedValue: false,
605
+ decision,
606
+ mutation,
607
+ execution: exec,
608
+ ...(exec.bindingCorrelation ? { bindingDigest: exec.bindingCorrelation.bindingDigest, runId: exec.bindingCorrelation.runId } : {}),
609
+ ...(exec.executionTerminal ? { terminalDisposition: exec.executionTerminal.disposition } : {}),
610
+ ...executionMetadata(exec),
611
+ reasons: [...reasons, reason],
612
+ };
406
613
  }
407
614
  }
408
615
  // solidify → Capsule
409
- const sol = solidify({
616
+ const sol = await runPostSelectionStep('solidify', () => solidify({
410
617
  geneId, trigger: cycleSignals, summary: input.summary, confidence: input.confidence,
411
618
  outcome: exec.outcome, proofOfWork: exec.proofOfWork, strongEvidence: exec.strongEvidence,
412
- });
619
+ ...(exec.failureIdentity ? { failureIdentity: exec.failureIdentity } : {}),
620
+ }));
413
621
  reasons.push(...sol.reasons);
414
- await store.put(sol.capsule);
415
- await ingestor.ingest({ type: 'capsule.produced', human: { title: `Capsule ${sol.resolutionStatus}` }, payload: { cycleId: input.cycleId, capsuleId: sol.capsule.asset_id, resolutionStatus: sol.resolutionStatus, producedValue: sol.producedValue, blastRadius: sol.capsule.blast_radius } });
416
- // EvolutionEvent(capsule_id 引 Capsule; outcome 真值在此)
417
- const event = buildEvolutionEvent({
622
+ await runPostSelectionStep('capsule_store', () => store.put(sol.capsule));
623
+ await runPostSelectionStep('capsule_event', () => ingestor.ingest({ type: 'capsule.produced', human: { title: `Capsule ${sol.resolutionStatus}` }, payload: { cycleId: input.cycleId, capsuleId: sol.capsule.asset_id, resolutionStatus: sol.resolutionStatus, producedValue: sol.producedValue, blastRadius: sol.capsule.blast_radius } }));
624
+ // EvolutionEvent(capsule_id 引 Capsule; outcome 真值在此).
625
+ // Selection stamp: selected_asset_id / kauto_member / evolver_version / selection_stage=applied
626
+ // live in event.meta so production cohort collection can separate writer membership from
627
+ // selected/applied member share. Only true kauto membership is stamped (omit-not-null).
628
+ const selectedCandidate = decision.selectedGeneId
629
+ ? input.candidates.find((c) => (c.geneId === decision.selectedGeneId
630
+ && (decision.selectedAssetId === undefined || c.assetId === decision.selectedAssetId)))
631
+ ?? input.candidates.find((c) => c.geneId === decision.selectedGeneId)
632
+ ?? (input.distilledFallback ?? []).find((c) => (c.geneId === decision.selectedGeneId
633
+ && (decision.selectedAssetId === undefined || c.assetId === decision.selectedAssetId)))
634
+ : undefined;
635
+ const envFp = (this.deps.envFingerprint ?? captureEnvFingerprint)();
636
+ const event = await runPostSelectionStep('evolution_event_build', () => buildEvolutionEvent({
418
637
  intent: cycleCategory, signals: cycleSignals, genesUsed: decision.selectedGeneId ? [decision.selectedGeneId] : [],
419
638
  mutationId: mutation.id, blastRadius: sol.capsule.blast_radius, outcome: exec.outcome,
420
639
  capsuleId: sol.capsule.asset_id, sourceType: decision.selectedGeneId ? 'reused' : 'generated',
421
- });
422
- await store.put(event);
423
- await ingestor.ingest({ type: 'evolution_event.projected', human: { title: `世代记录 ${exec.outcome.status}` }, payload: { cycleId: input.cycleId, eventId: event.asset_id, capsuleId: sol.capsule.asset_id, outcome: exec.outcome } });
640
+ selection: {
641
+ selectionStage: 'applied',
642
+ ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}),
643
+ ...(selectedCandidate?.kautoMember === true ? { kautoMember: true } : {}),
644
+ ...(envFp.evolver_version ? { evolverVersion: envFp.evolver_version } : {}),
645
+ },
646
+ }));
647
+ await runPostSelectionStep('evolution_event_store', () => store.put(event));
648
+ await runPostSelectionStep('evolution_event_projection', () => ingestor.ingest({ type: 'evolution_event.projected', human: { title: `世代记录 ${exec.outcome.status}` }, payload: { cycleId: input.cycleId, eventId: event.asset_id, capsuleId: sol.capsule.asset_id, outcome: exec.outcome } }));
424
649
  if (exec.outcome.status === 'failed') {
425
- await ingestor.ingest({
650
+ await runPostSelectionStep('terminal_event', () => ingestor.ingest({
426
651
  type: 'cycle.failed',
427
652
  human: { title: `cycle ${input.cycleId} 失败` },
428
653
  payload: {
@@ -443,15 +668,30 @@ export class CycleEngine {
443
668
  : {}),
444
669
  }, exec.sessionLog),
445
670
  },
446
- });
671
+ }));
447
672
  advance('failed');
448
673
  }
449
674
  else {
450
- await ingestor.ingest({ type: 'cycle.solidified', human: { title: `cycle ${input.cycleId} 固化` }, payload: { cycleId: input.cycleId, capsuleId: sol.capsule.asset_id, outcome: exec.outcome, gene: geneId, env: envKey, producedValue: sol.producedValue } });
675
+ await runPostSelectionStep('terminal_event', () => ingestor.ingest({ type: 'cycle.solidified', human: { title: `cycle ${input.cycleId} 固化` }, payload: { cycleId: input.cycleId, capsuleId: sol.capsule.asset_id, outcome: exec.outcome, gene: geneId, env: envKey, producedValue: sol.producedValue } }));
451
676
  advance('solidified');
452
677
  }
453
678
  // 人格统计回写(用途②): 用本轮真实 outcome/score 回写当轮人格桶, 供下一轮自然选择靠拢. 未注入 ⇒ no-op.
454
679
  await recordPersonalityOutcome(exec.outcome.status, exec.outcome.score);
455
- return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: sol.producedValue, decision, mutation, capsule: sol.capsule, event, resolutionStatus: sol.resolutionStatus, ...executionMetadata(exec), reasons };
680
+ return {
681
+ cycleId: input.cycleId,
682
+ triggered: true,
683
+ finalStage: stage,
684
+ producedValue: sol.producedValue,
685
+ decision,
686
+ mutation,
687
+ capsule: sol.capsule,
688
+ event,
689
+ resolutionStatus: sol.resolutionStatus,
690
+ execution: exec,
691
+ ...(exec.bindingCorrelation ? { bindingDigest: exec.bindingCorrelation.bindingDigest, runId: exec.bindingCorrelation.runId } : {}),
692
+ ...(exec.executionTerminal ? { terminalDisposition: exec.executionTerminal.disposition } : {}),
693
+ ...executionMetadata(exec),
694
+ reasons,
695
+ };
456
696
  }
457
697
  }
@@ -1,4 +1,19 @@
1
1
  import { type EvolutionEvent, type GepCategory } from '../wire/index.js';
2
+ /** Selection-side lifecycle stamp carried in EvolutionEvent.meta (schema-allowed object). */
3
+ export type EvolutionSelectionStage = 'selected' | 'applied';
4
+ export interface EvolutionSelectionStamp {
5
+ /** Content asset_id of the selected Gene when known (sha256:…). */
6
+ selectedAssetId?: string;
7
+ /** True only when assembly stamped kautoMember on the selected candidate. */
8
+ kautoMember?: boolean;
9
+ /** Producer evolver version from env fingerprint, when known. */
10
+ evolverVersion?: string;
11
+ /**
12
+ * selected = gene was chosen for the cycle;
13
+ * applied = cycle produced an EvolutionEvent after execution (this builder always emits applied).
14
+ */
15
+ selectionStage?: EvolutionSelectionStage;
16
+ }
2
17
  export interface BuildEvolutionEventInput {
3
18
  intent: GepCategory;
4
19
  signals: readonly string[];
@@ -15,9 +30,20 @@ export interface BuildEvolutionEventInput {
15
30
  capsuleId: string | null;
16
31
  sourceType: 'generated' | 'reused' | 'reference';
17
32
  parent?: string | null;
33
+ /** Optional selection lifecycle stamp; omitted keys stay absent (omit-not-null). */
34
+ selection?: EvolutionSelectionStamp;
18
35
  }
36
+ /**
37
+ * Build the schema-allowed `meta` object for selection telemetry.
38
+ * Absent optional fields are omitted so they do not enter the asset_id hash as null.
39
+ */
40
+ export declare function buildEvolutionSelectionMeta(selection: EvolutionSelectionStamp | undefined): Record<string, unknown> | undefined;
19
41
  /**
20
42
  * EvolutionEvent = 世代记录(outcome 真值在此, 硬化 A4). 单向引 Capsule(capsule_id);
21
43
  * 写入序列: capsule asset_id 先算 → 填 capsule_id → 再算 event asset_id.
44
+ *
45
+ * Optional selection stamp lands in `meta` (schema-allowed) so production cohort
46
+ * collection can separate writer membership from selected/applied member share
47
+ * without inventing a LearningAsset bridge.
22
48
  */
23
49
  export declare function buildEvolutionEvent(input: BuildEvolutionEventInput): EvolutionEvent;
@@ -1,10 +1,41 @@
1
1
  import { ulid as makeUlid } from 'ulid';
2
2
  import { computeAssetId, SCHEMA_VERSION } from '../wire/index.js';
3
+ function nonEmptyString(value) {
4
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
5
+ }
6
+ /**
7
+ * Build the schema-allowed `meta` object for selection telemetry.
8
+ * Absent optional fields are omitted so they do not enter the asset_id hash as null.
9
+ */
10
+ export function buildEvolutionSelectionMeta(selection) {
11
+ if (!selection)
12
+ return undefined;
13
+ const selectedAssetId = nonEmptyString(selection.selectedAssetId);
14
+ const evolverVersion = nonEmptyString(selection.evolverVersion);
15
+ const selectionStage = selection.selectionStage === 'selected' || selection.selectionStage === 'applied'
16
+ ? selection.selectionStage
17
+ : undefined;
18
+ const meta = {};
19
+ if (selectedAssetId)
20
+ meta['selected_asset_id'] = selectedAssetId;
21
+ if (selection.kautoMember === true)
22
+ meta['kauto_member'] = true;
23
+ if (evolverVersion)
24
+ meta['evolver_version'] = evolverVersion;
25
+ if (selectionStage)
26
+ meta['selection_stage'] = selectionStage;
27
+ return Object.keys(meta).length > 0 ? meta : undefined;
28
+ }
3
29
  /**
4
30
  * EvolutionEvent = 世代记录(outcome 真值在此, 硬化 A4). 单向引 Capsule(capsule_id);
5
31
  * 写入序列: capsule asset_id 先算 → 填 capsule_id → 再算 event asset_id.
32
+ *
33
+ * Optional selection stamp lands in `meta` (schema-allowed) so production cohort
34
+ * collection can separate writer membership from selected/applied member share
35
+ * without inventing a LearningAsset bridge.
6
36
  */
7
37
  export function buildEvolutionEvent(input) {
38
+ const meta = buildEvolutionSelectionMeta(input.selection);
8
39
  const base = {
9
40
  type: 'EvolutionEvent',
10
41
  schema_version: SCHEMA_VERSION,
@@ -18,6 +49,7 @@ export function buildEvolutionEvent(input) {
18
49
  outcome: input.outcome,
19
50
  capsule_id: input.capsuleId,
20
51
  source_type: input.sourceType,
52
+ ...(meta ? { meta } : {}),
21
53
  };
22
54
  const asset_id = computeAssetId(base);
23
55
  return { ...base, asset_id };
@@ -1,3 +1,4 @@
1
+ import type { SelectionPolicy, Ucb1History } from './ucb1.js';
1
2
  export type PlateauSeverity = 'suggested' | 'required';
2
3
  export interface PlateauState {
3
4
  active: boolean;
@@ -6,12 +7,16 @@ export interface PlateauState {
6
7
  }
7
8
  /** Exploration control fed into selection. Absent → pure deterministic top-score selection. */
8
9
  export interface ExplorationInput {
10
+ /** Selection policy requested by the composition layer. Default preserves legacy engine-health + drift. */
11
+ policy?: SelectionPolicy;
9
12
  /** Force drift on even without a plateau (e.g. an explicit explore policy). */
10
13
  driftEnabled?: boolean;
11
14
  /** Plateau detected upstream; an active plateau forces drift intensity up. */
12
15
  plateau?: PlateauState;
13
16
  /** Total attempts across the candidate pool (drives the maturity decay of the drift offset). */
14
17
  totalAttempts?: number;
18
+ /** Event-log-derived state for UCB1. Undefined means fail-safe to legacy drift. */
19
+ ucb1History?: Ucb1History;
15
20
  }
16
21
  /**
17
22
  * Adaptive drift intensity (ported v1): 1/sqrt(Ne) + an offset that decays from 0.3 → 0.02 as the pool
@@ -25,6 +30,8 @@ export interface DriftDecision {
25
30
  index: number;
26
31
  intensity: number;
27
32
  }
33
+ /** Deterministic top-N window shared by legacy random drift and UCB1. */
34
+ export declare function explorationWindowSize(eligibleCount: number, exp: ExplorationInput | undefined): number;
28
35
  /**
29
36
  * Pick which ranked candidate to take among `eligibleCount` above-floor candidates.
30
37
  * No exploration → deterministic top (index 0). With exploration → compute intensity (an active plateau
@@ -15,8 +15,9 @@ const PLATEAU_FORCE = 10;
15
15
  export function computeDriftIntensity(ne, totalAttempts = 0) {
16
16
  if (!Number.isFinite(ne) || ne <= 1)
17
17
  return 0.7;
18
+ const attempts = Number.isFinite(totalAttempts) ? Math.max(0, totalAttempts) : 0;
18
19
  const maturityThreshold = ne * MATURITY_ATTEMPTS_PER_GENE;
19
- const maturity = maturityThreshold > 0 ? Math.min(1, totalAttempts / maturityThreshold) : 0;
20
+ const maturity = maturityThreshold > 0 ? Math.min(1, attempts / maturityThreshold) : 0;
20
21
  const offset = DRIFT_OFFSET_MAX - (DRIFT_OFFSET_MAX - DRIFT_OFFSET_MIN) * maturity;
21
22
  return Math.min(1, 1 / Math.sqrt(ne) + offset);
22
23
  }
@@ -34,6 +35,18 @@ export function detectPlateau(recentOutcomes) {
34
35
  return { active: true, severity: 'suggested', count };
35
36
  return { active: false, severity: 'suggested', count };
36
37
  }
38
+ function explorationActive(exp) {
39
+ return Boolean(exp && (exp.driftEnabled || exp.plateau?.active));
40
+ }
41
+ /** Deterministic top-N window shared by legacy random drift and UCB1. */
42
+ export function explorationWindowSize(eligibleCount, exp) {
43
+ if (!explorationActive(exp) || eligibleCount <= 1)
44
+ return Math.min(1, Math.max(0, eligibleCount));
45
+ let intensity = computeDriftIntensity(eligibleCount, exp.totalAttempts ?? 0);
46
+ if (exp.plateau?.active)
47
+ intensity = Math.max(intensity, exp.plateau.severity === 'required' ? 1.0 : 0.7);
48
+ return Math.min(eligibleCount, Math.max(2, Math.ceil(eligibleCount * intensity)));
49
+ }
37
50
  /**
38
51
  * Pick which ranked candidate to take among `eligibleCount` above-floor candidates.
39
52
  * No exploration → deterministic top (index 0). With exploration → compute intensity (an active plateau
@@ -41,7 +54,7 @@ export function detectPlateau(recentOutcomes) {
41
54
  * with intensity). rng() is called at most twice; pass a seeded rng for deterministic tests.
42
55
  */
43
56
  export function driftSelect(eligibleCount, exp, rng) {
44
- if (!exp || (!exp.driftEnabled && !exp.plateau?.active) || eligibleCount <= 1) {
57
+ if (!explorationActive(exp) || eligibleCount <= 1) {
45
58
  return { driftMode: 'deterministic', index: 0, intensity: 0 };
46
59
  }
47
60
  let intensity = computeDriftIntensity(eligibleCount, exp.totalAttempts ?? 0);
@@ -49,7 +62,7 @@ export function driftSelect(eligibleCount, exp, rng) {
49
62
  intensity = Math.max(intensity, exp.plateau.severity === 'required' ? 1.0 : 0.7);
50
63
  if (rng() >= intensity)
51
64
  return { driftMode: 'deterministic', index: 0, intensity };
52
- const topN = Math.min(eligibleCount, Math.max(2, Math.ceil(eligibleCount * intensity)));
65
+ const topN = explorationWindowSize(eligibleCount, exp);
53
66
  const index = Math.min(topN - 1, Math.max(0, Math.floor(rng() * topN)));
54
67
  return { driftMode: exp.plateau?.active ? 'plateau_drift' : 'drift', index, intensity };
55
68
  }