@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,17 +1,45 @@
1
1
  import { ulid as makeUlid } from 'ulid';
2
2
  import { computeAssetId, SCHEMA_VERSION } from '../wire/index.js';
3
- /** ProofOfWork 是否表明有实际产出(解锁非 coding agent, 批注#17/#19). */
3
+ import { gitDiffOf, artifactHashOf, externalReceiptOf, toolCallTraceOf, proofOfWorkForWrite } from '../schema/proofOfWork.js';
4
+ /** ProofOfWork 是否表明有实际产出(解锁非 coding agent, 批注#17/#19). #961: 兼容读 helper(snake_case 优先, 回退旧 camelCase 存量). */
4
5
  export function proofIndicatesOutput(p) {
5
6
  if (!p)
6
7
  return false;
7
8
  switch (p.kind) {
8
- case 'git_diff': return (p.gitDiff?.files ?? 0) > 0 || (p.gitDiff?.lines ?? 0) > 0;
9
- case 'artifact_hash': return Boolean(p.artifactHash?.sha256);
10
- case 'external_receipt': return Boolean(p.externalReceipt?.receiptId);
11
- case 'tool_call_trace': return (p.toolCallTrace?.calls ?? 0) > 0;
9
+ case 'git_diff': return (gitDiffOf(p)?.files ?? 0) > 0 || (gitDiffOf(p)?.lines ?? 0) > 0;
10
+ case 'artifact_hash': return Boolean(artifactHashOf(p)?.sha256);
11
+ case 'external_receipt': {
12
+ const r = externalReceiptOf(p);
13
+ return Boolean(r?.receipt_id ?? r?.receiptId);
14
+ }
15
+ case 'tool_call_trace': return (toolCallTraceOf(p)?.calls ?? 0) > 0;
12
16
  default: return false;
13
17
  }
14
18
  }
19
+ function nonEmpty(s) {
20
+ return typeof s === 'string' && s.trim().length > 0 ? s.trim() : undefined;
21
+ }
22
+ function failureIdentityTrace(input) {
23
+ if (input.outcome.status !== 'failed' || !input.failureIdentity)
24
+ return undefined;
25
+ const trace = { stage: 'validate' };
26
+ const root = nonEmpty(input.failureIdentity.rootAttemptId);
27
+ const execution = nonEmpty(input.failureIdentity.executionId);
28
+ const failure = nonEmpty(input.failureIdentity.failureId);
29
+ const verifier = nonEmpty(input.failureIdentity.verifierDigest);
30
+ const artifact = nonEmpty(input.failureIdentity.artifactDigest);
31
+ if (root)
32
+ trace['root_attempt_id'] = root;
33
+ if (execution)
34
+ trace['execution_id'] = execution;
35
+ if (failure)
36
+ trace['failure_id'] = failure;
37
+ if (verifier)
38
+ trace['verifier_digest'] = verifier;
39
+ if (artifact)
40
+ trace['artifact_digest'] = artifact;
41
+ return Object.keys(trace).length > 1 ? [trace] : undefined;
42
+ }
15
43
  /**
16
44
  * 两级 resolution(M4A-8) + ProofOfWork 判价值(M4A-5):
17
45
  * - 失败: 分 < 阈 → regressed, 否则 inconclusive.
@@ -44,7 +72,8 @@ export function solidify(input) {
44
72
  // (artifact_hash/external_receipt/tool_call_trace) gd is undefined and blast falls back to {0,0} — which here
45
73
  // means "blast unknown", NOT "no change". Consumers that read blast=0 as a no-op signal (e.g. cycleFailure's
46
74
  // local_gene_no_blast bucket) MUST first confirm the proof kind is git_diff; the cycleEngine guard does this.
47
- const gd = input.proofOfWork?.kind === 'git_diff' ? input.proofOfWork.gitDiff : undefined;
75
+ const gd = input.proofOfWork?.kind === 'git_diff' ? gitDiffOf(input.proofOfWork) : undefined;
76
+ const identityTrace = failureIdentityTrace(input);
48
77
  const base = {
49
78
  type: 'Capsule',
50
79
  schema_version: SCHEMA_VERSION,
@@ -56,7 +85,8 @@ export function solidify(input) {
56
85
  blast_radius: { files: gd?.files ?? 0, lines: gd?.lines ?? 0 },
57
86
  outcome: input.outcome,
58
87
  resolution_status: status,
59
- ...(input.proofOfWork ? { proof_of_work: input.proofOfWork } : {}),
88
+ ...(input.proofOfWork ? { proof_of_work: proofOfWorkForWrite(input.proofOfWork) } : {}),
89
+ ...(identityTrace ? { execution_trace: identityTrace } : {}),
60
90
  };
61
91
  const asset_id = computeAssetId(base);
62
92
  return { capsule: { ...base, asset_id }, resolutionStatus: status, producedValue, reasons };
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ import type { RepairIssue } from './repair.js';
2
+ /** Field-level issues reported by the Hub, keyed to the asset index they belong to (`-1` = bundle-level). */
3
+ export interface HubRejectionReport {
4
+ issues: RepairIssue[];
5
+ /** Issues for one asset of the published bundle, by its position in `payload.assets`. */
6
+ byAssetIndex: Map<number, RepairIssue[]>;
7
+ }
8
+ export interface HubRejectionOptions {
9
+ /** The bundle's asset types in `payload.assets` order, so a `<type>_*` rule reason routes to its own asset. */
10
+ assetTypes?: readonly (string | undefined)[];
11
+ }
12
+ export declare function hubRejectionIssues(body: unknown, options?: HubRejectionOptions): HubRejectionReport;
@@ -0,0 +1,109 @@
1
+ const MAX_ISSUES = 50;
2
+ const MAX_MESSAGE_CHARS = 300;
3
+ const REASON_TOKEN = /^[a-z][a-z0-9_]*$/;
4
+ const REASON_PREFIXES = [
5
+ { prefix: 'gene', assetType: 'Gene' },
6
+ { prefix: 'capsule', assetType: 'Capsule' },
7
+ { prefix: 'event', assetType: 'EvolutionEvent' },
8
+ { prefix: 'bundle' },
9
+ ];
10
+ const REPAIRABLE_REASON_PATHS = new Map([
11
+ ['asset_id_verification_failed', 'asset_id'],
12
+ ['missing_asset_id', 'asset_id'],
13
+ ]);
14
+ export function hubRejectionIssues(body, options = {}) {
15
+ const fieldReport = fieldLevelIssues(body);
16
+ if (fieldReport.issues.length > 0)
17
+ return fieldReport;
18
+ return ruleReasonIssues(body, options.assetTypes ?? []);
19
+ }
20
+ function fieldLevelIssues(body) {
21
+ const report = { issues: [], byAssetIndex: new Map() };
22
+ for (const raw of detailRows(body).slice(0, MAX_ISSUES)) {
23
+ const detail = asRecord(raw);
24
+ if (!detail)
25
+ continue;
26
+ const path = pathSegments(detail['path']);
27
+ const message = text(detail['message']) ?? text(detail['code']);
28
+ if (!message)
29
+ continue;
30
+ const issue = {
31
+ path: assetRelativePath(path),
32
+ keyword: text(detail['code']) ?? 'invalid',
33
+ message: message.slice(0, MAX_MESSAGE_CHARS),
34
+ source: 'hub',
35
+ };
36
+ report.issues.push(issue);
37
+ const index = assetIndex(path);
38
+ const bucket = report.byAssetIndex.get(index);
39
+ if (bucket)
40
+ bucket.push(issue);
41
+ else
42
+ report.byAssetIndex.set(index, [issue]);
43
+ }
44
+ return report;
45
+ }
46
+ function ruleReasonIssues(body, assetTypes) {
47
+ const report = { issues: [], byAssetIndex: new Map() };
48
+ const reason = ruleReason(body);
49
+ if (!reason)
50
+ return report;
51
+ const prefix = REASON_PREFIXES.find((entry) => reason.token.startsWith(`${entry.prefix}_`));
52
+ if (!prefix)
53
+ return report;
54
+ const issue = {
55
+ path: REPAIRABLE_REASON_PATHS.get(reason.token.slice(prefix.prefix.length + 1)) ?? '',
56
+ keyword: reason.token,
57
+ message: reason.message.slice(0, MAX_MESSAGE_CHARS),
58
+ source: 'hub',
59
+ };
60
+ const index = prefix.assetType === undefined ? -1 : assetTypes.indexOf(prefix.assetType);
61
+ report.issues.push(issue);
62
+ report.byAssetIndex.set(index, [issue]);
63
+ return report;
64
+ }
65
+ function ruleReason(body) {
66
+ const root = asRecord(body);
67
+ if (!root)
68
+ return undefined;
69
+ const payload = asRecord(root['payload']);
70
+ const raw = [root['error'], root['reason'], payload?.['error'], payload?.['reason']]
71
+ .map((candidate) => text(candidate))
72
+ .find((candidate) => candidate !== undefined);
73
+ if (!raw)
74
+ return undefined;
75
+ const token = raw.split(":", 1)[0]?.trim() ?? '';
76
+ return REASON_TOKEN.test(token) ? { token, message: raw } : undefined;
77
+ }
78
+ function detailRows(body) {
79
+ const root = asRecord(body);
80
+ if (!root)
81
+ return [];
82
+ const payload = asRecord(root['payload']);
83
+ for (const candidate of [root['details'], root['errors'], payload?.['details'], payload?.['errors']]) {
84
+ if (Array.isArray(candidate))
85
+ return candidate;
86
+ }
87
+ return [];
88
+ }
89
+ function pathSegments(value) {
90
+ if (!Array.isArray(value))
91
+ return typeof value === 'string' ? value.split('.') : [];
92
+ return value.filter((segment) => typeof segment === 'string' || typeof segment === 'number');
93
+ }
94
+ /** `['payload','assets',0,'constraints','forbidden_paths']` → `constraints.forbidden_paths`. */
95
+ function assetRelativePath(path) {
96
+ const at = path.findIndex((segment) => typeof segment === 'number');
97
+ const tail = at >= 0 ? path.slice(at + 1) : path.filter((segment) => segment !== 'payload');
98
+ return tail.join('.');
99
+ }
100
+ function assetIndex(path) {
101
+ const found = path.find((segment) => typeof segment === 'number');
102
+ return typeof found === 'number' ? found : -1;
103
+ }
104
+ function text(value) {
105
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
106
+ }
107
+ function asRecord(value) {
108
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
109
+ }
@@ -0,0 +1,2 @@
1
+ export { repairAssetRecord, type RepairChange, type RepairIssue, type RepairOptions, type RepairReport, type RepairStatus, } from './repair.js';
2
+ export { hubRejectionIssues, type HubRejectionReport } from './hubRejection.js';
@@ -0,0 +1,2 @@
1
+ export { repairAssetRecord, } from './repair.js';
2
+ export { hubRejectionIssues } from './hubRejection.js';
@@ -0,0 +1,33 @@
1
+ import type { AssetRecord } from '../assetstore/index.js';
2
+ export type RepairStatus = 'already_valid' | 'repaired' | 'unrepairable';
3
+ export interface RepairIssue {
4
+ path: string;
5
+ keyword: string;
6
+ message: string;
7
+ /** `schema` = found by the local gep-sdk gate; `hub` = reported by the network in its rejection body. */
8
+ source: 'schema' | 'hub';
9
+ }
10
+ export interface RepairChange {
11
+ path: string;
12
+ action: 'added' | 'removed' | 'replaced';
13
+ note: string;
14
+ }
15
+ export interface RepairReport {
16
+ status: RepairStatus;
17
+ /** The accepted-by-schema record. Absent only when blockers remain (`status: 'unrepairable'`). */
18
+ asset?: AssetRecord;
19
+ changes: RepairChange[];
20
+ blockers: RepairIssue[];
21
+ }
22
+ export interface RepairOptions {
23
+ /** Field-level issues the network itself reported — see `hubRejectionIssues`. Stricter than the GEP schema
24
+ * (the Hub adds envelope rules of its own), so they are folded in rather than re-derived locally. */
25
+ hubIssues?: readonly RepairIssue[];
26
+ }
27
+ type JsonRecord = Record<string, unknown>;
28
+ /**
29
+ * Repair `input` into a record the GEP schema (and, when supplied, the Hub's own rules) accepts.
30
+ * Pure: `input` is never mutated, and nothing is written anywhere — the caller decides what to do with the result.
31
+ */
32
+ export declare function repairAssetRecord(input: JsonRecord, options?: RepairOptions): RepairReport;
33
+ export {};
@@ -0,0 +1,155 @@
1
+ // Asset repair — turn a GEP record the network refuses into one it accepts, WITHOUT inventing content.
2
+ //
3
+ // Two producers of refused records exist and both were wasting assets outright:
4
+ // * publish: the Hub rejects a bundle field-by-field (`details[]`), the operator gets a flat error, and the
5
+ // distilled asset is dropped on the floor.
6
+ // * reuse/fetch: the Hub delivers a lossy projection of a published record (dropped `schema_version`, dropped
7
+ // logical `id`, emptied `constraints.forbidden_paths`, extra non-schema keys such as `model_name`), which
8
+ // then fails the local wire gate and is discarded — reported to the operator as "not found".
9
+ //
10
+ // The line this module refuses to cross: a repair may only restore fields whose value is DERIVABLE (recomputed
11
+ // `asset_id`), CONSTANT (`schema_version`), or a strictly-narrowing default (`constraints`). Anything carrying
12
+ // an evidence or meaning claim — a Capsule's `outcome`/`confidence`/`blast_radius`, a Gene's `strategy`, a
13
+ // human summary — is never fabricated: it is reported as a blocker so the operator (or a re-distill) supplies it.
14
+ import { computeAssetId, wireSchemaIssues } from '../wire/index.js';
15
+ import { SCHEMA_VERSION } from '@evomap/gep-sdk';
16
+ const DEFAULT_MAX_FILES = 12;
17
+ const DEFAULT_FORBIDDEN_PATHS = ['.git', 'node_modules'];
18
+ const CONTENT_ASSET_ID = /^sha256:[0-9a-f]{64}$/;
19
+ const SEMVER = /^\d+\.\d+\.\d+$/;
20
+ /**
21
+ * Repair `input` into a record the GEP schema (and, when supplied, the Hub's own rules) accepts.
22
+ * Pure: `input` is never mutated, and nothing is written anywhere — the caller decides what to do with the result.
23
+ */
24
+ export function repairAssetRecord(input, options = {}) {
25
+ const hubIssues = options.hubIssues ?? [];
26
+ const before = wireSchemaIssues(input);
27
+ if (before.length === 0 && hubIssues.length === 0 && assetIdIsCurrent(input)) {
28
+ return { status: 'already_valid', asset: input, changes: [], blockers: [] };
29
+ }
30
+ const changes = [];
31
+ const draft = { ...input };
32
+ restoreSchemaVersion(draft, changes);
33
+ dropUnknownProperties(draft, before, changes);
34
+ restoreGeneConstraints(draft, changes);
35
+ restoreLogicalId(draft, changes);
36
+ restoreAssetId(draft, changes);
37
+ const remaining = wireSchemaIssues(draft);
38
+ const blockers = [...remaining.map(schemaBlocker), ...unresolvedHubIssues(hubIssues, changes)];
39
+ if (blockers.length > 0)
40
+ return { status: 'unrepairable', changes, blockers };
41
+ return { status: 'repaired', asset: draft, changes, blockers: [] };
42
+ }
43
+ /**
44
+ * Hub rules the local schema cannot see (a minimum summary length, a bundle arity) stay blockers — but a hub
45
+ * complaint about a field this pass just mended is answered, and keeping it would report every repairable
46
+ * asset as unrepairable.
47
+ */
48
+ function unresolvedHubIssues(issues, changes) {
49
+ return issues.filter((issue) => !changes.some((change) => pathsOverlap(change.path, issue.path)));
50
+ }
51
+ function pathsOverlap(repaired, reported) {
52
+ return repaired === reported
53
+ || reported.startsWith(`${repaired}.`)
54
+ || repaired.startsWith(`${reported}.`);
55
+ }
56
+ /** True when the record already declares the id its own content hashes to — the invariant `restoreAssetId` holds. */
57
+ function assetIdIsCurrent(record) {
58
+ const declared = record['asset_id'];
59
+ return typeof declared === 'string' && declared === safeComputeAssetId(record);
60
+ }
61
+ function restoreSchemaVersion(draft, changes) {
62
+ const current = draft['schema_version'];
63
+ if (typeof current === 'string' && SEMVER.test(current))
64
+ return;
65
+ draft['schema_version'] = SCHEMA_VERSION;
66
+ changes.push({
67
+ path: 'schema_version',
68
+ action: current === undefined ? 'added' : 'replaced',
69
+ note: `set to the SDK schema version ${SCHEMA_VERSION}`,
70
+ });
71
+ }
72
+ // `additionalProperties: false` means an unknown key is fatal, so a delivery that adds `model_name` is
73
+ // unusable as-is. Dropping is the only content-preserving move: the key is not part of the GEP contract, so
74
+ // nothing that reads the record by contract can miss it. Every dropped key is named in the report.
75
+ function dropUnknownProperties(draft, issues, changes) {
76
+ for (const issue of issues) {
77
+ if (issue.keyword !== 'additionalProperties' || !issue.property)
78
+ continue;
79
+ if (!(issue.property in draft))
80
+ continue;
81
+ delete draft[issue.property];
82
+ changes.push({ path: issue.path, action: 'removed', note: 'not declared by the GEP schema' });
83
+ }
84
+ }
85
+ // A Gene's constraints are a CEILING on what an executing agent may touch. Restoring the default is therefore
86
+ // safe in one direction only: the default must be at least as restrictive as a missing/empty value, which
87
+ // (no ceiling at all) it always is.
88
+ function restoreGeneConstraints(draft, changes) {
89
+ if (draft['type'] !== 'Gene')
90
+ return;
91
+ const current = asRecord(draft['constraints']);
92
+ if (!current) {
93
+ draft['constraints'] = { max_files: DEFAULT_MAX_FILES, forbidden_paths: [...DEFAULT_FORBIDDEN_PATHS] };
94
+ changes.push({ path: 'constraints', action: 'added', note: 'restored the default execution ceiling' });
95
+ return;
96
+ }
97
+ const repaired = { ...current };
98
+ const maxFiles = repaired['max_files'];
99
+ if (typeof maxFiles !== 'number' || !Number.isInteger(maxFiles) || maxFiles < 1) {
100
+ repaired['max_files'] = DEFAULT_MAX_FILES;
101
+ changes.push({ path: 'constraints.max_files', action: maxFiles === undefined ? 'added' : 'replaced', note: `restored the default ceiling ${DEFAULT_MAX_FILES}` });
102
+ }
103
+ const forbidden = repaired['forbidden_paths'];
104
+ const usable = Array.isArray(forbidden) && forbidden.length > 0 && forbidden.every((entry) => typeof entry === 'string');
105
+ if (!usable) {
106
+ repaired['forbidden_paths'] = [...DEFAULT_FORBIDDEN_PATHS];
107
+ changes.push({ path: 'constraints.forbidden_paths', action: forbidden === undefined ? 'added' : 'replaced', note: 'restored the default forbidden paths' });
108
+ }
109
+ draft['constraints'] = repaired;
110
+ }
111
+ // A logical id names the asset for humans and for Capsule→Gene binding; it carries no claim about the content.
112
+ // A delivery that dropped it cannot be re-bound to its original name (that name is not recoverable from the
113
+ // bytes), so the derived one is marked `repaired_` — visibly synthetic, and stable for the same content.
114
+ function restoreLogicalId(draft, changes) {
115
+ const current = draft['id'];
116
+ if (typeof current === 'string' && current.length > 0)
117
+ return;
118
+ const type = typeof draft['type'] === 'string' ? draft['type'].toLowerCase() : 'asset';
119
+ const digest = safeComputeAssetId(draft)?.slice('sha256:'.length, 'sha256:'.length + 12);
120
+ if (!digest)
121
+ return;
122
+ draft['id'] = `${type}_repaired_${digest}`;
123
+ changes.push({ path: 'id', action: 'added', note: 'derived a stable placeholder id from the content digest' });
124
+ }
125
+ // Content-addressing means `asset_id` is a FUNCTION of the rest of the record, so it is the one field that must
126
+ // be recomputed last — every repair above changes what the content hashes to.
127
+ function restoreAssetId(draft, changes) {
128
+ const declared = draft['asset_id'];
129
+ const computed = safeComputeAssetId(draft);
130
+ if (!computed || !CONTENT_ASSET_ID.test(computed))
131
+ return;
132
+ if (declared === computed)
133
+ return;
134
+ draft['asset_id'] = computed;
135
+ changes.push({
136
+ path: 'asset_id',
137
+ action: typeof declared === 'string' ? 'replaced' : 'added',
138
+ note: 'recomputed the content id over the repaired record',
139
+ });
140
+ }
141
+ function safeComputeAssetId(record) {
142
+ try {
143
+ const id = computeAssetId(record);
144
+ return typeof id === 'string' && id.length > 0 ? id : undefined;
145
+ }
146
+ catch {
147
+ return undefined;
148
+ }
149
+ }
150
+ function schemaBlocker(issue) {
151
+ return { path: issue.path, keyword: issue.keyword, message: issue.message, source: 'schema' };
152
+ }
153
+ function asRecord(value) {
154
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
155
+ }
@@ -118,7 +118,7 @@ export function parseAssetSyncRecord(value) {
118
118
  || !remoteAssetId) {
119
119
  return null;
120
120
  }
121
- const logicalId = stringField(record, 'logicalId');
121
+ const logicalId = rawStringField(record, 'logicalId');
122
122
  const status = stringField(record, 'status');
123
123
  const runKey = optionalStrictString(record, 'runKey');
124
124
  const inventoryKey = optionalStrictString(record, 'inventoryKey');
@@ -359,6 +359,10 @@ function stringField(value, key) {
359
359
  const raw = value[key];
360
360
  return typeof raw === 'string' && raw.trim() ? raw.trim() : undefined;
361
361
  }
362
+ function rawStringField(value, key) {
363
+ const raw = value[key];
364
+ return typeof raw === 'string' && raw.length > 0 ? raw : undefined;
365
+ }
362
366
  function optionalStrictString(value, key) {
363
367
  const raw = value[key];
364
368
  if (raw === undefined)
@@ -16,9 +16,8 @@ export interface AssetStoreFileHealth {
16
16
  corruptRows: number;
17
17
  hashMismatchRows: number;
18
18
  /**
19
- * Rows that fail content-hash verification but are provenance-marked as an unverified hub reuse
20
- * (evolver-v2#570). These are EXPECTED the hub rewrote the delivered bytes and reuse froze them untrusted —
21
- * so they are a benign category, counted separately and never degrading the store.
19
+ * Rows that fail content-hash verification but carry an active, content-bound unverified provenance waiver.
20
+ * They remain untrusted and are counted separately instead of being misclassified as store corruption.
22
21
  */
23
22
  unverifiedRows: number;
24
23
  schemaInvalidRows: number;