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.
- package/CHANGELOG.md +81 -0
- 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,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* LoopBeeRelation — spec §4.6.
|
|
4
|
+
*
|
|
5
|
+
* The loop↔bee coupling row. M2 scope:
|
|
6
|
+
* - four roles: main / supporting / candidate / retired.
|
|
7
|
+
* - exactly one main per loop (enforced by partial unique index + service).
|
|
8
|
+
* - FK to loop_release (TEXT id) and bee_release (INTEGER id).
|
|
9
|
+
* - cannot link to a retired loop (enforced in service).
|
|
10
|
+
*
|
|
11
|
+
* Out of scope (deferred to later slices):
|
|
12
|
+
* - share/desktop fields on the relation itself (none in spec §4.6).
|
|
13
|
+
* - evolution evidence references (M4).
|
|
14
|
+
*
|
|
15
|
+
* Field semantics — see the spec for full prose. Highlights:
|
|
16
|
+
*
|
|
17
|
+
* - `id` is the autoincrement row id; storage key.
|
|
18
|
+
* - `loop_release_id` is the FK to loop_release.id (TEXT).
|
|
19
|
+
* - `bee_release_id` is the FK to bee_release.id (INTEGER). Note that
|
|
20
|
+
* `bee_release.id` is an autoincrement INTEGER (not bee_name) — this
|
|
21
|
+
* matches the existing 4.x schema convention used by bee_manifest,
|
|
22
|
+
* bee_segment_ref, bee_file, bee_change.
|
|
23
|
+
* - `role` is the four-value union. A loop can have at most one `main`
|
|
24
|
+
* row; multiple supporting / candidate / retired rows are allowed.
|
|
25
|
+
* - `reason` is an NL string the LLM authors at crystallization time
|
|
26
|
+
* (e.g. "primary bee — implements the loop's success_criteria").
|
|
27
|
+
* - `created_at` is server-stamped from the system clock; the input
|
|
28
|
+
* schema does not let clients supply it (would be a backdate hole).
|
|
29
|
+
*/
|
|
30
|
+
export const LoopBeeRelationRoleSchema = z.enum([
|
|
31
|
+
"main",
|
|
32
|
+
"supporting",
|
|
33
|
+
"candidate",
|
|
34
|
+
"retired",
|
|
35
|
+
]);
|
|
36
|
+
export const LOOP_BEE_RELATION_ROLES = [
|
|
37
|
+
"main",
|
|
38
|
+
"supporting",
|
|
39
|
+
"candidate",
|
|
40
|
+
"retired",
|
|
41
|
+
];
|
|
42
|
+
/**
|
|
43
|
+
* Zod schema for the create payload (LoopBeeRelationInput).
|
|
44
|
+
*
|
|
45
|
+
* `loop_release_id` must match the same kebab-case pattern as
|
|
46
|
+
* `LoopRelease.id` — this is a defensive duplicate of the FK target's
|
|
47
|
+
* shape, but the authoritative check is the FK constraint at insert
|
|
48
|
+
* time (better-sqlite3 with `foreign_keys = ON`).
|
|
49
|
+
*
|
|
50
|
+
* `bee_release_id` is a positive integer; matching the
|
|
51
|
+
* `bee_release.id` autoincrement INTEGER PK.
|
|
52
|
+
*/
|
|
53
|
+
export const LoopBeeRelationInputSchema = z.object({
|
|
54
|
+
loop_release_id: z
|
|
55
|
+
.string()
|
|
56
|
+
.min(1)
|
|
57
|
+
.max(64)
|
|
58
|
+
.regex(/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/, {
|
|
59
|
+
message: "loop_release_id must be kebab-case starting with a lowercase letter",
|
|
60
|
+
}),
|
|
61
|
+
bee_release_id: z
|
|
62
|
+
.number()
|
|
63
|
+
.int()
|
|
64
|
+
.positive()
|
|
65
|
+
.max(2 ** 31 - 1, "bee_release_id out of 32-bit range"),
|
|
66
|
+
role: LoopBeeRelationRoleSchema,
|
|
67
|
+
reason: z
|
|
68
|
+
.string()
|
|
69
|
+
.trim()
|
|
70
|
+
.min(1, "reason is required (LLM-authored NL explanation)")
|
|
71
|
+
.max(2000),
|
|
72
|
+
});
|
|
73
|
+
/**
|
|
74
|
+
* Full Zod schema for a LoopBeeRelation row as persisted. Differs from
|
|
75
|
+
* the input schema only in that `schema_version` is exposed (and
|
|
76
|
+
* defaulted to the fixed literal). The literal constraint still rejects
|
|
77
|
+
* any client-supplied value other than `"peaks.loop-bee-relation/1"`;
|
|
78
|
+
* the default means omitting the key is fine and the output always
|
|
79
|
+
* carries the constant.
|
|
80
|
+
*
|
|
81
|
+
* We expose this as the canonical "row schema" used by service.create /
|
|
82
|
+
* read paths.
|
|
83
|
+
*/
|
|
84
|
+
export const LoopBeeRelationSchema = LoopBeeRelationInputSchema.extend({
|
|
85
|
+
id: z.number().int().positive(),
|
|
86
|
+
schema_version: z
|
|
87
|
+
.literal("peaks.loop-bee-relation/1")
|
|
88
|
+
.default("peaks.loop-bee-relation/1"),
|
|
89
|
+
created_at: z.string().datetime(),
|
|
90
|
+
});
|
|
91
|
+
/**
|
|
92
|
+
* Convenience: strict-parse an unknown payload into a LoopBeeRelation
|
|
93
|
+
* row. Throws ZodError on failure. This is the public validation
|
|
94
|
+
* boundary.
|
|
95
|
+
*/
|
|
96
|
+
export function parseLoopBeeRelation(input) {
|
|
97
|
+
return LoopBeeRelationSchema.parse(input);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Convenience: safe-parse that returns a Result-like shape so callers
|
|
101
|
+
* can render findings without try/catch noise.
|
|
102
|
+
*/
|
|
103
|
+
export function safeParseLoopBeeRelation(input) {
|
|
104
|
+
const r = LoopBeeRelationSchema.safeParse(input);
|
|
105
|
+
if (r.success)
|
|
106
|
+
return { ok: true, row: r.data };
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
findings: r.error.issues.map((i) => ({
|
|
110
|
+
path: i.path.join("."),
|
|
111
|
+
message: i.message,
|
|
112
|
+
})),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { type LoopRelease, type LoopReleaseInput, type LoopReleaseLifecycleStatus } from "./loop-release-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Thin service wrapper around the loop_release store. M1 keeps the
|
|
5
|
+
* surface tiny on purpose: create / read / list / search. Promotion,
|
|
6
|
+
* retirement, evaluation, and the `loop_bee_relation` joins are
|
|
7
|
+
* deliberately deferred to M2 / M4 / M5 — shipping them here would
|
|
8
|
+
* lock in an interface that the ratchet has not yet ratified.
|
|
9
|
+
*
|
|
10
|
+
* The constructor takes an open better-sqlite3 database (the existing
|
|
11
|
+
* `peaks state` boundary) so callers do not have to thread a path
|
|
12
|
+
* through this layer; this matches the `retainRelease` pattern in
|
|
13
|
+
* `src/services/skillhub/release-retain.ts`.
|
|
14
|
+
*
|
|
15
|
+
* All write paths re-validate the input via the Zod schema, mirroring
|
|
16
|
+
* the boundary-check discipline used elsewhere (e.g.
|
|
17
|
+
* `lintManifestStrict` in `src/services/sediment/manifest-lint.ts`).
|
|
18
|
+
*/
|
|
19
|
+
export declare class LoopReleaseService {
|
|
20
|
+
private readonly db;
|
|
21
|
+
constructor(db: Database.Database);
|
|
22
|
+
/**
|
|
23
|
+
* Persist a new LoopRelease row. Re-validates the input via the
|
|
24
|
+
* Zod schema; throws ZodError on failure.
|
|
25
|
+
*/
|
|
26
|
+
create(input: LoopReleaseInput): LoopRelease;
|
|
27
|
+
/** Read a LoopRelease row by id; returns undefined if absent. */
|
|
28
|
+
read(id: string): LoopRelease | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* List LoopRelease rows. Filter by `status` when supplied; without
|
|
31
|
+
* it, returns ALL rows (across `candidate` / `stable` / `retired`).
|
|
32
|
+
* Sort is newest-first by `archived_at`.
|
|
33
|
+
*/
|
|
34
|
+
list(opts?: {
|
|
35
|
+
status?: LoopReleaseLifecycleStatus;
|
|
36
|
+
}): LoopRelease[];
|
|
37
|
+
/**
|
|
38
|
+
* Search LoopRelease rows whose `scenario` contains the query
|
|
39
|
+
* string (case-insensitive substring match via LIKE). Filter by
|
|
40
|
+
* `status` to scope the search.
|
|
41
|
+
*/
|
|
42
|
+
search(opts: {
|
|
43
|
+
query: string;
|
|
44
|
+
status?: LoopReleaseLifecycleStatus;
|
|
45
|
+
}): LoopRelease[];
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { LoopReleaseSchema, } from "./loop-release-types.js";
|
|
2
|
+
import { ensureLoopReleaseTable, insertLoopRelease, getLoopRelease, listLoopReleasesByStatus, searchLoopReleasesByScenario, } from "./loop-release-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* Thin service wrapper around the loop_release store. M1 keeps the
|
|
5
|
+
* surface tiny on purpose: create / read / list / search. Promotion,
|
|
6
|
+
* retirement, evaluation, and the `loop_bee_relation` joins are
|
|
7
|
+
* deliberately deferred to M2 / M4 / M5 — shipping them here would
|
|
8
|
+
* lock in an interface that the ratchet has not yet ratified.
|
|
9
|
+
*
|
|
10
|
+
* The constructor takes an open better-sqlite3 database (the existing
|
|
11
|
+
* `peaks state` boundary) so callers do not have to thread a path
|
|
12
|
+
* through this layer; this matches the `retainRelease` pattern in
|
|
13
|
+
* `src/services/skillhub/release-retain.ts`.
|
|
14
|
+
*
|
|
15
|
+
* All write paths re-validate the input via the Zod schema, mirroring
|
|
16
|
+
* the boundary-check discipline used elsewhere (e.g.
|
|
17
|
+
* `lintManifestStrict` in `src/services/sediment/manifest-lint.ts`).
|
|
18
|
+
*/
|
|
19
|
+
export class LoopReleaseService {
|
|
20
|
+
db;
|
|
21
|
+
constructor(db) {
|
|
22
|
+
this.db = db;
|
|
23
|
+
// Idempotent — safe to call on every constructor invocation; the
|
|
24
|
+
// openStateDb pipeline already applied the SQL migration, but a
|
|
25
|
+
// caller-built DB (e.g. tests) needs this.
|
|
26
|
+
ensureLoopReleaseTable(this.db);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Persist a new LoopRelease row. Re-validates the input via the
|
|
30
|
+
* Zod schema; throws ZodError on failure.
|
|
31
|
+
*/
|
|
32
|
+
create(input) {
|
|
33
|
+
const row = LoopReleaseSchema.parse(input);
|
|
34
|
+
insertLoopRelease(this.db, row);
|
|
35
|
+
return row;
|
|
36
|
+
}
|
|
37
|
+
/** Read a LoopRelease row by id; returns undefined if absent. */
|
|
38
|
+
read(id) {
|
|
39
|
+
return getLoopRelease(this.db, id);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* List LoopRelease rows. Filter by `status` when supplied; without
|
|
43
|
+
* it, returns ALL rows (across `candidate` / `stable` / `retired`).
|
|
44
|
+
* Sort is newest-first by `archived_at`.
|
|
45
|
+
*/
|
|
46
|
+
list(opts = {}) {
|
|
47
|
+
if (opts.status)
|
|
48
|
+
return listLoopReleasesByStatus(this.db, opts.status);
|
|
49
|
+
const all = ["candidate", "stable", "retired"];
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const s of all)
|
|
52
|
+
out.push(...listLoopReleasesByStatus(this.db, s));
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Search LoopRelease rows whose `scenario` contains the query
|
|
57
|
+
* string (case-insensitive substring match via LIKE). Filter by
|
|
58
|
+
* `status` to scope the search.
|
|
59
|
+
*/
|
|
60
|
+
search(opts) {
|
|
61
|
+
const hits = searchLoopReleasesByScenario(this.db, opts.query);
|
|
62
|
+
if (!opts.status)
|
|
63
|
+
return hits;
|
|
64
|
+
return hits.filter((r) => r.lifecycle_status === opts.status);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { LoopRelease, LoopReleaseLifecycleStatus } from "./loop-release-types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Low-level SQLite access for the `loop_release` table. The
|
|
5
|
+
* migration is registered with the existing `openStateDb()` pipeline
|
|
6
|
+
* via `src/services/skillhub/migrations/002-loop-release.sql` and
|
|
7
|
+
* `004-loop-bee-extension.sql` (it applies every `*.sql` file in
|
|
8
|
+
* lexicographic order). The function `ensureLoopReleaseTable` below
|
|
9
|
+
* is a belt-and-suspenders re-applier for callers that pass a
|
|
10
|
+
* database they built themselves (e.g. tests).
|
|
11
|
+
*
|
|
12
|
+
* Defense in depth:
|
|
13
|
+
* - All JSON columns are stored as TEXT and parsed at the boundary.
|
|
14
|
+
* No big JSON BLOB at the SQLite layer — the row stays small and
|
|
15
|
+
* queryable.
|
|
16
|
+
* - The migration is idempotent (CREATE TABLE IF NOT EXISTS, CREATE
|
|
17
|
+
* INDEX IF NOT EXISTS) so re-running it is safe.
|
|
18
|
+
* - The M3 migration (`004-loop-bee-extension.sql`) is non-breaking:
|
|
19
|
+
* it only adds new columns with DEFAULTs and does NOT touch any
|
|
20
|
+
* pre-existing column on `bee_release` (AC-3).
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Re-apply the loop_release table migration against an already-open
|
|
24
|
+
* database. Used by tests that build their own DB without the openStateDb
|
|
25
|
+
* pipeline. Idempotent.
|
|
26
|
+
*
|
|
27
|
+
* M3: the CREATE here now includes the four share / desktop extension
|
|
28
|
+
* columns from migration 004 (`shareable`, `share_excluded_paths`,
|
|
29
|
+
* `desktop_visible`, `export_bundle_format`). For an existing M2-era
|
|
30
|
+
* database, the migration runner will already have applied 004 and
|
|
31
|
+
* the columns will exist; for a freshly-built DB via this helper the
|
|
32
|
+
* columns are baked in. Either way the schema converges.
|
|
33
|
+
*/
|
|
34
|
+
export declare function ensureLoopReleaseTable(db: Database.Database): void;
|
|
35
|
+
/**
|
|
36
|
+
* Insert a LoopRelease row. The row's `schema_version` is taken from
|
|
37
|
+
* the input (must equal `peaks.loop/1`); `archived_at` is stamped
|
|
38
|
+
* from the server clock here, so callers cannot backdate a row.
|
|
39
|
+
*
|
|
40
|
+
* Idempotent in the table sense (UNIQUE on `id`) — re-inserting the
|
|
41
|
+
* same id throws a UNIQUE-constraint error; callers wanting upsert
|
|
42
|
+
* semantics should use a separate path (M5 / crystallization event).
|
|
43
|
+
*/
|
|
44
|
+
export declare function insertLoopRelease(db: Database.Database, row: LoopRelease): void;
|
|
45
|
+
/** Read a single LoopRelease row by id; returns undefined if absent. */
|
|
46
|
+
export declare function getLoopRelease(db: Database.Database, id: string): LoopRelease | undefined;
|
|
47
|
+
/** List LoopRelease rows filtered by lifecycle_status. */
|
|
48
|
+
export declare function listLoopReleasesByStatus(db: Database.Database, status: LoopReleaseLifecycleStatus): LoopRelease[];
|
|
49
|
+
/**
|
|
50
|
+
* Search LoopRelease rows whose `scenario` contains the query string.
|
|
51
|
+
* M1 uses a case-insensitive LIKE; FTS5 promotion is deferred. The
|
|
52
|
+
* query is treated as a literal substring (no regex, no SQL
|
|
53
|
+
* injection surface) — it is escaped via parameter binding.
|
|
54
|
+
*/
|
|
55
|
+
export declare function searchLoopReleasesByScenario(db: Database.Database, query: string): LoopRelease[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level SQLite access for the `loop_release` table. The
|
|
3
|
+
* migration is registered with the existing `openStateDb()` pipeline
|
|
4
|
+
* via `src/services/skillhub/migrations/002-loop-release.sql` and
|
|
5
|
+
* `004-loop-bee-extension.sql` (it applies every `*.sql` file in
|
|
6
|
+
* lexicographic order). The function `ensureLoopReleaseTable` below
|
|
7
|
+
* 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 M3 migration (`004-loop-bee-extension.sql`) is non-breaking:
|
|
17
|
+
* it only adds new columns with DEFAULTs and does NOT touch any
|
|
18
|
+
* pre-existing column on `bee_release` (AC-3).
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Re-apply the loop_release table migration against an already-open
|
|
22
|
+
* database. Used by tests that build their own DB without the openStateDb
|
|
23
|
+
* pipeline. Idempotent.
|
|
24
|
+
*
|
|
25
|
+
* M3: the CREATE here now includes the four share / desktop extension
|
|
26
|
+
* columns from migration 004 (`shareable`, `share_excluded_paths`,
|
|
27
|
+
* `desktop_visible`, `export_bundle_format`). For an existing M2-era
|
|
28
|
+
* database, the migration runner will already have applied 004 and
|
|
29
|
+
* the columns will exist; for a freshly-built DB via this helper the
|
|
30
|
+
* columns are baked in. Either way the schema converges.
|
|
31
|
+
*/
|
|
32
|
+
export function ensureLoopReleaseTable(db) {
|
|
33
|
+
db.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS loop_release (
|
|
35
|
+
id TEXT PRIMARY KEY,
|
|
36
|
+
name TEXT NOT NULL,
|
|
37
|
+
scenario TEXT NOT NULL,
|
|
38
|
+
trigger_policy TEXT NOT NULL,
|
|
39
|
+
success_criteria_json TEXT NOT NULL,
|
|
40
|
+
interaction_policy TEXT NOT NULL,
|
|
41
|
+
feedback_policy TEXT NOT NULL,
|
|
42
|
+
evolution_policy TEXT NOT NULL,
|
|
43
|
+
evaluator_policy_json TEXT NOT NULL,
|
|
44
|
+
linked_bees_json TEXT NOT NULL DEFAULT '[]',
|
|
45
|
+
run_history_json TEXT NOT NULL DEFAULT '[]',
|
|
46
|
+
crystallization_evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
47
|
+
lifecycle_status TEXT NOT NULL CHECK (lifecycle_status IN ('candidate','stable','retired')),
|
|
48
|
+
version TEXT NOT NULL,
|
|
49
|
+
schema_version TEXT NOT NULL CHECK (schema_version = 'peaks.loop/1'),
|
|
50
|
+
archived_at TEXT NOT NULL,
|
|
51
|
+
shareable INTEGER NOT NULL DEFAULT 1 CHECK (shareable IN (0, 1)),
|
|
52
|
+
share_excluded_paths TEXT NOT NULL DEFAULT '[]',
|
|
53
|
+
desktop_visible INTEGER NOT NULL DEFAULT 1 CHECK (desktop_visible IN (0, 1)),
|
|
54
|
+
export_bundle_format TEXT NOT NULL DEFAULT 'peaks.bundle/1' CHECK (export_bundle_format = 'peaks.bundle/1')
|
|
55
|
+
);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_loop_release_lifecycle_status ON loop_release(lifecycle_status);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_loop_release_scenario ON loop_release(scenario);
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
function rowToLoopRelease(row) {
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
name: row.name,
|
|
64
|
+
scenario: row.scenario,
|
|
65
|
+
trigger_policy: row.trigger_policy,
|
|
66
|
+
success_criteria: JSON.parse(row.success_criteria_json),
|
|
67
|
+
interaction_policy: row.interaction_policy,
|
|
68
|
+
feedback_policy: row.feedback_policy,
|
|
69
|
+
evolution_policy: row.evolution_policy,
|
|
70
|
+
evaluator_policy: JSON.parse(row.evaluator_policy_json),
|
|
71
|
+
linked_bees: JSON.parse(row.linked_bees_json),
|
|
72
|
+
run_history: JSON.parse(row.run_history_json),
|
|
73
|
+
crystallization_evidence: JSON.parse(row.crystallization_evidence_json),
|
|
74
|
+
lifecycle_status: row.lifecycle_status,
|
|
75
|
+
version: row.version,
|
|
76
|
+
schema_version: row.schema_version,
|
|
77
|
+
// M3: coerce the SQLite 0/1 INTEGER convention to JS boolean, and
|
|
78
|
+
// parse the JSON-encoded path list at the boundary.
|
|
79
|
+
shareable: row.shareable === 1,
|
|
80
|
+
share_excluded_paths: JSON.parse(row.share_excluded_paths),
|
|
81
|
+
desktop_visible: row.desktop_visible === 1,
|
|
82
|
+
export_bundle_format: row.export_bundle_format,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Insert a LoopRelease row. The row's `schema_version` is taken from
|
|
87
|
+
* the input (must equal `peaks.loop/1`); `archived_at` is stamped
|
|
88
|
+
* from the server clock here, so callers cannot backdate a row.
|
|
89
|
+
*
|
|
90
|
+
* Idempotent in the table sense (UNIQUE on `id`) — re-inserting the
|
|
91
|
+
* same id throws a UNIQUE-constraint error; callers wanting upsert
|
|
92
|
+
* semantics should use a separate path (M5 / crystallization event).
|
|
93
|
+
*/
|
|
94
|
+
export function insertLoopRelease(db, row) {
|
|
95
|
+
const stmt = db.prepare(`INSERT INTO loop_release (
|
|
96
|
+
id, name, scenario, trigger_policy,
|
|
97
|
+
success_criteria_json, interaction_policy, feedback_policy, evolution_policy,
|
|
98
|
+
evaluator_policy_json, linked_bees_json, run_history_json, crystallization_evidence_json,
|
|
99
|
+
lifecycle_status, version, schema_version, archived_at,
|
|
100
|
+
shareable, share_excluded_paths, desktop_visible, export_bundle_format
|
|
101
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
102
|
+
stmt.run(row.id, row.name, row.scenario, row.trigger_policy, JSON.stringify(row.success_criteria), row.interaction_policy, row.feedback_policy, row.evolution_policy, JSON.stringify(row.evaluator_policy), JSON.stringify(row.linked_bees), JSON.stringify(row.run_history), JSON.stringify(row.crystallization_evidence), row.lifecycle_status, row.version, row.schema_version, new Date().toISOString(),
|
|
103
|
+
// M3: serialize the booleans to the SQLite 0/1 convention.
|
|
104
|
+
row.shareable ? 1 : 0, JSON.stringify(row.share_excluded_paths), row.desktop_visible ? 1 : 0, row.export_bundle_format);
|
|
105
|
+
}
|
|
106
|
+
/** Read a single LoopRelease row by id; returns undefined if absent. */
|
|
107
|
+
export function getLoopRelease(db, id) {
|
|
108
|
+
const row = db
|
|
109
|
+
.prepare("SELECT * FROM loop_release WHERE id = ?")
|
|
110
|
+
.get(id);
|
|
111
|
+
if (!row)
|
|
112
|
+
return undefined;
|
|
113
|
+
return rowToLoopRelease(row);
|
|
114
|
+
}
|
|
115
|
+
/** List LoopRelease rows filtered by lifecycle_status. */
|
|
116
|
+
export function listLoopReleasesByStatus(db, status) {
|
|
117
|
+
const rows = db
|
|
118
|
+
.prepare("SELECT * FROM loop_release WHERE lifecycle_status = ? ORDER BY archived_at DESC, id ASC")
|
|
119
|
+
.all(status);
|
|
120
|
+
return rows.map(rowToLoopRelease);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Search LoopRelease rows whose `scenario` contains the query string.
|
|
124
|
+
* M1 uses a case-insensitive LIKE; FTS5 promotion is deferred. The
|
|
125
|
+
* query is treated as a literal substring (no regex, no SQL
|
|
126
|
+
* injection surface) — it is escaped via parameter binding.
|
|
127
|
+
*/
|
|
128
|
+
export function searchLoopReleasesByScenario(db, query) {
|
|
129
|
+
const like = `%${query}%`;
|
|
130
|
+
const rows = db
|
|
131
|
+
.prepare("SELECT * FROM loop_release WHERE scenario LIKE ? COLLATE NOCASE ORDER BY archived_at DESC, id ASC")
|
|
132
|
+
.all(like);
|
|
133
|
+
return rows.map(rowToLoopRelease);
|
|
134
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* LoopRelease — spec §4.1 (Loop Engineering Asset).
|
|
4
|
+
*
|
|
5
|
+
* M1 scope was the §4.1 fields. M3 (this slice) ADDS the share /
|
|
6
|
+
* desktop extension fields from §4.1 / §7A:
|
|
7
|
+
* - shareable (boolean, default true)
|
|
8
|
+
* - share_excluded_paths (string[], default [])
|
|
9
|
+
* - desktop_visible (boolean, default true)
|
|
10
|
+
* - export_bundle_format ('peaks.bundle/1', CLI-written constant)
|
|
11
|
+
*
|
|
12
|
+
* The four M3 fields are added with `.default(...)` so an in-memory
|
|
13
|
+
* object built from a pre-M3 snapshot still parses after the
|
|
14
|
+
* migration runs. NO enforcement on `shareable=false` is added in M3
|
|
15
|
+
* — the schema-only addition is the contract; full export /
|
|
16
|
+
* desktop enforcement lands in M7 (see plan
|
|
17
|
+
* m3-bee-release-extension.md and spec §7A).
|
|
18
|
+
*
|
|
19
|
+
* Field semantics — see the spec for full prose. Highlights:
|
|
20
|
+
*
|
|
21
|
+
* - `id` is the stable kebab-case loop id; the storage key.
|
|
22
|
+
* - `schema_version` is the constant literal `"peaks.loop/1"`; a
|
|
23
|
+
* schema-version bump requires a new table / new enum case and a
|
|
24
|
+
* migration — never edit this literal in place.
|
|
25
|
+
* - `lifecycle_status` is `candidate` / `stable` / `retired`; the
|
|
26
|
+
* default on first crystallization is `candidate`. LLMs cannot
|
|
27
|
+
* auto-promote to `stable` (see spec §5.6).
|
|
28
|
+
* - `linked_bees` is a denormalized pointer list of bee ids; the
|
|
29
|
+
* authoritative loop↔bee relations live in `loop_bee_relation`
|
|
30
|
+
* (M2). Keeping it here matches the spec §4.1 row shape and lets
|
|
31
|
+
* M1 readers iterate the loop without an extra join.
|
|
32
|
+
* - `success_criteria` / `evaluator_policy` are NL strings, not
|
|
33
|
+
* machine-checked predicates; the evaluator definitions live in
|
|
34
|
+
* the M4 evolution layer.
|
|
35
|
+
* - `share_excluded_paths` is a JSON-encoded TEXT column at the
|
|
36
|
+
* SQLite layer; the Zod schema accepts a string[] and the store
|
|
37
|
+
* does the JSON.stringify/parse boundary work. Matches the
|
|
38
|
+
* existing `loop_release` style (success_criteria_json, …).
|
|
39
|
+
* - `export_bundle_format` is pinned to the literal
|
|
40
|
+
* `"peaks.bundle/1"` per the spec §7A.2 contract — no other
|
|
41
|
+
* value is accepted in this schema version.
|
|
42
|
+
*/
|
|
43
|
+
export declare const LoopReleaseLifecycleStatusSchema: z.ZodEnum<["candidate", "stable", "retired"]>;
|
|
44
|
+
export type LoopReleaseLifecycleStatus = z.infer<typeof LoopReleaseLifecycleStatusSchema>;
|
|
45
|
+
/**
|
|
46
|
+
* The M3 share / desktop extension fields, listed for code-search /
|
|
47
|
+
* lint discoverability. They are now part of the schema (with safe
|
|
48
|
+
* defaults) — see `LoopReleaseM3ExtensionSchema` below.
|
|
49
|
+
*
|
|
50
|
+
* - shareable: defaults to true.
|
|
51
|
+
* - share_excluded_paths: string[] (defaults to []).
|
|
52
|
+
* - desktop_visible: defaults to true.
|
|
53
|
+
* - export_bundle_format: constant "peaks.bundle/1" (CLI-written).
|
|
54
|
+
*/
|
|
55
|
+
export declare const LOOP_RELEASE_M3_FIELDS: readonly ["shareable", "share_excluded_paths", "desktop_visible", "export_bundle_format"];
|
|
56
|
+
/**
|
|
57
|
+
* M3 share / desktop extension schema. Added to `LoopReleaseInputSchema`
|
|
58
|
+
* via `.extend(...)` so existing callers that omit these fields still
|
|
59
|
+
* parse — the Zod defaults below supply the spec values
|
|
60
|
+
* (true / [] / true / "peaks.bundle/1").
|
|
61
|
+
*
|
|
62
|
+
* `export_bundle_format` is pinned to a literal per spec §7A.2: the
|
|
63
|
+
* CLI is the only writer and only `"peaks.bundle/1"` is accepted. A
|
|
64
|
+
* schema-version bump is required to introduce another value.
|
|
65
|
+
*/
|
|
66
|
+
export declare const LoopReleaseM3ExtensionSchema: z.ZodObject<{
|
|
67
|
+
shareable: z.ZodDefault<z.ZodBoolean>;
|
|
68
|
+
share_excluded_paths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
69
|
+
desktop_visible: z.ZodDefault<z.ZodBoolean>;
|
|
70
|
+
export_bundle_format: z.ZodDefault<z.ZodLiteral<"peaks.bundle/1">>;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
export_bundle_format: "peaks.bundle/1";
|
|
73
|
+
shareable: boolean;
|
|
74
|
+
share_excluded_paths: string[];
|
|
75
|
+
desktop_visible: boolean;
|
|
76
|
+
}, {
|
|
77
|
+
export_bundle_format?: "peaks.bundle/1" | undefined;
|
|
78
|
+
shareable?: boolean | undefined;
|
|
79
|
+
share_excluded_paths?: string[] | undefined;
|
|
80
|
+
desktop_visible?: boolean | undefined;
|
|
81
|
+
}>;
|
|
82
|
+
/** Convenience type for the M3 share / desktop subset. */
|
|
83
|
+
export type LoopReleaseM3Extension = z.infer<typeof LoopReleaseM3ExtensionSchema>;
|
|
84
|
+
/**
|
|
85
|
+
* Zod schema for the create payload (LoopReleaseInput). Mirrors the
|
|
86
|
+
* §4.1 row shape; `schema_version` is defaulted (clients cannot
|
|
87
|
+
* override it) and `created_at` is not part of the input — the store
|
|
88
|
+
* stamps it from the server clock.
|
|
89
|
+
*/
|
|
90
|
+
export declare const LoopReleaseInputSchema: z.ZodObject<{
|
|
91
|
+
id: z.ZodString;
|
|
92
|
+
name: z.ZodString;
|
|
93
|
+
scenario: z.ZodString;
|
|
94
|
+
trigger_policy: z.ZodString;
|
|
95
|
+
success_criteria: z.ZodArray<z.ZodString, "many">;
|
|
96
|
+
interaction_policy: z.ZodString;
|
|
97
|
+
feedback_policy: z.ZodString;
|
|
98
|
+
evolution_policy: z.ZodString;
|
|
99
|
+
evaluator_policy: z.ZodArray<z.ZodString, "many">;
|
|
100
|
+
linked_bees: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
101
|
+
run_history: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
102
|
+
crystallization_evidence: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
103
|
+
lifecycle_status: z.ZodEnum<["candidate", "stable", "retired"]>;
|
|
104
|
+
version: z.ZodString;
|
|
105
|
+
} & {
|
|
106
|
+
shareable: z.ZodDefault<z.ZodBoolean>;
|
|
107
|
+
share_excluded_paths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
108
|
+
desktop_visible: z.ZodDefault<z.ZodBoolean>;
|
|
109
|
+
export_bundle_format: z.ZodDefault<z.ZodLiteral<"peaks.bundle/1">>;
|
|
110
|
+
}, "strip", z.ZodTypeAny, {
|
|
111
|
+
version: string;
|
|
112
|
+
name: string;
|
|
113
|
+
id: string;
|
|
114
|
+
scenario: string;
|
|
115
|
+
trigger_policy: string;
|
|
116
|
+
interaction_policy: string;
|
|
117
|
+
feedback_policy: string;
|
|
118
|
+
evolution_policy: string;
|
|
119
|
+
lifecycle_status: "candidate" | "stable" | "retired";
|
|
120
|
+
export_bundle_format: "peaks.bundle/1";
|
|
121
|
+
success_criteria: string[];
|
|
122
|
+
evaluator_policy: string[];
|
|
123
|
+
linked_bees: string[];
|
|
124
|
+
run_history: string[];
|
|
125
|
+
crystallization_evidence: string[];
|
|
126
|
+
shareable: boolean;
|
|
127
|
+
share_excluded_paths: string[];
|
|
128
|
+
desktop_visible: boolean;
|
|
129
|
+
}, {
|
|
130
|
+
version: string;
|
|
131
|
+
name: string;
|
|
132
|
+
id: string;
|
|
133
|
+
scenario: string;
|
|
134
|
+
trigger_policy: string;
|
|
135
|
+
interaction_policy: string;
|
|
136
|
+
feedback_policy: string;
|
|
137
|
+
evolution_policy: string;
|
|
138
|
+
lifecycle_status: "candidate" | "stable" | "retired";
|
|
139
|
+
success_criteria: string[];
|
|
140
|
+
evaluator_policy: string[];
|
|
141
|
+
export_bundle_format?: "peaks.bundle/1" | undefined;
|
|
142
|
+
linked_bees?: string[] | undefined;
|
|
143
|
+
run_history?: string[] | undefined;
|
|
144
|
+
crystallization_evidence?: string[] | undefined;
|
|
145
|
+
shareable?: boolean | undefined;
|
|
146
|
+
share_excluded_paths?: string[] | undefined;
|
|
147
|
+
desktop_visible?: boolean | undefined;
|
|
148
|
+
}>;
|
|
149
|
+
/**
|
|
150
|
+
* `LoopReleaseInput` is the *input* shape clients supply: defaulted
|
|
151
|
+
* fields (linked_bees / run_history / crystallization_evidence) are
|
|
152
|
+
* optional here. We use `z.input` (not `z.infer`) so the inferred
|
|
153
|
+
* type matches the create-call signature; the parsed output type —
|
|
154
|
+
* with all defaulted fields filled — is `LoopRelease`.
|
|
155
|
+
*/
|
|
156
|
+
export type LoopReleaseInput = z.input<typeof LoopReleaseInputSchema>;
|
|
157
|
+
/**
|
|
158
|
+
* Full Zod schema for a LoopRelease row as persisted. Differs from the
|
|
159
|
+
* input schema only in that `schema_version` is exposed (and defaulted
|
|
160
|
+
* to the fixed literal). The literal constraint still rejects any
|
|
161
|
+
* client-supplied value other than `"peaks.loop/1"`; the default
|
|
162
|
+
* means omitting the key is fine and the output always carries the
|
|
163
|
+
* constant.
|
|
164
|
+
*
|
|
165
|
+
* We expose this as the canonical "row schema" used by lint and
|
|
166
|
+
* service.create/read paths.
|
|
167
|
+
*/
|
|
168
|
+
export declare const LoopReleaseSchema: z.ZodObject<{
|
|
169
|
+
id: z.ZodString;
|
|
170
|
+
name: z.ZodString;
|
|
171
|
+
scenario: z.ZodString;
|
|
172
|
+
trigger_policy: z.ZodString;
|
|
173
|
+
success_criteria: z.ZodArray<z.ZodString, "many">;
|
|
174
|
+
interaction_policy: z.ZodString;
|
|
175
|
+
feedback_policy: z.ZodString;
|
|
176
|
+
evolution_policy: z.ZodString;
|
|
177
|
+
evaluator_policy: z.ZodArray<z.ZodString, "many">;
|
|
178
|
+
linked_bees: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
179
|
+
run_history: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
180
|
+
crystallization_evidence: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
181
|
+
lifecycle_status: z.ZodEnum<["candidate", "stable", "retired"]>;
|
|
182
|
+
version: z.ZodString;
|
|
183
|
+
} & {
|
|
184
|
+
shareable: z.ZodDefault<z.ZodBoolean>;
|
|
185
|
+
share_excluded_paths: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
186
|
+
desktop_visible: z.ZodDefault<z.ZodBoolean>;
|
|
187
|
+
export_bundle_format: z.ZodDefault<z.ZodLiteral<"peaks.bundle/1">>;
|
|
188
|
+
} & {
|
|
189
|
+
schema_version: z.ZodDefault<z.ZodLiteral<"peaks.loop/1">>;
|
|
190
|
+
}, "strip", z.ZodTypeAny, {
|
|
191
|
+
schema_version: "peaks.loop/1";
|
|
192
|
+
version: string;
|
|
193
|
+
name: string;
|
|
194
|
+
id: string;
|
|
195
|
+
scenario: string;
|
|
196
|
+
trigger_policy: string;
|
|
197
|
+
interaction_policy: string;
|
|
198
|
+
feedback_policy: string;
|
|
199
|
+
evolution_policy: string;
|
|
200
|
+
lifecycle_status: "candidate" | "stable" | "retired";
|
|
201
|
+
export_bundle_format: "peaks.bundle/1";
|
|
202
|
+
success_criteria: string[];
|
|
203
|
+
evaluator_policy: string[];
|
|
204
|
+
linked_bees: string[];
|
|
205
|
+
run_history: string[];
|
|
206
|
+
crystallization_evidence: string[];
|
|
207
|
+
shareable: boolean;
|
|
208
|
+
share_excluded_paths: string[];
|
|
209
|
+
desktop_visible: boolean;
|
|
210
|
+
}, {
|
|
211
|
+
version: string;
|
|
212
|
+
name: string;
|
|
213
|
+
id: string;
|
|
214
|
+
scenario: string;
|
|
215
|
+
trigger_policy: string;
|
|
216
|
+
interaction_policy: string;
|
|
217
|
+
feedback_policy: string;
|
|
218
|
+
evolution_policy: string;
|
|
219
|
+
lifecycle_status: "candidate" | "stable" | "retired";
|
|
220
|
+
success_criteria: string[];
|
|
221
|
+
evaluator_policy: string[];
|
|
222
|
+
schema_version?: "peaks.loop/1" | undefined;
|
|
223
|
+
export_bundle_format?: "peaks.bundle/1" | undefined;
|
|
224
|
+
linked_bees?: string[] | undefined;
|
|
225
|
+
run_history?: string[] | undefined;
|
|
226
|
+
crystallization_evidence?: string[] | undefined;
|
|
227
|
+
shareable?: boolean | undefined;
|
|
228
|
+
share_excluded_paths?: string[] | undefined;
|
|
229
|
+
desktop_visible?: boolean | undefined;
|
|
230
|
+
}>;
|
|
231
|
+
export type LoopRelease = z.infer<typeof LoopReleaseSchema>;
|
|
232
|
+
/**
|
|
233
|
+
* Convenience: strict-parse an unknown payload into a LoopRelease row.
|
|
234
|
+
* Throws ZodError on failure. This is the public validation boundary.
|
|
235
|
+
*/
|
|
236
|
+
export declare function parseLoopRelease(input: unknown): LoopRelease;
|
|
237
|
+
/**
|
|
238
|
+
* Convenience: safe-parse that returns a Result-like shape so callers
|
|
239
|
+
* (CLI layer in M5) can render findings without try/catch noise.
|
|
240
|
+
*/
|
|
241
|
+
export declare function safeParseLoopRelease(input: unknown): {
|
|
242
|
+
ok: true;
|
|
243
|
+
row: LoopRelease;
|
|
244
|
+
} | {
|
|
245
|
+
ok: false;
|
|
246
|
+
findings: Array<{
|
|
247
|
+
path: string;
|
|
248
|
+
message: string;
|
|
249
|
+
}>;
|
|
250
|
+
};
|