synergyspec-selfevolving 1.3.0 → 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.
- package/README.md +50 -19
- package/dist/commands/learn.d.ts +12 -1
- package/dist/commands/learn.js +373 -31
- package/dist/commands/self-evolution-episode.d.ts +177 -0
- package/dist/commands/self-evolution-episode.js +423 -0
- package/dist/commands/self-evolution.d.ts +12 -190
- package/dist/commands/self-evolution.js +179 -786
- package/dist/commands/workflow/status.js +3 -1
- package/dist/core/archive.d.ts +0 -1
- package/dist/core/archive.js +0 -58
- package/dist/core/artifact-graph/instruction-loader.d.ts +2 -4
- package/dist/core/artifact-graph/instruction-loader.js +3 -31
- package/dist/core/config-prompts.js +4 -0
- package/dist/core/fitness/health/health-metrics.d.ts +26 -56
- package/dist/core/fitness/health/health-metrics.js +19 -58
- package/dist/core/fitness/health/index.d.ts +15 -2
- package/dist/core/fitness/health/index.js +25 -1
- package/dist/core/fitness/health/local-source.d.ts +43 -4
- package/dist/core/fitness/health/local-source.js +181 -25
- package/dist/core/fitness/health/metric-source.d.ts +48 -19
- package/dist/core/fitness/health/metric-source.js +8 -18
- package/dist/core/fitness/health/resolve-source.js +4 -1
- package/dist/core/fitness/loss.d.ts +7 -7
- package/dist/core/fitness/loss.js +6 -6
- package/dist/core/fitness/sample.d.ts +10 -0
- package/dist/core/fitness/test-failures.d.ts +30 -0
- package/dist/core/fitness/test-failures.js +123 -0
- package/dist/core/learn/credit-path.d.ts +36 -0
- package/dist/core/learn/credit-path.js +198 -0
- package/dist/core/learn/trajectory-discovery.d.ts +39 -0
- package/dist/core/learn/trajectory-discovery.js +140 -0
- package/dist/core/learn.d.ts +39 -5
- package/dist/core/learn.js +131 -14
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.js +52 -1
- package/dist/core/self-evolution/candidate-fitness.d.ts +23 -1
- package/dist/core/self-evolution/candidate-fitness.js +31 -5
- package/dist/core/self-evolution/candidates.d.ts +0 -9
- package/dist/core/self-evolution/canonical-targets.d.ts +8 -4
- package/dist/core/self-evolution/canonical-targets.js +8 -4
- package/dist/core/self-evolution/critic-agent.d.ts +150 -0
- package/dist/core/self-evolution/critic-agent.js +487 -0
- package/dist/core/self-evolution/edits-contract.d.ts +53 -0
- package/dist/core/self-evolution/edits-contract.js +89 -0
- package/dist/core/self-evolution/episode-orchestrator.d.ts +197 -0
- package/dist/core/self-evolution/episode-orchestrator.js +534 -0
- package/dist/core/self-evolution/episode-store.d.ts +266 -0
- package/dist/core/self-evolution/episode-store.js +573 -0
- package/dist/core/self-evolution/evolution-switches.d.ts +1 -1
- package/dist/core/self-evolution/evolution-switches.js +5 -10
- package/dist/core/self-evolution/evolving-agent.d.ts +162 -0
- package/dist/core/self-evolution/evolving-agent.js +449 -0
- package/dist/core/self-evolution/health-baseline.d.ts +25 -6
- package/dist/core/self-evolution/health-baseline.js +30 -6
- package/dist/core/self-evolution/host-harness.d.ts +1 -2
- package/dist/core/self-evolution/host-harness.js +1 -2
- package/dist/core/self-evolution/index.d.ts +10 -6
- package/dist/core/self-evolution/index.js +19 -6
- package/dist/core/self-evolution/learn-hints.d.ts +31 -0
- package/dist/core/self-evolution/learn-hints.js +16 -0
- package/dist/core/self-evolution/learn-observation-adapter.d.ts +35 -0
- package/dist/core/self-evolution/learn-observation-adapter.js +285 -10
- package/dist/core/self-evolution/line-diff.d.ts +60 -0
- package/dist/core/self-evolution/line-diff.js +130 -0
- package/dist/core/self-evolution/policy/fs-safe.d.ts +19 -0
- package/dist/core/self-evolution/policy/fs-safe.js +89 -0
- package/dist/core/self-evolution/policy/index.d.ts +13 -0
- package/dist/core/self-evolution/policy/index.js +13 -0
- package/dist/core/self-evolution/policy/policy-store.d.ts +217 -0
- package/dist/core/self-evolution/policy/policy-store.js +774 -0
- package/dist/core/self-evolution/policy/reject-buffer.d.ts +48 -0
- package/dist/core/self-evolution/policy/reject-buffer.js +168 -0
- package/dist/core/self-evolution/promote.d.ts +1 -1
- package/dist/core/self-evolution/promote.js +6 -33
- package/dist/core/self-evolution/promotion.js +1 -2
- package/dist/core/self-evolution/proposer-agent.d.ts +41 -0
- package/dist/core/self-evolution/proposer-agent.js +94 -13
- package/dist/core/self-evolution/proposer-slice.d.ts +26 -0
- package/dist/core/self-evolution/proposer-slice.js +54 -0
- package/dist/core/self-evolution/reward-agent.d.ts +234 -0
- package/dist/core/self-evolution/reward-agent.js +564 -0
- package/dist/core/self-evolution/scope-gate.d.ts +66 -0
- package/dist/core/self-evolution/scope-gate.js +107 -0
- package/dist/core/self-evolution/success-channel.d.ts +79 -0
- package/dist/core/self-evolution/success-channel.js +361 -0
- package/dist/core/self-evolution/target-evolution.d.ts +11 -0
- package/dist/core/self-evolution/target-evolution.js +2 -0
- package/dist/core/self-evolution/tool-evolution.js +2 -13
- package/dist/core/self-evolution/verdict.d.ts +8 -5
- package/dist/core/self-evolution/verdict.js +4 -7
- package/dist/core/templates/skill-templates.d.ts +1 -0
- package/dist/core/templates/skill-templates.js +1 -0
- package/dist/core/templates/workflow-manifest.js +2 -0
- package/dist/core/templates/workflows/learn.d.ts +4 -2
- package/dist/core/templates/workflows/learn.js +25 -166
- package/dist/core/templates/workflows/self-evolving.d.ts +13 -0
- package/dist/core/templates/workflows/self-evolving.js +127 -0
- package/dist/core/trajectory/facts.d.ts +16 -0
- package/dist/core/trajectory/facts.js +12 -4
- package/dist/core/trajectory/skeleton.d.ts +43 -0
- package/dist/core/trajectory/skeleton.js +239 -0
- package/dist/dashboard/data.d.ts +25 -51
- package/dist/dashboard/data.js +68 -180
- package/dist/dashboard/react-client.js +458 -503
- package/dist/dashboard/react-styles.js +3 -3
- package/dist/dashboard/server.js +23 -17
- package/dist/ui/ascii-patterns.d.ts +7 -15
- package/dist/ui/ascii-patterns.js +123 -54
- package/dist/ui/welcome-screen.d.ts +0 -14
- package/dist/ui/welcome-screen.js +16 -35
- package/package.json +3 -1
- package/scripts/code-health.py +1066 -638
- package/scripts/slop_rules.yaml +2151 -0
|
@@ -23,6 +23,8 @@ import { relativePath } from './shared.js';
|
|
|
23
23
|
import { validateLearnEvolutionHint, } from './learn-hints.js';
|
|
24
24
|
import { findCanonicalTargetsByFile, listCanonicalTargets, lookupCanonicalTarget, } from './canonical-targets.js';
|
|
25
25
|
import { isCanonicalTargetEvolvable, explicitTargetIds, } from './target-evolution.js';
|
|
26
|
+
import { renderCreditPath } from '../learn/credit-path.js';
|
|
27
|
+
import { buildProposerSlice } from './proposer-slice.js';
|
|
26
28
|
import { detectRepoMode, resolveTargetLocalFiles } from './local-targets.js';
|
|
27
29
|
import { getSchemaDir } from '../artifact-graph/resolver.js';
|
|
28
30
|
import { SKILL_NAMESPACES } from '../config.js';
|
|
@@ -126,6 +128,33 @@ export async function resolveTargetLocalFilesReadonly(targetId, repoRoot) {
|
|
|
126
128
|
}
|
|
127
129
|
return out;
|
|
128
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The producing canonical target for a credit path's LAST resolved node.
|
|
133
|
+
* Static artifact→template map: a path that reached a design section was
|
|
134
|
+
* produced through the design template; one that stopped at a task, through
|
|
135
|
+
* the tasks template. Paths ending at `test`/`use-case` have no nameable
|
|
136
|
+
* producer (the usecases template is oracle-frozen and not a canonical
|
|
137
|
+
* target). When a policy is supplied and the resolved target is FROZEN, the
|
|
138
|
+
* target is still NAMED — annotated `[frozen]` — because the address is
|
|
139
|
+
* observational; policy gates candidates, not evidence text.
|
|
140
|
+
*/
|
|
141
|
+
const PRODUCING_TARGET_BY_LAST_NODE = {
|
|
142
|
+
'design-section': 'artifact-template:design',
|
|
143
|
+
task: 'artifact-template:tasks',
|
|
144
|
+
};
|
|
145
|
+
/** Raw producing-target id for a path's last node (no policy annotation). */
|
|
146
|
+
export function producingTargetIdForCreditPath(creditPath) {
|
|
147
|
+
const last = creditPath.nodes[creditPath.nodes.length - 1];
|
|
148
|
+
return last ? PRODUCING_TARGET_BY_LAST_NODE[last.kind] : undefined;
|
|
149
|
+
}
|
|
150
|
+
function resolveProducingTarget(creditPath, policy) {
|
|
151
|
+
const targetId = producingTargetIdForCreditPath(creditPath);
|
|
152
|
+
if (!targetId)
|
|
153
|
+
return undefined;
|
|
154
|
+
if (policy && !isCanonicalTargetEvolvable(targetId, policy))
|
|
155
|
+
return `${targetId} [frozen]`;
|
|
156
|
+
return targetId;
|
|
157
|
+
}
|
|
129
158
|
/**
|
|
130
159
|
* Derive structured evolution hints from a learn report's signals.
|
|
131
160
|
*
|
|
@@ -215,13 +244,41 @@ export function generateEvolutionHints(report, policy) {
|
|
|
215
244
|
});
|
|
216
245
|
}
|
|
217
246
|
}
|
|
218
|
-
// Heuristic (c): Failure evidence
|
|
247
|
+
// Heuristic (c): Failure evidence → add-verification-step. Provenance is
|
|
248
|
+
// forwarded onto each evidence item: 'observed' items were re-sourced from
|
|
249
|
+
// the observed runner output (the source the gate trusts), 'authored' from
|
|
250
|
+
// the agent-written artifacts; absent = legacy/authored.
|
|
219
251
|
if (report.artifacts.failureEvidence.length > 0) {
|
|
220
|
-
const
|
|
252
|
+
const observedItems = report.artifacts.failureEvidence.filter((item) => item.source === 'observed');
|
|
253
|
+
// Dedup: authored items keep the legacy one-per-artifact-file rule
|
|
254
|
+
// (byte-identical baseline); observed items dedupe per failing TEST, so
|
|
255
|
+
// two failures in the same test file both survive.
|
|
256
|
+
const dedupSeen = new Set();
|
|
257
|
+
const evidence = report.artifacts.failureEvidence
|
|
258
|
+
.map((item) => ({
|
|
221
259
|
file: toPosix(item.file),
|
|
222
260
|
quoteOrSummary: limitText(item.line, 200),
|
|
223
261
|
severity: 'high',
|
|
224
|
-
|
|
262
|
+
...(item.source ? { provenance: item.source } : {}),
|
|
263
|
+
// The ADDRESS: the neutral credit path completed with the producing
|
|
264
|
+
// canonical target this layer (not learn) resolves. SUSPECT framing —
|
|
265
|
+
// recurrence across changes (the grouping machinery) is the confirmer.
|
|
266
|
+
...(item.creditPath
|
|
267
|
+
? { address: renderCreditPath(item.creditPath, resolveProducingTarget(item.creditPath, policy)) }
|
|
268
|
+
: {}),
|
|
269
|
+
}))
|
|
270
|
+
.filter((item) => {
|
|
271
|
+
const key = item.provenance === 'observed' ? `${item.file}::${item.quoteOrSummary}` : item.file;
|
|
272
|
+
if (dedupSeen.has(key))
|
|
273
|
+
return false;
|
|
274
|
+
dedupSeen.add(key);
|
|
275
|
+
return true;
|
|
276
|
+
})
|
|
277
|
+
.slice(0, 6);
|
|
278
|
+
const creditPaths = report.artifacts.failureEvidence
|
|
279
|
+
.map((item) => item.creditPath)
|
|
280
|
+
.filter((p) => p !== undefined)
|
|
281
|
+
.slice(0, 6);
|
|
225
282
|
if (evidence.length > 0) {
|
|
226
283
|
const proposedChangeType = 'add-verification-step';
|
|
227
284
|
drafts.push({
|
|
@@ -229,13 +286,16 @@ export function generateEvolutionHints(report, policy) {
|
|
|
229
286
|
sourceChange: report.changeName,
|
|
230
287
|
affectedTargetKind: 'workflow-prompt',
|
|
231
288
|
proposedChangeType,
|
|
232
|
-
problem: limitText(
|
|
289
|
+
problem: limitText(observedItems.length > 0
|
|
290
|
+
? `The observed test run recorded ${observedItems.length} failing test(s); the workflow may be missing a verification gate.`
|
|
291
|
+
: `Verification evidence still contains ${report.artifacts.failureEvidence.length} failure-like line(s); the workflow may be missing a verification gate.`, 500),
|
|
233
292
|
evidence,
|
|
234
293
|
confidence: 'medium',
|
|
235
294
|
risk: 'high',
|
|
236
295
|
expectedBenefit: 'Add a verification step that catches the failure pattern before archive.',
|
|
237
296
|
evalNeeded: evalSuitesFor('workflow-prompt'),
|
|
238
297
|
thresholdKey: `workflow-prompt:unspecified:${proposedChangeType}`,
|
|
298
|
+
...(creditPaths.length > 0 ? { creditPaths } : {}),
|
|
239
299
|
});
|
|
240
300
|
}
|
|
241
301
|
}
|
|
@@ -263,6 +323,29 @@ export function generateEvolutionHints(report, policy) {
|
|
|
263
323
|
});
|
|
264
324
|
}
|
|
265
325
|
}
|
|
326
|
+
// Health-contributor enrichment (observe-only): when the fitness sample
|
|
327
|
+
// names the worst per-file/per-function health offenders, append them as
|
|
328
|
+
// evidence onto the artifact-template drafts the heuristics ALREADY produced
|
|
329
|
+
// — code shape is a property of the generated implementation, whose producing
|
|
330
|
+
// surface is the artifact-template chain. Never creates a new hint or
|
|
331
|
+
// trigger: a clean run with offenders but no drafts stays abstained
|
|
332
|
+
// (abstain-on-success preserved), and workflow-prompt drafts are untouched.
|
|
333
|
+
const healthOffenders = report.fitnessSample?.healthContributors ?? [];
|
|
334
|
+
if (healthOffenders.length > 0) {
|
|
335
|
+
const offenderEvidence = healthOffenders
|
|
336
|
+
.slice(0, 3)
|
|
337
|
+
.map((offender) => ({
|
|
338
|
+
file: toPosix(offender.file),
|
|
339
|
+
quoteOrSummary: limitText(describeHealthOffender(offender), 200),
|
|
340
|
+
severity: 'medium',
|
|
341
|
+
}));
|
|
342
|
+
for (const draft of drafts) {
|
|
343
|
+
if (draft.affectedTargetKind !== 'artifact-template')
|
|
344
|
+
continue;
|
|
345
|
+
// Existing convention: each draft carries at most 6 evidence items.
|
|
346
|
+
draft.evidence = [...draft.evidence, ...offenderEvidence].slice(0, 6);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
266
349
|
// Backward-pass: stamp every hint from this change with the change's loss
|
|
267
350
|
// (functional ⊕ health) so the accumulator/GA can select on fitness, not just
|
|
268
351
|
// occurrence. Same value for all hints from one change; absent when no
|
|
@@ -277,7 +360,19 @@ export function generateEvolutionHints(report, policy) {
|
|
|
277
360
|
// an evolvable target; when EXACTLY one is evolvable the hint is pinned to it
|
|
278
361
|
// (e.g. a generic artifact-template hint becomes an `artifact-template:design`
|
|
279
362
|
// hint when only design may evolve). Omitting `policy` skips scoping.
|
|
280
|
-
const scoped = scopeHintsByPolicy(drafts, policy);
|
|
363
|
+
const { kept: scoped, droppedFrozen } = scopeHintsByPolicy(drafts, policy);
|
|
364
|
+
// Evolution focus (ses_148b/1483/1488: design-only policy + workflow-kind-only
|
|
365
|
+
// signals ⇒ every draft silently dropped ⇒ "Target: none" on all three runs):
|
|
366
|
+
// when the policy is frozen-by-default with explicit evolvable targets and
|
|
367
|
+
// EVERY draft fell to the frozen filter, re-aim the dropped drafts' evidence
|
|
368
|
+
// at the focused target(s) as one synthesized low-confidence
|
|
369
|
+
// `origin: 'policy-focus'` hint each, instead of dropping the change's
|
|
370
|
+
// signals on the floor. Fires only on a zero-survivor scope (a surviving hint
|
|
371
|
+
// for the focused target is always the better vehicle) and is disabled by
|
|
372
|
+
// `selfEvolution.focus: false` / `learn --no-focus`.
|
|
373
|
+
if (scoped.length === 0 && droppedFrozen.length > 0) {
|
|
374
|
+
scoped.push(...buildPolicyFocusHints(report, droppedFrozen, policy, nextId));
|
|
375
|
+
}
|
|
281
376
|
// Defensive: drop anything that fails the contract validator.
|
|
282
377
|
const validHints = [];
|
|
283
378
|
for (const draft of scoped) {
|
|
@@ -315,20 +410,30 @@ export function generateEvolutionHints(report, policy) {
|
|
|
315
410
|
* targets, so no policy value can name them; a CLI-named id is pinned only when
|
|
316
411
|
* it is a registered, same-kind target that is evolvable under the resolved
|
|
317
412
|
* policy (so `--freeze-target` still wins).
|
|
413
|
+
*
|
|
414
|
+
* Returns the kept hints PLUS the drafts dropped by the frozen filter
|
|
415
|
+
* (`droppedFrozen`: a pinned hint whose target is frozen, or a kind-only hint
|
|
416
|
+
* whose kind has zero evolvable targets) — the evolution-focus pass re-aims
|
|
417
|
+
* their evidence instead of losing it silently.
|
|
318
418
|
*/
|
|
319
419
|
function scopeHintsByPolicy(drafts, policy) {
|
|
320
420
|
if (!policy)
|
|
321
|
-
return drafts;
|
|
421
|
+
return { kept: drafts, droppedFrozen: [] };
|
|
322
422
|
const kept = [];
|
|
423
|
+
const droppedFrozen = [];
|
|
323
424
|
for (const draft of drafts) {
|
|
324
425
|
if (draft.affectedTargetId) {
|
|
325
426
|
if (isCanonicalTargetEvolvable(draft.affectedTargetId, policy))
|
|
326
427
|
kept.push(draft);
|
|
428
|
+
else
|
|
429
|
+
droppedFrozen.push(draft);
|
|
327
430
|
continue;
|
|
328
431
|
}
|
|
329
432
|
const pinId = resolveKindOnlyPinTarget(draft, policy);
|
|
330
|
-
if (pinId === null)
|
|
331
|
-
|
|
433
|
+
if (pinId === null) {
|
|
434
|
+
droppedFrozen.push(draft); // kind has no evolvable target → drop
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
332
437
|
if (pinId !== undefined) {
|
|
333
438
|
kept.push({
|
|
334
439
|
...draft,
|
|
@@ -341,7 +446,7 @@ function scopeHintsByPolicy(drafts, policy) {
|
|
|
341
446
|
}
|
|
342
447
|
kept.push(draft); // ambiguous → keep kind-only/unspecified
|
|
343
448
|
}
|
|
344
|
-
return kept;
|
|
449
|
+
return { kept, droppedFrozen };
|
|
345
450
|
}
|
|
346
451
|
/**
|
|
347
452
|
* Decide the concrete target a kind-only hint should be pinned to under the
|
|
@@ -367,6 +472,101 @@ export function resolveKindOnlyPinTarget(draft, policy) {
|
|
|
367
472
|
return evolvable[0].id;
|
|
368
473
|
return undefined;
|
|
369
474
|
}
|
|
475
|
+
/**
|
|
476
|
+
* The evolution-focus target ids: the explicitly evolvable, REGISTERED
|
|
477
|
+
* canonical targets of a frozen-by-default policy, with the focus switch ON.
|
|
478
|
+
*
|
|
479
|
+
* Empty (focus inactive) when:
|
|
480
|
+
* - the switch is off (`selfEvolution.focus: false` / `learn --no-focus`),
|
|
481
|
+
* - the policy default is `evolvable` (nothing is dropped as frozen-kind in the
|
|
482
|
+
* way focus rescues — and "evolve everything" needs no focus), or
|
|
483
|
+
* - no explicit target survives (freeze-wins already resolved into `explicit`).
|
|
484
|
+
*
|
|
485
|
+
* Exported for the learn command's transparency rendering, so the printed focus
|
|
486
|
+
* line cannot drift from the hint-synthesis pass.
|
|
487
|
+
*/
|
|
488
|
+
export function resolveEvolutionFocusTargets(policy) {
|
|
489
|
+
if (!policy)
|
|
490
|
+
return [];
|
|
491
|
+
if (policy.focusEnabled === false)
|
|
492
|
+
return [];
|
|
493
|
+
if (policy.default !== 'frozen')
|
|
494
|
+
return [];
|
|
495
|
+
return [...policy.explicit.entries()]
|
|
496
|
+
.filter(([id, evolve]) => evolve && lookupCanonicalTarget(id) !== undefined)
|
|
497
|
+
.map(([id]) => id)
|
|
498
|
+
.sort((left, right) => left.localeCompare(right));
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Synthesize the `origin: 'policy-focus'` hint(s) that re-aim a zero-survivor
|
|
502
|
+
* change's policy-dropped signals at the focused target(s).
|
|
503
|
+
*
|
|
504
|
+
* Honesty contract (SUSPECT framing, like credit-path addresses):
|
|
505
|
+
* - evidence is the UNION of the dropped drafts' REAL evidence items — nothing
|
|
506
|
+
* is fabricated, and a change with no dropped signals gets no focus hint
|
|
507
|
+
* ("Target: none" stays the correct evidence-gated abstention);
|
|
508
|
+
* - `confidence` is pinned to `low`: the causal link from the evidence to the
|
|
509
|
+
* focused target is a hypothesis for the proposer to evaluate, not a finding;
|
|
510
|
+
* - the downstream gates are untouched — the focus hint only buys ENTRY into
|
|
511
|
+
* propose/gate/evidence/loss, never promotion.
|
|
512
|
+
*/
|
|
513
|
+
function buildPolicyFocusHints(report, droppedFrozen, policy, nextId) {
|
|
514
|
+
const focusIds = resolveEvolutionFocusTargets(policy);
|
|
515
|
+
if (focusIds.length === 0)
|
|
516
|
+
return [];
|
|
517
|
+
// Union the dropped drafts' real evidence (dedup by file+quote, cap 6 — the
|
|
518
|
+
// existing per-hint evidence convention).
|
|
519
|
+
const evidence = [];
|
|
520
|
+
const seen = new Set();
|
|
521
|
+
for (const draft of droppedFrozen) {
|
|
522
|
+
for (const item of draft.evidence) {
|
|
523
|
+
const key = `${item.file}\u0000${item.quoteOrSummary}`;
|
|
524
|
+
if (seen.has(key))
|
|
525
|
+
continue;
|
|
526
|
+
seen.add(key);
|
|
527
|
+
evidence.push(item);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
const cappedEvidence = evidence.slice(0, 6);
|
|
531
|
+
if (cappedEvidence.length === 0)
|
|
532
|
+
return [];
|
|
533
|
+
// The strongest dropped intent labels the synthesized change type.
|
|
534
|
+
const droppedTypes = new Set(droppedFrozen.map((draft) => draft.proposedChangeType));
|
|
535
|
+
const proposedChangeType = droppedTypes.has('add-verification-step')
|
|
536
|
+
? 'add-verification-step'
|
|
537
|
+
: droppedTypes.has('tighten-output-contract')
|
|
538
|
+
? 'tighten-output-contract'
|
|
539
|
+
: 'clarify-instruction';
|
|
540
|
+
const frozenKinds = [...new Set(droppedFrozen.map((draft) => draft.affectedTargetKind))]
|
|
541
|
+
.sort()
|
|
542
|
+
.join(', ');
|
|
543
|
+
const changeLoss = report.fitnessSample?.loss?.loss;
|
|
544
|
+
return focusIds.flatMap((focusId) => {
|
|
545
|
+
const target = lookupCanonicalTarget(focusId);
|
|
546
|
+
if (!target)
|
|
547
|
+
return [];
|
|
548
|
+
const hint = {
|
|
549
|
+
id: nextId(`focus-${focusId}`),
|
|
550
|
+
sourceChange: report.changeName,
|
|
551
|
+
affectedTargetKind: target.kind,
|
|
552
|
+
affectedTargetId: focusId,
|
|
553
|
+
proposedChangeType,
|
|
554
|
+
problem: limitText(`Evolution focus: the policy evolves only ${focusIds.join(', ')}; ${droppedFrozen.length} learn signal(s) from this change bound to frozen kind(s) (${frozenKinds}) and would otherwise be dropped. Evaluate whether ${focusId} could be improved so these signals do not recur — the evidence is re-aimed (SUSPECT framing), not proof this target caused it.`, 500),
|
|
555
|
+
evidence: cappedEvidence,
|
|
556
|
+
confidence: 'low',
|
|
557
|
+
risk: 'medium',
|
|
558
|
+
expectedBenefit: limitText(`A focused improvement to ${focusId} grounded in this change's observed signals, instead of dropping them because their originating kind is frozen.`, 500),
|
|
559
|
+
evalNeeded: evalSuitesFor(target.kind),
|
|
560
|
+
// `focusId` is already a full `<kind>:<name>` target id, so the grouping
|
|
561
|
+
// key is `<id>:<changeType>` (no doubled kind prefix).
|
|
562
|
+
thresholdKey: `${focusId}:${proposedChangeType}`,
|
|
563
|
+
origin: 'policy-focus',
|
|
564
|
+
};
|
|
565
|
+
if (typeof changeLoss === 'number')
|
|
566
|
+
hint.loss = changeLoss;
|
|
567
|
+
return [hint];
|
|
568
|
+
});
|
|
569
|
+
}
|
|
370
570
|
/**
|
|
371
571
|
* Surface an AMBIGUOUS kind-only evolution hint as an action-required
|
|
372
572
|
* observation. After {@link scopeHintsByPolicy} runs, a hint that still has no
|
|
@@ -428,6 +628,67 @@ export function detectUnbindableHintObservations(hints, policy) {
|
|
|
428
628
|
}
|
|
429
629
|
return observations;
|
|
430
630
|
}
|
|
631
|
+
/**
|
|
632
|
+
* One health offender as evidence prose: 'health offender: cyclomatic 41 in
|
|
633
|
+
* foo() (182 lines)'. The metric+value lead so truncation never eats the
|
|
634
|
+
* signal; function/length are the navigable address parts when known.
|
|
635
|
+
*/
|
|
636
|
+
function describeHealthOffender(offender) {
|
|
637
|
+
const value = Number.isInteger(offender.value)
|
|
638
|
+
? String(offender.value)
|
|
639
|
+
: offender.value.toFixed(1);
|
|
640
|
+
let text = `health offender: ${offender.metric} ${value}`;
|
|
641
|
+
if (offender.function)
|
|
642
|
+
text += ` in ${offender.function}()`;
|
|
643
|
+
if (offender.functionLength !== undefined)
|
|
644
|
+
text += ` (${offender.functionLength} lines)`;
|
|
645
|
+
return text;
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The producing canonical target health offenders route to. Code shape (the
|
|
649
|
+
* thing the offenders measure) is a property of the generated IMPLEMENTATION;
|
|
650
|
+
* the adapter has no artifact contents to walk a per-offender credit path
|
|
651
|
+
* with, so the trace target is the STATIC implementation producer — the tasks
|
|
652
|
+
* template that shapes how implementation work is cut. Mirrors
|
|
653
|
+
* {@link PRODUCING_TARGET_BY_LAST_NODE}'s task → tasks-template mapping.
|
|
654
|
+
*/
|
|
655
|
+
const HEALTH_ROUTING_TARGET_ID = 'artifact-template:tasks';
|
|
656
|
+
/**
|
|
657
|
+
* Surface health offenders whose routing lands on a FROZEN target as ONE
|
|
658
|
+
* aggregated ACTION-REQUIRED observation — the honest dead-end. Without this,
|
|
659
|
+
* a freezing policy makes the offender signal vanish silently: the enrichment
|
|
660
|
+
* in {@link generateEvolutionHints} only rides on drafts that survive scoping.
|
|
661
|
+
* Mirrors {@link detectUnbindableHintObservations}' shape and wiring (call it
|
|
662
|
+
* wherever that one is called, on evolving runs). Returns `[]` when `policy`
|
|
663
|
+
* is absent or the routing target is evolvable — the enrichment path already
|
|
664
|
+
* carries the signal there — so the common case stays byte-identical.
|
|
665
|
+
*/
|
|
666
|
+
export function detectFrozenHealthRoutingObservations(report, policy) {
|
|
667
|
+
if (!policy)
|
|
668
|
+
return [];
|
|
669
|
+
const offenders = report.fitnessSample?.healthContributors ?? [];
|
|
670
|
+
if (offenders.length === 0)
|
|
671
|
+
return [];
|
|
672
|
+
if (isCanonicalTargetEvolvable(HEALTH_ROUTING_TARGET_ID, policy))
|
|
673
|
+
return [];
|
|
674
|
+
return [
|
|
675
|
+
{
|
|
676
|
+
code: 'health-routing-frozen',
|
|
677
|
+
// ACTION, not DEFECT: the routing worked — the destination is frozen by
|
|
678
|
+
// operator policy, so the fix is an operator decision (unfreeze or fix
|
|
679
|
+
// the code by hand), not a tool repair. Same triage rationale as
|
|
680
|
+
// `evolution-target-unresolved`.
|
|
681
|
+
severity: 'action',
|
|
682
|
+
summary: limitText(`${offenders.length} health offender(s) trace to frozen target ${HEALTH_ROUTING_TARGET_ID} — ` +
|
|
683
|
+
`routing dead-end; pass --evolve-target ${HEALTH_ROUTING_TARGET_ID} or address the code manually`, 420),
|
|
684
|
+
evidence: offenders.slice(0, 6).map((offender) => ({
|
|
685
|
+
file: toPosix(offender.file),
|
|
686
|
+
detail: describeHealthOffender(offender),
|
|
687
|
+
})),
|
|
688
|
+
tags: ['health', 'routing', 'frozen', 'action-required'],
|
|
689
|
+
},
|
|
690
|
+
];
|
|
691
|
+
}
|
|
431
692
|
function inferTemplateObservation(candidate) {
|
|
432
693
|
const tags = candidate.tags;
|
|
433
694
|
const templateTag = tags.find((tag) => /^template:/i.test(tag));
|
|
@@ -540,6 +801,13 @@ function uniqueByFile(items) {
|
|
|
540
801
|
* learn → propose bridge: it writes an ARTIFACT under `learn-handoffs/` (never a
|
|
541
802
|
* canonical file), so it is proposal-only. Reuses the existing
|
|
542
803
|
* `learn-handoffs/<change>/<timestamp>/` convention. Returns the file path.
|
|
804
|
+
*
|
|
805
|
+
* When any hint carries credit paths, a sibling `slice.md` is written next to
|
|
806
|
+
* `hints.json` (and referenced via the optional top-level `slice` key) so the
|
|
807
|
+
* HOST author on the `--from-edits` path can read the real artifact text along
|
|
808
|
+
* the failing paths — the same CREDIT-PATH SLICE the `--agent` prompt renders.
|
|
809
|
+
* Old readers ignore unknown top-level keys (`extractHintCandidates` reads only
|
|
810
|
+
* the hint arrays), so the addition is back-compatible.
|
|
543
811
|
*/
|
|
544
812
|
export async function persistLearnHints(opts) {
|
|
545
813
|
const stamp = (opts.now ? opts.now() : new Date())
|
|
@@ -548,7 +816,14 @@ export async function persistLearnHints(opts) {
|
|
|
548
816
|
const dir = path.join(opts.projectRoot, '.synergyspec-selfevolving', 'learn-handoffs', opts.changeName, stamp);
|
|
549
817
|
await fs.mkdir(dir, { recursive: true });
|
|
550
818
|
const file = path.join(dir, 'hints.json');
|
|
551
|
-
|
|
819
|
+
const slice = buildProposerSlice(opts.hints);
|
|
820
|
+
if (slice.length > 0) {
|
|
821
|
+
await fs.writeFile(path.join(dir, 'slice.md'), `# Credit-path slice: ${opts.changeName}\n\n${slice}\n`, 'utf8');
|
|
822
|
+
}
|
|
823
|
+
await fs.writeFile(file, JSON.stringify({
|
|
824
|
+
evolutionHints: opts.hints,
|
|
825
|
+
...(slice.length > 0 ? { slice: 'slice.md' } : {}),
|
|
826
|
+
}, null, 2) + '\n', 'utf8');
|
|
552
827
|
return file;
|
|
553
828
|
}
|
|
554
829
|
function evalSuitesFor(kind) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-level diff for the 演进智能体 EVOLVING AGENT (optimizer.step) — loop v2
|
|
3
|
+
* (self-evolution as in-context RL).
|
|
4
|
+
*
|
|
5
|
+
* The EVOLVING AGENT makes ONE bounded edit ≤ L changed lines (added + removed).
|
|
6
|
+
* To enforce that budget and to feed the 范围⊆诊断 scope⊆diagnosis gate
|
|
7
|
+
* ({@link import('./scope-gate.js')}), we need an honest line-level account of
|
|
8
|
+
* which lines the proposed full-file replacement actually changed — not the
|
|
9
|
+
* whole-file-replacement unified diff `edits-contract.ts` renders (that counts
|
|
10
|
+
* every line as removed-then-added).
|
|
11
|
+
*
|
|
12
|
+
* This is a classic LCS (longest common subsequence) line diff: the lines NOT
|
|
13
|
+
* on the LCS are the real changes. `linesAdded` / `linesRemoved` are the true
|
|
14
|
+
* insert/delete counts, and `changedRanges` are the 1-based line spans of the
|
|
15
|
+
* inserted lines IN THE PROPOSED FILE (a pure deletion that inserts nothing
|
|
16
|
+
* produces no range — there is no proposed line to anchor it to; its removal
|
|
17
|
+
* still counts toward `linesRemoved` and the budget).
|
|
18
|
+
*
|
|
19
|
+
* Pure + dependency-free so it is golden-testable and reusable by the gate.
|
|
20
|
+
*/
|
|
21
|
+
/** A 1-based, inclusive span of changed (inserted) lines in the PROPOSED file. */
|
|
22
|
+
export interface ChangedRange {
|
|
23
|
+
/** 1-based first changed line in the proposed file. */
|
|
24
|
+
startLine: number;
|
|
25
|
+
/** 1-based last changed line in the proposed file (inclusive). */
|
|
26
|
+
endLine: number;
|
|
27
|
+
}
|
|
28
|
+
/** Result of {@link lineDiff} for one (current → proposed) file pair. */
|
|
29
|
+
export interface LineDiffResult {
|
|
30
|
+
/** Count of lines present in the proposed file but not the current file. */
|
|
31
|
+
linesAdded: number;
|
|
32
|
+
/** Count of lines present in the current file but not the proposed file. */
|
|
33
|
+
linesRemoved: number;
|
|
34
|
+
/** Coalesced 1-based spans of the inserted lines in the proposed file. */
|
|
35
|
+
changedRanges: ChangedRange[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Compute the LCS-based line diff between `currentContent` and
|
|
39
|
+
* `proposedContent`. Lines are matched with the standard dynamic-programming
|
|
40
|
+
* LCS so that an unchanged block in the middle of an edit is not double-counted
|
|
41
|
+
* as remove+add. `changedRanges` covers the inserted lines of the proposed file
|
|
42
|
+
* (1-based), coalescing adjacent inserts into one span.
|
|
43
|
+
*/
|
|
44
|
+
export declare function lineDiff(currentContent: string, proposedContent: string): LineDiffResult;
|
|
45
|
+
/** One edit: the COMPLETE new contents for a file the agent proposes to rewrite. */
|
|
46
|
+
export interface DiffEdit {
|
|
47
|
+
relPath: string;
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sum added + removed lines across every edit, comparing each edit's proposed
|
|
52
|
+
* content to the current on-disk content of the same file. An edit whose
|
|
53
|
+
* `relPath` has no current entry is diffed against empty content (every
|
|
54
|
+
* proposed line is an insert) — defensive; the EVOLVING AGENT only edits
|
|
55
|
+
* existing lineage files, so this should not happen in practice.
|
|
56
|
+
*
|
|
57
|
+
* This is the value the EVOLVING AGENT's ≤ L budget is checked against.
|
|
58
|
+
*/
|
|
59
|
+
export declare function countChangedLines(edits: readonly DiffEdit[], currentFiles: readonly DiffEdit[]): number;
|
|
60
|
+
//# sourceMappingURL=line-diff.d.ts.map
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-level diff for the 演进智能体 EVOLVING AGENT (optimizer.step) — loop v2
|
|
3
|
+
* (self-evolution as in-context RL).
|
|
4
|
+
*
|
|
5
|
+
* The EVOLVING AGENT makes ONE bounded edit ≤ L changed lines (added + removed).
|
|
6
|
+
* To enforce that budget and to feed the 范围⊆诊断 scope⊆diagnosis gate
|
|
7
|
+
* ({@link import('./scope-gate.js')}), we need an honest line-level account of
|
|
8
|
+
* which lines the proposed full-file replacement actually changed — not the
|
|
9
|
+
* whole-file-replacement unified diff `edits-contract.ts` renders (that counts
|
|
10
|
+
* every line as removed-then-added).
|
|
11
|
+
*
|
|
12
|
+
* This is a classic LCS (longest common subsequence) line diff: the lines NOT
|
|
13
|
+
* on the LCS are the real changes. `linesAdded` / `linesRemoved` are the true
|
|
14
|
+
* insert/delete counts, and `changedRanges` are the 1-based line spans of the
|
|
15
|
+
* inserted lines IN THE PROPOSED FILE (a pure deletion that inserts nothing
|
|
16
|
+
* produces no range — there is no proposed line to anchor it to; its removal
|
|
17
|
+
* still counts toward `linesRemoved` and the budget).
|
|
18
|
+
*
|
|
19
|
+
* Pure + dependency-free so it is golden-testable and reusable by the gate.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Split content into lines for diffing. A trailing newline is normalized away
|
|
23
|
+
* first (so `"a\n"` and `"a"` are one line, matching `renderUnifiedDiff`'s
|
|
24
|
+
* convention); empty content is zero lines, never `['']`.
|
|
25
|
+
*/
|
|
26
|
+
function toLines(content) {
|
|
27
|
+
if (content.length === 0)
|
|
28
|
+
return [];
|
|
29
|
+
return content.replace(/\n$/, '').split('\n');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compute the LCS-based line diff between `currentContent` and
|
|
33
|
+
* `proposedContent`. Lines are matched with the standard dynamic-programming
|
|
34
|
+
* LCS so that an unchanged block in the middle of an edit is not double-counted
|
|
35
|
+
* as remove+add. `changedRanges` covers the inserted lines of the proposed file
|
|
36
|
+
* (1-based), coalescing adjacent inserts into one span.
|
|
37
|
+
*/
|
|
38
|
+
export function lineDiff(currentContent, proposedContent) {
|
|
39
|
+
const a = toLines(currentContent); // current
|
|
40
|
+
const b = toLines(proposedContent); // proposed
|
|
41
|
+
const n = a.length;
|
|
42
|
+
const m = b.length;
|
|
43
|
+
// LCS length table: lcs[i][j] = LCS length of a[i..] and b[j..].
|
|
44
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
45
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
46
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
47
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
let linesAdded = 0;
|
|
51
|
+
let linesRemoved = 0;
|
|
52
|
+
// 1-based line numbers (in b) of every inserted proposed line.
|
|
53
|
+
const insertedProposedLines = [];
|
|
54
|
+
let i = 0;
|
|
55
|
+
let j = 0;
|
|
56
|
+
while (i < n && j < m) {
|
|
57
|
+
if (a[i] === b[j]) {
|
|
58
|
+
// Common line: advance both, no change.
|
|
59
|
+
i++;
|
|
60
|
+
j++;
|
|
61
|
+
}
|
|
62
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
63
|
+
// Dropping a[i] keeps a longer LCS → a[i] was removed.
|
|
64
|
+
linesRemoved++;
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Dropping b[j] keeps a longer LCS → b[j] was inserted.
|
|
69
|
+
linesAdded++;
|
|
70
|
+
insertedProposedLines.push(j + 1);
|
|
71
|
+
j++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Tail: leftover current lines are removals, leftover proposed lines inserts.
|
|
75
|
+
while (i < n) {
|
|
76
|
+
linesRemoved++;
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
while (j < m) {
|
|
80
|
+
linesAdded++;
|
|
81
|
+
insertedProposedLines.push(j + 1);
|
|
82
|
+
j++;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
linesAdded,
|
|
86
|
+
linesRemoved,
|
|
87
|
+
changedRanges: coalesce(insertedProposedLines),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Coalesce a sorted list of 1-based line numbers into inclusive ranges. */
|
|
91
|
+
function coalesce(lines) {
|
|
92
|
+
if (lines.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const ranges = [];
|
|
95
|
+
let start = lines[0];
|
|
96
|
+
let prev = lines[0];
|
|
97
|
+
for (let k = 1; k < lines.length; k++) {
|
|
98
|
+
const cur = lines[k];
|
|
99
|
+
if (cur === prev + 1) {
|
|
100
|
+
prev = cur;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
ranges.push({ startLine: start, endLine: prev });
|
|
104
|
+
start = cur;
|
|
105
|
+
prev = cur;
|
|
106
|
+
}
|
|
107
|
+
ranges.push({ startLine: start, endLine: prev });
|
|
108
|
+
return ranges;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Sum added + removed lines across every edit, comparing each edit's proposed
|
|
112
|
+
* content to the current on-disk content of the same file. An edit whose
|
|
113
|
+
* `relPath` has no current entry is diffed against empty content (every
|
|
114
|
+
* proposed line is an insert) — defensive; the EVOLVING AGENT only edits
|
|
115
|
+
* existing lineage files, so this should not happen in practice.
|
|
116
|
+
*
|
|
117
|
+
* This is the value the EVOLVING AGENT's ≤ L budget is checked against.
|
|
118
|
+
*/
|
|
119
|
+
export function countChangedLines(edits, currentFiles) {
|
|
120
|
+
const currentByPath = new Map(currentFiles.map((f) => [f.relPath.replace(/\\/g, '/'), f.content]));
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (const edit of edits) {
|
|
123
|
+
const rel = edit.relPath.replace(/\\/g, '/');
|
|
124
|
+
const current = currentByPath.get(rel) ?? '';
|
|
125
|
+
const d = lineDiff(current, edit.content);
|
|
126
|
+
total += d.linesAdded + d.linesRemoved;
|
|
127
|
+
}
|
|
128
|
+
return total;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=line-diff.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Throw if `abs` resolves outside `repoRoot`. */
|
|
2
|
+
export declare function assertWithinRepo(repoRoot: string, abs: string): void;
|
|
3
|
+
/**
|
|
4
|
+
* Atomic write via sibling tmp file + rename; cleans up on rename failure. After
|
|
5
|
+
* the rename, fsync the file and (best-effort) its parent directory so a host
|
|
6
|
+
* crash cannot lose a just-renamed record a later separate process relies on —
|
|
7
|
+
* the rename is only durable once the directory entry itself is flushed.
|
|
8
|
+
*/
|
|
9
|
+
export declare function writeFileAtomic(abs: string, content: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Append `content` to a file and fsync the file descriptor so a host crash
|
|
12
|
+
* cannot lose the just-appended record. Used by the append-only stores (the
|
|
13
|
+
* 版本账本 ledger and the 否决缓冲 reject-buffer): a later SEPARATE process
|
|
14
|
+
* (`resumeEpisode`) relies on these records being durable, so the OS write cache
|
|
15
|
+
* must be flushed before we return. Does NOT change the bytes written (one
|
|
16
|
+
* `append` of `content`); only adds the fsync. Byte content is the caller's.
|
|
17
|
+
*/
|
|
18
|
+
export declare function appendFileDurable(abs: string, content: string): Promise<void>;
|
|
19
|
+
//# sourceMappingURL=fs-safe.d.ts.map
|