peaks-loop 4.0.0-beta.2 → 4.0.0-beta.4

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 (72) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README-en.md +16 -3
  3. package/README.md +16 -3
  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/evolution-commands.d.ts +27 -0
  9. package/dist/src/cli/commands/evolution-commands.js +393 -0
  10. package/dist/src/cli/commands/loop-commands.d.ts +19 -13
  11. package/dist/src/cli/commands/loop-commands.js +147 -13
  12. package/dist/src/cli/commands/sediment-commands.js +9 -0
  13. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.d.ts +67 -0
  14. package/dist/src/cli/commands/skill-loop-engineering-readiness-commands.js +173 -0
  15. package/dist/src/cli/program.js +17 -0
  16. package/dist/src/services/crystallization/crystallization-service.d.ts +239 -0
  17. package/dist/src/services/crystallization/crystallization-service.js +397 -0
  18. package/dist/src/services/crystallization/crystallization-store.d.ts +75 -0
  19. package/dist/src/services/crystallization/crystallization-store.js +193 -0
  20. package/dist/src/services/crystallization/crystallization-types.d.ts +313 -0
  21. package/dist/src/services/crystallization/crystallization-types.js +299 -0
  22. package/dist/src/services/crystallization/evidence-brief-builder.d.ts +237 -0
  23. package/dist/src/services/crystallization/evidence-brief-builder.js +240 -0
  24. package/dist/src/services/evolution/evolution-service.d.ts +137 -0
  25. package/dist/src/services/evolution/evolution-service.js +281 -0
  26. package/dist/src/services/evolution/evolution-store.d.ts +57 -0
  27. package/dist/src/services/evolution/evolution-store.js +232 -0
  28. package/dist/src/services/evolution/evolution-types.d.ts +706 -0
  29. package/dist/src/services/evolution/evolution-types.js +286 -0
  30. package/dist/src/services/evolution/independent-evaluator-runner.d.ts +90 -0
  31. package/dist/src/services/evolution/independent-evaluator-runner.js +106 -0
  32. package/dist/src/services/evolution/regression-skeptic-runner.d.ts +64 -0
  33. package/dist/src/services/evolution/regression-skeptic-runner.js +113 -0
  34. package/dist/src/services/loop/loop-bee-relation-service.d.ts +107 -0
  35. package/dist/src/services/loop/loop-bee-relation-service.js +211 -0
  36. package/dist/src/services/loop/loop-bee-relation-store.d.ts +53 -0
  37. package/dist/src/services/loop/loop-bee-relation-store.js +130 -0
  38. package/dist/src/services/loop/loop-bee-relation-types.d.ts +118 -0
  39. package/dist/src/services/loop/loop-bee-relation-types.js +114 -0
  40. package/dist/src/services/loop/loop-release-service.d.ts +46 -0
  41. package/dist/src/services/loop/loop-release-service.js +66 -0
  42. package/dist/src/services/loop/loop-release-store.d.ts +55 -0
  43. package/dist/src/services/loop/loop-release-store.js +134 -0
  44. package/dist/src/services/loop/loop-release-types.d.ts +250 -0
  45. package/dist/src/services/loop/loop-release-types.js +162 -0
  46. package/dist/src/services/share/bundle-reader.d.ts +100 -0
  47. package/dist/src/services/share/bundle-reader.js +434 -0
  48. package/dist/src/services/share/bundle-types.d.ts +392 -0
  49. package/dist/src/services/share/bundle-types.js +318 -0
  50. package/dist/src/services/share/bundle-writer.d.ts +97 -0
  51. package/dist/src/services/share/bundle-writer.js +429 -0
  52. package/dist/src/services/share/run-state-contract.d.ts +133 -0
  53. package/dist/src/services/share/run-state-contract.js +138 -0
  54. package/dist/src/services/skillhub/release-import.js +7 -2
  55. package/dist/src/services/skillhub/release-retain.js +6 -1
  56. package/dist/src/services/skillhub/sqlite-store.js +17 -1
  57. package/dist/src/services/skillhub/types.d.ts +10 -0
  58. package/dist/src/services/standards/loop-engineering-lint.d.ts +38 -0
  59. package/dist/src/services/standards/loop-engineering-lint.js +101 -0
  60. package/dist/src/services/standards/loop-engineering-readiness-lint.d.ts +111 -0
  61. package/dist/src/services/standards/loop-engineering-readiness-lint.js +248 -0
  62. package/dist/src/shared/version.d.ts +1 -1
  63. package/dist/src/shared/version.js +1 -1
  64. package/examples/video-demo/README.md +57 -0
  65. package/examples/video-demo/out/en-closing-960.png +0 -0
  66. package/examples/video-demo/out/zh-closing-960.png +0 -0
  67. package/examples/video-demo/preview/peaks-loop-demo-en.mp4 +0 -0
  68. package/examples/video-demo/preview/peaks-loop-demo.mp4 +0 -0
  69. package/package.json +6 -2
  70. package/skills/peaks-code/SKILL.md +13 -0
  71. package/skills/peaks-content/SKILL.md +272 -0
  72. 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
+ }