@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
@@ -3,11 +3,12 @@ import type { ProvenanceStore } from '../assetstore/provenance.js';
3
3
  import type { ReviewLedger } from '../assetstore/reviewLedger.js';
4
4
  import type { GepCategory } from '../wire/index.js';
5
5
  import type { ProblemPattern } from '../schema/problem.js';
6
- import type { GeneCandidateInput } from './geneSelection.js';
6
+ import type { GeneCandidateInput, SelectionGuardMode } from './geneSelection.js';
7
7
  import { CycleEngine, type CycleInput, type CycleResult, type SolidifyPermitGate } from './cycleEngine.js';
8
8
  import { type PendingSignalsContext } from '../assetstore/pendingSignals.js';
9
9
  import { type ReuseOutcomeSummary, type ReuseOutcomeEvent } from '../ops/reuseOutcomes.js';
10
10
  import type { MemoryGraphAdvice } from './memoryGraph.js';
11
+ import type { SelectionPolicy } from './ucb1.js';
11
12
  export interface RunCycleOptions {
12
13
  cycleId: string;
13
14
  problem: ProblemPattern;
@@ -20,6 +21,12 @@ export interface RunCycleOptions {
20
21
  summary: string;
21
22
  confidence: number;
22
23
  selectionFloor?: number;
24
+ /** Emergency rollback for semantic IDF. Also skips pre-admission corpus collection. */
25
+ disableSemanticIdf?: boolean;
26
+ /** Experimental plateau policy. Omit for current engine-health + random drift behavior. */
27
+ selectionPolicy?: SelectionPolicy;
28
+ /** Versioned relevance-guard rollout. Omit for legacy direct-call behavior. */
29
+ selectionGuard?: SelectionGuardMode;
23
30
  /** Optional explicit gene chosen by GEP / an external runtime; forwarded to CycleEngine after assembly. */
24
31
  forcedGeneId?: string;
25
32
  /** The agent runtime's work: run the mutation, return the outcome. */
@@ -18,6 +18,12 @@ export async function runEvolutionCycle(engine, store, opts) {
18
18
  console.warn(`[ExplicitSignals] Failed to consume pending signals (non-fatal): ${message}`);
19
19
  }
20
20
  }
21
+ // Curriculum targets must be present before candidate assembly; injecting them only inside CycleEngine would
22
+ // be too late to recall a gene whose signals_match is curriculum_target:*. The engine repeats this enrichment
23
+ // for direct runCycle callers; the operation is deterministic and set-idempotent.
24
+ const baseSignals = signals;
25
+ const preparedCurriculum = engine.prepareCurriculumSignals(baseSignals);
26
+ const selectionSignals = preparedCurriculum.signals;
21
27
  // #268 phase 1 + #274 slice 3: fold cross-runtime reuse counts AND observed recall into the pool (soft re-order).
22
28
  // Absent both → no map → default-off. Recall is folded at a lower weight (RECALL_WEIGHT). Assembly sums a gene's
23
29
  // ids → sentiment; bounded + clamped in geneSelection; the hard trust/review/ban gates in assembly run first.
@@ -31,6 +37,7 @@ export async function runEvolutionCycle(engine, store, opts) {
31
37
  }
32
38
  }
33
39
  const asmOpts = {
40
+ includeSemanticCorpus: opts.disableSemanticIdf !== true,
34
41
  ...(opts.candidateLimit ? { limit: opts.candidateLimit } : {}),
35
42
  ...(opts.provenance ? { provenance: opts.provenance } : {}),
36
43
  ...(opts.review ? { review: opts.review } : {}),
@@ -38,7 +45,7 @@ export async function runEvolutionCycle(engine, store, opts) {
38
45
  ...(opts.hubCandidates && opts.hubCandidates.length > 0 ? { hubCandidates: opts.hubCandidates } : {}),
39
46
  ...(reuseCounts.size > 0 ? { reuseCounts } : {}),
40
47
  };
41
- const { candidates: assembledCandidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store, signals, asmOpts);
48
+ const { candidates: assembledCandidates, distilledFallback, semanticCorpus, antiWarnings } = await assembleSelectionPool(store, selectionSignals, asmOpts);
42
49
  const memoryByGene = new Map((opts.memoryGraphAdvice?.genes ?? []).map((evidence) => [evidence.geneId, evidence]));
43
50
  const candidates = assembledCandidates.map((candidate) => {
44
51
  const evidence = memoryByGene.get(candidate.geneId) ?? (candidate.assetId ? memoryByGene.get(candidate.assetId) : undefined);
@@ -53,10 +60,15 @@ export async function runEvolutionCycle(engine, store, opts) {
53
60
  return engine.runCycle({
54
61
  cycleId: opts.cycleId,
55
62
  problem: opts.problem,
56
- signals,
63
+ signals: baseSignals,
64
+ curriculumSignals: preparedCurriculum.curriculumSignals,
57
65
  category: opts.category,
58
66
  ...(opts.strategyName !== undefined ? { strategyName: opts.strategyName } : {}),
59
67
  candidates,
68
+ semanticCorpus,
69
+ ...(opts.disableSemanticIdf ? { disableSemanticIdf: true } : {}),
70
+ ...(opts.selectionPolicy ? { selectionPolicy: opts.selectionPolicy } : {}),
71
+ ...(opts.selectionGuard ? { selectionGuard: opts.selectionGuard } : {}),
60
72
  target: opts.target,
61
73
  expectedEffect: opts.expectedEffect,
62
74
  summary: opts.summary,
@@ -67,7 +79,7 @@ export async function runEvolutionCycle(engine, store, opts) {
67
79
  ...(opts.failureContext !== undefined ? { failureContext: opts.failureContext } : {}),
68
80
  ...(opts.solidifyPermit ? { solidifyPermit: opts.solidifyPermit } : {}),
69
81
  // #97: forward the distilled-gene fallback pool so a no-signal-match cycle reuses a distilled strategy
70
- // (instead of a blind innovate) when nothing clears the floor.
82
+ // (instead of a blind innovate) when normal selection has no reusable positive choice.
71
83
  ...(memoryDistilledFallback.length > 0 ? { distilledFallback: memoryDistilledFallback } : {}),
72
84
  ...(antiWarnings.length > 0 ? { antiWarnings } : {}),
73
85
  ...(memoryEvidence.length > 0 ? { memoryEvidence } : {}),
@@ -0,0 +1,34 @@
1
+ import { type GeneLearningView } from '../assetstore/learningHistory.js';
2
+ import type { AssetStoreProvider } from '../assetstore/provider.js';
3
+ export type PublishEligibilityReason = 'eligible' | 'no_proven_success';
4
+ export interface GenePublishEvidence {
5
+ geneId: string;
6
+ /** Real, value-producing successes (inert zero-work successes excluded upstream). */
7
+ success: number;
8
+ failed: number;
9
+ inert: number;
10
+ total: number;
11
+ eligible: boolean;
12
+ reason: PublishEligibilityReason;
13
+ }
14
+ /**
15
+ * The single publish/upload eligibility predicate: a gene may be published iff
16
+ * it has at least one capsule with a `success` outcome.
17
+ *
18
+ * We count `success + inert`, NOT just `success`. aggregateLearningHistory
19
+ * demotes a `success` capsule that carries no `proof_of_work` to `inert`
20
+ * (#195), but the evox capsule builder (asset_builder.rs) does NOT emit
21
+ * `proof_of_work` — a genuine successful run surfaces there as `inert`. Gating
22
+ * publish on the proof-only `success` count would therefore block genes that
23
+ * actually succeeded in production. "Has a success outcome" is the honest bar
24
+ * for the publish gate; the proof/inert distinction stays where it belongs
25
+ * (success-rate, auto-promote), not here. A gene with only failures — or one
26
+ * never run at all — has `success + inert === 0` and is not publishable.
27
+ */
28
+ export declare function isGenePublishEligible(view: Pick<GeneLearningView, 'success' | 'inert'>): boolean;
29
+ /**
30
+ * Read-only: derive a gene's outcome evidence and whether it clears the publish
31
+ * bar. Returns `no_proven_success` for a gene the store has never seen succeed,
32
+ * so callers can surface a precise, self-serviceable reason.
33
+ */
34
+ export declare function assessGenePublishEvidence(store: AssetStoreProvider, geneId: string): Promise<GenePublishEvidence>;
@@ -0,0 +1,52 @@
1
+ // Proven-success gate for publishing / uploading a gene (#581).
2
+ //
3
+ // A gene's `confidence` is minted from model self-report (solidify), text
4
+ // completeness (conversationDistiller), or is absent (like) — none of which
5
+ // prove the gene ever WORKED. Publishing such a gene to the market pairs it
6
+ // with a capsule and lets the Hub quality gate reject it after a round-trip
7
+ // ("never truly succeeded"). This predicate is the single, outcome-driven bar
8
+ // every publish/upload surface routes through, so an unproven gene is stopped
9
+ // locally with a clear reason instead of leaking to the Hub.
10
+ //
11
+ // Deliberately DISTINCT from `probationWouldPromote` (genePromotion.ts):
12
+ // auto-promotion runs with NO human in the loop, so it demands a strong, clean
13
+ // record (>= minSuccess successes AND zero failures). Publishing is human-
14
+ // initiated, so the bar is only "has this gene truly succeeded at least once"
15
+ // — a gene with many successes and one stray failure is still worth publishing,
16
+ // and blocking it on `failed === 0` would be wrong.
17
+ import { aggregateLearningHistory } from '../assetstore/learningHistory.js';
18
+ /**
19
+ * The single publish/upload eligibility predicate: a gene may be published iff
20
+ * it has at least one capsule with a `success` outcome.
21
+ *
22
+ * We count `success + inert`, NOT just `success`. aggregateLearningHistory
23
+ * demotes a `success` capsule that carries no `proof_of_work` to `inert`
24
+ * (#195), but the evox capsule builder (asset_builder.rs) does NOT emit
25
+ * `proof_of_work` — a genuine successful run surfaces there as `inert`. Gating
26
+ * publish on the proof-only `success` count would therefore block genes that
27
+ * actually succeeded in production. "Has a success outcome" is the honest bar
28
+ * for the publish gate; the proof/inert distinction stays where it belongs
29
+ * (success-rate, auto-promote), not here. A gene with only failures — or one
30
+ * never run at all — has `success + inert === 0` and is not publishable.
31
+ */
32
+ export function isGenePublishEligible(view) {
33
+ return view.success + (view.inert ?? 0) >= 1;
34
+ }
35
+ /**
36
+ * Read-only: derive a gene's outcome evidence and whether it clears the publish
37
+ * bar. Returns `no_proven_success` for a gene the store has never seen succeed,
38
+ * so callers can surface a precise, self-serviceable reason.
39
+ */
40
+ export async function assessGenePublishEvidence(store, geneId) {
41
+ const view = await aggregateLearningHistory(store, geneId);
42
+ const eligible = isGenePublishEligible(view);
43
+ return {
44
+ geneId,
45
+ success: view.success,
46
+ failed: view.failed,
47
+ inert: view.inert ?? 0,
48
+ total: view.total,
49
+ eligible,
50
+ reason: eligible ? 'eligible' : 'no_proven_success',
51
+ };
52
+ }
@@ -0,0 +1,53 @@
1
+ import type { RootEvent } from '../events/eventSchema.js';
2
+ export declare const SELECTION_POLICIES: readonly ["engine-health", "ucb1-shadow", "ucb1"];
3
+ export type SelectionPolicy = (typeof SELECTION_POLICIES)[number];
4
+ export declare const UCB1_SELECTION_POLICY_VERSION = "ucb1-v1";
5
+ export declare const UCB1_REWARD_POLICY_VERSION = "productive-binary-v1";
6
+ export interface Ucb1ArmStats {
7
+ armId: string;
8
+ pulls: number;
9
+ completedPulls: number;
10
+ rewardSum: number;
11
+ meanReward: number;
12
+ }
13
+ export interface Ucb1History {
14
+ arms: ReadonlyMap<string, Ucb1ArmStats>;
15
+ /** Every non-ad-hoc decision counts immediately, including decisions whose terminal event is still pending. */
16
+ totalPulls: number;
17
+ }
18
+ export interface Ucb1Arm {
19
+ armId: string;
20
+ baseScore: number;
21
+ explorationEligible: boolean;
22
+ stats: Ucb1ArmStats;
23
+ }
24
+ export interface Ucb1Choice {
25
+ armId: string;
26
+ pulls: number;
27
+ completedPulls: number;
28
+ totalPulls: number;
29
+ meanReward: number;
30
+ /** Null represents the canonical +Infinity cold-start index without putting Infinity on the event wire. */
31
+ bonus: number | null;
32
+ /** Null represents the canonical +Infinity cold-start index without putting Infinity on the event wire. */
33
+ index: number | null;
34
+ coldStart: boolean;
35
+ }
36
+ export type Ucb1FallbackReason = 'empty_pool' | 'ineligible_arm' | 'missing_history';
37
+ export interface Ucb1Decision {
38
+ choice: Ucb1Choice | null;
39
+ fallbackReason?: Ucb1FallbackReason;
40
+ }
41
+ /**
42
+ * Rebuild UCB1 state from the append-only root event log. Decisions are pulls immediately, so later readers no
43
+ * longer treat an in-flight arm as cold. The read-select-append sequence is not an atomic reservation across
44
+ * workers. A terminal event adds reward exactly once per cycle; duplicate rows collapse to their latest projection.
45
+ */
46
+ export declare function deriveUcb1History(events: readonly RootEvent[]): Ucb1History;
47
+ /** Combine current asset identity with legacy gene-only history when that bridge is unambiguous. */
48
+ export declare function ucb1StatsForCandidate(history: Ucb1History, geneId: string, assetId?: string, includeLegacyGeneHistory?: boolean): Ucb1ArmStats;
49
+ /**
50
+ * Canonical UCB1: mean reward + sqrt(2 ln(total pulls) / arm pulls). Cold arms have +Infinity and are ordered
51
+ * deterministically by base score then arm identity. The caller supplies only the already-gated exploration window.
52
+ */
53
+ export declare function chooseUcb1Arm(arms: readonly Ucb1Arm[], historyTotalPulls: number): Ucb1Decision;
@@ -0,0 +1,156 @@
1
+ export const SELECTION_POLICIES = ['engine-health', 'ucb1-shadow', 'ucb1'];
2
+ export const UCB1_SELECTION_POLICY_VERSION = 'ucb1-v1';
3
+ export const UCB1_REWARD_POLICY_VERSION = 'productive-binary-v1';
4
+ function nonEmptyString(value) {
5
+ if (typeof value !== 'string')
6
+ return undefined;
7
+ const normalized = value.trim();
8
+ return normalized.length > 0 ? normalized : undefined;
9
+ }
10
+ function decisionProjection(event) {
11
+ if (event.type !== 'decision.gene_selected')
12
+ return undefined;
13
+ const payload = event.payload;
14
+ const cycleId = nonEmptyString(payload['cycleId']);
15
+ const geneId = nonEmptyString(payload['selectedGeneId']);
16
+ if (!cycleId)
17
+ return undefined;
18
+ if (!geneId || geneId === 'ad-hoc')
19
+ return { cycleId, decision: null };
20
+ return {
21
+ cycleId,
22
+ decision: { armId: nonEmptyString(payload['selectedAssetId']) ?? geneId },
23
+ };
24
+ }
25
+ function terminalReward(event) {
26
+ if (event.type !== 'cycle.failed' && event.type !== 'cycle.solidified')
27
+ return null;
28
+ const payload = event.payload;
29
+ const cycleId = nonEmptyString(payload['cycleId']);
30
+ if (!cycleId)
31
+ return null;
32
+ if (event.type === 'cycle.failed' || payload['producedValue'] === false)
33
+ return { cycleId, reward: 0 };
34
+ // Legacy solidified events predate producedValue. Preserve the existing confidence replay compatibility policy.
35
+ return { cycleId, reward: 1 };
36
+ }
37
+ /**
38
+ * Rebuild UCB1 state from the append-only root event log. Decisions are pulls immediately, so later readers no
39
+ * longer treat an in-flight arm as cold. The read-select-append sequence is not an atomic reservation across
40
+ * workers. A terminal event adds reward exactly once per cycle; duplicate rows collapse to their latest projection.
41
+ */
42
+ export function deriveUcb1History(events) {
43
+ const cycles = new Map();
44
+ for (const event of events) {
45
+ const decision = decisionProjection(event);
46
+ // A restarted/re-emitted decision for the same cycleId begins a new pending lifecycle. Never let a terminal
47
+ // from the earlier lifecycle leak reward into this newer arm.
48
+ if (decision) {
49
+ if (decision.decision)
50
+ cycles.set(decision.cycleId, { decision: decision.decision });
51
+ else
52
+ cycles.delete(decision.cycleId);
53
+ }
54
+ const terminal = terminalReward(event);
55
+ if (terminal) {
56
+ const cycle = cycles.get(terminal.cycleId);
57
+ if (cycle)
58
+ cycle.reward = terminal.reward;
59
+ }
60
+ }
61
+ const mutable = new Map();
62
+ for (const cycle of cycles.values()) {
63
+ const current = mutable.get(cycle.decision.armId) ?? { pulls: 0, completedPulls: 0, rewardSum: 0 };
64
+ current.pulls += 1;
65
+ if (cycle.reward !== undefined) {
66
+ current.completedPulls += 1;
67
+ current.rewardSum += cycle.reward;
68
+ }
69
+ mutable.set(cycle.decision.armId, current);
70
+ }
71
+ const arms = new Map();
72
+ for (const [armId, stats] of mutable) {
73
+ arms.set(armId, {
74
+ armId,
75
+ ...stats,
76
+ meanReward: stats.completedPulls > 0 ? stats.rewardSum / stats.completedPulls : 0,
77
+ });
78
+ }
79
+ return { arms, totalPulls: cycles.size };
80
+ }
81
+ function emptyStats(armId) {
82
+ return { armId, pulls: 0, completedPulls: 0, rewardSum: 0, meanReward: 0 };
83
+ }
84
+ /** Combine current asset identity with legacy gene-only history when that bridge is unambiguous. */
85
+ export function ucb1StatsForCandidate(history, geneId, assetId, includeLegacyGeneHistory = true) {
86
+ const armId = assetId ?? geneId;
87
+ const identities = assetId === undefined
88
+ ? [geneId]
89
+ : [...new Set([assetId, ...(includeLegacyGeneHistory ? [geneId] : [])])];
90
+ const matches = identities.map((identity) => history.arms.get(identity)).filter((stats) => stats !== undefined);
91
+ if (matches.length === 0)
92
+ return emptyStats(armId);
93
+ const pulls = matches.reduce((sum, stats) => sum + stats.pulls, 0);
94
+ const completedPulls = matches.reduce((sum, stats) => sum + stats.completedPulls, 0);
95
+ const rewardSum = matches.reduce((sum, stats) => sum + stats.rewardSum, 0);
96
+ return {
97
+ armId,
98
+ pulls,
99
+ completedPulls,
100
+ rewardSum,
101
+ meanReward: completedPulls > 0 ? rewardSum / completedPulls : 0,
102
+ };
103
+ }
104
+ function finiteBaseScore(value) {
105
+ return Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
106
+ }
107
+ function finiteCount(value) {
108
+ return Number.isFinite(value) ? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(Math.max(0, value))) : 0;
109
+ }
110
+ function finiteMeanReward(value) {
111
+ return Number.isFinite(value) ? Math.max(0, Math.min(1, value)) : 0;
112
+ }
113
+ function saturatingCountSum(total, value) {
114
+ const count = finiteCount(value);
115
+ return total >= Number.MAX_SAFE_INTEGER - count ? Number.MAX_SAFE_INTEGER : total + count;
116
+ }
117
+ /**
118
+ * Canonical UCB1: mean reward + sqrt(2 ln(total pulls) / arm pulls). Cold arms have +Infinity and are ordered
119
+ * deterministically by base score then arm identity. The caller supplies only the already-gated exploration window.
120
+ */
121
+ export function chooseUcb1Arm(arms, historyTotalPulls) {
122
+ if (arms.length === 0)
123
+ return { choice: null, fallbackReason: 'empty_pool' };
124
+ if (arms.some((arm) => !arm.explorationEligible))
125
+ return { choice: null, fallbackReason: 'ineligible_arm' };
126
+ const totalPulls = Math.max(1, finiteCount(historyTotalPulls), arms.reduce((sum, arm) => saturatingCountSum(sum, arm.stats.pulls), 0));
127
+ const ranked = arms.map((arm) => {
128
+ const pulls = finiteCount(arm.stats.pulls);
129
+ const meanReward = finiteMeanReward(arm.stats.meanReward);
130
+ const coldStart = pulls === 0;
131
+ const bonus = coldStart ? null : Math.sqrt((2 * Math.log(totalPulls)) / pulls);
132
+ const index = coldStart ? null : meanReward + bonus;
133
+ return { arm, pulls, meanReward, coldStart, bonus, index };
134
+ }).sort((left, right) => {
135
+ if (left.coldStart !== right.coldStart)
136
+ return left.coldStart ? -1 : 1;
137
+ if (left.index !== null && right.index !== null && left.index !== right.index)
138
+ return right.index - left.index;
139
+ const leftScore = finiteBaseScore(left.arm.baseScore);
140
+ const rightScore = finiteBaseScore(right.arm.baseScore);
141
+ return rightScore !== leftScore ? rightScore - leftScore : left.arm.armId.localeCompare(right.arm.armId);
142
+ });
143
+ const winner = ranked[0];
144
+ return {
145
+ choice: {
146
+ armId: winner.arm.armId,
147
+ pulls: winner.pulls,
148
+ completedPulls: finiteCount(winner.arm.stats.completedPulls),
149
+ totalPulls,
150
+ meanReward: winner.meanReward,
151
+ bonus: winner.bonus,
152
+ index: winner.index,
153
+ coldStart: winner.coldStart,
154
+ },
155
+ };
156
+ }
@@ -1,4 +1,4 @@
1
- import type { AssetSyncRecord } from './assetSyncLedger.js';
1
+ import type { AssetSyncInventorySegmentRecord, AssetSyncRecord, AssetSyncRunRecord } from './assetSyncLedger.js';
2
2
  import type { ProvenanceRecord } from './provenance.js';
3
3
  import type { ReviewRecord } from './reviewLedger.js';
4
4
  export type AssetSidecarKind = 'provenance' | 'review' | 'asset-sync';
@@ -20,4 +20,9 @@ export declare function parseSidecarJsonl<T>(raw: string, parseRecord: (value: u
20
20
  export declare function assertTrustSidecarHealthy<T>(sidecar: Extract<AssetSidecarKind, 'provenance' | 'review'>, parsed: ParsedSidecarJsonl<T>): void;
21
21
  export declare function parseProvenanceRecord(value: unknown): ProvenanceRecord | null;
22
22
  export declare function parseReviewRecord(value: unknown): ReviewRecord | null;
23
- export declare function parseAssetSyncRecord(value: unknown): AssetSyncRecord | null;
23
+ export declare function parseAssetSyncRecord(value: unknown): AssetSyncRecord | null;
24
+ export declare const ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES: number;
25
+ export type AssetSyncSidecarRecord = AssetSyncRecord | AssetSyncRunRecord | AssetSyncInventorySegmentRecord;
26
+ export declare function parseAssetSyncRunRecord(value: unknown): AssetSyncRunRecord | null;
27
+ export declare function parseAssetSyncInventorySegmentRecord(value: unknown): AssetSyncInventorySegmentRecord | null;
28
+ export declare function parseAssetSyncSidecarRecord(value: unknown): AssetSyncSidecarRecord | null;
@@ -62,6 +62,9 @@ export function parseProvenanceRecord(value) {
62
62
  const decidedBy = stringField(record, 'decidedBy');
63
63
  const promotedBy = stringField(record, 'promotedBy');
64
64
  const reason = stringField(record, 'reason');
65
+ const frozenContentId = stringField(record, 'frozenContentId');
66
+ if (frozenContentId !== undefined && !/^sha256:[0-9a-f]{64}$/.test(frozenContentId))
67
+ return null;
65
68
  return {
66
69
  assetId,
67
70
  source,
@@ -71,6 +74,7 @@ export function parseProvenanceRecord(value) {
71
74
  ...(decidedBy ? { decidedBy } : {}),
72
75
  ...(promotedBy ? { promotedBy } : {}),
73
76
  ...(reason ? { reason } : {}),
77
+ ...(frozenContentId ? { frozenContentId } : {}),
74
78
  };
75
79
  }
76
80
  export function parseReviewRecord(value) {
@@ -116,6 +120,10 @@ export function parseAssetSyncRecord(value) {
116
120
  }
117
121
  const logicalId = stringField(record, 'logicalId');
118
122
  const status = stringField(record, 'status');
123
+ const runKey = optionalStrictString(record, 'runKey');
124
+ const inventoryKey = optionalStrictString(record, 'inventoryKey');
125
+ if (runKey === null || inventoryKey === null)
126
+ return null;
119
127
  const forced = record['forced'] === true;
120
128
  const collisionWithAssetId = stringField(record, 'collisionWithAssetId');
121
129
  return {
@@ -125,12 +133,223 @@ export function parseAssetSyncRecord(value) {
125
133
  scope,
126
134
  syncedAt,
127
135
  remoteAssetId,
136
+ ...(runKey ? { runKey } : {}),
137
+ ...(inventoryKey ? { inventoryKey } : {}),
128
138
  ...(logicalId ? { logicalId } : {}),
129
139
  ...(status ? { status } : {}),
130
140
  ...(forced ? { forced: true } : {}),
131
141
  ...(collisionWithAssetId ? { collisionWithAssetId } : {}),
132
142
  };
133
143
  }
144
+ const MAX_INVENTORY_SEGMENTS = 24;
145
+ const MAX_INVENTORY_ITEMS_PER_SEGMENT = 10_000;
146
+ const MAX_INVENTORY_STRING_LENGTH = 4096;
147
+ export const ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES = 2 * 1024 * 1024;
148
+ export function parseAssetSyncRunRecord(value) {
149
+ const record = objectRecord(value);
150
+ if (!record || record['recordType'] !== 'run')
151
+ return null;
152
+ const runId = optionalStrictString(record, 'runId');
153
+ const runKey = optionalStrictString(record, 'runKey');
154
+ const state = optionalStrictString(record, 'state');
155
+ const currentTimestamp = optionalStrictString(record, 'syncedAt');
156
+ const legacyTimestamp = optionalStrictString(record, 'at');
157
+ if (!runId
158
+ || !runKey
159
+ || state === null
160
+ || !isRunState(state)
161
+ || currentTimestamp === null
162
+ || legacyTimestamp === null
163
+ || (currentTimestamp && legacyTimestamp && currentTimestamp !== legacyTimestamp)) {
164
+ return null;
165
+ }
166
+ const syncedAt = currentTimestamp ?? legacyTimestamp;
167
+ if (!syncedAt || Number.isNaN(Date.parse(syncedAt)))
168
+ return null;
169
+ const remoteAssetId = optionalStrictString(record, 'remoteAssetId');
170
+ const outcomeValue = optionalStrictString(record, 'outcome');
171
+ const reason = optionalStrictString(record, 'reason');
172
+ if (remoteAssetId === null || outcomeValue === null || reason === null)
173
+ return null;
174
+ const outcome = isRunOutcome(outcomeValue) ? outcomeValue : undefined;
175
+ if (outcomeValue !== undefined && !outcome)
176
+ return null;
177
+ let plan;
178
+ const rawPlan = record['plan'];
179
+ if (rawPlan !== undefined) {
180
+ if (!Array.isArray(rawPlan) || rawPlan.length === 0)
181
+ return null;
182
+ const normalized = rawPlan.map((entry) => typeof entry === 'string' ? entry.trim() : '');
183
+ if (normalized.some((entry, index) => !entry || entry !== rawPlan[index]))
184
+ return null;
185
+ if (new Set(normalized).size !== normalized.length)
186
+ return null;
187
+ plan = Object.freeze(normalized);
188
+ }
189
+ if (state === 'started' && (remoteAssetId || outcome || reason))
190
+ return null;
191
+ if (state === 'progress') {
192
+ if (!remoteAssetId || !outcome || plan)
193
+ return null;
194
+ if ((outcome === 'failed') !== Boolean(reason))
195
+ return null;
196
+ }
197
+ if (state === 'completed' && (remoteAssetId || outcome || reason || plan))
198
+ return null;
199
+ return immutableRunRecord({
200
+ recordType: 'run',
201
+ runId,
202
+ runKey,
203
+ state,
204
+ ...(remoteAssetId ? { remoteAssetId } : {}),
205
+ ...(outcome ? { outcome } : {}),
206
+ ...(reason ? { reason } : {}),
207
+ ...(plan ? { plan } : {}),
208
+ syncedAt,
209
+ });
210
+ }
211
+ export function parseAssetSyncInventorySegmentRecord(value) {
212
+ const record = objectRecord(value);
213
+ if (!record || record['recordType'] !== 'inventory_scan')
214
+ return null;
215
+ try {
216
+ if (Buffer.byteLength(JSON.stringify(record), 'utf8') > ASSET_SYNC_INVENTORY_MAX_SEGMENT_BYTES)
217
+ return null;
218
+ }
219
+ catch {
220
+ return null;
221
+ }
222
+ const scanId = strictBoundedString(record['scanId']);
223
+ const inventoryKey = strictBoundedString(record['inventoryKey']);
224
+ const scope = strictBoundedString(record['scope']);
225
+ const syncedAt = strictBoundedString(record['syncedAt']);
226
+ const index = record['index'];
227
+ const inputCursorFingerprints = parseInventoryCursorFingerprints(record['inputCursorFingerprints']);
228
+ const nextCursorFingerprints = parseInventoryCursorFingerprints(record['nextCursorFingerprints']);
229
+ const anonymousBlocked = record['anonymousBlocked'];
230
+ const cursorHeld = record['cursorHeld'];
231
+ const batchId = record['batchId'];
232
+ const batchIndex = record['batchIndex'];
233
+ const batchSize = record['batchSize'];
234
+ const rawItems = record['items'];
235
+ const hasBatchMetadata = batchId !== undefined || batchIndex !== undefined || batchSize !== undefined;
236
+ if (!scanId
237
+ || !inventoryKey
238
+ || (scope !== 'purchased' && scope !== 'published' && scope !== 'all')
239
+ || !syncedAt
240
+ || Number.isNaN(Date.parse(syncedAt))
241
+ || !Number.isInteger(index)
242
+ || index < 0
243
+ || index >= MAX_INVENTORY_SEGMENTS
244
+ || !inputCursorFingerprints
245
+ || !nextCursorFingerprints
246
+ || !Number.isInteger(anonymousBlocked)
247
+ || anonymousBlocked < 0
248
+ || anonymousBlocked > MAX_INVENTORY_ITEMS_PER_SEGMENT
249
+ || !Array.isArray(rawItems)
250
+ || rawItems.length > MAX_INVENTORY_ITEMS_PER_SEGMENT
251
+ || (cursorHeld !== undefined && cursorHeld !== true)
252
+ || (hasBatchMetadata && (!strictBoundedString(batchId)
253
+ || !Number.isInteger(batchIndex)
254
+ || !Number.isInteger(batchSize)
255
+ || batchSize < 2
256
+ || batchSize > MAX_INVENTORY_SEGMENTS
257
+ || batchIndex < 0
258
+ || batchIndex >= batchSize
259
+ || index - batchIndex < 0
260
+ || index + (batchSize - batchIndex) > MAX_INVENTORY_SEGMENTS))) {
261
+ return null;
262
+ }
263
+ if (scope === 'purchased'
264
+ && (inputCursorFingerprints.published !== null || nextCursorFingerprints.published !== null))
265
+ return null;
266
+ if (cursorHeld === true
267
+ && (inputCursorFingerprints.purchased !== nextCursorFingerprints.purchased
268
+ || inputCursorFingerprints.published !== nextCursorFingerprints.published))
269
+ return null;
270
+ if (scope === 'published'
271
+ && (inputCursorFingerprints.purchased !== null || nextCursorFingerprints.purchased !== null))
272
+ return null;
273
+ if (index === 0
274
+ && (inputCursorFingerprints.purchased !== null || inputCursorFingerprints.published !== null))
275
+ return null;
276
+ const items = [];
277
+ const seen = new Set();
278
+ for (const rawItem of rawItems) {
279
+ const item = objectRecord(rawItem);
280
+ if (!item)
281
+ return null;
282
+ const remoteAssetId = strictBoundedString(item['remoteAssetId']);
283
+ const outcome = strictBoundedString(item['outcome']);
284
+ if (!remoteAssetId || !isInventoryOutcome(outcome) || seen.has(remoteAssetId))
285
+ return null;
286
+ seen.add(remoteAssetId);
287
+ items.push(Object.freeze({ remoteAssetId, outcome }));
288
+ }
289
+ if (items.length + anonymousBlocked > MAX_INVENTORY_ITEMS_PER_SEGMENT)
290
+ return null;
291
+ return Object.freeze({
292
+ recordType: 'inventory_scan',
293
+ scanId,
294
+ inventoryKey,
295
+ scope,
296
+ index: index,
297
+ inputCursorFingerprints,
298
+ nextCursorFingerprints,
299
+ items: Object.freeze(items),
300
+ anonymousBlocked: anonymousBlocked,
301
+ ...(cursorHeld === true ? { cursorHeld: true } : {}),
302
+ ...(hasBatchMetadata ? {
303
+ batchId: batchId,
304
+ batchIndex: batchIndex,
305
+ batchSize: batchSize,
306
+ } : {}),
307
+ syncedAt,
308
+ });
309
+ }
310
+ export function parseAssetSyncSidecarRecord(value) {
311
+ const record = objectRecord(value);
312
+ if (!record)
313
+ return null;
314
+ if (record['recordType'] !== undefined) {
315
+ if (record['recordType'] === 'run')
316
+ return parseAssetSyncRunRecord(record);
317
+ if (record['recordType'] === 'inventory_scan')
318
+ return parseAssetSyncInventorySegmentRecord(record);
319
+ return null;
320
+ }
321
+ return parseAssetSyncRecord(record);
322
+ }
323
+ function parseInventoryCursorFingerprints(value) {
324
+ const record = objectRecord(value);
325
+ if (!record || !Object.hasOwn(record, 'purchased') || !Object.hasOwn(record, 'published'))
326
+ return null;
327
+ const purchased = strictNullableCursorFingerprint(record['purchased']);
328
+ const published = strictNullableCursorFingerprint(record['published']);
329
+ if (purchased === undefined || published === undefined)
330
+ return null;
331
+ return Object.freeze({ purchased, published });
332
+ }
333
+ function strictNullableCursorFingerprint(value) {
334
+ if (value === null)
335
+ return null;
336
+ return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value) ? value : undefined;
337
+ }
338
+ function strictBoundedString(value) {
339
+ return typeof value === 'string'
340
+ && value.length <= MAX_INVENTORY_STRING_LENGTH
341
+ && value.trim()
342
+ && value === value.trim()
343
+ ? value
344
+ : undefined;
345
+ }
346
+ function isInventoryOutcome(value) {
347
+ return value === 'imported'
348
+ || value === 'already_local'
349
+ || value === 'blocked'
350
+ || value === 'failed'
351
+ || value === 'pending';
352
+ }
134
353
  function objectRecord(value) {
135
354
  return value && typeof value === 'object' && !Array.isArray(value)
136
355
  ? value
@@ -139,4 +358,23 @@ function objectRecord(value) {
139
358
  function stringField(value, key) {
140
359
  const raw = value[key];
141
360
  return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
361
+ }
362
+ function optionalStrictString(value, key) {
363
+ const raw = value[key];
364
+ if (raw === undefined)
365
+ return undefined;
366
+ return typeof raw === 'string' && raw.trim() && raw === raw.trim() ? raw : null;
367
+ }
368
+ function immutableRunRecord(record) {
369
+ const plan = record.plan ? Object.freeze([...record.plan]) : undefined;
370
+ return Object.freeze({ ...record, ...(plan ? { plan } : {}) });
371
+ }
372
+ function isRunState(value) {
373
+ return value === 'started' || value === 'progress' || value === 'completed';
374
+ }
375
+ function isRunOutcome(value) {
376
+ return value === 'imported'
377
+ || value === 'already_local'
378
+ || value === 'failed'
379
+ || value === 'remote_missing';
142
380
  }