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.
- package/CHANGELOG.md +105 -87
- package/README-en.md +17 -4
- package/README.md +17 -4
- package/dist/src/cli/commands/asset-commands.d.ts +45 -0
- package/dist/src/cli/commands/asset-commands.js +404 -0
- package/dist/src/cli/commands/bee-commands.d.ts +26 -0
- package/dist/src/cli/commands/bee-commands.js +169 -0
- package/dist/src/cli/commands/{solo-commands.d.ts → code-commands.d.ts} +24 -24
- package/dist/src/cli/commands/{solo-commands.js → code-commands.js} +78 -78
- package/dist/src/cli/commands/core/skill-command.js +1 -1
- package/dist/src/cli/commands/dispatch-from-dag.js +1 -1
- package/dist/src/cli/commands/evolution-commands.d.ts +27 -0
- package/dist/src/cli/commands/evolution-commands.js +393 -0
- package/dist/src/cli/commands/hook-handle.js +7 -7
- package/dist/src/cli/commands/job-commands.js +2 -2
- package/dist/src/cli/commands/loop-commands.d.ts +19 -13
- package/dist/src/cli/commands/loop-commands.js +147 -13
- package/dist/src/cli/commands/sediment-commands.js +9 -0
- package/dist/src/cli/commands/session-auto-compact-hook-command.js +2 -2
- package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.d.ts +67 -0
- package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.js +173 -0
- package/dist/src/cli/commands/sub-agent-shared.d.ts +1 -1
- package/dist/src/cli/commands/user-touchpoint-commands.d.ts +4 -4
- package/dist/src/cli/commands/user-touchpoint-commands.js +14 -14
- package/dist/src/cli/commands/workflow-commands.js +22 -22
- package/dist/src/cli/program.js +23 -6
- package/dist/src/services/artifacts/repair-cycle-service.js +1 -1
- package/dist/src/services/audit/enforcers/active-skill-resolver.d.ts +1 -1
- package/dist/src/services/audit/enforcers/active-skill-resolver.js +1 -1
- package/dist/src/services/audit/enforcers/{solo-code-ban.d.ts → code-ban.d.ts} +6 -6
- package/dist/src/services/audit/enforcers/{solo-code-ban.js → code-ban.js} +6 -6
- package/dist/src/services/audit/red-line-catalog.d.ts +2 -2
- package/dist/src/services/audit/red-line-catalog.js +5 -5
- package/dist/src/services/audit/types.d.ts +2 -2
- package/dist/src/services/{solo → code}/auto-compact-orchestrator.d.ts +1 -1
- package/dist/src/services/{solo → code}/auto-compact-orchestrator.js +3 -3
- package/dist/src/services/{solo → code}/dag-orchestrator.d.ts +1 -1
- package/dist/src/services/{solo → code}/dag-orchestrator.js +1 -1
- package/dist/src/services/{solo → code}/emit-handoff.d.ts +1 -1
- package/dist/src/services/{solo → code}/emit-handoff.js +1 -1
- package/dist/src/services/{solo → code}/job-shape-decision.d.ts +1 -1
- package/dist/src/services/{solo → code}/job-shape-decision.js +5 -5
- package/dist/src/services/{solo → code}/mode-gate.d.ts +9 -9
- package/dist/src/services/{solo → code}/mode-gate.js +5 -5
- package/dist/src/services/{solo → code}/post-compact-detector.d.ts +3 -3
- package/dist/src/services/{solo → code}/post-compact-detector.js +4 -4
- package/dist/src/services/{solo → code}/step-08-gate.d.ts +4 -4
- package/dist/src/services/{solo → code}/step-08-gate.js +4 -4
- package/dist/src/services/{solo → code}/user-touchpoint-classifier.d.ts +1 -1
- package/dist/src/services/{solo → code}/user-touchpoint-classifier.js +1 -1
- package/dist/src/services/context/auto-compact-dispatcher.d.ts +2 -2
- package/dist/src/services/context/auto-compact-dispatcher.js +2 -2
- package/dist/src/services/context/auto-compact-types.d.ts +1 -1
- package/dist/src/services/crystallization/crystallization-service.d.ts +239 -0
- package/dist/src/services/crystallization/crystallization-service.js +397 -0
- package/dist/src/services/crystallization/crystallization-store.d.ts +75 -0
- package/dist/src/services/crystallization/crystallization-store.js +193 -0
- package/dist/src/services/crystallization/crystallization-types.d.ts +313 -0
- package/dist/src/services/crystallization/crystallization-types.js +299 -0
- package/dist/src/services/crystallization/evidence-brief-builder.d.ts +237 -0
- package/dist/src/services/crystallization/evidence-brief-builder.js +240 -0
- package/dist/src/services/dispatch/slice-dag.d.ts +2 -2
- package/dist/src/services/dispatch/test-tool-detection.d.ts +1 -1
- package/dist/src/services/dispatch/test-tool-detection.js +1 -1
- package/dist/src/services/evolution/evolution-service.d.ts +137 -0
- package/dist/src/services/evolution/evolution-service.js +281 -0
- package/dist/src/services/evolution/evolution-store.d.ts +57 -0
- package/dist/src/services/evolution/evolution-store.js +232 -0
- package/dist/src/services/evolution/evolution-types.d.ts +706 -0
- package/dist/src/services/evolution/evolution-types.js +286 -0
- package/dist/src/services/evolution/independent-evaluator-runner.d.ts +90 -0
- package/dist/src/services/evolution/independent-evaluator-runner.js +106 -0
- package/dist/src/services/evolution/regression-skeptic-runner.d.ts +64 -0
- package/dist/src/services/evolution/regression-skeptic-runner.js +113 -0
- package/dist/src/services/feedback/feedback-promotion-service.js +2 -2
- package/dist/src/services/job/job-progress-store.d.ts +1 -1
- package/dist/src/services/job/job-progress-store.js +1 -1
- package/dist/src/services/loop/loop-bee-relation-service.d.ts +107 -0
- package/dist/src/services/loop/loop-bee-relation-service.js +211 -0
- package/dist/src/services/loop/loop-bee-relation-store.d.ts +53 -0
- package/dist/src/services/loop/loop-bee-relation-store.js +130 -0
- package/dist/src/services/loop/loop-bee-relation-types.d.ts +118 -0
- package/dist/src/services/loop/loop-bee-relation-types.js +114 -0
- package/dist/src/services/loop/loop-release-service.d.ts +46 -0
- package/dist/src/services/loop/loop-release-service.js +66 -0
- package/dist/src/services/loop/loop-release-store.d.ts +55 -0
- package/dist/src/services/loop/loop-release-store.js +134 -0
- package/dist/src/services/loop/loop-release-types.d.ts +250 -0
- package/dist/src/services/loop/loop-release-types.js +162 -0
- package/dist/src/services/preferences/preferences-service.d.ts +1 -1
- package/dist/src/services/preferences/preferences-types.d.ts +1 -1
- package/dist/src/services/rd/standards-diagnostic.d.ts +1 -1
- package/dist/src/services/rd/standards-diagnostic.js +2 -2
- package/dist/src/services/recommendations/capability-seed-items.js +1 -1
- package/dist/src/services/recommendations/capability-seed-mappings.js +1 -1
- package/dist/src/services/recommendations/recommendation-service.js +1 -1
- package/dist/src/services/refactor/refactor-service.d.ts +1 -1
- package/dist/src/services/share/bundle-reader.d.ts +100 -0
- package/dist/src/services/share/bundle-reader.js +434 -0
- package/dist/src/services/share/bundle-types.d.ts +392 -0
- package/dist/src/services/share/bundle-types.js +318 -0
- package/dist/src/services/share/bundle-writer.d.ts +97 -0
- package/dist/src/services/share/bundle-writer.js +429 -0
- package/dist/src/services/share/run-state-contract.d.ts +133 -0
- package/dist/src/services/share/run-state-contract.js +138 -0
- package/dist/src/services/signal/cancel-handler.js +3 -3
- package/dist/src/services/skill/resume-detector.js +1 -1
- package/dist/src/services/skillhub/release-import.js +7 -2
- package/dist/src/services/skillhub/release-retain.js +6 -1
- package/dist/src/services/skillhub/sqlite-store.js +17 -1
- package/dist/src/services/skillhub/types.d.ts +10 -0
- package/dist/src/services/skills/skill-presence-service.d.ts +1 -1
- package/dist/src/services/standards/loop-engineering-lint.d.ts +38 -0
- package/dist/src/services/standards/loop-engineering-lint.js +101 -0
- package/dist/src/services/standards/loop-engineering-readiness-lint.d.ts +111 -0
- package/dist/src/services/standards/loop-engineering-readiness-lint.js +248 -0
- package/dist/src/services/upgrade/1x-detector-service.js +1 -1
- package/dist/src/services/workflow/pipeline-verify-service.d.ts +1 -1
- package/dist/src/services/workflow/pipeline-verify-service.js +3 -3
- package/dist/src/services/workflow/workflow-autonomous-service.d.ts +3 -3
- package/dist/src/services/workflow/workflow-autonomous-service.js +2 -2
- package/dist/src/services/workflow/workflow-router-service.d.ts +6 -6
- package/dist/src/services/workflow/workflow-router-service.js +33 -33
- package/dist/src/services/workspace/claude-settings-template.d.ts +1 -1
- package/dist/src/services/workspace/claude-settings-template.js +5 -5
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/examples/video-demo/README.md +57 -0
- package/examples/video-demo/out/en-closing-960.png +0 -0
- package/examples/video-demo/out/zh-closing-960.png +0 -0
- package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
- package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
- package/output-styles/peaks-skill-swarm.md +3 -3
- package/package.json +6 -2
- package/skills/bee/peaks-qa/SKILL.md +1 -1
- package/skills/bee/peaks-qa/references/artifact-per-request.md +3 -3
- package/skills/bee/peaks-qa/references/qa-sub-agent-dispatch.md +6 -6
- package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +1 -1
- package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
- package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +11 -11
- package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +3 -3
- package/skills/bee/peaks-ui/SKILL.md +8 -8
- package/skills/peaks-code/SKILL.md +34 -21
- package/skills/peaks-code/references/anchoring-and-session-info.md +2 -2
- package/skills/peaks-code/references/artifact-contracts.md +1 -1
- package/skills/peaks-code/references/boundaries.md +3 -3
- package/skills/peaks-code/references/codegraph-orchestration.md +1 -1
- package/skills/peaks-code/references/dag-orchestrator.md +7 -7
- package/skills/peaks-code/references/external-references.md +2 -2
- package/skills/peaks-code/references/fanout-mandatory.md +3 -3
- package/skills/peaks-code/references/fast-mode.md +2 -2
- package/skills/peaks-code/references/frontend-only-mode.md +9 -9
- package/skills/peaks-code/references/job-loop.md +1 -1
- package/skills/peaks-code/references/local-artifact-workspace.md +2 -2
- package/skills/peaks-code/references/micro-cycle.md +14 -14
- package/skills/peaks-code/references/mode-selection-with-stale-presence.md +5 -5
- package/skills/peaks-code/references/mode-selection.md +3 -3
- package/skills/peaks-code/references/openspec-workflow.md +16 -16
- package/skills/peaks-code/references/periodic-checkpoint.md +4 -4
- package/skills/peaks-code/references/quality-gate-cheatsheet.md +1 -1
- package/skills/peaks-code/references/refactor-mode.md +1 -1
- package/skills/peaks-code/references/runbook.md +15 -15
- package/skills/peaks-code/references/step-0-55-1x-detection.md +1 -1
- package/skills/peaks-code/references/step-0-8-gate.md +11 -11
- package/skills/peaks-code/references/sub-agent-dispatch.md +8 -8
- package/skills/peaks-code/references/swarm-dispatch-contract.md +17 -17
- package/skills/peaks-code/references/workflow-gates-and-types.md +2 -2
- package/skills/peaks-code/references/workflow.md +5 -5
- package/skills/peaks-content/SKILL.md +272 -0
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +307 -0
- package/skills/peaks-resume/SKILL.md +2 -2
- package/skills/peaks-status/SKILL.md +2 -2
- package/skills/peaks-test/SKILL.md +2 -2
- /package/dist/src/services/{solo → code}/batch-heartbeat-poller.d.ts +0 -0
- /package/dist/src/services/{solo → code}/batch-heartbeat-poller.js +0 -0
- /package/dist/src/services/{solo → code}/status-line-renderer.d.ts +0 -0
- /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
|
+
}
|