@evomap/evolver-core 2.0.0-beta.2 → 2.0.0-beta.3

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 (58) hide show
  1. package/dist/algo/candidateAssembly.js +9 -6
  2. package/dist/algo/cycleEngine.d.ts +11 -0
  3. package/dist/algo/cycleEngine.js +12 -6
  4. package/dist/algo/cycleFailureClassifier.d.ts +1 -1
  5. package/dist/algo/geneSelection.d.ts +12 -1
  6. package/dist/algo/geneSelection.js +24 -8
  7. package/dist/algo/index.d.ts +1 -0
  8. package/dist/algo/index.js +1 -0
  9. package/dist/algo/memoryGraph.d.ts +62 -0
  10. package/dist/algo/memoryGraph.js +86 -0
  11. package/dist/algo/orchestrator.d.ts +3 -0
  12. package/dist/algo/orchestrator.js +14 -2
  13. package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
  14. package/dist/assetstore/assetSidecarRecords.js +142 -0
  15. package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
  16. package/dist/assetstore/assetSidecarRecovery.js +288 -0
  17. package/dist/assetstore/assetStoreHealth.d.ts +75 -0
  18. package/dist/assetstore/assetStoreHealth.js +277 -0
  19. package/dist/assetstore/assetStoreLayout.d.ts +2 -0
  20. package/dist/assetstore/assetStoreLayout.js +6 -0
  21. package/dist/assetstore/assetStoreStorage.d.ts +42 -0
  22. package/dist/assetstore/assetStoreStorage.js +318 -0
  23. package/dist/assetstore/assetSyncLedger.d.ts +5 -1
  24. package/dist/assetstore/assetSyncLedger.js +44 -64
  25. package/dist/assetstore/index.d.ts +2 -0
  26. package/dist/assetstore/index.js +2 -0
  27. package/dist/assetstore/localJsonl.d.ts +1 -0
  28. package/dist/assetstore/localJsonl.js +36 -32
  29. package/dist/assetstore/provenance.d.ts +13 -0
  30. package/dist/assetstore/provenance.js +60 -84
  31. package/dist/assetstore/provider.d.ts +2 -0
  32. package/dist/assetstore/reviewFilter.js +3 -1
  33. package/dist/assetstore/reviewLedger.d.ts +8 -2
  34. package/dist/assetstore/reviewLedger.js +71 -45
  35. package/dist/benchmark/index.d.ts +2 -1
  36. package/dist/benchmark/index.js +2 -1
  37. package/dist/benchmark/triggerShift.d.ts +62 -0
  38. package/dist/benchmark/triggerShift.js +106 -0
  39. package/dist/events/ingest.d.ts +1 -1
  40. package/dist/events/ingest.js +2 -0
  41. package/dist/events/paths.d.ts +1 -1
  42. package/dist/events/paths.js +2 -2
  43. package/dist/exec/autoExec.d.ts +6 -1
  44. package/dist/exec/autoExec.js +31 -0
  45. package/dist/exec/autonomousCycle.d.ts +2 -0
  46. package/dist/exec/autonomousCycle.js +5 -0
  47. package/dist/exec/claudeBridge.d.ts +7 -2
  48. package/dist/exec/claudeBridge.js +92 -14
  49. package/dist/exec/prompt.js +9 -0
  50. package/dist/exec/runnerRegistry.d.ts +56 -12
  51. package/dist/exec/runnerRegistry.js +272 -22
  52. package/dist/hub/bindings.js +12 -2
  53. package/dist/ops/savingsCore.js +1 -2
  54. package/dist/ops/selfUpdate.d.ts +10 -1
  55. package/dist/ops/selfUpdate.js +64 -15
  56. package/dist/util/fileLock.d.ts +19 -2
  57. package/dist/util/fileLock.js +166 -31
  58. package/package.json +5 -1
@@ -18,14 +18,15 @@ function candidateIds(geneId, assetId) {
18
18
  function isBannedCandidate(banned, geneId, assetId) {
19
19
  return candidateIds(geneId, assetId).some((id) => banned.has(id));
20
20
  }
21
- function passesInjectedCandidateGates(candidate, opts, provenance) {
21
+ function passesInjectedCandidateGates(candidate, opts, provenance, review) {
22
22
  const assetId = candidate.assetId;
23
23
  if (opts.provenance && !opts.includeUntrusted) {
24
24
  if (!assetId || provenance?.get(assetId)?.trusted !== true)
25
25
  return false;
26
26
  }
27
27
  if (opts.review) {
28
- if (!assetId || !opts.review.isApproved(assetId))
28
+ const reviewRecord = assetId ? review?.get(assetId) : undefined;
29
+ if (!assetId || (reviewRecord !== undefined && reviewRecord.state !== 'approved'))
29
30
  return false;
30
31
  }
31
32
  return true;
@@ -80,6 +81,7 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
80
81
  const banned = await computeBans(store, signals, limit);
81
82
  const sigSet = new Set(signals);
82
83
  const provenance = opts.provenance?.snapshot();
84
+ const review = opts.review?.snapshot();
83
85
  const out = [];
84
86
  const distilledFallback = [];
85
87
  const antiWarnings = [];
@@ -92,8 +94,9 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
92
94
  // out too. No record → eligible (cycle/migrate genes). Symmetric to the provenance trust-first filter above.
93
95
  // Probation (#306): includeProbation lets a quarantined draft be TRIED so it can earn evidence; a rejected
94
96
  // draft is never tried. Off (default) keeps quarantined drafts out until approval.
95
- if (opts.review && !opts.review.isApproved(String(g.asset_id))
96
- && (!opts.includeProbation || opts.review.get(String(g.asset_id))?.state === 'rejected'))
97
+ const reviewRecord = review?.get(String(g.asset_id));
98
+ if (opts.review && reviewRecord !== undefined && reviewRecord.state !== 'approved'
99
+ && (!opts.includeProbation || reviewRecord.state === 'rejected'))
97
100
  continue;
98
101
  const geneId = typeof g['id'] === 'string' ? String(g['id']) : String(g.asset_id);
99
102
  const assetId = String(g.asset_id);
@@ -148,7 +151,7 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
148
151
  if (opts.hubCandidates && opts.hubCandidates.length > 0) {
149
152
  const localIds = new Set(out.map((c) => c.geneId));
150
153
  for (const h of opts.hubCandidates) {
151
- if (!passesInjectedCandidateGates(h, opts, provenance))
154
+ if (!passesInjectedCandidateGates(h, opts, provenance, review))
152
155
  continue;
153
156
  if (localIds.has(h.geneId))
154
157
  continue; // a trusted local gene already covers this id
@@ -166,7 +169,7 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
166
169
  // AntiGene is negative memory that changes an autonomous prompt. Unlike legacy/cycle-authored Genes, it must
167
170
  // never inherit ReviewLedger's backward-compatible "no record = approved" default: no ledger, no record,
168
171
  // quarantined, and rejected all fail closed. Only an explicit human approval can enable warning injection.
169
- if (!opts.review?.isExplicitlyApproved(String(a.asset_id)))
172
+ if (review?.get(String(a.asset_id))?.state !== 'approved')
170
173
  continue;
171
174
  const trigger = asStrings(a['trigger']);
172
175
  const avoid = asStrings(a['avoid']);
@@ -10,11 +10,13 @@ import type { PersonalityStore } from '../personality/store.js';
10
10
  import { type ResolutionStatus } from './solidify.js';
11
11
  import type { ProofOfWork } from '../schema/proofOfWork.js';
12
12
  import { type ClassifyRecentEvent } from './cycleFailureClassifier.js';
13
+ import type { MemoryGraphGeneEvidence } from './memoryGraph.js';
13
14
  export interface TriggerEval {
14
15
  trigger: boolean;
15
16
  reasons: string[];
16
17
  valueScore: number;
17
18
  }
19
+ export type ExecutionFailureKind = 'spawn_failed' | 'timeout' | 'cancelled' | 'permission_denied' | 'non_zero_exit' | 'invalid_output' | 'runtime_error';
18
20
  export interface ExecutionResult {
19
21
  outcome: {
20
22
  status: 'success' | 'failed';
@@ -23,6 +25,9 @@ export interface ExecutionResult {
23
25
  };
24
26
  proofOfWork?: ProofOfWork;
25
27
  strongEvidence?: boolean;
28
+ /** Structured runner failure metadata. Safe to persist; unlike sessionLog it contains no transcript text. */
29
+ failureKind?: ExecutionFailureKind;
30
+ exitCode?: number | null;
26
31
  /**
27
32
  * The agent run's transcript (stdout, plus stderr when present), attached by the execution layer on a FAILED
28
33
  * outcome. It is the host-side context classifyCycleFailure needs to reach the host_no_transcript /
@@ -102,6 +107,8 @@ export interface CycleInput {
102
107
  distilledFallback?: readonly GeneCandidateInput[];
103
108
  /** Advisory-only AntiGene warnings matched upstream for this cycle's base signals. */
104
109
  antiWarnings?: readonly AntiWarning[];
110
+ /** Bounded structured outcome evidence for explainability and prompt enrichment. */
111
+ memoryEvidence?: readonly MemoryGraphGeneEvidence[];
105
112
  /**
106
113
  * Optional triage context for the cycle.failed event (PORT v1 #279 issue-reporter half). When the caller can
107
114
  * supply a session transcript or a list of recent failed cycles, classifyCycleFailure runs and the resulting
@@ -125,11 +132,15 @@ export interface CycleResult {
125
132
  cycleId: string;
126
133
  triggered: boolean;
127
134
  finalStage: CycleStage;
135
+ /** Real CycleEngine returns this on every path; optional keeps injected legacy engines source-compatible. */
136
+ producedValue?: boolean;
128
137
  decision?: GeneDecision;
129
138
  mutation?: Mutation;
130
139
  capsule?: Capsule;
131
140
  event?: EvolutionEvent;
132
141
  resolutionStatus?: ResolutionStatus;
142
+ failureKind?: ExecutionFailureKind;
143
+ exitCode?: number | null;
133
144
  reasons: string[];
134
145
  }
135
146
  /**
@@ -200,6 +200,10 @@ export class CycleEngine {
200
200
  });
201
201
  return r.reason ? { failure_class: r.failureClass, failure_class_reason: r.reason } : { failure_class: r.failureClass };
202
202
  };
203
+ const executionMetadata = (exec) => ({
204
+ ...(exec.failureKind !== undefined ? { failureKind: exec.failureKind } : {}),
205
+ ...(exec.exitCode !== undefined ? { exitCode: exec.exitCode } : {}),
206
+ });
203
207
  const strategy = resolveStrategy({ name: input.strategyName, signals: cycleSignals, cycleCount: historyCycleCount(ingestor, 1000, input.cycleId) });
204
208
  const cycleCategory = categoryForStrategy(input.category, strategy);
205
209
  let stage = 'none';
@@ -226,7 +230,7 @@ export class CycleEngine {
226
230
  if (!trig.trigger) {
227
231
  await ingestor.ingest({ type: 'cycle.aborted', human: { title: `cycle ${input.cycleId} 抑制` }, payload: { cycleId: input.cycleId, reasons: trig.reasons } });
228
232
  advance('aborted');
229
- return { cycleId: input.cycleId, triggered: false, finalStage: stage, reasons: trig.reasons };
233
+ return { cycleId: input.cycleId, triggered: false, finalStage: stage, producedValue: false, reasons: trig.reasons };
230
234
  }
231
235
  // 选 gene(可解释决策). Exploration: when recent cycles plateau, enable drift to escape local optima.
232
236
  const recentOutcomes = recentCycleOutcomes(ingestor, 100);
@@ -297,11 +301,11 @@ export class CycleEngine {
297
301
  const epi = epigeneticPenaltyForIds(candidateIds(c), envKey, geneOutcomes);
298
302
  return epi > 0 ? { ...c, epigeneticPenalty: epi } : c;
299
303
  });
300
- const decision = (await this.deps.selection.run({ signals: baseSignals, 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 } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
304
+ const decision = (await this.deps.selection.run({ signals: baseSignals, 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 } : {}) }));
301
305
  await ingestor.ingest({
302
306
  type: 'decision.gene_selected',
303
307
  human: { title: `选 gene ${decision.selectedGeneId ?? '(innovate)'}`, why: decision.candidates.map((c) => `${c.geneId}:${c.score.toFixed(3)}`).join(', ') || '无候选→innovate' },
304
- payload: { cycleId: input.cycleId, selectedGeneId: decision.selectedGeneId, ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}), candidates: decision.candidates, ...(decision.antiWarnings && decision.antiWarnings.length > 0 ? { antiWarnings: decision.antiWarnings } : {}), weightsVersion: decision.weightsVersion, strategy: decision.strategyName, strategyPreset: strategy.name, ...(plateau.active ? { plateau } : {}), ...(inertBanned.size > 0 ? { inertBanned: [...inertBanned] } : {}) },
308
+ 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] } : {}) },
305
309
  });
306
310
  advance('gene_selected');
307
311
  const geneId = decision.selectedGeneId ?? 'ad-hoc';
@@ -331,7 +335,7 @@ export class CycleEngine {
331
335
  await ingestor.ingest({ type: 'cycle.failed', human: { title: `cycle ${input.cycleId} 执行抛错` }, payload: { cycleId: input.cycleId, error: msg, gene: geneId, env: envKey, ...failureClassPayload({ geneId }, msg) } });
332
336
  advance('failed');
333
337
  await recordPersonalityOutcome('failed', null);
334
- return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, reasons: [...reasons, `执行异常: ${msg}`] };
338
+ return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, reasons: [...reasons, `执行异常: ${msg}`] };
335
339
  }
336
340
  if (input.solidifyPermit) {
337
341
  let permit;
@@ -360,12 +364,13 @@ export class CycleEngine {
360
364
  reason: permit.reason,
361
365
  gene: geneId,
362
366
  env: envKey,
367
+ ...executionMetadata(exec),
363
368
  ...failureClassPayload({ geneId }),
364
369
  },
365
370
  });
366
371
  advance('failed');
367
372
  await recordPersonalityOutcome('failed', null);
368
- return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, reasons: [...reasons, reason] };
373
+ return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, ...executionMetadata(exec), reasons: [...reasons, reason] };
369
374
  }
370
375
  }
371
376
  // solidify → Capsule
@@ -394,6 +399,7 @@ export class CycleEngine {
394
399
  outcome: exec.outcome,
395
400
  gene: geneId,
396
401
  env: envKey,
402
+ ...executionMetadata(exec),
397
403
  // Only forward blast radius as a no-op signal when it was measured from a git_diff proof. solidify.ts
398
404
  // forces blast to {0,0} for non-git_diff proofs (artifact_hash/external_receipt/tool_call_trace),
399
405
  // where {0,0} means "blast unknown", NOT "no change" — forwarding it would mis-classify a productive
@@ -414,6 +420,6 @@ export class CycleEngine {
414
420
  }
415
421
  // 人格统计回写(用途②): 用本轮真实 outcome/score 回写当轮人格桶, 供下一轮自然选择靠拢. 未注入 ⇒ no-op.
416
422
  await recordPersonalityOutcome(exec.outcome.status, exec.outcome.score);
417
- return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, capsule: sol.capsule, event, resolutionStatus: sol.resolutionStatus, reasons };
423
+ return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: sol.producedValue, decision, mutation, capsule: sol.capsule, event, resolutionStatus: sol.resolutionStatus, ...executionMetadata(exec), reasons };
418
424
  }
419
425
  }
@@ -1,5 +1,5 @@
1
1
  /** Bucket the failure falls into. unclassified = default-open (treat as a real evolver bug worth filing).
2
- * Bucket string matches V1 (EvoMap/evolver-private-dev#279 commit a89e123e) byte-for-byte so wire / UI
2
+ * Bucket string matches V1 byte-for-byte so wire / UI
3
3
  * consumers can branch on the same value regardless of producer. */
4
4
  export type CycleFailureClass = 'host_no_transcript' | 'host_provider_error' | 'local_gene_no_blast' | 'unclassified';
5
5
  /** Shape of a recent cycle event the classifier reads. Loose so callers can pass partial DTOs. */
@@ -5,6 +5,7 @@ import type { GeneLearningView } from '../assetstore/learningHistory.js';
5
5
  import type { AssetRecord } from '../assetstore/provider.js';
6
6
  import { type ExplorationInput } from './exploration.js';
7
7
  import type { GenerationSource } from '../wire/index.js';
8
+ import type { MemoryGraphGeneEvidence } from './memoryGraph.js';
8
9
  /** 一个候选 gene 的选择期素材. */
9
10
  export interface GeneCandidateInput {
10
11
  geneId: string;
@@ -35,6 +36,8 @@ export interface GeneCandidateInput {
35
36
  * set the hard gates already produced, so a high confidence can never resurrect a gene that was excluded.
36
37
  */
37
38
  confidence?: number;
39
+ /** Scoped local MemoryGraph outcome signal in [-1,1]. Data-only and never executable prompt content. */
40
+ memoryBoost?: number;
38
41
  /**
39
42
  * Cross-runtime reuse sentiment in [-1, 1] for this gene (#268 phase 1): the net of self-reported reuse
40
43
  * SUCCESSes vs negatives (failed/mismatched/stale/unsafe), computed upstream from reuse-outcome events
@@ -78,6 +81,8 @@ export interface SelectionInput {
78
81
  * for prompt rendering only; they never enter scoring, fallback, or forced selection.
79
82
  */
80
83
  antiWarnings?: readonly AntiWarning[];
84
+ /** Structured, scoped outcome evidence selected upstream. Contains no raw memory text or instructions. */
85
+ memoryEvidence?: readonly MemoryGraphGeneEvidence[];
81
86
  }
82
87
  export interface ScoredCandidate {
83
88
  geneId: string;
@@ -105,6 +110,10 @@ export interface GeneDecision {
105
110
  antiWarnings?: AntiWarning[];
106
111
  weightsVersion: string;
107
112
  strategyName: string;
113
+ /** Human-readable explanation for the winning candidate. */
114
+ selectedReason?: string;
115
+ /** Bounded structured outcome evidence for prompt enrichment. */
116
+ memoryEvidence?: MemoryGraphGeneEvidence[];
108
117
  }
109
118
  /**
110
119
  * Weight of the preferred-gene confidence factor (fourth factor, positive cross-cycle learning). Kept small so
@@ -118,12 +127,14 @@ export declare const CONFIDENCE_WEIGHT = 0.15;
118
127
  * ±REUSE_WEIGHT and can never dominate health/signal-match.
119
128
  */
120
129
  export declare const REUSE_WEIGHT = 0.1;
130
+ /** Weight of scoped local MemoryGraph outcome evidence. */
131
+ export declare const MEMORY_GRAPH_WEIGHT = 0.12;
121
132
  /**
122
133
  * Version of the full engine-health weight vector (health 0.6 + signal-match 0.4 − epigenetic penalty
123
134
  * + CONFIDENCE_WEIGHT × confidence + REUSE_WEIGHT × reuse-sentiment). Bumped whenever a factor is added so golden
124
135
  * weight snapshots track the change. Composed from the health-weights version so a change to either layer shows.
125
136
  */
126
- export declare const SELECTION_WEIGHTS_VERSION = "sel-3(gh-1,conf=0.15,reuse=0.1)";
137
+ export declare const SELECTION_WEIGHTS_VERSION = "sel-4(gh-1,conf=0.15,memory=0.12,reuse=0.1)";
127
138
  /** 实现1: engine 健康分主导(health 0.6 + 信号匹配 0.4). */
128
139
  export declare const engineHealthSelection: Strategy<SelectionInput, GeneDecision>;
129
140
  /** 实现2: 纯信号匹配采样(忽略 health, 对照基线 — 经验主义要可对比). */
@@ -4,9 +4,17 @@ import { tagOverlapScore, bagCosine } from '../signals/expand.js';
4
4
  import { driftSelect } from './exploration.js';
5
5
  import { isDistilledGeneId } from './geneIntake.js';
6
6
  function decisionWithWarnings(input, decision) {
7
+ const selected = decision.selectedGeneId
8
+ ? decision.candidates.find((candidate) => candidate.geneId === decision.selectedGeneId || candidate.assetId === decision.selectedAssetId)
9
+ : undefined;
10
+ const enriched = {
11
+ ...decision,
12
+ ...(selected ? { selectedReason: selected.reasons.join('; ') } : {}),
13
+ ...(input.memoryEvidence && input.memoryEvidence.length > 0 ? { memoryEvidence: [...input.memoryEvidence] } : {}),
14
+ };
7
15
  return input.antiWarnings && input.antiWarnings.length > 0
8
- ? { ...decision, antiWarnings: [...input.antiWarnings] }
9
- : decision;
16
+ ? { ...enriched, antiWarnings: [...input.antiWarnings] }
17
+ : enriched;
10
18
  }
11
19
  function signalMatchScore(signals, match) {
12
20
  if (signals.length === 0 || match.length === 0)
@@ -29,12 +37,14 @@ export const CONFIDENCE_WEIGHT = 0.15;
29
37
  * ±REUSE_WEIGHT and can never dominate health/signal-match.
30
38
  */
31
39
  export const REUSE_WEIGHT = 0.1;
40
+ /** Weight of scoped local MemoryGraph outcome evidence. */
41
+ export const MEMORY_GRAPH_WEIGHT = 0.12;
32
42
  /**
33
43
  * Version of the full engine-health weight vector (health 0.6 + signal-match 0.4 − epigenetic penalty
34
44
  * + CONFIDENCE_WEIGHT × confidence + REUSE_WEIGHT × reuse-sentiment). Bumped whenever a factor is added so golden
35
45
  * weight snapshots track the change. Composed from the health-weights version so a change to either layer shows.
36
46
  */
37
- export const SELECTION_WEIGHTS_VERSION = `sel-3(${HEALTH_WEIGHTS_VERSION},conf=${CONFIDENCE_WEIGHT},reuse=${REUSE_WEIGHT})`;
47
+ export const SELECTION_WEIGHTS_VERSION = `sel-4(${HEALTH_WEIGHTS_VERSION},conf=${CONFIDENCE_WEIGHT},memory=${MEMORY_GRAPH_WEIGHT},reuse=${REUSE_WEIGHT})`;
38
48
  /**
39
49
  * Match score with semantic signal expansion (ported from v1): literal coverage first, then fill the
40
50
  * recall gap with semantic tag overlap, then with bag-of-words cosine similarity. A perfect literal match
@@ -65,13 +75,16 @@ function scoreCandidate(signals, c) {
65
75
  // Cross-runtime reuse sentiment (#268 phase 1): clamped to [-1,1] so an upstream bug cannot turn the soft nudge
66
76
  // into a dominant (or unbounded) term. Absent → 0 → no effect (default-off until a caller injects it).
67
77
  const reuse = Math.max(-1, Math.min(1, c.reuseAdjust ?? 0));
68
- const score = 0.6 * health.score + 0.4 * m.score - epi + CONFIDENCE_WEIGHT * conf + REUSE_WEIGHT * reuse;
78
+ const memory = Math.max(-1, Math.min(1, c.memoryBoost ?? 0));
79
+ const score = 0.6 * health.score + 0.4 * m.score - epi + CONFIDENCE_WEIGHT * conf + MEMORY_GRAPH_WEIGHT * memory + REUSE_WEIGHT * reuse;
69
80
  const breakdown = m.tag > 0 || m.cos > 0 ? `(literal=${m.literal.toFixed(2)}+tag=${m.tag.toFixed(2)}+cos=${m.cos.toFixed(2)})` : '';
70
81
  const reasons = [`health=${health.score.toFixed(3)}(succ=${health.successRate.toFixed(2)},reuse=${health.reuseCount})`, `信号匹配=${m.score.toFixed(2)}${breakdown}`];
71
82
  if (epi > 0)
72
83
  reasons.push(`epigenetic 环境抑制 -${epi.toFixed(2)}`);
73
84
  if (conf > 0)
74
85
  reasons.push(`preferred-gene confidence +${(CONFIDENCE_WEIGHT * conf).toFixed(3)} (conf=${conf.toFixed(2)})`);
86
+ if (memory !== 0)
87
+ reasons.push(`scoped memory-graph outcome ${memory >= 0 ? '+' : ''}${(MEMORY_GRAPH_WEIGHT * memory).toFixed(3)} (boost=${memory.toFixed(2)})`);
75
88
  if (reuse !== 0)
76
89
  reasons.push(`cross-runtime reuse ${reuse >= 0 ? '+' : ''}${(REUSE_WEIGHT * reuse).toFixed(3)} (sentiment=${reuse.toFixed(2)})`);
77
90
  return { geneId: c.geneId, ...(c.assetId ? { assetId: c.assetId } : {}), score, health, reasons };
@@ -164,11 +177,14 @@ export const engineHealthSelection = {
164
177
  // (0 < score <= floor) would be preempted by an unrelated zero-evidence distilled gene; V1 (which has no floor)
165
178
  // keeps such a match. When candidates scored > 0 but below floor, defer to V2's floor policy (innovate).
166
179
  const noSignalMatch = !forced.forceRejected && forced.scoredForChoice.every((s) => s.score <= 0);
167
- const fb = noSignalMatch
168
- ? (input.distilledFallback ?? []).find((c) => isReusableFallbackCandidate(c) && (c.epigeneticPenalty ?? 0) === 0)
180
+ const fallback = noSignalMatch
181
+ ? (input.distilledFallback ?? [])
182
+ .filter((candidate) => isReusableFallbackCandidate(candidate) && (candidate.epigeneticPenalty ?? 0) === 0)
183
+ .map((candidate) => ({ candidate, scored: scoreCandidate(input.signals, candidate) }))
184
+ .sort((left, right) => right.scored.score - left.scored.score || left.candidate.geneId.localeCompare(right.candidate.geneId))[0]
169
185
  : undefined;
170
- if (fb) {
171
- const fbScored = scoreCandidate(input.signals, fb);
186
+ if (fallback) {
187
+ const { candidate: fb, scored: fbScored } = fallback;
172
188
  fbScored.reasons.push('distilled_fallback(#97): 无信号匹配, 低置信度复用蒸馏 gene');
173
189
  scored.push(fbScored);
174
190
  selectedGeneId = fb.geneId;
@@ -2,6 +2,7 @@ export * from './geneHealth.js';
2
2
  export * from './exploration.js';
3
3
  export * from './epigenetics.js';
4
4
  export * from './confidence.js';
5
+ export * from './memoryGraph.js';
5
6
  export * from './capabilityCandidates.js';
6
7
  export * from './conversationSniffer.js';
7
8
  export * from './geneIntake.js';
@@ -2,6 +2,7 @@ export * from './geneHealth.js';
2
2
  export * from './exploration.js';
3
3
  export * from './epigenetics.js';
4
4
  export * from './confidence.js';
5
+ export * from './memoryGraph.js';
5
6
  export * from './capabilityCandidates.js';
6
7
  export * from './conversationSniffer.js';
7
8
  export * from './geneIntake.js';
@@ -0,0 +1,62 @@
1
+ export type MemoryGraphOutcomeStatus = 'success' | 'failed';
2
+ export interface MemoryGraphOutcomeRecord {
3
+ version: 2;
4
+ kind: 'outcome';
5
+ provenance: 'v2_local' | 'v1_import';
6
+ workspaceScope: string;
7
+ userScope: string;
8
+ signalFingerprint: string;
9
+ signals: readonly string[];
10
+ geneId: string;
11
+ status: MemoryGraphOutcomeStatus;
12
+ score: number;
13
+ at: string;
14
+ /** Stable local-source identity used to make one-time imports crash-retry safe. */
15
+ sourceFingerprint?: string;
16
+ successCount?: number;
17
+ failCount?: number;
18
+ }
19
+ export interface MemoryGraphGeneEvidence {
20
+ geneId: string;
21
+ boost: number;
22
+ expectedSuccess: number;
23
+ successCount: number;
24
+ failCount: number;
25
+ attempts: number;
26
+ similarity: number;
27
+ lastAt: string;
28
+ }
29
+ export interface MemoryGraphDiagnostics {
30
+ bytesRead: number;
31
+ recordsRead: number;
32
+ corruptLines: number;
33
+ oversizedLines: number;
34
+ scopeRejected: number;
35
+ provenanceRejected: number;
36
+ truncated: boolean;
37
+ recovery: 'healthy' | 'degraded' | 'recovered' | 'empty';
38
+ /** The graph was healthy enough to address, but another process held its maintenance lock. */
39
+ busy?: boolean;
40
+ }
41
+ export interface MemoryGraphAdvice {
42
+ genes: readonly MemoryGraphGeneEvidence[];
43
+ diagnostics: MemoryGraphDiagnostics;
44
+ }
45
+ export interface MemoryGraphQueryInput {
46
+ workspace: string;
47
+ signals: readonly string[];
48
+ }
49
+ export interface MemoryGraphRecordInput extends MemoryGraphQueryInput {
50
+ geneId: string;
51
+ status: MemoryGraphOutcomeStatus;
52
+ score: number;
53
+ at: string;
54
+ }
55
+ export interface MemoryGraphProvider {
56
+ query(input: MemoryGraphQueryInput): Promise<MemoryGraphAdvice> | MemoryGraphAdvice;
57
+ recordOutcome(input: MemoryGraphRecordInput): Promise<void> | void;
58
+ }
59
+ export declare function normalizeMemorySignals(signals: readonly string[]): string[];
60
+ export declare function memorySignalFingerprint(signals: readonly string[]): string;
61
+ export declare function safeMemoryGeneId(value: string): string;
62
+ export declare function deriveMemoryGraphAdvice(records: readonly MemoryGraphOutcomeRecord[], signals: readonly string[], nowMs: number, diagnostics: MemoryGraphDiagnostics): MemoryGraphAdvice;
@@ -0,0 +1,86 @@
1
+ const MAX_SIGNALS = 32;
2
+ const MAX_SIGNAL_CHARS = 120;
3
+ const MAX_GENE_CHARS = 240;
4
+ const MIN_SIMILARITY = 0.34;
5
+ const HALF_LIFE_DAYS = 30;
6
+ export function normalizeMemorySignals(signals) {
7
+ return [...new Set(signals
8
+ .filter((signal) => typeof signal === 'string')
9
+ .map((signal) => signal.trim().toLowerCase().slice(0, MAX_SIGNAL_CHARS))
10
+ .filter(Boolean))]
11
+ .sort()
12
+ .slice(0, MAX_SIGNALS);
13
+ }
14
+ export function memorySignalFingerprint(signals) {
15
+ return normalizeMemorySignals(signals).join('|') || '(none)';
16
+ }
17
+ export function safeMemoryGeneId(value) {
18
+ return value.trim().slice(0, MAX_GENE_CHARS);
19
+ }
20
+ export function deriveMemoryGraphAdvice(records, signals, nowMs, diagnostics) {
21
+ const currentSignals = normalizeMemorySignals(signals);
22
+ const aggregates = new Map();
23
+ for (const record of records) {
24
+ const similarity = jaccard(currentSignals, record.signals);
25
+ if (similarity < MIN_SIMILARITY)
26
+ continue;
27
+ const successCount = boundedCount(record.successCount ?? (record.status === 'success' ? 1 : 0));
28
+ const failCount = boundedCount(record.failCount ?? (record.status === 'failed' ? 1 : 0));
29
+ const total = successCount + failCount;
30
+ if (total === 0)
31
+ continue;
32
+ const decay = decayWeight(record.at, nowMs);
33
+ const weight = similarity * decay;
34
+ const current = aggregates.get(record.geneId) ?? { success: 0, fail: 0, weightedSimilarity: 0, weight: 0, lastAt: record.at };
35
+ current.success += successCount * weight;
36
+ current.fail += failCount * weight;
37
+ current.weightedSimilarity += similarity * total;
38
+ current.weight += total;
39
+ if (Date.parse(record.at) > Date.parse(current.lastAt))
40
+ current.lastAt = record.at;
41
+ aggregates.set(record.geneId, current);
42
+ }
43
+ const genes = [];
44
+ for (const [geneId, aggregate] of aggregates) {
45
+ const successCount = Math.round(aggregate.success);
46
+ const failCount = Math.round(aggregate.fail);
47
+ const attempts = successCount + failCount;
48
+ if (attempts === 0)
49
+ continue;
50
+ const expectedSuccess = (aggregate.success + 1) / (aggregate.success + aggregate.fail + 2);
51
+ const similarity = aggregate.weight > 0 ? Math.min(1, aggregate.weightedSimilarity / aggregate.weight) : 0;
52
+ const confidence = Math.min(1, Math.log2(attempts + 1) / 3);
53
+ const boost = clamp((expectedSuccess - 0.5) * 2 * similarity * confidence, -1, 1);
54
+ genes.push({ geneId, boost, expectedSuccess, successCount, failCount, attempts, similarity, lastAt: aggregate.lastAt });
55
+ }
56
+ genes.sort((left, right) => Math.abs(right.boost) - Math.abs(left.boost) || right.attempts - left.attempts || left.geneId.localeCompare(right.geneId));
57
+ return { genes: genes.slice(0, 64), diagnostics };
58
+ }
59
+ function boundedCount(value) {
60
+ if (!Number.isFinite(value) || value <= 0)
61
+ return 0;
62
+ return Math.min(1_000_000, Math.floor(value));
63
+ }
64
+ function jaccard(left, right) {
65
+ const a = new Set(normalizeMemorySignals(left));
66
+ const b = new Set(normalizeMemorySignals(right));
67
+ if (a.size === 0 && b.size === 0)
68
+ return 1;
69
+ if (a.size === 0 || b.size === 0)
70
+ return 0;
71
+ let intersection = 0;
72
+ for (const value of a)
73
+ if (b.has(value))
74
+ intersection += 1;
75
+ return intersection / (a.size + b.size - intersection);
76
+ }
77
+ function decayWeight(at, nowMs) {
78
+ const timestamp = Date.parse(at);
79
+ if (!Number.isFinite(timestamp))
80
+ return 0;
81
+ const ageDays = Math.max(0, nowMs - timestamp) / 86_400_000;
82
+ return Math.pow(0.5, ageDays / HALF_LIFE_DAYS);
83
+ }
84
+ function clamp(value, min, max) {
85
+ return Math.max(min, Math.min(max, value));
86
+ }
@@ -7,6 +7,7 @@ import type { GeneCandidateInput } 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
+ import type { MemoryGraphAdvice } from './memoryGraph.js';
10
11
  export interface RunCycleOptions {
11
12
  cycleId: string;
12
13
  problem: ProblemPattern;
@@ -60,6 +61,8 @@ export interface RunCycleOptions {
60
61
  * agent self-report) gains teeth on selection. Omit → no recall contribution. Never feeds quarantine.
61
62
  */
62
63
  recallEvents?: readonly ReuseOutcomeEvent[];
64
+ /** Scoped local MemoryGraph query result. Omit to keep selection unchanged. */
65
+ memoryGraphAdvice?: MemoryGraphAdvice;
63
66
  }
64
67
  /** Drive one full evolution cycle end-to-end: assemble candidates from the store, then run the cycle. */
65
68
  export declare function runEvolutionCycle(engine: CycleEngine, store: AssetStoreProvider, opts: RunCycleOptions): Promise<CycleResult>;
@@ -38,7 +38,18 @@ export async function runEvolutionCycle(engine, store, opts) {
38
38
  ...(opts.hubCandidates && opts.hubCandidates.length > 0 ? { hubCandidates: opts.hubCandidates } : {}),
39
39
  ...(reuseCounts.size > 0 ? { reuseCounts } : {}),
40
40
  };
41
- const { candidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store, signals, asmOpts);
41
+ const { candidates: assembledCandidates, distilledFallback, antiWarnings } = await assembleSelectionPool(store, signals, asmOpts);
42
+ const memoryByGene = new Map((opts.memoryGraphAdvice?.genes ?? []).map((evidence) => [evidence.geneId, evidence]));
43
+ const candidates = assembledCandidates.map((candidate) => {
44
+ const evidence = memoryByGene.get(candidate.geneId) ?? (candidate.assetId ? memoryByGene.get(candidate.assetId) : undefined);
45
+ return evidence ? { ...candidate, memoryBoost: evidence.boost } : candidate;
46
+ });
47
+ const memoryDistilledFallback = distilledFallback.map((candidate) => {
48
+ const evidence = memoryByGene.get(candidate.geneId) ?? (candidate.assetId ? memoryByGene.get(candidate.assetId) : undefined);
49
+ return evidence ? { ...candidate, memoryBoost: evidence.boost } : candidate;
50
+ });
51
+ const memoryEligible = [...candidates, ...memoryDistilledFallback];
52
+ const memoryEvidence = opts.memoryGraphAdvice?.genes.filter((evidence) => memoryEligible.some((candidate) => candidate.geneId === evidence.geneId || candidate.assetId === evidence.geneId)).slice(0, 3) ?? [];
42
53
  return engine.runCycle({
43
54
  cycleId: opts.cycleId,
44
55
  problem: opts.problem,
@@ -57,7 +68,8 @@ export async function runEvolutionCycle(engine, store, opts) {
57
68
  ...(opts.solidifyPermit ? { solidifyPermit: opts.solidifyPermit } : {}),
58
69
  // #97: forward the distilled-gene fallback pool so a no-signal-match cycle reuses a distilled strategy
59
70
  // (instead of a blind innovate) when nothing clears the floor.
60
- ...(distilledFallback.length > 0 ? { distilledFallback } : {}),
71
+ ...(memoryDistilledFallback.length > 0 ? { distilledFallback: memoryDistilledFallback } : {}),
61
72
  ...(antiWarnings.length > 0 ? { antiWarnings } : {}),
73
+ ...(memoryEvidence.length > 0 ? { memoryEvidence } : {}),
62
74
  });
63
75
  }
@@ -0,0 +1,23 @@
1
+ import type { AssetSyncRecord } from './assetSyncLedger.js';
2
+ import type { ProvenanceRecord } from './provenance.js';
3
+ import type { ReviewRecord } from './reviewLedger.js';
4
+ export type AssetSidecarKind = 'provenance' | 'review' | 'asset-sync';
5
+ export type AssetSidecarCorruptionReason = 'invalid_row' | 'unterminated';
6
+ export interface ParsedSidecarJsonl<T> {
7
+ records: T[];
8
+ rows: number;
9
+ validRows: number;
10
+ corruptRows: number;
11
+ unterminated: boolean;
12
+ }
13
+ export declare class CorruptAssetSidecarError extends Error {
14
+ readonly sidecar: AssetSidecarKind;
15
+ readonly reason: AssetSidecarCorruptionReason;
16
+ readonly code = "CORRUPT_ASSET_SIDECAR";
17
+ constructor(sidecar: AssetSidecarKind, reason: AssetSidecarCorruptionReason);
18
+ }
19
+ export declare function parseSidecarJsonl<T>(raw: string, parseRecord: (value: unknown) => T | null): ParsedSidecarJsonl<T>;
20
+ export declare function assertTrustSidecarHealthy<T>(sidecar: Extract<AssetSidecarKind, 'provenance' | 'review'>, parsed: ParsedSidecarJsonl<T>): void;
21
+ export declare function parseProvenanceRecord(value: unknown): ProvenanceRecord | null;
22
+ export declare function parseReviewRecord(value: unknown): ReviewRecord | null;
23
+ export declare function parseAssetSyncRecord(value: unknown): AssetSyncRecord | null;