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.
- package/README-en.md +16 -3
- package/README.md +16 -3
- 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/evolution-commands.d.ts +27 -0
- package/dist/src/cli/commands/evolution-commands.js +393 -0
- 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/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/program.js +17 -0
- 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/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/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/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/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/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/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/package.json +6 -2
- package/skills/peaks-code/SKILL.md +13 -0
- package/skills/peaks-content/SKILL.md +272 -0
- package/skills/peaks-issue-fix-orchestrator/SKILL.md +307 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { type CrystallizationEvent, type CrystallizationEventInput, type CrystallizationEventStatus, type EvidenceBrief } from "./crystallization-types.js";
|
|
4
|
+
import { listCrystallizationEvents } from "./crystallization-store.js";
|
|
5
|
+
import { type LoopReleaseInput } from "../loop/loop-release-types.js";
|
|
6
|
+
/**
|
|
7
|
+
* CrystallizationService — spec §5 (post-run crystallization)
|
|
8
|
+
* + §4.5 + §4.7 + §10 RL-2 / RL-3 / RL-7.
|
|
9
|
+
*
|
|
10
|
+
* Hard rules enforced at THIS layer (defense in depth; the Zod
|
|
11
|
+
* schemas above and the brief refine guard add the second wall):
|
|
12
|
+
*
|
|
13
|
+
* - AC-4 / RL-2: a crystallization_event is written only when the
|
|
14
|
+
* candidate task has `task_status === 'completed'` AND
|
|
15
|
+
* `gates_passed === true` AND `evidence_collected === true`.
|
|
16
|
+
* Any pre-run attempt (status !== 'completed') is rejected
|
|
17
|
+
* with `CRYSTALLIZATION_PRE_RUN`. This is the pre-run block
|
|
18
|
+
* enforced by AC-4, AC-5, AC-6, AC-15, AC-16, AC-17 of the M5
|
|
19
|
+
* slice.
|
|
20
|
+
* - AC-6 / RL-3: writing a (loop_release, main_bee_release,
|
|
21
|
+
* loop_bee_relation) pair happens in a SINGLE TRANSACTION. The
|
|
22
|
+
* brief persist on crystallization_event also lives inside
|
|
23
|
+
* the same transaction (the brief is the durable evidence;
|
|
24
|
+
* dropping either side would break consistency).
|
|
25
|
+
* - AC-7 / RL-1: this layer never asks the user to hand-author
|
|
26
|
+
* JSON / manifest / CLI verb; all inputs are typed payloads
|
|
27
|
+
* the LLM submits on the user's behalf.
|
|
28
|
+
* - AC-15 / RL-7: the brief is mandatory; the
|
|
29
|
+
* `EvidenceBriefSchema.refine` guard rejects partial briefs
|
|
30
|
+
* at the parse boundary.
|
|
31
|
+
* - AC-16 / RL-7: the brief has all 4 sections
|
|
32
|
+
* (what_happened / why_it_matters / what_learned /
|
|
33
|
+
* what_action). The service throws `BriefSectionError` if the
|
|
34
|
+
* payload fails the guard.
|
|
35
|
+
*
|
|
36
|
+
* Sub-domain error codes:
|
|
37
|
+
*
|
|
38
|
+
* - CRYSTALLIZATION_PRE_RUN: task_status / gates_passed /
|
|
39
|
+
* evidence_collected precondition failed.
|
|
40
|
+
* - CRYSTALLIZATION_INVALID_TASK_STATE: a task input field is
|
|
41
|
+
* malformed (e.g. unknown task_status).
|
|
42
|
+
* - MISSING_BRIEF_SECTION: a brief is missing at least one of
|
|
43
|
+
* its 4 sections (raised from the evidence-brief-builder
|
|
44
|
+
* refine; surfaced as-is by the service).
|
|
45
|
+
* - CRYSTALLIZATION_TX_FAILED: a transaction-side effect failed
|
|
46
|
+
* (FK constraint, duplicate key, …). The service re-raises
|
|
47
|
+
* richer domain errors first; this is the catch-all for
|
|
48
|
+
* unexpected SQL errors.
|
|
49
|
+
*/
|
|
50
|
+
export declare const CRYSTALLIZATION_TASK_STATUSES: readonly ["completed", "in_progress", "scratch", "blocked", "failed"];
|
|
51
|
+
export type CrystallizationTaskStatus = (typeof CRYSTALLIZATION_TASK_STATUSES)[number];
|
|
52
|
+
/**
|
|
53
|
+
* The pre-run task state the service requires. Crystallization is
|
|
54
|
+
* GATED on these three flags. The validator rejects any input that
|
|
55
|
+
* does not match. Adding a new flag means a schema-version bump; for
|
|
56
|
+
* now the three are required.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CrystallizationTaskStateSchema: z.ZodObject<{
|
|
59
|
+
task_id: z.ZodString;
|
|
60
|
+
task_status: z.ZodLiteral<"completed">;
|
|
61
|
+
gates_passed: z.ZodLiteral<true>;
|
|
62
|
+
evidence_collected: z.ZodLiteral<true>;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
task_id: string;
|
|
65
|
+
task_status: "completed";
|
|
66
|
+
gates_passed: true;
|
|
67
|
+
evidence_collected: true;
|
|
68
|
+
}, {
|
|
69
|
+
task_id: string;
|
|
70
|
+
task_status: "completed";
|
|
71
|
+
gates_passed: true;
|
|
72
|
+
evidence_collected: true;
|
|
73
|
+
}>;
|
|
74
|
+
export type CrystallizationTaskState = z.infer<typeof CrystallizationTaskStateSchema>;
|
|
75
|
+
export type CrystallizationIntegrityErrorCode = "CRYSTALLIZATION_PRE_RUN" | "CRYSTALLIZATION_INVALID_TASK_STATE" | "CRYSTALLIZATION_TX_FAILED" | "MISSING_BRIEF_SECTION";
|
|
76
|
+
export declare class CrystallizationIntegrityError extends Error {
|
|
77
|
+
readonly code: CrystallizationIntegrityErrorCode | "MISSING_BRIEF_SECTION";
|
|
78
|
+
readonly findings: ReadonlyArray<{
|
|
79
|
+
path: string;
|
|
80
|
+
message: string;
|
|
81
|
+
}>;
|
|
82
|
+
constructor(code: CrystallizationIntegrityErrorCode, message: string, findings?: ReadonlyArray<{
|
|
83
|
+
path: string;
|
|
84
|
+
message: string;
|
|
85
|
+
}>);
|
|
86
|
+
}
|
|
87
|
+
export interface CrystallizePayload {
|
|
88
|
+
/** The candidate task state. Must satisfy the pre-run gate. */
|
|
89
|
+
task: CrystallizationTaskState;
|
|
90
|
+
/** The loop_release to create (new). Must include main bee via relation. */
|
|
91
|
+
loop_input: LoopReleaseInput;
|
|
92
|
+
/**
|
|
93
|
+
* The main bee_release to create. Note: bee_release requires a
|
|
94
|
+
* manifest row to keep the existing 4.x schema consistent;
|
|
95
|
+
* `bee_input` here carries ONLY the manifest-level fields the
|
|
96
|
+
* service uses. A future slice will swap in the full
|
|
97
|
+
* retainRelease() flow.
|
|
98
|
+
*/
|
|
99
|
+
bee_input: BeeCrystallizeInput;
|
|
100
|
+
/**
|
|
101
|
+
* Optional NL reason the LLM authored; surfaced on the
|
|
102
|
+
* loop_bee_relation row (spec §4.6 `reason`).
|
|
103
|
+
*/
|
|
104
|
+
bee_relation_reason: string;
|
|
105
|
+
/**
|
|
106
|
+
* The 4-section evidence brief (spec §4.7).
|
|
107
|
+
*/
|
|
108
|
+
evidence_brief: EvidenceBrief;
|
|
109
|
+
/** Optional structured bullets (RL-7: counts may support the brief, not replace it). */
|
|
110
|
+
evidence_bullets?: string[];
|
|
111
|
+
/** Source trace pointers for the brief (spec §4.5). */
|
|
112
|
+
source_trace_pointers?: string[];
|
|
113
|
+
/** Optional evaluator summary (independent scorers — spec §4.5). */
|
|
114
|
+
evaluator_summary?: string;
|
|
115
|
+
/** Optional user decision summary (spec §4.5). */
|
|
116
|
+
user_decision_summary?: string;
|
|
117
|
+
/** Trigger classification (spec §5.4). */
|
|
118
|
+
trigger: CrystallizationEventInput["trigger"];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Minimum bee_release payload the service needs to create a main bee
|
|
122
|
+
* alongside a loop. The full bee_release schema (see
|
|
123
|
+
* `src/services/skillhub/release-retain.ts`) carries manifest rows,
|
|
124
|
+
* segment refs, file rows, etc.; M5 only writes the bee_release header
|
|
125
|
+
* + a single bee_manifest row sufficient to keep the 4.x FK
|
|
126
|
+
* constraints green. Full bee retaining lands in a future slice.
|
|
127
|
+
*
|
|
128
|
+
* Schema definition is hoisted above the interface so the typed
|
|
129
|
+
* interface can reference the inferred Zod type without forward-
|
|
130
|
+
* reference gymnastics.
|
|
131
|
+
*/
|
|
132
|
+
declare const BeeNameSchema: z.ZodString;
|
|
133
|
+
export interface BeeCrystallizeInput {
|
|
134
|
+
bee_name: z.infer<typeof BeeNameSchema>;
|
|
135
|
+
version: string;
|
|
136
|
+
description: string;
|
|
137
|
+
user_intent_raw?: string;
|
|
138
|
+
parent_version?: string;
|
|
139
|
+
changelog?: string;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The crystallization result. The brief is returned AS PERSISTED
|
|
143
|
+
* (re-parsed through the schema) so callers receive the canonical
|
|
144
|
+
* shape, not the loose payload.
|
|
145
|
+
*/
|
|
146
|
+
export interface CrystallizationResult {
|
|
147
|
+
loop_release_id: string;
|
|
148
|
+
bee_release_id: number;
|
|
149
|
+
loop_bee_relation_id: number;
|
|
150
|
+
crystallization_event_id: string;
|
|
151
|
+
loop_release_lifecycle_status: CrystallizationEventStatus;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Service interface for the post-run crystallization flow.
|
|
155
|
+
*/
|
|
156
|
+
export declare class CrystallizationService {
|
|
157
|
+
private readonly db;
|
|
158
|
+
constructor(db: Database.Database);
|
|
159
|
+
/**
|
|
160
|
+
* Read a CrystallizationEvent by id.
|
|
161
|
+
*/
|
|
162
|
+
read(id: string): CrystallizationEvent | undefined;
|
|
163
|
+
/**
|
|
164
|
+
* List events. Filter by lifecycle_status and/or the FK pointer
|
|
165
|
+
* columns. When no filters are supplied, lists ALL events
|
|
166
|
+
* (newest-first).
|
|
167
|
+
*/
|
|
168
|
+
list(opts?: Parameters<typeof listCrystallizationEvents>[1]): {
|
|
169
|
+
schema_version: "peaks.crystallization/1";
|
|
170
|
+
id: string;
|
|
171
|
+
lifecycle_status: "candidate" | "stable" | "retired";
|
|
172
|
+
trigger: "user_explicit" | "llm_suggested" | "success_default_prompt" | "similar_task_recurrence";
|
|
173
|
+
evidence_brief: {
|
|
174
|
+
what_happened: string;
|
|
175
|
+
why_it_matters: string;
|
|
176
|
+
what_learned: string;
|
|
177
|
+
what_action: string;
|
|
178
|
+
};
|
|
179
|
+
evidence_bullets: string[];
|
|
180
|
+
source_trace_pointers: string[];
|
|
181
|
+
evaluator_summary: string;
|
|
182
|
+
user_decision_summary: string;
|
|
183
|
+
created_at: string;
|
|
184
|
+
created_loop_release_id?: string | undefined;
|
|
185
|
+
updated_loop_release_id?: string | undefined;
|
|
186
|
+
created_bee_release_id?: number | undefined;
|
|
187
|
+
updated_bee_release_id?: number | undefined;
|
|
188
|
+
}[];
|
|
189
|
+
/**
|
|
190
|
+
* Update a crystallization event's lifecycle_status. Always allowed
|
|
191
|
+
* (no schema-version gate). Returns the updated row or undefined if
|
|
192
|
+
* the id is absent.
|
|
193
|
+
*
|
|
194
|
+
* A `stable` transition is the natural post-crystallization state;
|
|
195
|
+
* the service does NOT enforce user-confirmation here (per spec
|
|
196
|
+
* §5.6 promotion happens on a separate `peaks loop promote` path).
|
|
197
|
+
* `retired` is the dispose path (`peaks asset dispose`).
|
|
198
|
+
*/
|
|
199
|
+
updateStatus(id: string, next: CrystallizationEventStatus): CrystallizationEvent | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* Pre-run gate: throws if any of `task_status`, `gates_passed`,
|
|
202
|
+
* `evidence_collected` is not as required. Called from
|
|
203
|
+
* `crystallize` BEFORE the transaction begins.
|
|
204
|
+
*
|
|
205
|
+
* Reachable as a public method so the CLI / tests can probe the
|
|
206
|
+
* gate in isolation.
|
|
207
|
+
*/
|
|
208
|
+
assertReady(task: CrystallizationTaskState): CrystallizationTaskState;
|
|
209
|
+
/**
|
|
210
|
+
* Run the post-run crystallization flow.
|
|
211
|
+
*
|
|
212
|
+
* Steps (all in ONE better-sqlite3 transaction, atomic):
|
|
213
|
+
*
|
|
214
|
+
* 1. Pre-run gate (task must be completed / gates_passed /
|
|
215
|
+
* evidence_collected).
|
|
216
|
+
* 2. Re-validate the 4-section brief (EvidenceBriefSchema.parse,
|
|
217
|
+
* EvidenceBriefSchema.refine).
|
|
218
|
+
* 3. Insert loop_release.
|
|
219
|
+
* 4. Insert bee_release header (+ a single bee_manifest row to
|
|
220
|
+
* keep the 4.x FK contract).
|
|
221
|
+
* 5. Insert loop_bee_relation row with role='main'.
|
|
222
|
+
* 6. Insert crystallization_event with the brief persisted.
|
|
223
|
+
*
|
|
224
|
+
* Returns the four ids + the new loop's lifecycle_status. On any
|
|
225
|
+
* failure the transaction is rolled back and the partial rows
|
|
226
|
+
* disappear.
|
|
227
|
+
*
|
|
228
|
+
* Throws:
|
|
229
|
+
* - CrystallizationIntegrityError(CRYSTALLIZATION_PRE_RUN) on
|
|
230
|
+
* the pre-run gate.
|
|
231
|
+
* - BriefSectionError or ZodError if the brief is malformed;
|
|
232
|
+
* surfaced as
|
|
233
|
+
* CrystallizationIntegrityError(MISSING_BRIEF_SECTION).
|
|
234
|
+
* - CrystallizationIntegrityError(CRYSTALLIZATION_TX_FAILED)
|
|
235
|
+
* on any SQL transaction failure.
|
|
236
|
+
*/
|
|
237
|
+
crystallize(payload: CrystallizePayload): CrystallizationResult;
|
|
238
|
+
}
|
|
239
|
+
export {};
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { z, ZodError } from "zod";
|
|
2
|
+
import { CrystallizationEventSchema, EvidenceBriefSchema, } from "./crystallization-types.js";
|
|
3
|
+
import { BriefSectionError } from "./evidence-brief-builder.js";
|
|
4
|
+
import { CRYSTALLIZATION_SCHEMA_VERSION, ensureCrystallizationEventTable, getCrystallizationEvent, listCrystallizationEvents, newCrystallizationId, updateCrystallizationEventStatus, } from "./crystallization-store.js";
|
|
5
|
+
import { insertLoopRelease } from "../loop/loop-release-store.js";
|
|
6
|
+
import { LoopReleaseSchema } from "../loop/loop-release-types.js";
|
|
7
|
+
import { insertLoopBeeRelation } from "../loop/loop-bee-relation-store.js";
|
|
8
|
+
import { LoopBeeRelationSchema, } from "../loop/loop-bee-relation-types.js";
|
|
9
|
+
/**
|
|
10
|
+
* CrystallizationService — spec §5 (post-run crystallization)
|
|
11
|
+
* + §4.5 + §4.7 + §10 RL-2 / RL-3 / RL-7.
|
|
12
|
+
*
|
|
13
|
+
* Hard rules enforced at THIS layer (defense in depth; the Zod
|
|
14
|
+
* schemas above and the brief refine guard add the second wall):
|
|
15
|
+
*
|
|
16
|
+
* - AC-4 / RL-2: a crystallization_event is written only when the
|
|
17
|
+
* candidate task has `task_status === 'completed'` AND
|
|
18
|
+
* `gates_passed === true` AND `evidence_collected === true`.
|
|
19
|
+
* Any pre-run attempt (status !== 'completed') is rejected
|
|
20
|
+
* with `CRYSTALLIZATION_PRE_RUN`. This is the pre-run block
|
|
21
|
+
* enforced by AC-4, AC-5, AC-6, AC-15, AC-16, AC-17 of the M5
|
|
22
|
+
* slice.
|
|
23
|
+
* - AC-6 / RL-3: writing a (loop_release, main_bee_release,
|
|
24
|
+
* loop_bee_relation) pair happens in a SINGLE TRANSACTION. The
|
|
25
|
+
* brief persist on crystallization_event also lives inside
|
|
26
|
+
* the same transaction (the brief is the durable evidence;
|
|
27
|
+
* dropping either side would break consistency).
|
|
28
|
+
* - AC-7 / RL-1: this layer never asks the user to hand-author
|
|
29
|
+
* JSON / manifest / CLI verb; all inputs are typed payloads
|
|
30
|
+
* the LLM submits on the user's behalf.
|
|
31
|
+
* - AC-15 / RL-7: the brief is mandatory; the
|
|
32
|
+
* `EvidenceBriefSchema.refine` guard rejects partial briefs
|
|
33
|
+
* at the parse boundary.
|
|
34
|
+
* - AC-16 / RL-7: the brief has all 4 sections
|
|
35
|
+
* (what_happened / why_it_matters / what_learned /
|
|
36
|
+
* what_action). The service throws `BriefSectionError` if the
|
|
37
|
+
* payload fails the guard.
|
|
38
|
+
*
|
|
39
|
+
* Sub-domain error codes:
|
|
40
|
+
*
|
|
41
|
+
* - CRYSTALLIZATION_PRE_RUN: task_status / gates_passed /
|
|
42
|
+
* evidence_collected precondition failed.
|
|
43
|
+
* - CRYSTALLIZATION_INVALID_TASK_STATE: a task input field is
|
|
44
|
+
* malformed (e.g. unknown task_status).
|
|
45
|
+
* - MISSING_BRIEF_SECTION: a brief is missing at least one of
|
|
46
|
+
* its 4 sections (raised from the evidence-brief-builder
|
|
47
|
+
* refine; surfaced as-is by the service).
|
|
48
|
+
* - CRYSTALLIZATION_TX_FAILED: a transaction-side effect failed
|
|
49
|
+
* (FK constraint, duplicate key, …). The service re-raises
|
|
50
|
+
* richer domain errors first; this is the catch-all for
|
|
51
|
+
* unexpected SQL errors.
|
|
52
|
+
*/
|
|
53
|
+
/* ---------------------------------------------------------------------- */
|
|
54
|
+
/* Pre-run task gate — AC-4 / RL-2 */
|
|
55
|
+
/* ---------------------------------------------------------------------- */
|
|
56
|
+
export const CRYSTALLIZATION_TASK_STATUSES = [
|
|
57
|
+
"completed",
|
|
58
|
+
"in_progress",
|
|
59
|
+
"scratch",
|
|
60
|
+
"blocked",
|
|
61
|
+
"failed",
|
|
62
|
+
];
|
|
63
|
+
/**
|
|
64
|
+
* The pre-run task state the service requires. Crystallization is
|
|
65
|
+
* GATED on these three flags. The validator rejects any input that
|
|
66
|
+
* does not match. Adding a new flag means a schema-version bump; for
|
|
67
|
+
* now the three are required.
|
|
68
|
+
*/
|
|
69
|
+
export const CrystallizationTaskStateSchema = z.object({
|
|
70
|
+
task_id: z.string().trim().min(1).max(256),
|
|
71
|
+
task_status: z.literal("completed", {
|
|
72
|
+
errorMap: () => ({
|
|
73
|
+
message: "task_status must be 'completed' for crystallization (AC-4 / RL-2)",
|
|
74
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
gates_passed: z.literal(true, {
|
|
77
|
+
errorMap: () => ({
|
|
78
|
+
message: "gates_passed must be true for crystallization (AC-4 / RL-2)",
|
|
79
|
+
}),
|
|
80
|
+
}),
|
|
81
|
+
evidence_collected: z.literal(true, {
|
|
82
|
+
errorMap: () => ({
|
|
83
|
+
message: "evidence_collected must be true for crystallization (AC-4 / RL-2)",
|
|
84
|
+
}),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
export class CrystallizationIntegrityError extends Error {
|
|
88
|
+
code;
|
|
89
|
+
findings;
|
|
90
|
+
constructor(code, message, findings = []) {
|
|
91
|
+
super(message);
|
|
92
|
+
this.name = "CrystallizationIntegrityError";
|
|
93
|
+
this.code = code;
|
|
94
|
+
this.findings = findings;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Minimum bee_release payload the service needs to create a main bee
|
|
99
|
+
* alongside a loop. The full bee_release schema (see
|
|
100
|
+
* `src/services/skillhub/release-retain.ts`) carries manifest rows,
|
|
101
|
+
* segment refs, file rows, etc.; M5 only writes the bee_release header
|
|
102
|
+
* + a single bee_manifest row sufficient to keep the 4.x FK
|
|
103
|
+
* constraints green. Full bee retaining lands in a future slice.
|
|
104
|
+
*
|
|
105
|
+
* Schema definition is hoisted above the interface so the typed
|
|
106
|
+
* interface can reference the inferred Zod type without forward-
|
|
107
|
+
* reference gymnastics.
|
|
108
|
+
*/
|
|
109
|
+
const BeeNameSchema = z
|
|
110
|
+
.string()
|
|
111
|
+
.trim()
|
|
112
|
+
.min(1, "bee_name is required")
|
|
113
|
+
.max(200)
|
|
114
|
+
.regex(/^[a-z0-9][a-z0-9._-]*$/, {
|
|
115
|
+
message: "bee_name must be kebab-/snake-case starting with a lowercase letter or digit",
|
|
116
|
+
});
|
|
117
|
+
const BeeCrystallizeInputSchema = z.object({
|
|
118
|
+
bee_name: BeeNameSchema,
|
|
119
|
+
version: z
|
|
120
|
+
.string()
|
|
121
|
+
.regex(/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/, {
|
|
122
|
+
message: "version must be a semver string (e.g. 0.1.0)",
|
|
123
|
+
}),
|
|
124
|
+
description: z.string().trim().min(1).max(4000),
|
|
125
|
+
user_intent_raw: z.string().trim().max(4000).optional(),
|
|
126
|
+
parent_version: z.string().trim().max(64).optional(),
|
|
127
|
+
changelog: z.string().trim().max(8000).optional(),
|
|
128
|
+
});
|
|
129
|
+
/**
|
|
130
|
+
* Service interface for the post-run crystallization flow.
|
|
131
|
+
*/
|
|
132
|
+
export class CrystallizationService {
|
|
133
|
+
db;
|
|
134
|
+
constructor(db) {
|
|
135
|
+
this.db = db;
|
|
136
|
+
// Idempotent — safe to call on every constructor invocation; the
|
|
137
|
+
// openStateDb pipeline already applied the SQL migration, but a
|
|
138
|
+
// caller-built DB (e.g. tests) needs this.
|
|
139
|
+
ensureCrystallizationEventTable(this.db);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Read a CrystallizationEvent by id.
|
|
143
|
+
*/
|
|
144
|
+
read(id) {
|
|
145
|
+
return getCrystallizationEvent(this.db, id);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* List events. Filter by lifecycle_status and/or the FK pointer
|
|
149
|
+
* columns. When no filters are supplied, lists ALL events
|
|
150
|
+
* (newest-first).
|
|
151
|
+
*/
|
|
152
|
+
list(opts = {}) {
|
|
153
|
+
return listCrystallizationEvents(this.db, opts);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Update a crystallization event's lifecycle_status. Always allowed
|
|
157
|
+
* (no schema-version gate). Returns the updated row or undefined if
|
|
158
|
+
* the id is absent.
|
|
159
|
+
*
|
|
160
|
+
* A `stable` transition is the natural post-crystallization state;
|
|
161
|
+
* the service does NOT enforce user-confirmation here (per spec
|
|
162
|
+
* §5.6 promotion happens on a separate `peaks loop promote` path).
|
|
163
|
+
* `retired` is the dispose path (`peaks asset dispose`).
|
|
164
|
+
*/
|
|
165
|
+
updateStatus(id, next) {
|
|
166
|
+
return updateCrystallizationEventStatus(this.db, id, next);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Pre-run gate: throws if any of `task_status`, `gates_passed`,
|
|
170
|
+
* `evidence_collected` is not as required. Called from
|
|
171
|
+
* `crystallize` BEFORE the transaction begins.
|
|
172
|
+
*
|
|
173
|
+
* Reachable as a public method so the CLI / tests can probe the
|
|
174
|
+
* gate in isolation.
|
|
175
|
+
*/
|
|
176
|
+
assertReady(task) {
|
|
177
|
+
let parsed;
|
|
178
|
+
try {
|
|
179
|
+
parsed = CrystallizationTaskStateSchema.parse(task);
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
if (err instanceof ZodError) {
|
|
183
|
+
const findings = err.issues.map((i) => ({
|
|
184
|
+
path: i.path.join("."),
|
|
185
|
+
message: i.message,
|
|
186
|
+
}));
|
|
187
|
+
const first = err.issues[0];
|
|
188
|
+
throw new CrystallizationIntegrityError("CRYSTALLIZATION_PRE_RUN", `pre-run gate failed: ${first?.message ?? "invalid task state"}`, findings);
|
|
189
|
+
}
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
// Defense in depth — re-assert at the service boundary.
|
|
193
|
+
if (parsed.task_status !== "completed" ||
|
|
194
|
+
parsed.gates_passed !== true ||
|
|
195
|
+
parsed.evidence_collected !== true) {
|
|
196
|
+
throw new CrystallizationIntegrityError("CRYSTALLIZATION_PRE_RUN", "pre-run gate failed: task must be completed with gates_passed=true AND evidence_collected=true (AC-4 / RL-2)", [
|
|
197
|
+
{
|
|
198
|
+
path: "task",
|
|
199
|
+
message: "task_status must equal 'completed' and gates_passed and evidence_collected must both be true",
|
|
200
|
+
},
|
|
201
|
+
]);
|
|
202
|
+
}
|
|
203
|
+
return parsed;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Run the post-run crystallization flow.
|
|
207
|
+
*
|
|
208
|
+
* Steps (all in ONE better-sqlite3 transaction, atomic):
|
|
209
|
+
*
|
|
210
|
+
* 1. Pre-run gate (task must be completed / gates_passed /
|
|
211
|
+
* evidence_collected).
|
|
212
|
+
* 2. Re-validate the 4-section brief (EvidenceBriefSchema.parse,
|
|
213
|
+
* EvidenceBriefSchema.refine).
|
|
214
|
+
* 3. Insert loop_release.
|
|
215
|
+
* 4. Insert bee_release header (+ a single bee_manifest row to
|
|
216
|
+
* keep the 4.x FK contract).
|
|
217
|
+
* 5. Insert loop_bee_relation row with role='main'.
|
|
218
|
+
* 6. Insert crystallization_event with the brief persisted.
|
|
219
|
+
*
|
|
220
|
+
* Returns the four ids + the new loop's lifecycle_status. On any
|
|
221
|
+
* failure the transaction is rolled back and the partial rows
|
|
222
|
+
* disappear.
|
|
223
|
+
*
|
|
224
|
+
* Throws:
|
|
225
|
+
* - CrystallizationIntegrityError(CRYSTALLIZATION_PRE_RUN) on
|
|
226
|
+
* the pre-run gate.
|
|
227
|
+
* - BriefSectionError or ZodError if the brief is malformed;
|
|
228
|
+
* surfaced as
|
|
229
|
+
* CrystallizationIntegrityError(MISSING_BRIEF_SECTION).
|
|
230
|
+
* - CrystallizationIntegrityError(CRYSTALLIZATION_TX_FAILED)
|
|
231
|
+
* on any SQL transaction failure.
|
|
232
|
+
*/
|
|
233
|
+
crystallize(payload) {
|
|
234
|
+
const task = this.assertReady(payload.task);
|
|
235
|
+
// Re-validate the 4-section brief up-front; the schema refines
|
|
236
|
+
// guard rejects partial briefs. This is the AC-15 / AC-16 / AC-17
|
|
237
|
+
// gate.
|
|
238
|
+
let brief;
|
|
239
|
+
try {
|
|
240
|
+
brief = payload.evidence_brief;
|
|
241
|
+
// Defensive re-parse even though the input is typed; ensures
|
|
242
|
+
// `parse()` runs the .refine() guard.
|
|
243
|
+
EvidenceBriefSchema.parse(brief);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (err instanceof ZodError || err instanceof BriefSectionError) {
|
|
247
|
+
const findings = err instanceof ZodError
|
|
248
|
+
? err.issues.map((i) => ({
|
|
249
|
+
path: i.path.join("."),
|
|
250
|
+
message: i.message,
|
|
251
|
+
}))
|
|
252
|
+
: err.findings;
|
|
253
|
+
throw new CrystallizationIntegrityError("MISSING_BRIEF_SECTION", "evidence_brief is missing at least one of the 4 required sections (spec §4.7 / RL-7)", [...findings]);
|
|
254
|
+
}
|
|
255
|
+
throw err;
|
|
256
|
+
}
|
|
257
|
+
// Re-validate loop_input via Zod so a hand-crafted caller input
|
|
258
|
+
// cannot bypass the schema. (M8 dogfood follow-up.)
|
|
259
|
+
const loopRow = LoopReleaseSchema.parse(payload.loop_input);
|
|
260
|
+
// Re-validate bee_input.
|
|
261
|
+
const beeRow = BeeCrystallizeInputSchema.parse(payload.bee_input);
|
|
262
|
+
// Loop-bee relation input (the relation's `reason` is the NL
|
|
263
|
+
// account the LLM authored). We validate the shape WITHOUT
|
|
264
|
+
// bee_release_id (which is filled in by the bee insert below),
|
|
265
|
+
// then validate the final tuple after the SQL writes.
|
|
266
|
+
const relationInputShape = LoopBeeRelationSchema.omit({
|
|
267
|
+
id: true,
|
|
268
|
+
created_at: true,
|
|
269
|
+
bee_release_id: true,
|
|
270
|
+
}).parse({
|
|
271
|
+
loop_release_id: loopRow.id,
|
|
272
|
+
role: "main",
|
|
273
|
+
reason: payload.bee_relation_reason,
|
|
274
|
+
});
|
|
275
|
+
const eventId = newCrystallizationId();
|
|
276
|
+
const eventCreatedAt = new Date().toISOString();
|
|
277
|
+
let loopReleaseId = loopRow.id;
|
|
278
|
+
let beeReleaseId = -1;
|
|
279
|
+
let relationId = -1;
|
|
280
|
+
let eventPersistedId = eventId;
|
|
281
|
+
const tx = this.db.transaction(() => {
|
|
282
|
+
// 1. Insert loop_release.
|
|
283
|
+
insertLoopRelease(this.db, loopRow);
|
|
284
|
+
// 2. Insert bee_release header. The existing
|
|
285
|
+
// `retainRelease` adds segment_ref / file / change rows; the
|
|
286
|
+
// minimal crystallization write here only opens the bee_release
|
|
287
|
+
// header row + a bee_manifest row. Future slices can promote
|
|
288
|
+
// the bee retain path to a richer flow.
|
|
289
|
+
const beeInsert = this.db.prepare(`INSERT INTO bee_release (
|
|
290
|
+
bee_name, version, source, archived_at, archived_by,
|
|
291
|
+
user_intent_raw, description, parent_version, changelog,
|
|
292
|
+
shareable, desktop_visible
|
|
293
|
+
) VALUES (?, ?, 'user', ?, ?, ?, ?, ?, ?, 1, 1)`);
|
|
294
|
+
const beeInfo = beeInsert.run(beeRow.bee_name, beeRow.version, eventCreatedAt, "llm", beeRow.user_intent_raw ?? null, beeRow.description, beeRow.parent_version ?? null, beeRow.changelog ?? null);
|
|
295
|
+
beeReleaseId = Number(beeInfo.lastInsertRowid);
|
|
296
|
+
// Update the bee_release_pointer row so the latest version is
|
|
297
|
+
// discoverable via list tooling.
|
|
298
|
+
this.db
|
|
299
|
+
.prepare(`INSERT OR REPLACE INTO bee_release_pointer (bee_name, latest_version, released_at) VALUES (?, ?, ?)`)
|
|
300
|
+
.run(beeRow.bee_name, beeRow.version, eventCreatedAt);
|
|
301
|
+
// 3. Insert bee_manifest stub. Mirrors `retainRelease` minimum
|
|
302
|
+
// surface: promotion = 'candidate', requires_human = 0,
|
|
303
|
+
// requires_smoke = 0. Min-cycles left NULL.
|
|
304
|
+
this.db
|
|
305
|
+
.prepare(`INSERT INTO bee_manifest (
|
|
306
|
+
release_id, schema_version, description, segments_json,
|
|
307
|
+
entrypoint_preamble, promotion, min_cycles,
|
|
308
|
+
requires_human, requires_smoke, retire_on_misses
|
|
309
|
+
) VALUES (?, ?, ?, '[]', '', 'candidate', NULL, 0, 0, NULL)`)
|
|
310
|
+
.run(beeReleaseId, "peaks.bee/1", beeRow.description);
|
|
311
|
+
// 4. Insert the loop_bee_relation row (FKs now valid). Re-parse
|
|
312
|
+
// through the input schema so the bee_release_id is validated
|
|
313
|
+
// too — defense in depth. `id` is autoincrement (not in input).
|
|
314
|
+
const fullRelation = LoopBeeRelationSchema.omit({
|
|
315
|
+
id: true,
|
|
316
|
+
created_at: true,
|
|
317
|
+
}).parse({
|
|
318
|
+
loop_release_id: relationInputShape.loop_release_id,
|
|
319
|
+
bee_release_id: beeReleaseId,
|
|
320
|
+
role: relationInputShape.role,
|
|
321
|
+
reason: relationInputShape.reason,
|
|
322
|
+
schema_version: "peaks.loop-bee-relation/1",
|
|
323
|
+
});
|
|
324
|
+
const relationRow = insertLoopBeeRelation(this.db, fullRelation);
|
|
325
|
+
relationId = relationRow.id;
|
|
326
|
+
// 5. Persist the crystallization_event row with the brief inline
|
|
327
|
+
// (the brief is the durable evidence — AC-17).
|
|
328
|
+
const eventInput = {
|
|
329
|
+
trigger: payload.trigger,
|
|
330
|
+
evidence_brief: brief,
|
|
331
|
+
evidence_bullets: payload.evidence_bullets ?? [],
|
|
332
|
+
source_trace_pointers: payload.source_trace_pointers ?? [],
|
|
333
|
+
evaluator_summary: payload.evaluator_summary ?? "",
|
|
334
|
+
user_decision_summary: payload.user_decision_summary ?? "",
|
|
335
|
+
created_loop_release_id: loopReleaseId,
|
|
336
|
+
created_bee_release_id: beeReleaseId,
|
|
337
|
+
// updated_* are not set on a CREATE crystallization; they will
|
|
338
|
+
// be set on future UPDATE crystallizations.
|
|
339
|
+
lifecycle_status: "candidate",
|
|
340
|
+
};
|
|
341
|
+
const persisted = insertCrystallizationEventRaw(this.db, eventInput, eventId, eventCreatedAt);
|
|
342
|
+
eventPersistedId = persisted.id;
|
|
343
|
+
});
|
|
344
|
+
try {
|
|
345
|
+
tx();
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
// Map known SQLite constraint errors to friendlier codes; raise
|
|
349
|
+
// CrystallizationIntegrityError so the CLI can render the JSON
|
|
350
|
+
// envelope.
|
|
351
|
+
if (err instanceof CrystallizationIntegrityError ||
|
|
352
|
+
err instanceof BriefSectionError) {
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
356
|
+
throw new CrystallizationIntegrityError("CRYSTALLIZATION_TX_FAILED", `crystallization transaction failed: ${message}`, [
|
|
357
|
+
{
|
|
358
|
+
path: "tx",
|
|
359
|
+
message: "transaction rolled back; verify loop/bee ids are unique and FK targets exist",
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
// Silence the unused-variable lint; the parsed task is the source
|
|
364
|
+
// of truth at the entry boundary.
|
|
365
|
+
void task;
|
|
366
|
+
return {
|
|
367
|
+
loop_release_id: loopReleaseId,
|
|
368
|
+
bee_release_id: beeReleaseId,
|
|
369
|
+
loop_bee_relation_id: relationId,
|
|
370
|
+
crystallization_event_id: eventPersistedId,
|
|
371
|
+
loop_release_lifecycle_status: "candidate",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/* ---------------------------------------------------------------------- */
|
|
376
|
+
/* Internal: thin writer that injects the deterministic eventId + */
|
|
377
|
+
/* created_at so the crystallization flow can land all rows in */
|
|
378
|
+
/* one tx before any non-idempotent residue lands. */
|
|
379
|
+
/* ---------------------------------------------------------------------- */
|
|
380
|
+
function insertCrystallizationEventRaw(db, row, id, createdAt) {
|
|
381
|
+
const persisted = CrystallizationEventSchema.parse({
|
|
382
|
+
...row,
|
|
383
|
+
id,
|
|
384
|
+
schema_version: CRYSTALLIZATION_SCHEMA_VERSION,
|
|
385
|
+
created_at: createdAt,
|
|
386
|
+
});
|
|
387
|
+
const stmt = db.prepare(`INSERT INTO crystallization_event (
|
|
388
|
+
id, trigger, evidence_brief_json, evidence_bullets_json,
|
|
389
|
+
source_trace_pointers_json, evaluator_summary,
|
|
390
|
+
user_decision_summary,
|
|
391
|
+
created_loop_release_id, updated_loop_release_id,
|
|
392
|
+
created_bee_release_id, updated_bee_release_id,
|
|
393
|
+
lifecycle_status, schema_version, created_at
|
|
394
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
395
|
+
stmt.run(persisted.id, persisted.trigger, JSON.stringify(persisted.evidence_brief), JSON.stringify(persisted.evidence_bullets), JSON.stringify(persisted.source_trace_pointers), persisted.evaluator_summary, persisted.user_decision_summary, persisted.created_loop_release_id ?? null, persisted.updated_loop_release_id ?? null, persisted.created_bee_release_id ?? null, persisted.updated_bee_release_id ?? null, persisted.lifecycle_status, persisted.schema_version, persisted.created_at);
|
|
396
|
+
return persisted;
|
|
397
|
+
}
|