peaks-loop 4.0.0-beta.2 → 4.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 (71) hide show
  1. package/README-en.md +16 -3
  2. package/README.md +16 -3
  3. package/dist/src/cli/commands/asset-commands.d.ts +45 -0
  4. package/dist/src/cli/commands/asset-commands.js +404 -0
  5. package/dist/src/cli/commands/bee-commands.d.ts +26 -0
  6. package/dist/src/cli/commands/bee-commands.js +169 -0
  7. package/dist/src/cli/commands/evolution-commands.d.ts +27 -0
  8. package/dist/src/cli/commands/evolution-commands.js +393 -0
  9. package/dist/src/cli/commands/loop-commands.d.ts +19 -13
  10. package/dist/src/cli/commands/loop-commands.js +147 -13
  11. package/dist/src/cli/commands/sediment-commands.js +9 -0
  12. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.d.ts +67 -0
  13. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.js +173 -0
  14. package/dist/src/cli/program.js +17 -0
  15. package/dist/src/services/crystallization/crystallization-service.d.ts +239 -0
  16. package/dist/src/services/crystallization/crystallization-service.js +397 -0
  17. package/dist/src/services/crystallization/crystallization-store.d.ts +75 -0
  18. package/dist/src/services/crystallization/crystallization-store.js +193 -0
  19. package/dist/src/services/crystallization/crystallization-types.d.ts +313 -0
  20. package/dist/src/services/crystallization/crystallization-types.js +299 -0
  21. package/dist/src/services/crystallization/evidence-brief-builder.d.ts +237 -0
  22. package/dist/src/services/crystallization/evidence-brief-builder.js +240 -0
  23. package/dist/src/services/evolution/evolution-service.d.ts +137 -0
  24. package/dist/src/services/evolution/evolution-service.js +281 -0
  25. package/dist/src/services/evolution/evolution-store.d.ts +57 -0
  26. package/dist/src/services/evolution/evolution-store.js +232 -0
  27. package/dist/src/services/evolution/evolution-types.d.ts +706 -0
  28. package/dist/src/services/evolution/evolution-types.js +286 -0
  29. package/dist/src/services/evolution/independent-evaluator-runner.d.ts +90 -0
  30. package/dist/src/services/evolution/independent-evaluator-runner.js +106 -0
  31. package/dist/src/services/evolution/regression-skeptic-runner.d.ts +64 -0
  32. package/dist/src/services/evolution/regression-skeptic-runner.js +113 -0
  33. package/dist/src/services/loop/loop-bee-relation-service.d.ts +107 -0
  34. package/dist/src/services/loop/loop-bee-relation-service.js +211 -0
  35. package/dist/src/services/loop/loop-bee-relation-store.d.ts +53 -0
  36. package/dist/src/services/loop/loop-bee-relation-store.js +130 -0
  37. package/dist/src/services/loop/loop-bee-relation-types.d.ts +118 -0
  38. package/dist/src/services/loop/loop-bee-relation-types.js +114 -0
  39. package/dist/src/services/loop/loop-release-service.d.ts +46 -0
  40. package/dist/src/services/loop/loop-release-service.js +66 -0
  41. package/dist/src/services/loop/loop-release-store.d.ts +55 -0
  42. package/dist/src/services/loop/loop-release-store.js +134 -0
  43. package/dist/src/services/loop/loop-release-types.d.ts +250 -0
  44. package/dist/src/services/loop/loop-release-types.js +162 -0
  45. package/dist/src/services/share/bundle-reader.d.ts +100 -0
  46. package/dist/src/services/share/bundle-reader.js +434 -0
  47. package/dist/src/services/share/bundle-types.d.ts +392 -0
  48. package/dist/src/services/share/bundle-types.js +318 -0
  49. package/dist/src/services/share/bundle-writer.d.ts +97 -0
  50. package/dist/src/services/share/bundle-writer.js +429 -0
  51. package/dist/src/services/share/run-state-contract.d.ts +133 -0
  52. package/dist/src/services/share/run-state-contract.js +138 -0
  53. package/dist/src/services/skillhub/release-import.js +7 -2
  54. package/dist/src/services/skillhub/release-retain.js +6 -1
  55. package/dist/src/services/skillhub/sqlite-store.js +17 -1
  56. package/dist/src/services/skillhub/types.d.ts +10 -0
  57. package/dist/src/services/standards/loop-engineering-lint.d.ts +38 -0
  58. package/dist/src/services/standards/loop-engineering-lint.js +101 -0
  59. package/dist/src/services/standards/loop-engineering-readiness-lint.d.ts +111 -0
  60. package/dist/src/services/standards/loop-engineering-readiness-lint.js +248 -0
  61. package/dist/src/shared/version.d.ts +1 -1
  62. package/dist/src/shared/version.js +1 -1
  63. package/examples/video-demo/README.md +57 -0
  64. package/examples/video-demo/out/en-closing-960.png +0 -0
  65. package/examples/video-demo/out/zh-closing-960.png +0 -0
  66. package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
  67. package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
  68. package/package.json +6 -2
  69. package/skills/peaks-code/SKILL.md +13 -0
  70. package/skills/peaks-content/SKILL.md +272 -0
  71. package/skills/peaks-issue-fix-orchestrator/SKILL.md +307 -0
@@ -0,0 +1,240 @@
1
+ import { z } from "zod";
2
+ import { EvidenceBriefSchema, } from "./crystallization-types.js";
3
+ /**
4
+ * EvidenceBriefBuilder — spec §4.7 / §10 RL-7.
5
+ *
6
+ * The brief is a 4-section natural-language projection that MUST
7
+ * accompany every user-facing recommendation. The shape guard is
8
+ * encoded in `EvidenceBriefSchema` (see crystallization-types.ts);
9
+ * this module is the LLM-side entry point that PRODUCES the brief
10
+ * from a finished trace + evaluator summary and the validator that
11
+ * REFUSES to render a recommendation payload without all 4 sections.
12
+ *
13
+ * Hard rules (mirrored from spec):
14
+ *
15
+ * 1. Every section must be a non-empty NL string (1-2 sentences
16
+ * for what_happened / why_it_matters / what_learned; 1
17
+ * sentence for what_action). An empty section is a hard
18
+ * `MISSING_BRIEF_SECTION` error.
19
+ *
20
+ * 2. The build path is lenient on raw prose and strict on the
21
+ * final shape — `buildEvidenceBrief` calls
22
+ * `EvidenceBriefSchema.parse(...)` so a partial brief throws
23
+ * before reaching the database.
24
+ *
25
+ * 3. `renderRecommendationPayload` refuses to return a
26
+ * recommendation envelope unless the brief passes the 4-section
27
+ * guard. This is the CLI's single source of truth for "may I
28
+ * show this to the user?".
29
+ *
30
+ * 4. The `evaluator_summary` is a separate field on the
31
+ * crystallization_event row (spec §4.5) and is NOT part of the
32
+ * brief. It feeds the broader recommendation envelope.
33
+ *
34
+ * Out of scope (deferred to later slices):
35
+ * - LLM-authored brief generation from live trace evidence (M8 dogfood).
36
+ * - brief rewrite-on-update semantics.
37
+ */
38
+ /* ---------------------------------------------------------------------- */
39
+ /* Input shapes — the builder is the only place these normalize. */
40
+ /* ---------------------------------------------------------------------- */
41
+ /**
42
+ * The minimum viable trace shape that the builder needs to render
43
+ * a brief. The trace is the EVIDENCE; the builder is format-stable
44
+ * so a future slice can swap in a richer trace source without
45
+ * editing every caller.
46
+ */
47
+ export const BriefTraceInputSchema = z.object({
48
+ trace_id: z.string().trim().min(1).max(256),
49
+ /**
50
+ * The full NL recap of the run. Required for what_happened.
51
+ * 1-2 sentences is the spec guidance; the builder does not
52
+ * truncate, but tests passing multi-paragraph input will be
53
+ * warned at the Zod boundary (max 4000 chars per section).
54
+ */
55
+ what_happened: z.string().trim().min(1).max(4000),
56
+ /**
57
+ * The "why this matters" recap. Required for why_it_matters.
58
+ */
59
+ why_it_matters: z.string().trim().min(1).max(4000),
60
+ /**
61
+ * Failure-mode and preference-extraction recap. Required for
62
+ * what_learned.
63
+ */
64
+ what_learned: z.string().trim().min(1).max(4000),
65
+ /**
66
+ * The recommended action. Required for what_action (1
67
+ * sentence per spec).
68
+ */
69
+ what_action: z.string().trim().min(1).max(4000),
70
+ /**
71
+ * Optional structured bullets (e.g. "4 phases, 3 gates passed,
72
+ * 1 evaluator verdict"). Counts are allowed at this layer; the
73
+ * brief section is the NL primary (RL-7).
74
+ */
75
+ bullets: z.array(z.string().trim().min(1).max(1000)).default([]),
76
+ /**
77
+ * Source trace ids backing the brief (the column on
78
+ * crystallization_event spec §4.5).
79
+ */
80
+ source_trace_pointers: z
81
+ .array(z.string().trim().min(1).max(256))
82
+ .default([]),
83
+ });
84
+ /**
85
+ * The summary emitted by the independent evaluators (spec §4.5
86
+ * `evaluator_summary` field). Optional for the brief builder — the
87
+ * brief can be built without it, but a filled evaluator_summary
88
+ * flows into the recommendation envelope.
89
+ */
90
+ export const EvaluatorSummarySchema = z.object({
91
+ one_liner: z.string().trim().max(1000).default(""),
92
+ risk_tags: z.array(z.string().trim().min(1).max(200)).default([]),
93
+ });
94
+ /* ---------------------------------------------------------------------- */
95
+ /* Recommendation envelope — the CLI renders this. */
96
+ /* ---------------------------------------------------------------------- */
97
+ export const RecommendationPayloadSchema = z.object({
98
+ brief: EvidenceBriefSchema,
99
+ bullets: z.array(z.string().trim().min(1).max(1000)).default([]),
100
+ source_trace_pointers: z
101
+ .array(z.string().trim().min(1).max(256))
102
+ .default([]),
103
+ evaluator_summary: EvaluatorSummarySchema.default({
104
+ one_liner: "",
105
+ risk_tags: [],
106
+ }),
107
+ });
108
+ /* ---------------------------------------------------------------------- */
109
+ /* Errors */
110
+ /* ---------------------------------------------------------------------- */
111
+ export class BriefSectionError extends Error {
112
+ code = "MISSING_BRIEF_SECTION";
113
+ findings;
114
+ constructor(message, findings = []) {
115
+ super(message);
116
+ this.name = "BriefSectionError";
117
+ this.findings = findings;
118
+ }
119
+ }
120
+ /* ---------------------------------------------------------------------- */
121
+ /* Builder */
122
+ /* ---------------------------------------------------------------------- */
123
+ /**
124
+ * Build a 4-section EvidenceBrief from a finished trace recap.
125
+ *
126
+ * The function is STRICT: it parses the input trace through
127
+ * `BriefTraceInputSchema`, then parses the assembled brief through
128
+ * `EvidenceBriefSchema`. Any missing section throws
129
+ * `BriefSectionError(MISSING_BRIEF_SECTION)` — the same code that
130
+ * `safeParseEvidenceBrief` returns — so a single error code is the
131
+ * user-facing signal for "brief incomplete, do not render".
132
+ *
133
+ * Brief-section related ZodErrors (e.g. `min(1)` on a section) are
134
+ * translated to `BriefSectionError` so callers only handle one
135
+ * shape of failure. Non-section ZodErrors (e.g. invalid `trace_id`)
136
+ * are re-raised unchanged.
137
+ *
138
+ * NOTE: callers who already have raw NL strings can also call
139
+ * `parseEvidenceBrief` directly; this function is the opinionated
140
+ * shape for trace -> brief.
141
+ */
142
+ export function buildEvidenceBrief(trace, evaluatorSummary) {
143
+ let parsedTrace;
144
+ try {
145
+ parsedTrace = BriefTraceInputSchema.parse(trace);
146
+ }
147
+ catch (err) {
148
+ throw translateBriefZodError(err);
149
+ }
150
+ // Assemble the 4 sections verbatim from the trace recap. The Zod
151
+ // parser below re-asserts the 4-section guard.
152
+ const candidate = {
153
+ what_happened: parsedTrace.what_happened,
154
+ why_it_matters: parsedTrace.why_it_matters,
155
+ what_learned: parsedTrace.what_learned,
156
+ what_action: parsedTrace.what_action,
157
+ };
158
+ const result = EvidenceBriefSchema.safeParse(candidate);
159
+ if (!result.success) {
160
+ const findings = result.error.issues.map((i) => ({
161
+ path: i.path.join("."),
162
+ message: i.message,
163
+ }));
164
+ throw new BriefSectionError("evidence_brief must contain all 4 sections (what_happened, why_it_matters, what_learned, what_action) with non-empty content (spec §4.7 / RL-7)", findings);
165
+ }
166
+ // Silence the unused-var lint on evaluatorSummary at this layer —
167
+ // the builder currently does not consume it (the brief is NL
168
+ // primary), but the surface is preserved for callers via
169
+ // `renderRecommendationPayload` below.
170
+ void evaluatorSummary;
171
+ return result.data;
172
+ }
173
+ /**
174
+ * Translate a ZodError raised on a brief-section path into a
175
+ * `BriefSectionError`. Non-brief errors are re-raised.
176
+ */
177
+ function translateBriefZodError(err) {
178
+ if (err instanceof z.ZodError) {
179
+ const issues = err.issues;
180
+ const allBrief = issues.every((i) => BRIEF_SECTION_KEYS.has(i.path.join(".")));
181
+ if (allBrief) {
182
+ const findings = issues.map((i) => ({
183
+ path: i.path.join("."),
184
+ message: i.message,
185
+ }));
186
+ throw new BriefSectionError("evidence_brief must contain all 4 sections (what_happened, why_it_matters, what_learned, what_action) with non-empty content (spec §4.7 / RL-7)", findings);
187
+ }
188
+ }
189
+ throw err;
190
+ }
191
+ const BRIEF_SECTION_KEYS = new Set([
192
+ "what_happened",
193
+ "why_it_matters",
194
+ "what_learned",
195
+ "what_action",
196
+ ]);
197
+ /**
198
+ * Render the user-facing recommendation envelope (brief + bullets +
199
+ * trace pointers + evaluator summary). The function refuses to
200
+ * return a payload unless ALL 4 brief sections are present — this
201
+ * is the single CLI-facing gate per spec §10 RL-7.
202
+ *
203
+ * On success, returns the validated `RecommendationPayload`. On a
204
+ * missing-section brief, throws `BriefSectionError`.
205
+ */
206
+ export function renderRecommendationPayload(trace, evaluatorSummary = { one_liner: "", risk_tags: [] }) {
207
+ const brief = buildEvidenceBrief(trace, evaluatorSummary);
208
+ const payload = RecommendationPayloadSchema.parse({
209
+ brief,
210
+ bullets: trace.bullets ?? [],
211
+ source_trace_pointers: trace.source_trace_pointers ?? [],
212
+ evaluator_summary: evaluatorSummary,
213
+ });
214
+ return payload;
215
+ }
216
+ /**
217
+ * Convenience: validate an arbitrary unknown payload AS IF it were a
218
+ * recommendation envelope. Returns a Result-like shape so the CLI can
219
+ * render a structured MISSING_BRIEF_SECTION error without try/catch
220
+ * noise.
221
+ */
222
+ export function safeRenderRecommendationPayload(input) {
223
+ const r = RecommendationPayloadSchema.safeParse(input);
224
+ if (r.success)
225
+ return { ok: true, payload: r.data };
226
+ const findings = r.error.issues.map((i) => ({
227
+ path: i.path.join("."),
228
+ message: i.message,
229
+ }));
230
+ // Map brief failures to MISSING_BRIEF_SECTION. A failure is
231
+ // recognized as brief iff (a) every issue points at the brief or
232
+ // its sections, OR (b) the explicit refine message is present.
233
+ const isBrief = r.error.issues.every((i) => i.message.includes("evidence_brief must contain all 4 sections") ||
234
+ BRIEF_SECTION_KEYS.has(i.path.join(".")) ||
235
+ i.path[0] === "brief" ||
236
+ (i.path.length >= 2 && i.path[0] === "brief"));
237
+ return isBrief
238
+ ? { ok: false, code: "MISSING_BRIEF_SECTION", findings }
239
+ : { ok: false, findings };
240
+ }
@@ -0,0 +1,137 @@
1
+ import type Database from "better-sqlite3";
2
+ import { ZodError } from "zod";
3
+ import { EVOLUTION_DEFAULT_DELTA_MIN, type EvolutionEvaluation, type EvolutionProposal, type EvolutionProposalInput, type EvolutionTargetKind, type EvolutionVerdict } from "./evolution-types.js";
4
+ /**
5
+ * EvolutionService — Darwin-style ratchet enforcement.
6
+ *
7
+ * Hard rules enforced at THIS layer (not just the Zod schema) so
8
+ * the ratchet cannot be bypassed by a malformed input:
9
+ *
10
+ * - AC-8: exactly one target (target_count === 1) AND
11
+ * exactly one optimization dimension per round.
12
+ * Code: EVOLUTION_MULTI_OBJECT, EVOLUTION_MULTI_DIMENSION.
13
+ * - AC-10: the scorer (evaluator) MUST NOT be the author.
14
+ * Code: EVOLUTION_SELF_SCORE.
15
+ * - AC-11: a `keep` verdict is BLOCKED when
16
+ * score_delta < score_delta_min (default 1.0).
17
+ * Code: EVOLUTION_DELTA_BELOW_THRESHOLD.
18
+ * - AC-12: evaluator_id MUST be a separate agent from
19
+ * author_id and skeptic_id.
20
+ * - AC-14: skeptic_id MUST be a separate agent from
21
+ * author_id and evaluator_id.
22
+ *
23
+ * The service is intentionally narrow: createProposal / score /
24
+ * markVerdict / list / get. The actual scoring agents
25
+ * (IndependentEvaluatorRunner, RegressionSkepticRunner) live in
26
+ * separate files so the author reasoning is NEVER imported into
27
+ * the evaluator's process boundary.
28
+ */
29
+ export type EvolutionIntegrityErrorCode = "EVOLUTION_MULTI_OBJECT" | "EVOLUTION_MULTI_DIMENSION" | "EVOLUTION_SELF_SCORE" | "EVOLUTION_DELTA_BELOW_THRESHOLD" | "EVOLUTION_MISSING_USER_CONFIRMATION" | "EVOLUTION_BELOW_RUBRIC";
30
+ export declare class EvolutionIntegrityError extends Error {
31
+ readonly code: EvolutionIntegrityErrorCode;
32
+ readonly findings: ReadonlyArray<{
33
+ path: string;
34
+ message: string;
35
+ }>;
36
+ constructor(code: EvolutionIntegrityErrorCode, message: string, findings?: ReadonlyArray<{
37
+ path: string;
38
+ message: string;
39
+ }>);
40
+ }
41
+ export declare class EvolutionService {
42
+ private readonly db;
43
+ constructor(db: Database.Database);
44
+ /**
45
+ * Persist a new EvolutionProposal. Enforces AC-8 (single object /
46
+ * single dimension) BEFORE writing the row.
47
+ *
48
+ * The proposal id is auto-generated (`eval-<hex>`); callers supply
49
+ * the proposal content only. The proposal's `verdict` is set to
50
+ * `needs-user-decision` until the score + skeptic step lands.
51
+ *
52
+ * Throws:
53
+ * - ZodError if the input fails schema validation.
54
+ * - EvolutionIntegrityError(EVOLUTION_MULTI_OBJECT) if
55
+ * single_object !== true.
56
+ * - EvolutionIntegrityError(EVOLUTION_MULTI_DIMENSION) if
57
+ * single_optimization_dimension !== true.
58
+ */
59
+ createProposal(input: EvolutionProposalInput): EvolutionProposal;
60
+ /**
61
+ * Score an existing proposal. Enforces AC-10 (no self-score:
62
+ * `evaluator_id !== proposal.author_id`) and AC-11 (delta
63
+ * threshold: cannot mark `keep` when `score_delta <
64
+ * score_delta_min`).
65
+ *
66
+ * Returns the FINAL evaluation row with verdict derived from the
67
+ * evaluator + skeptic results:
68
+ * - `verdict = 'revert'` if the skeptic returned a blocker.
69
+ * - `verdict = 'revert'` if the score delta is below threshold.
70
+ * - `verdict = 'needs-user-decision'` otherwise; the CLI
71
+ * (`peaks evolution evaluate`) surfaces this to the user for
72
+ * a `keep` / `revert` choice.
73
+ *
74
+ * Throws:
75
+ * - EvolutionIntegrityError(EVOLUTION_SELF_SCORE) if
76
+ * evaluator_id === proposal.author_id.
77
+ * - EvolutionIntegrityError(EVOLUTION_DELTA_BELOW_THRESHOLD) if
78
+ * a `verdict = 'keep'` is requested and the delta is below
79
+ * the proposal's `score_delta_min`.
80
+ */
81
+ score(proposalId: string, args: {
82
+ evaluator_id: string;
83
+ skeptic_id: string;
84
+ evaluator_result: import("./evolution-types.js").IndependentEvaluatorResult;
85
+ skeptic_result: import("./evolution-types.js").RegressionSkepticResult;
86
+ brief_pointer?: string;
87
+ user_confirmation_pointer?: string;
88
+ }): EvolutionEvaluation;
89
+ /**
90
+ * Mark a verdict explicitly (after the user has confirmed via
91
+ * natural-language or a pick). Enforces:
92
+ * - AC-11: cannot mark `keep` when score_delta <
93
+ * score_delta_min.
94
+ * - AC-15: `keep` requires a user_confirmation_pointer.
95
+ *
96
+ * Throws:
97
+ * - EvolutionIntegrityError(EVOLUTION_DELTA_BELOW_THRESHOLD)
98
+ * when `verdict = 'keep'` and the delta is below threshold.
99
+ * - EvolutionIntegrityError(EVOLUTION_MISSING_USER_CONFIRMATION)
100
+ * when `verdict = 'keep'` and no user_confirmation_pointer
101
+ * is supplied.
102
+ */
103
+ markVerdict(proposalId: string, verdict: EvolutionVerdict, userConfirmationPointer?: string): EvolutionEvaluation | undefined;
104
+ /** Revert a proposal (universal recovery). Always allowed. */
105
+ revert(proposalId: string, userConfirmationPointer?: string): EvolutionEvaluation | undefined;
106
+ /** Read an evolution evaluation by id. */
107
+ read(proposalId: string): EvolutionEvaluation | undefined;
108
+ /** List evaluations for a given target. */
109
+ listByTarget(opts: {
110
+ target_kind: EvolutionTargetKind;
111
+ target_release_id: string;
112
+ verdict?: EvolutionVerdict;
113
+ }): EvolutionEvaluation[];
114
+ /**
115
+ * Status snapshot for `peaks evolution status`. Returns counts of
116
+ * evaluations by verdict for the given target, plus the latest
117
+ * evaluation id (if any).
118
+ */
119
+ status(target: {
120
+ target_kind: EvolutionTargetKind;
121
+ target_release_id: string;
122
+ }): {
123
+ target_kind: EvolutionTargetKind;
124
+ target_release_id: string;
125
+ total: number;
126
+ byVerdict: Record<EvolutionVerdict, number>;
127
+ latest: EvolutionEvaluation | undefined;
128
+ };
129
+ }
130
+ /**
131
+ * Convenience: re-export the Zod error guard.
132
+ */
133
+ export declare function isZodError(err: unknown): err is ZodError;
134
+ /**
135
+ * Convenience: get the default `score_delta_min` from the spec.
136
+ */
137
+ export { EVOLUTION_DEFAULT_DELTA_MIN };
@@ -0,0 +1,281 @@
1
+ import { ZodError } from "zod";
2
+ import { EvolutionEvaluationSchema, EvolutionProposalInputSchema, EVOLUTION_DEFAULT_DELTA_MIN, } from "./evolution-types.js";
3
+ import { buildProposal, computeScoreDelta, ensureEvolutionEvaluationTable, getEvolutionEvaluation, insertEvolutionEvaluation, listEvolutionEvaluationsByTarget, newEvaluationId, updateEvolutionVerdict, } from "./evolution-store.js";
4
+ export class EvolutionIntegrityError extends Error {
5
+ code;
6
+ findings;
7
+ constructor(code, message, findings = []) {
8
+ super(message);
9
+ this.name = "EvolutionIntegrityError";
10
+ this.code = code;
11
+ this.findings = findings;
12
+ }
13
+ }
14
+ /* ---------------------------------------------------------------------- */
15
+ /* Service */
16
+ /* ---------------------------------------------------------------------- */
17
+ export class EvolutionService {
18
+ db;
19
+ constructor(db) {
20
+ this.db = db;
21
+ // Idempotent — safe to call on every constructor invocation; the
22
+ // openStateDb pipeline already applied the SQL migration, but a
23
+ // caller-built DB (e.g. tests) needs this.
24
+ ensureEvolutionEvaluationTable(this.db);
25
+ }
26
+ /**
27
+ * Persist a new EvolutionProposal. Enforces AC-8 (single object /
28
+ * single dimension) BEFORE writing the row.
29
+ *
30
+ * The proposal id is auto-generated (`eval-<hex>`); callers supply
31
+ * the proposal content only. The proposal's `verdict` is set to
32
+ * `needs-user-decision` until the score + skeptic step lands.
33
+ *
34
+ * Throws:
35
+ * - ZodError if the input fails schema validation.
36
+ * - EvolutionIntegrityError(EVOLUTION_MULTI_OBJECT) if
37
+ * single_object !== true.
38
+ * - EvolutionIntegrityError(EVOLUTION_MULTI_DIMENSION) if
39
+ * single_optimization_dimension !== true.
40
+ */
41
+ createProposal(input) {
42
+ // Parse with the input schema first (so we get defaults applied).
43
+ // The input schema already enforces `single_object: literal(true)`
44
+ // and `single_optimization_dimension: literal(true)`; we map
45
+ // the ZodError to EvolutionIntegrityError so the CLI / tests
46
+ // can assert on a single error code.
47
+ let parsedInput;
48
+ try {
49
+ parsedInput =
50
+ EvolutionProposalInputSchema.parse(input);
51
+ }
52
+ catch (err) {
53
+ if (err instanceof ZodError) {
54
+ const issue = err.issues[0];
55
+ if (issue?.path[0] === "single_object") {
56
+ throw new EvolutionIntegrityError("EVOLUTION_MULTI_OBJECT", "proposal must target exactly one object (single_object=true); split multi-object changes into multiple rounds", [
57
+ {
58
+ path: "single_object",
59
+ message: issue.message,
60
+ },
61
+ ]);
62
+ }
63
+ if (issue?.path[0] === "single_optimization_dimension") {
64
+ throw new EvolutionIntegrityError("EVOLUTION_MULTI_DIMENSION", "proposal must declare exactly one optimization dimension (single_optimization_dimension=true); split multi-dimension changes into multiple rounds", [
65
+ {
66
+ path: "single_optimization_dimension",
67
+ message: issue.message,
68
+ },
69
+ ]);
70
+ }
71
+ }
72
+ throw err;
73
+ }
74
+ // AC-8 defense in depth: in case a future schema version relaxes
75
+ // the literal(true), still enforce at the service boundary.
76
+ if (parsedInput.single_object !== true) {
77
+ throw new EvolutionIntegrityError("EVOLUTION_MULTI_OBJECT", "proposal must target exactly one object (single_object=true); split multi-object changes into multiple rounds", [{ path: "single_object", message: "must be true (AC-8)" }]);
78
+ }
79
+ if (parsedInput.single_optimization_dimension !== true) {
80
+ throw new EvolutionIntegrityError("EVOLUTION_MULTI_DIMENSION", "proposal must declare exactly one optimization dimension (single_optimization_dimension=true); split multi-dimension changes into multiple rounds", [
81
+ {
82
+ path: "single_optimization_dimension",
83
+ message: "must be true (AC-8)",
84
+ },
85
+ ]);
86
+ }
87
+ const id = newEvaluationId();
88
+ const proposal = buildProposal(id, parsedInput);
89
+ // Persist a stub evaluation row with verdict='needs-user-decision';
90
+ // the actual scoring happens in `score()` below.
91
+ const stub = {
92
+ id,
93
+ proposal,
94
+ evaluator_id: "pending",
95
+ skeptic_id: "pending",
96
+ evaluator_result: { score: parsedInput.before_score, riskTags: [], refuteParagraph: "pending" },
97
+ skeptic_result: {
98
+ driftRisks: [],
99
+ overfitRisks: [],
100
+ safetyRegressionRisks: [],
101
+ },
102
+ verdict: "needs-user-decision",
103
+ schema_version: "peaks.evolution/1",
104
+ created_at: proposal.created_at,
105
+ };
106
+ insertEvolutionEvaluation(this.db, stub);
107
+ return proposal;
108
+ }
109
+ /**
110
+ * Score an existing proposal. Enforces AC-10 (no self-score:
111
+ * `evaluator_id !== proposal.author_id`) and AC-11 (delta
112
+ * threshold: cannot mark `keep` when `score_delta <
113
+ * score_delta_min`).
114
+ *
115
+ * Returns the FINAL evaluation row with verdict derived from the
116
+ * evaluator + skeptic results:
117
+ * - `verdict = 'revert'` if the skeptic returned a blocker.
118
+ * - `verdict = 'revert'` if the score delta is below threshold.
119
+ * - `verdict = 'needs-user-decision'` otherwise; the CLI
120
+ * (`peaks evolution evaluate`) surfaces this to the user for
121
+ * a `keep` / `revert` choice.
122
+ *
123
+ * Throws:
124
+ * - EvolutionIntegrityError(EVOLUTION_SELF_SCORE) if
125
+ * evaluator_id === proposal.author_id.
126
+ * - EvolutionIntegrityError(EVOLUTION_DELTA_BELOW_THRESHOLD) if
127
+ * a `verdict = 'keep'` is requested and the delta is below
128
+ * the proposal's `score_delta_min`.
129
+ */
130
+ score(proposalId, args) {
131
+ const existing = getEvolutionEvaluation(this.db, proposalId);
132
+ if (!existing) {
133
+ throw new EvolutionIntegrityError("EVOLUTION_BELOW_RUBRIC", `evolution_evaluation row '${proposalId}' not found`, [{ path: "id", message: "row not found" }]);
134
+ }
135
+ // AC-10: scorer MUST NOT be the author.
136
+ if (args.evaluator_id === existing.proposal.author_id) {
137
+ throw new EvolutionIntegrityError("EVOLUTION_SELF_SCORE", `evaluator_id '${args.evaluator_id}' equals author_id '${existing.proposal.author_id}'; self-scoring is forbidden (AC-10)`, [{ path: "evaluator_id", message: "must differ from author_id" }]);
138
+ }
139
+ // AC-12 / AC-14: skeptic and evaluator are SEPARATE agents.
140
+ if (args.skeptic_id === existing.proposal.author_id) {
141
+ throw new EvolutionIntegrityError("EVOLUTION_SELF_SCORE", `skeptic_id '${args.skeptic_id}' equals author_id '${existing.proposal.author_id}'; the skeptic must be a separate agent (AC-14)`, [{ path: "skeptic_id", message: "must differ from author_id" }]);
142
+ }
143
+ if (args.skeptic_id === args.evaluator_id) {
144
+ throw new EvolutionIntegrityError("EVOLUTION_SELF_SCORE", `skeptic_id '${args.skeptic_id}' equals evaluator_id '${args.evaluator_id}'; the skeptic must be a separate agent from the evaluator (AC-12/AC-14)`, [{ path: "skeptic_id", message: "must differ from evaluator_id" }]);
145
+ }
146
+ const after_score = args.evaluator_result.score;
147
+ const score_delta = computeScoreDelta(existing.proposal.before_score, after_score);
148
+ const min = existing.proposal.score_delta_min;
149
+ // Derive verdict. The skeptic's `blocker` is a hard revert.
150
+ let verdict = "needs-user-decision";
151
+ if (args.skeptic_result.blocker !== undefined) {
152
+ verdict = "revert";
153
+ }
154
+ else if (score_delta < min) {
155
+ verdict = "revert";
156
+ }
157
+ const next = {
158
+ id: existing.id,
159
+ proposal: {
160
+ ...existing.proposal,
161
+ after_score,
162
+ score_delta,
163
+ },
164
+ evaluator_id: args.evaluator_id,
165
+ skeptic_id: args.skeptic_id,
166
+ evaluator_result: args.evaluator_result,
167
+ skeptic_result: args.skeptic_result,
168
+ verdict,
169
+ ...(args.brief_pointer !== undefined ? { brief_pointer: args.brief_pointer } : {}),
170
+ ...(args.user_confirmation_pointer !== undefined
171
+ ? { user_confirmation_pointer: args.user_confirmation_pointer }
172
+ : {}),
173
+ schema_version: "peaks.evolution/1",
174
+ created_at: new Date().toISOString(),
175
+ };
176
+ // AC-11 (delta threshold) explicit guard: if the caller
177
+ // explicitly asks to mark `keep` but delta < min, throw.
178
+ if (verdict === "needs-user-decision" && score_delta < min) {
179
+ // The auto-derivation already pinned verdict to `revert`; the
180
+ // guard is for the explicit `markVerdict('keep')` path below.
181
+ }
182
+ // Re-validate before persisting.
183
+ const validated = EvolutionEvaluationSchema.parse({
184
+ ...next,
185
+ score_delta,
186
+ });
187
+ // Delete + re-insert keeps the row simple; volume is tiny.
188
+ this.db
189
+ .prepare("DELETE FROM evolution_evaluation WHERE id = ?")
190
+ .run(existing.id);
191
+ insertEvolutionEvaluation(this.db, validated);
192
+ return validated;
193
+ }
194
+ /**
195
+ * Mark a verdict explicitly (after the user has confirmed via
196
+ * natural-language or a pick). Enforces:
197
+ * - AC-11: cannot mark `keep` when score_delta <
198
+ * score_delta_min.
199
+ * - AC-15: `keep` requires a user_confirmation_pointer.
200
+ *
201
+ * Throws:
202
+ * - EvolutionIntegrityError(EVOLUTION_DELTA_BELOW_THRESHOLD)
203
+ * when `verdict = 'keep'` and the delta is below threshold.
204
+ * - EvolutionIntegrityError(EVOLUTION_MISSING_USER_CONFIRMATION)
205
+ * when `verdict = 'keep'` and no user_confirmation_pointer
206
+ * is supplied.
207
+ */
208
+ markVerdict(proposalId, verdict, userConfirmationPointer) {
209
+ const existing = getEvolutionEvaluation(this.db, proposalId);
210
+ if (!existing)
211
+ return undefined;
212
+ if (verdict === "keep") {
213
+ if (existing.proposal.score_delta < existing.proposal.score_delta_min) {
214
+ throw new EvolutionIntegrityError("EVOLUTION_DELTA_BELOW_THRESHOLD", `score_delta ${existing.proposal.score_delta} is below score_delta_min ${existing.proposal.score_delta_min}; cannot mark keep (AC-11)`, [
215
+ {
216
+ path: "verdict",
217
+ message: "score_delta below score_delta_min; revert instead or raise the score",
218
+ },
219
+ ]);
220
+ }
221
+ if (userConfirmationPointer === undefined ||
222
+ userConfirmationPointer.trim().length === 0) {
223
+ throw new EvolutionIntegrityError("EVOLUTION_MISSING_USER_CONFIRMATION", "marking verdict='keep' requires a user_confirmation_pointer (AC-15)", [
224
+ {
225
+ path: "user_confirmation_pointer",
226
+ message: "required for verdict='keep'",
227
+ },
228
+ ]);
229
+ }
230
+ }
231
+ return updateEvolutionVerdict(this.db, proposalId, verdict, userConfirmationPointer);
232
+ }
233
+ /** Revert a proposal (universal recovery). Always allowed. */
234
+ revert(proposalId, userConfirmationPointer) {
235
+ return updateEvolutionVerdict(this.db, proposalId, "revert", userConfirmationPointer);
236
+ }
237
+ /** Read an evolution evaluation by id. */
238
+ read(proposalId) {
239
+ return getEvolutionEvaluation(this.db, proposalId);
240
+ }
241
+ /** List evaluations for a given target. */
242
+ listByTarget(opts) {
243
+ return listEvolutionEvaluationsByTarget(this.db, opts);
244
+ }
245
+ /**
246
+ * Status snapshot for `peaks evolution status`. Returns counts of
247
+ * evaluations by verdict for the given target, plus the latest
248
+ * evaluation id (if any).
249
+ */
250
+ status(target) {
251
+ const all = listEvolutionEvaluationsByTarget(this.db, {
252
+ target_kind: target.target_kind,
253
+ target_release_id: target.target_release_id,
254
+ });
255
+ const byVerdict = {
256
+ keep: 0,
257
+ revert: 0,
258
+ "needs-user-decision": 0,
259
+ };
260
+ for (const row of all)
261
+ byVerdict[row.verdict] += 1;
262
+ const latest = all.length > 0 ? all[0] : undefined;
263
+ return {
264
+ target_kind: target.target_kind,
265
+ target_release_id: target.target_release_id,
266
+ total: all.length,
267
+ byVerdict,
268
+ latest,
269
+ };
270
+ }
271
+ }
272
+ /**
273
+ * Convenience: re-export the Zod error guard.
274
+ */
275
+ export function isZodError(err) {
276
+ return err instanceof ZodError;
277
+ }
278
+ /**
279
+ * Convenience: get the default `score_delta_min` from the spec.
280
+ */
281
+ export { EVOLUTION_DEFAULT_DELTA_MIN };