peaks-loop 4.0.0-beta → 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 (179) hide show
  1. package/CHANGELOG.md +105 -87
  2. package/README-en.md +17 -4
  3. package/README.md +17 -4
  4. package/dist/src/cli/commands/asset-commands.d.ts +45 -0
  5. package/dist/src/cli/commands/asset-commands.js +404 -0
  6. package/dist/src/cli/commands/bee-commands.d.ts +26 -0
  7. package/dist/src/cli/commands/bee-commands.js +169 -0
  8. package/dist/src/cli/commands/{solo-commands.d.ts → code-commands.d.ts} +24 -24
  9. package/dist/src/cli/commands/{solo-commands.js → code-commands.js} +78 -78
  10. package/dist/src/cli/commands/core/skill-command.js +1 -1
  11. package/dist/src/cli/commands/dispatch-from-dag.js +1 -1
  12. package/dist/src/cli/commands/evolution-commands.d.ts +27 -0
  13. package/dist/src/cli/commands/evolution-commands.js +393 -0
  14. package/dist/src/cli/commands/hook-handle.js +7 -7
  15. package/dist/src/cli/commands/job-commands.js +2 -2
  16. package/dist/src/cli/commands/loop-commands.d.ts +19 -13
  17. package/dist/src/cli/commands/loop-commands.js +147 -13
  18. package/dist/src/cli/commands/sediment-commands.js +9 -0
  19. package/dist/src/cli/commands/session-auto-compact-hook-command.js +2 -2
  20. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.d.ts +67 -0
  21. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.js +173 -0
  22. package/dist/src/cli/commands/sub-agent-shared.d.ts +1 -1
  23. package/dist/src/cli/commands/user-touchpoint-commands.d.ts +4 -4
  24. package/dist/src/cli/commands/user-touchpoint-commands.js +14 -14
  25. package/dist/src/cli/commands/workflow-commands.js +22 -22
  26. package/dist/src/cli/program.js +23 -6
  27. package/dist/src/services/artifacts/repair-cycle-service.js +1 -1
  28. package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +1 -1
  29. package/dist/src/services/audit/enforcers/active-skill-resolver.js +1 -1
  30. package/dist/src/services/audit/enforcers/{solo-code-ban.d.ts → code-ban.d.ts} +6 -6
  31. package/dist/src/services/audit/enforcers/{solo-code-ban.js → code-ban.js} +6 -6
  32. package/dist/src/services/audit/red-line-catalog.d.ts +2 -2
  33. package/dist/src/services/audit/red-line-catalog.js +5 -5
  34. package/dist/src/services/audit/types.d.ts +2 -2
  35. package/dist/src/services/{solo → code}/auto-compact-orchestrator.d.ts +1 -1
  36. package/dist/src/services/{solo → code}/auto-compact-orchestrator.js +3 -3
  37. package/dist/src/services/{solo → code}/dag-orchestrator.d.ts +1 -1
  38. package/dist/src/services/{solo → code}/dag-orchestrator.js +1 -1
  39. package/dist/src/services/{solo → code}/emit-handoff.d.ts +1 -1
  40. package/dist/src/services/{solo → code}/emit-handoff.js +1 -1
  41. package/dist/src/services/{solo → code}/job-shape-decision.d.ts +1 -1
  42. package/dist/src/services/{solo → code}/job-shape-decision.js +5 -5
  43. package/dist/src/services/{solo → code}/mode-gate.d.ts +9 -9
  44. package/dist/src/services/{solo → code}/mode-gate.js +5 -5
  45. package/dist/src/services/{solo → code}/post-compact-detector.d.ts +3 -3
  46. package/dist/src/services/{solo → code}/post-compact-detector.js +4 -4
  47. package/dist/src/services/{solo → code}/step-08-gate.d.ts +4 -4
  48. package/dist/src/services/{solo → code}/step-08-gate.js +4 -4
  49. package/dist/src/services/{solo → code}/user-touchpoint-classifier.d.ts +1 -1
  50. package/dist/src/services/{solo → code}/user-touchpoint-classifier.js +1 -1
  51. package/dist/src/services/context/auto-compact-dispatcher.d.ts +2 -2
  52. package/dist/src/services/context/auto-compact-dispatcher.js +2 -2
  53. package/dist/src/services/context/auto-compact-types.d.ts +1 -1
  54. package/dist/src/services/crystallization/crystallization-service.d.ts +239 -0
  55. package/dist/src/services/crystallization/crystallization-service.js +397 -0
  56. package/dist/src/services/crystallization/crystallization-store.d.ts +75 -0
  57. package/dist/src/services/crystallization/crystallization-store.js +193 -0
  58. package/dist/src/services/crystallization/crystallization-types.d.ts +313 -0
  59. package/dist/src/services/crystallization/crystallization-types.js +299 -0
  60. package/dist/src/services/crystallization/evidence-brief-builder.d.ts +237 -0
  61. package/dist/src/services/crystallization/evidence-brief-builder.js +240 -0
  62. package/dist/src/services/dispatch/slice-dag.d.ts +2 -2
  63. package/dist/src/services/dispatch/test-tool-detection.d.ts +1 -1
  64. package/dist/src/services/dispatch/test-tool-detection.js +1 -1
  65. package/dist/src/services/evolution/evolution-service.d.ts +137 -0
  66. package/dist/src/services/evolution/evolution-service.js +281 -0
  67. package/dist/src/services/evolution/evolution-store.d.ts +57 -0
  68. package/dist/src/services/evolution/evolution-store.js +232 -0
  69. package/dist/src/services/evolution/evolution-types.d.ts +706 -0
  70. package/dist/src/services/evolution/evolution-types.js +286 -0
  71. package/dist/src/services/evolution/independent-evaluator-runner.d.ts +90 -0
  72. package/dist/src/services/evolution/independent-evaluator-runner.js +106 -0
  73. package/dist/src/services/evolution/regression-skeptic-runner.d.ts +64 -0
  74. package/dist/src/services/evolution/regression-skeptic-runner.js +113 -0
  75. package/dist/src/services/feedback/feedback-promotion-service.js +2 -2
  76. package/dist/src/services/job/job-progress-store.d.ts +1 -1
  77. package/dist/src/services/job/job-progress-store.js +1 -1
  78. package/dist/src/services/loop/loop-bee-relation-service.d.ts +107 -0
  79. package/dist/src/services/loop/loop-bee-relation-service.js +211 -0
  80. package/dist/src/services/loop/loop-bee-relation-store.d.ts +53 -0
  81. package/dist/src/services/loop/loop-bee-relation-store.js +130 -0
  82. package/dist/src/services/loop/loop-bee-relation-types.d.ts +118 -0
  83. package/dist/src/services/loop/loop-bee-relation-types.js +114 -0
  84. package/dist/src/services/loop/loop-release-service.d.ts +46 -0
  85. package/dist/src/services/loop/loop-release-service.js +66 -0
  86. package/dist/src/services/loop/loop-release-store.d.ts +55 -0
  87. package/dist/src/services/loop/loop-release-store.js +134 -0
  88. package/dist/src/services/loop/loop-release-types.d.ts +250 -0
  89. package/dist/src/services/loop/loop-release-types.js +162 -0
  90. package/dist/src/services/preferences/preferences-service.d.ts +1 -1
  91. package/dist/src/services/preferences/preferences-types.d.ts +1 -1
  92. package/dist/src/services/rd/standards-diagnostic.d.ts +1 -1
  93. package/dist/src/services/rd/standards-diagnostic.js +2 -2
  94. package/dist/src/services/recommendations/capability-seed-items.js +1 -1
  95. package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
  96. package/dist/src/services/recommendations/recommendation-service.js +1 -1
  97. package/dist/src/services/refactor/refactor-service.d.ts +1 -1
  98. package/dist/src/services/share/bundle-reader.d.ts +100 -0
  99. package/dist/src/services/share/bundle-reader.js +434 -0
  100. package/dist/src/services/share/bundle-types.d.ts +392 -0
  101. package/dist/src/services/share/bundle-types.js +318 -0
  102. package/dist/src/services/share/bundle-writer.d.ts +97 -0
  103. package/dist/src/services/share/bundle-writer.js +429 -0
  104. package/dist/src/services/share/run-state-contract.d.ts +133 -0
  105. package/dist/src/services/share/run-state-contract.js +138 -0
  106. package/dist/src/services/signal/cancel-handler.js +3 -3
  107. package/dist/src/services/skill/resume-detector.js +1 -1
  108. package/dist/src/services/skillhub/release-import.js +7 -2
  109. package/dist/src/services/skillhub/release-retain.js +6 -1
  110. package/dist/src/services/skillhub/sqlite-store.js +17 -1
  111. package/dist/src/services/skillhub/types.d.ts +10 -0
  112. package/dist/src/services/skills/skill-presence-service.d.ts +1 -1
  113. package/dist/src/services/standards/loop-engineering-lint.d.ts +38 -0
  114. package/dist/src/services/standards/loop-engineering-lint.js +101 -0
  115. package/dist/src/services/standards/loop-engineering-readiness-lint.d.ts +111 -0
  116. package/dist/src/services/standards/loop-engineering-readiness-lint.js +248 -0
  117. package/dist/src/services/upgrade/1x-detector-service.js +1 -1
  118. package/dist/src/services/workflow/pipeline-verify-service.d.ts +1 -1
  119. package/dist/src/services/workflow/pipeline-verify-service.js +3 -3
  120. package/dist/src/services/workflow/workflow-autonomous-service.d.ts +3 -3
  121. package/dist/src/services/workflow/workflow-autonomous-service.js +2 -2
  122. package/dist/src/services/workflow/workflow-router-service.d.ts +6 -6
  123. package/dist/src/services/workflow/workflow-router-service.js +33 -33
  124. package/dist/src/services/workspace/claude-settings-template.d.ts +1 -1
  125. package/dist/src/services/workspace/claude-settings-template.js +5 -5
  126. package/dist/src/shared/version.d.ts +1 -1
  127. package/dist/src/shared/version.js +1 -1
  128. package/examples/video-demo/README.md +57 -0
  129. package/examples/video-demo/out/en-closing-960.png +0 -0
  130. package/examples/video-demo/out/zh-closing-960.png +0 -0
  131. package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
  132. package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
  133. package/output-styles/peaks-skill-swarm.md +3 -3
  134. package/package.json +6 -2
  135. package/skills/bee/peaks-qa/SKILL.md +1 -1
  136. package/skills/bee/peaks-qa/references/artifact-per-request.md +3 -3
  137. package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +6 -6
  138. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +1 -1
  139. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  140. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +11 -11
  141. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  142. package/skills/bee/peaks-sc/SKILL.md +1 -1
  143. package/skills/bee/peaks-txt/SKILL.md +3 -3
  144. package/skills/bee/peaks-ui/SKILL.md +8 -8
  145. package/skills/peaks-code/SKILL.md +34 -21
  146. package/skills/peaks-code/references/anchoring-and-session-info.md +2 -2
  147. package/skills/peaks-code/references/artifact-contracts.md +1 -1
  148. package/skills/peaks-code/references/boundaries.md +3 -3
  149. package/skills/peaks-code/references/codegraph-orchestration.md +1 -1
  150. package/skills/peaks-code/references/dag-orchestrator.md +7 -7
  151. package/skills/peaks-code/references/external-references.md +2 -2
  152. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  153. package/skills/peaks-code/references/fast-mode.md +2 -2
  154. package/skills/peaks-code/references/frontend-only-mode.md +9 -9
  155. package/skills/peaks-code/references/job-loop.md +1 -1
  156. package/skills/peaks-code/references/local-artifact-workspace.md +2 -2
  157. package/skills/peaks-code/references/micro-cycle.md +14 -14
  158. package/skills/peaks-code/references/mode-selection-with-stale-presence.md +5 -5
  159. package/skills/peaks-code/references/mode-selection.md +3 -3
  160. package/skills/peaks-code/references/openspec-workflow.md +16 -16
  161. package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
  162. package/skills/peaks-code/references/quality-gate-cheatsheet.md +1 -1
  163. package/skills/peaks-code/references/refactor-mode.md +1 -1
  164. package/skills/peaks-code/references/runbook.md +15 -15
  165. package/skills/peaks-code/references/step-0-55-1x-detection.md +1 -1
  166. package/skills/peaks-code/references/step-0-8-gate.md +11 -11
  167. package/skills/peaks-code/references/sub-agent-dispatch.md +8 -8
  168. package/skills/peaks-code/references/swarm-dispatch-contract.md +17 -17
  169. package/skills/peaks-code/references/workflow-gates-and-types.md +2 -2
  170. package/skills/peaks-code/references/workflow.md +5 -5
  171. package/skills/peaks-content/SKILL.md +272 -0
  172. package/skills/peaks-issue-fix-orchestrator/SKILL.md +307 -0
  173. package/skills/peaks-resume/SKILL.md +2 -2
  174. package/skills/peaks-status/SKILL.md +2 -2
  175. package/skills/peaks-test/SKILL.md +2 -2
  176. /package/dist/src/services/{solo → code}/batch-heartbeat-poller.d.ts +0 -0
  177. /package/dist/src/services/{solo → code}/batch-heartbeat-poller.js +0 -0
  178. /package/dist/src/services/{solo → code}/status-line-renderer.d.ts +0 -0
  179. /package/dist/src/services/{solo → code}/status-line-renderer.js +0 -0
@@ -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 };
@@ -0,0 +1,57 @@
1
+ import type Database from "better-sqlite3";
2
+ import type { EvolutionEvaluation, EvolutionEvaluationInput, EvolutionProposal, EvolutionProposalInput, EvolutionTargetKind, EvolutionVerdict } from "./evolution-types.js";
3
+ /**
4
+ * Re-apply the evolution_evaluation table migration against an
5
+ * already-open database. Used by tests that build their own DB
6
+ * without the openStateDb pipeline. Idempotent.
7
+ */
8
+ export declare function ensureEvolutionEvaluationTable(db: Database.Database): void;
9
+ /**
10
+ * Compute the score_delta from before/after scores. Exported for
11
+ * the service layer to use during proposal evaluation.
12
+ */
13
+ export declare function computeScoreDelta(before: number, after: number): number;
14
+ /**
15
+ * Build a persisted EvolutionProposal from the input. The service
16
+ * layer is responsible for enforcing AC-8 (single object / single
17
+ * dimension) BEFORE calling this; the persisted shape always has
18
+ * `target_count = 1` and `dimensions.length = 1`.
19
+ */
20
+ export declare function buildProposal(id: string, input: EvolutionProposalInput, createdAt?: string): EvolutionProposal;
21
+ /**
22
+ * Insert a full EvolutionEvaluation row. The row's
23
+ * `schema_version` is taken from the input (must equal
24
+ * `peaks.evolution/1`); `created_at` is stamped from the server
25
+ * clock here, so callers cannot backdate a row.
26
+ */
27
+ export declare function insertEvolutionEvaluation(db: Database.Database, evalRow: EvolutionEvaluationInput): void;
28
+ /**
29
+ * Read a single EvolutionEvaluation row by id; returns undefined if
30
+ * absent.
31
+ */
32
+ export declare function getEvolutionEvaluation(db: Database.Database, id: string): EvolutionEvaluation | undefined;
33
+ /**
34
+ * List EvolutionEvaluation rows for a specific target. Filter by
35
+ * `verdict` to scope the listing.
36
+ */
37
+ export declare function listEvolutionEvaluationsByTarget(db: Database.Database, opts: {
38
+ target_kind: EvolutionTargetKind;
39
+ target_release_id: string;
40
+ verdict?: EvolutionVerdict;
41
+ }): EvolutionEvaluation[];
42
+ /**
43
+ * Update the verdict (and optional user_confirmation_pointer) of
44
+ * an existing EvolutionEvaluation row. Returns the updated row, or
45
+ * undefined if the id is absent.
46
+ *
47
+ * The service layer is responsible for enforcing that the
48
+ * transition `revert` is always allowed and `keep` requires
49
+ * `user_confirmation_pointer` set (AC-15).
50
+ */
51
+ export declare function updateEvolutionVerdict(db: Database.Database, id: string, verdict: EvolutionVerdict, userConfirmationPointer?: string): EvolutionEvaluation | undefined;
52
+ /**
53
+ * Test seam: produce a fresh id for an EvolutionEvaluation row.
54
+ * Format: `eval-<hex>` (lowercase, 12 hex chars). Matches the Zod
55
+ * `^eval-[0-9a-f-]{8,}$` pattern in EvolutionProposalSchema.
56
+ */
57
+ export declare function newEvaluationId(): string;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Low-level SQLite access for the `evolution_evaluation` table. The
3
+ * migration is registered with the existing `openStateDb()` pipeline
4
+ * via `src/services/skillhub/migrations/005-evolution-evaluation.sql`
5
+ * and is applied in lexicographic order alongside the other
6
+ * skillhub migrations. The function `ensureEvolutionEvaluationTable`
7
+ * below is a belt-and-suspenders re-applier for callers that pass a
8
+ * database they built themselves (e.g. tests).
9
+ *
10
+ * Defense in depth:
11
+ * - All JSON columns are stored as TEXT and parsed at the boundary.
12
+ * No big JSON BLOB at the SQLite layer — the row stays small and
13
+ * queryable.
14
+ * - The migration is idempotent (CREATE TABLE IF NOT EXISTS, CREATE
15
+ * INDEX IF NOT EXISTS) so re-running it is safe.
16
+ * - The single-object / single-dimension / self-score / delta
17
+ * rules are enforced at the SERVICE layer (not the DB); the DB
18
+ * stores the persisted, validated row.
19
+ */
20
+ const SCHEMA_VERSION = "peaks.evolution/1";
21
+ /**
22
+ * Re-apply the evolution_evaluation table migration against an
23
+ * already-open database. Used by tests that build their own DB
24
+ * without the openStateDb pipeline. Idempotent.
25
+ */
26
+ export function ensureEvolutionEvaluationTable(db) {
27
+ db.exec(`
28
+ CREATE TABLE IF NOT EXISTS evolution_evaluation (
29
+ id TEXT PRIMARY KEY,
30
+ target_kind TEXT NOT NULL CHECK (target_kind IN ('loop','bee','policy','gate','evaluator')),
31
+ target_release_id TEXT NOT NULL,
32
+ optimization_dimensions_json TEXT NOT NULL,
33
+ target_count INTEGER NOT NULL DEFAULT 1 CHECK (target_count = 1),
34
+ before_snapshot_json TEXT NOT NULL DEFAULT '{}',
35
+ after_snapshot_json TEXT NOT NULL DEFAULT '{}',
36
+ diff_json TEXT NOT NULL DEFAULT '{}',
37
+ before_score REAL NOT NULL,
38
+ after_score REAL NOT NULL,
39
+ score_delta_min REAL NOT NULL DEFAULT 1.0 CHECK (score_delta_min >= 0),
40
+ score_delta REAL NOT NULL,
41
+ author_id TEXT NOT NULL,
42
+ evaluator_id TEXT NOT NULL,
43
+ skeptic_id TEXT NOT NULL,
44
+ verdict TEXT NOT NULL CHECK (verdict IN ('keep','revert','needs-user-decision')),
45
+ user_confirmation_pointer TEXT,
46
+ brief_pointer TEXT,
47
+ rubric_json TEXT NOT NULL DEFAULT '{}',
48
+ red_lines_json TEXT NOT NULL DEFAULT '[]',
49
+ source_traces_json TEXT NOT NULL DEFAULT '[]',
50
+ schema_version TEXT NOT NULL CHECK (schema_version = 'peaks.evolution/1'),
51
+ created_at TEXT NOT NULL,
52
+ CHECK (length(id) > 0)
53
+ );
54
+ CREATE INDEX IF NOT EXISTS idx_evolution_evaluation_target
55
+ ON evolution_evaluation(target_kind, target_release_id);
56
+ CREATE INDEX IF NOT EXISTS idx_evolution_evaluation_verdict
57
+ ON evolution_evaluation(verdict);
58
+ CREATE INDEX IF NOT EXISTS idx_evolution_evaluation_author
59
+ ON evolution_evaluation(author_id);
60
+ CREATE INDEX IF NOT EXISTS idx_evolution_evaluation_evaluator
61
+ ON evolution_evaluation(evaluator_id);
62
+ `);
63
+ }
64
+ /**
65
+ * Compute the score_delta from before/after scores. Exported for
66
+ * the service layer to use during proposal evaluation.
67
+ */
68
+ export function computeScoreDelta(before, after) {
69
+ // Round to 6 decimals to avoid floating-point noise.
70
+ return Math.round((after - before) * 1_000_000) / 1_000_000;
71
+ }
72
+ /**
73
+ * Build a persisted EvolutionProposal from the input. The service
74
+ * layer is responsible for enforcing AC-8 (single object / single
75
+ * dimension) BEFORE calling this; the persisted shape always has
76
+ * `target_count = 1` and `dimensions.length = 1`.
77
+ */
78
+ export function buildProposal(id, input, createdAt) {
79
+ const before = input.before_score;
80
+ const after = input.after_score;
81
+ return {
82
+ id,
83
+ target_kind: input.target_kind,
84
+ target_release_id: input.target_release_id,
85
+ optimization_dimension: input.optimization_dimension,
86
+ dimensions: [input.optimization_dimension],
87
+ target_count: 1,
88
+ single_object: true,
89
+ single_optimization_dimension: true,
90
+ before_snapshot: input.before_snapshot ?? {},
91
+ after_snapshot: input.after_snapshot ?? {},
92
+ diff: input.diff ?? {},
93
+ before_score: before,
94
+ after_score: after,
95
+ score_delta_min: input.score_delta_min ?? 1,
96
+ score_delta: computeScoreDelta(before, after),
97
+ author_id: input.author_id,
98
+ rubric: input.rubric ?? {},
99
+ red_lines: input.red_lines ?? [],
100
+ source_traces: input.source_traces ?? [],
101
+ schema_version: SCHEMA_VERSION,
102
+ created_at: createdAt ?? new Date().toISOString(),
103
+ };
104
+ }
105
+ function rowToEvaluation(row) {
106
+ return {
107
+ id: row.id,
108
+ proposal: {
109
+ id: row.id,
110
+ target_kind: row.target_kind,
111
+ target_release_id: row.target_release_id,
112
+ optimization_dimension: JSON.parse(row.optimization_dimensions_json)[0],
113
+ dimensions: JSON.parse(row.optimization_dimensions_json),
114
+ target_count: row.target_count,
115
+ single_object: true,
116
+ single_optimization_dimension: true,
117
+ before_snapshot: JSON.parse(row.before_snapshot_json),
118
+ after_snapshot: JSON.parse(row.after_snapshot_json),
119
+ diff: JSON.parse(row.diff_json),
120
+ before_score: row.before_score,
121
+ after_score: row.after_score,
122
+ score_delta_min: row.score_delta_min,
123
+ score_delta: row.score_delta,
124
+ author_id: row.author_id,
125
+ rubric: JSON.parse(row.rubric_json),
126
+ red_lines: JSON.parse(row.red_lines_json),
127
+ source_traces: JSON.parse(row.source_traces_json),
128
+ schema_version: row.schema_version,
129
+ created_at: row.created_at,
130
+ },
131
+ evaluator_id: row.evaluator_id,
132
+ skeptic_id: row.skeptic_id,
133
+ evaluator_result: {
134
+ // We persist the final post-skeptic row only; the raw
135
+ // evaluator_result is not part of the row. Tests can verify
136
+ // the verdict derivation via the service layer.
137
+ score: row.after_score,
138
+ riskTags: [],
139
+ refuteParagraph: "",
140
+ },
141
+ skeptic_result: {
142
+ driftRisks: [],
143
+ overfitRisks: [],
144
+ safetyRegressionRisks: [],
145
+ },
146
+ verdict: row.verdict,
147
+ user_confirmation_pointer: row.user_confirmation_pointer ?? undefined,
148
+ brief_pointer: row.brief_pointer ?? undefined,
149
+ schema_version: row.schema_version,
150
+ created_at: row.created_at,
151
+ score_delta: row.score_delta,
152
+ };
153
+ }
154
+ /**
155
+ * Insert a full EvolutionEvaluation row. The row's
156
+ * `schema_version` is taken from the input (must equal
157
+ * `peaks.evolution/1`); `created_at` is stamped from the server
158
+ * clock here, so callers cannot backdate a row.
159
+ */
160
+ export function insertEvolutionEvaluation(db, evalRow) {
161
+ const stmt = db.prepare(`INSERT INTO evolution_evaluation (
162
+ id, target_kind, target_release_id, optimization_dimensions_json,
163
+ target_count, before_snapshot_json, after_snapshot_json, diff_json,
164
+ before_score, after_score, score_delta_min, score_delta,
165
+ author_id, evaluator_id, skeptic_id, verdict,
166
+ user_confirmation_pointer, brief_pointer,
167
+ rubric_json, red_lines_json, source_traces_json,
168
+ schema_version, created_at
169
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
170
+ stmt.run(evalRow.id, evalRow.proposal.target_kind, evalRow.proposal.target_release_id, JSON.stringify(evalRow.proposal.dimensions), evalRow.proposal.target_count, JSON.stringify(evalRow.proposal.before_snapshot), JSON.stringify(evalRow.proposal.after_snapshot), JSON.stringify(evalRow.proposal.diff), evalRow.proposal.before_score, evalRow.proposal.after_score, evalRow.proposal.score_delta_min, computeScoreDelta(evalRow.proposal.before_score, evalRow.proposal.after_score), evalRow.proposal.author_id, evalRow.evaluator_id, evalRow.skeptic_id, evalRow.verdict, evalRow.user_confirmation_pointer ?? null, evalRow.brief_pointer ?? null, JSON.stringify(evalRow.proposal.rubric), JSON.stringify(evalRow.proposal.red_lines), JSON.stringify(evalRow.proposal.source_traces), evalRow.schema_version, evalRow.created_at);
171
+ }
172
+ /**
173
+ * Read a single EvolutionEvaluation row by id; returns undefined if
174
+ * absent.
175
+ */
176
+ export function getEvolutionEvaluation(db, id) {
177
+ const row = db
178
+ .prepare("SELECT * FROM evolution_evaluation WHERE id = ?")
179
+ .get(id);
180
+ if (!row)
181
+ return undefined;
182
+ return rowToEvaluation(row);
183
+ }
184
+ /**
185
+ * List EvolutionEvaluation rows for a specific target. Filter by
186
+ * `verdict` to scope the listing.
187
+ */
188
+ export function listEvolutionEvaluationsByTarget(db, opts) {
189
+ const params = [opts.target_kind, opts.target_release_id];
190
+ let sql = "SELECT * FROM evolution_evaluation WHERE target_kind = ? AND target_release_id = ?";
191
+ if (opts.verdict) {
192
+ sql += " AND verdict = ?";
193
+ params.push(opts.verdict);
194
+ }
195
+ sql += " ORDER BY created_at DESC, id ASC";
196
+ const rows = db.prepare(sql).all(...params);
197
+ return rows.map(rowToEvaluation);
198
+ }
199
+ /**
200
+ * Update the verdict (and optional user_confirmation_pointer) of
201
+ * an existing EvolutionEvaluation row. Returns the updated row, or
202
+ * undefined if the id is absent.
203
+ *
204
+ * The service layer is responsible for enforcing that the
205
+ * transition `revert` is always allowed and `keep` requires
206
+ * `user_confirmation_pointer` set (AC-15).
207
+ */
208
+ export function updateEvolutionVerdict(db, id, verdict, userConfirmationPointer) {
209
+ const existing = getEvolutionEvaluation(db, id);
210
+ if (!existing)
211
+ return undefined;
212
+ const stmt = db.prepare(`UPDATE evolution_evaluation
213
+ SET verdict = ?,
214
+ user_confirmation_pointer = ?
215
+ WHERE id = ?`);
216
+ stmt.run(verdict, userConfirmationPointer ?? null, id);
217
+ return getEvolutionEvaluation(db, id);
218
+ }
219
+ /**
220
+ * Test seam: produce a fresh id for an EvolutionEvaluation row.
221
+ * Format: `eval-<hex>` (lowercase, 12 hex chars). Matches the Zod
222
+ * `^eval-[0-9a-f-]{8,}$` pattern in EvolutionProposalSchema.
223
+ */
224
+ export function newEvaluationId() {
225
+ // 12-char hex, lowercase, no dashes — the regex allows '-' so
226
+ // 12-char hex is fine. We avoid UUID's dashes to keep ids
227
+ // grep-friendly.
228
+ const hex = Math.floor(Math.random() * 0x1_000_000_000_000)
229
+ .toString(16)
230
+ .padStart(12, "0");
231
+ return `eval-${hex}`;
232
+ }