@stcrft/statecraft-core 1.1.2

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.md ADDED
@@ -0,0 +1,25 @@
1
+ # @stcrft/statecraft-core
2
+
3
+ Parser, AST, validation, and summarizer for the Statecraft board DSL. No CLI or UI—consumed by `@stcrft/statecraft` (CLI) and `@stcrft/statecraft-renderer`.
4
+
5
+ ## Flow
6
+
7
+ 1. **Parser** (`parser.ts`) — YAML string or file path → typed **Board** AST. Throws `ParseError` on invalid YAML or missing/wrong required fields.
8
+ 2. **AST** (`ast.ts`) — Types: `Board`, `Column`, `Task`. Columns are normalized to `{ name, limit? }`; `depends_on` to `string[]`.
9
+ 3. **Validation** (`validation.ts`) — `validate(board)` returns `{ valid, errors }`. Enforces canonical columns (Backlog, Ready, In Progress, Done), task status in columns, `depends_on` refs, WIP limits. Does not throw.
10
+ 4. **Summarize** (`summarize.ts`) — `summarize(board)` returns a plain-text summary (board name, column counts, task list, blocked section).
11
+
12
+ ## API
13
+
14
+ - `parseBoard(input: string): Board` — Parse from file path or raw YAML (heuristic: path if no newline and ends with `.yaml`/`.yml` or contains path sep).
15
+ - `parseBoardFromString(content: string): Board` — Parse raw YAML only.
16
+ - `validate(board: Board): ValidationResult` — Returns `{ valid, errors }`; use for CLI/UI.
17
+ - `summarize(board: Board): string` — Text summary for terminal or paste.
18
+
19
+ See `docs/spec.md` in the repo root for the board format.
20
+
21
+ ## Tests
22
+
23
+ ```bash
24
+ pnpm --filter @stcrft/statecraft-core test
25
+ ```
@@ -0,0 +1,96 @@
1
+ /**
2
+ * AST types for a parsed Statecraft board.
3
+ * Parser (YAML → AST) produces these; validation and tools consume them.
4
+ * @see docs/spec.md
5
+ */
6
+ /** Column: normalized shape. Parser converts string columns to `{ name }`. Optional `limit` is WIP cap (e.g. In Progress). */
7
+ interface Column {
8
+ name: string;
9
+ limit?: number;
10
+ }
11
+ /** Task: required `title` and `status`; optional fields per spec. `depends_on` normalized to array by parser. */
12
+ interface Task {
13
+ title: string;
14
+ status: string;
15
+ description?: string;
16
+ spec?: string;
17
+ owner?: string;
18
+ priority?: string;
19
+ /** Task ids this task depends on (normalized to array by parser). */
20
+ depends_on?: string[];
21
+ }
22
+ /** Board: one board per file. `columns` in order; `tasks` keyed by task id. */
23
+ interface Board {
24
+ board: string;
25
+ columns: Column[];
26
+ tasks: Record<string, Task>;
27
+ }
28
+
29
+ /** Thrown when YAML is invalid or required fields are missing/wrong. */
30
+ declare class ParseError extends Error {
31
+ constructor(message: string);
32
+ }
33
+ /**
34
+ * Parse YAML board content into a typed Board AST.
35
+ * @param content - Raw YAML string (board file content).
36
+ * @returns Board AST.
37
+ * @throws ParseError when YAML is invalid or required fields are missing/wrong.
38
+ */
39
+ declare function parseBoardFromString(content: string): Board;
40
+ /**
41
+ * Parse a board from a file path or raw YAML string.
42
+ * - If `input` looks like a path (no newline, ends with .yaml/.yml or contains path separator),
43
+ * the file is read from disk (relative to cwd) and parsed.
44
+ * - Otherwise `input` is parsed as raw YAML content.
45
+ *
46
+ * @param input - File path (absolute or relative to cwd) or raw YAML string.
47
+ * @returns Board AST.
48
+ * @throws ParseError when YAML is invalid or required fields are missing/wrong.
49
+ * @throws ParseError when input is a path and the file cannot be read (e.g. not found).
50
+ */
51
+ declare function parseBoard(input: string): Board;
52
+
53
+ /** A single validation error (or warning). */
54
+ interface ValidationError {
55
+ /** Human-readable message. */
56
+ message: string;
57
+ /** Optional path (e.g. "tasks.AUTH-12.status") for tooling. */
58
+ path?: string;
59
+ /** Optional code (e.g. "STATUS_INVALID") for programmatic handling. */
60
+ code?: string;
61
+ }
62
+ /** Result of validating a board. Does not throw; caller decides what to do with errors. */
63
+ interface ValidationResult {
64
+ /** True if there are no errors. */
65
+ valid: boolean;
66
+ /** List of validation errors (empty if valid). */
67
+ errors: ValidationError[];
68
+ /** Optional warnings (e.g. spec file missing). Empty if none. */
69
+ warnings?: ValidationError[];
70
+ }
71
+ /**
72
+ * Run all validation rules on a parsed board. Does not throw; returns a list of errors.
73
+ * @param board - Parsed board AST.
74
+ * @returns List of validation errors (empty if valid).
75
+ */
76
+ declare function validateBoard(board: Board): ValidationError[];
77
+ /**
78
+ * Validate a parsed board. Returns a result with `valid` and `errors`; does not throw.
79
+ * Use this as the public API; use `validateBoard` if you only need the error list.
80
+ *
81
+ * @param board - Parsed board AST.
82
+ * @returns ValidationResult with `valid: true` and empty `errors` when valid, otherwise `valid: false` and non-empty `errors`.
83
+ */
84
+ declare function validate(board: Board): ValidationResult;
85
+
86
+ /**
87
+ * Returns a plain-text summary of the board for terminal output or paste into chat/docs.
88
+ * Format: board name, per-column counts, optional WIP limit lines, task list, optional blocked section.
89
+ * @param board - Parsed board AST.
90
+ * @returns Summary string ending with a single newline.
91
+ */
92
+ declare function summarize(board: Board): string;
93
+
94
+ declare const VERSION: string;
95
+
96
+ export { type Board, type Column, ParseError, type Task, VERSION, type ValidationError, type ValidationResult, parseBoard, parseBoardFromString, summarize, validate, validateBoard };
package/dist/index.js ADDED
@@ -0,0 +1,326 @@
1
+ // src/parser.ts
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { parse } from "yaml";
5
+ var ParseError = class extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "ParseError";
9
+ }
10
+ };
11
+ function assertObject(value, path2) {
12
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
13
+ throw new ParseError(`${path2}: expected an object`);
14
+ }
15
+ }
16
+ function assertArray(value, path2) {
17
+ if (!Array.isArray(value)) {
18
+ throw new ParseError(`${path2}: expected an array`);
19
+ }
20
+ }
21
+ function assertString(value, path2) {
22
+ if (typeof value !== "string") {
23
+ throw new ParseError(`${path2}: expected a string`);
24
+ }
25
+ }
26
+ function parseBoardFromString(content) {
27
+ let raw;
28
+ try {
29
+ raw = parse(content);
30
+ } catch (err) {
31
+ const message = err instanceof Error ? err.message : String(err);
32
+ throw new ParseError(`Invalid YAML: ${message}`);
33
+ }
34
+ assertObject(raw, "root");
35
+ const root = raw;
36
+ if (!("board" in root)) {
37
+ throw new ParseError("Missing required field: board");
38
+ }
39
+ assertString(root.board, "board");
40
+ if (root.board.trim() === "") {
41
+ throw new ParseError("board must be a non-empty string");
42
+ }
43
+ if (!("columns" in root)) {
44
+ throw new ParseError("Missing required field: columns");
45
+ }
46
+ assertArray(root.columns, "columns");
47
+ if (root.columns.length === 0) {
48
+ throw new ParseError("columns must be a non-empty array");
49
+ }
50
+ const columns = root.columns.map((item, i) => {
51
+ const path2 = `columns[${i}]`;
52
+ if (typeof item === "string") {
53
+ if (item.trim() === "") {
54
+ throw new ParseError(`${path2}: column name must be non-empty`);
55
+ }
56
+ return { name: item };
57
+ }
58
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
59
+ const obj = item;
60
+ if (!("name" in obj)) {
61
+ throw new ParseError(`${path2}: column object must have "name"`);
62
+ }
63
+ assertString(obj.name, `${path2}.name`);
64
+ if (obj.name.trim() === "") {
65
+ throw new ParseError(`${path2}.name: must be non-empty`);
66
+ }
67
+ const col = { name: obj.name };
68
+ if ("limit" in obj && obj.limit !== void 0) {
69
+ if (typeof obj.limit !== "number" || !Number.isInteger(obj.limit) || obj.limit < 1) {
70
+ throw new ParseError(`${path2}.limit: must be a positive integer`);
71
+ }
72
+ col.limit = obj.limit;
73
+ }
74
+ return col;
75
+ }
76
+ throw new ParseError(`${path2}: expected a string or object with "name" (and optional "limit")`);
77
+ });
78
+ if (!("tasks" in root)) {
79
+ throw new ParseError("Missing required field: tasks");
80
+ }
81
+ if (root.tasks === null || typeof root.tasks !== "object" || Array.isArray(root.tasks)) {
82
+ throw new ParseError("tasks must be an object (map of task id to task)");
83
+ }
84
+ const tasksRaw = root.tasks;
85
+ const tasks = {};
86
+ for (const [id, rawTask] of Object.entries(tasksRaw)) {
87
+ const path2 = `tasks.${id}`;
88
+ assertObject(rawTask, path2);
89
+ const t = rawTask;
90
+ if (!("title" in t)) {
91
+ throw new ParseError(`${path2}: missing required field "title"`);
92
+ }
93
+ assertString(t.title, `${path2}.title`);
94
+ if (!("status" in t)) {
95
+ throw new ParseError(`${path2}: missing required field "status"`);
96
+ }
97
+ assertString(t.status, `${path2}.status`);
98
+ const task = {
99
+ title: t.title,
100
+ status: t.status
101
+ };
102
+ if ("description" in t && t.description !== void 0) {
103
+ assertString(t.description, `${path2}.description`);
104
+ task.description = t.description;
105
+ }
106
+ if ("spec" in t && t.spec !== void 0) {
107
+ assertString(t.spec, `${path2}.spec`);
108
+ task.spec = t.spec;
109
+ }
110
+ if ("owner" in t && t.owner !== void 0) {
111
+ assertString(t.owner, `${path2}.owner`);
112
+ task.owner = t.owner;
113
+ }
114
+ if ("priority" in t && t.priority !== void 0) {
115
+ assertString(t.priority, `${path2}.priority`);
116
+ task.priority = t.priority;
117
+ }
118
+ if ("depends_on" in t && t.depends_on !== void 0) {
119
+ if (typeof t.depends_on === "string") {
120
+ task.depends_on = [t.depends_on];
121
+ } else if (Array.isArray(t.depends_on)) {
122
+ const arr = t.depends_on;
123
+ for (let i = 0; i < arr.length; i++) {
124
+ assertString(arr[i], `${path2}.depends_on[${i}]`);
125
+ }
126
+ task.depends_on = arr;
127
+ } else {
128
+ throw new ParseError(`${path2}.depends_on: expected a string or array of strings`);
129
+ }
130
+ }
131
+ tasks[id] = task;
132
+ }
133
+ return {
134
+ board: root.board,
135
+ columns,
136
+ tasks
137
+ };
138
+ }
139
+ function looksLikePath(input) {
140
+ if (input.includes("\n")) return false;
141
+ const trimmed = input.trim();
142
+ if (trimmed.length === 0) return false;
143
+ return trimmed.endsWith(".yaml") || trimmed.endsWith(".yml") || trimmed.includes("/") || trimmed.includes(path.sep);
144
+ }
145
+ function parseBoard(input) {
146
+ if (looksLikePath(input)) {
147
+ const resolved = path.resolve(input.trim());
148
+ let content;
149
+ try {
150
+ content = fs.readFileSync(resolved, "utf-8");
151
+ } catch (err) {
152
+ const message = err instanceof Error ? err.message : String(err);
153
+ throw new ParseError(`Cannot read file "${resolved}": ${message}`);
154
+ }
155
+ return parseBoardFromString(content);
156
+ }
157
+ return parseBoardFromString(input);
158
+ }
159
+
160
+ // src/validation.ts
161
+ var CANONICAL_COLUMNS = ["Backlog", "Ready", "In Progress", "Done"];
162
+ function validateBoard(board) {
163
+ const errors = [];
164
+ const columnNames = board.columns.map((c) => c.name);
165
+ if (board.columns.length !== CANONICAL_COLUMNS.length) {
166
+ errors.push({
167
+ path: "columns",
168
+ message: `Board must have exactly ${CANONICAL_COLUMNS.length} columns: ${CANONICAL_COLUMNS.join(", ")}. Got ${board.columns.length}.`,
169
+ code: "COLUMNS_NOT_CANONICAL"
170
+ });
171
+ } else {
172
+ for (let i = 0; i < CANONICAL_COLUMNS.length; i++) {
173
+ const expected = CANONICAL_COLUMNS[i];
174
+ const actual = board.columns[i]?.name;
175
+ if (actual !== expected) {
176
+ errors.push({
177
+ path: `columns[${i}]`,
178
+ message: `Column at index ${i} must be "${expected}". Got "${actual}". Canonical order: ${CANONICAL_COLUMNS.join(", ")}.`,
179
+ code: "COLUMNS_NOT_CANONICAL"
180
+ });
181
+ }
182
+ }
183
+ }
184
+ const seen = /* @__PURE__ */ new Set();
185
+ for (let i = 0; i < board.columns.length; i++) {
186
+ const name = board.columns[i].name;
187
+ if (seen.has(name)) {
188
+ errors.push({
189
+ path: `columns[${i}]`,
190
+ message: `Duplicate column name: "${name}"`,
191
+ code: "DUPLICATE_COLUMN"
192
+ });
193
+ }
194
+ seen.add(name);
195
+ }
196
+ const taskIds = new Set(Object.keys(board.tasks));
197
+ for (const [id, task] of Object.entries(board.tasks)) {
198
+ const taskPath = `tasks.${id}`;
199
+ if (!columnNames.includes(task.status)) {
200
+ errors.push({
201
+ path: `${taskPath}.status`,
202
+ message: `Task status "${task.status}" does not match any column. Valid columns: ${columnNames.join(", ")}`,
203
+ code: "STATUS_INVALID"
204
+ });
205
+ }
206
+ if (task.depends_on) {
207
+ for (const refId of task.depends_on) {
208
+ if (!taskIds.has(refId)) {
209
+ errors.push({
210
+ path: `${taskPath}.depends_on`,
211
+ message: `Task "${id}" depends on "${refId}", which does not exist`,
212
+ code: "DEPENDS_ON_INVALID"
213
+ });
214
+ }
215
+ }
216
+ }
217
+ }
218
+ const countByStatus = /* @__PURE__ */ new Map();
219
+ for (const task of Object.values(board.tasks)) {
220
+ countByStatus.set(task.status, (countByStatus.get(task.status) ?? 0) + 1);
221
+ }
222
+ for (let i = 0; i < board.columns.length; i++) {
223
+ const col = board.columns[i];
224
+ if (col.limit == null) continue;
225
+ const count = countByStatus.get(col.name) ?? 0;
226
+ if (count > col.limit) {
227
+ errors.push({
228
+ path: `columns[${i}]`,
229
+ message: `Column "${col.name}" has limit ${col.limit} but ${count} task(s) in that status`,
230
+ code: "WIP_LIMIT_EXCEEDED"
231
+ });
232
+ }
233
+ }
234
+ return errors;
235
+ }
236
+ function validate(board) {
237
+ const errors = validateBoard(board);
238
+ return {
239
+ valid: errors.length === 0,
240
+ errors,
241
+ warnings: []
242
+ };
243
+ }
244
+
245
+ // src/summarize.ts
246
+ function summarize(board) {
247
+ const columnNames = board.columns.map((c) => c.name);
248
+ const lastColumnName = columnNames.length > 0 ? columnNames[columnNames.length - 1] : "";
249
+ const countByColumn = {};
250
+ for (const name of columnNames) {
251
+ countByColumn[name] = 0;
252
+ }
253
+ for (const task of Object.values(board.tasks)) {
254
+ if (task.status in countByColumn) {
255
+ countByColumn[task.status]++;
256
+ }
257
+ }
258
+ const lines = [];
259
+ lines.push(`Board: ${board.board}`);
260
+ lines.push("");
261
+ const columnPart = columnNames.map((name) => `${name} (${countByColumn[name] ?? 0})`).join(", ");
262
+ lines.push(`Columns: ${columnPart}`);
263
+ for (const col of board.columns) {
264
+ if (col.limit != null && col.limit >= 1) {
265
+ const count = countByColumn[col.name] ?? 0;
266
+ if (count >= col.limit) {
267
+ lines.push(` ${col.name} at WIP limit (${count}/${col.limit})`);
268
+ }
269
+ }
270
+ }
271
+ lines.push("");
272
+ lines.push("Tasks:");
273
+ const taskIds = Object.keys(board.tasks);
274
+ const statusToIndex = {};
275
+ columnNames.forEach((name, i) => {
276
+ statusToIndex[name] = i;
277
+ });
278
+ const sortedIds = taskIds.slice().sort((a, b) => {
279
+ const taskA = board.tasks[a];
280
+ const taskB = board.tasks[b];
281
+ const idxA = statusToIndex[taskA.status] ?? 999;
282
+ const idxB = statusToIndex[taskB.status] ?? 999;
283
+ if (idxA !== idxB) return idxA - idxB;
284
+ return a.localeCompare(b);
285
+ });
286
+ if (sortedIds.length === 0) {
287
+ lines.push(" (none)");
288
+ } else {
289
+ for (const id of sortedIds) {
290
+ const task = board.tasks[id];
291
+ lines.push(` ${id} [${task.status}] ${task.title}`);
292
+ }
293
+ }
294
+ const blocked = [];
295
+ for (const id of taskIds) {
296
+ const task = board.tasks[id];
297
+ const deps = task.depends_on ?? [];
298
+ const unmet = deps.filter((depId) => {
299
+ const dep = board.tasks[depId];
300
+ return dep && dep.status !== lastColumnName;
301
+ });
302
+ if (unmet.length > 0) {
303
+ blocked.push({ id, deps: unmet });
304
+ }
305
+ }
306
+ if (blocked.length > 0) {
307
+ lines.push("");
308
+ for (const { id, deps } of blocked) {
309
+ lines.push(`Blocked: ${id} (depends on ${deps.join(", ")})`);
310
+ }
311
+ }
312
+ return lines.join("\n") + "\n";
313
+ }
314
+
315
+ // src/index.ts
316
+ var VERSION = "1.1.1";
317
+ export {
318
+ ParseError,
319
+ VERSION,
320
+ parseBoard,
321
+ parseBoardFromString,
322
+ summarize,
323
+ validate,
324
+ validateBoard
325
+ };
326
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/parser.ts","../src/validation.ts","../src/summarize.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { parse } from \"yaml\";\nimport type { Board, Column, Task } from \"./ast.js\";\n\n/** Thrown when YAML is invalid or required fields are missing/wrong. */\nexport class ParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ParseError\";\n }\n}\n\nfunction assertObject(value: unknown, path: string): asserts value is Record<string, unknown> {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n throw new ParseError(`${path}: expected an object`);\n }\n}\n\nfunction assertArray(value: unknown, path: string): asserts value is unknown[] {\n if (!Array.isArray(value)) {\n throw new ParseError(`${path}: expected an array`);\n }\n}\n\nfunction assertString(value: unknown, path: string): asserts value is string {\n if (typeof value !== \"string\") {\n throw new ParseError(`${path}: expected a string`);\n }\n}\n\n/**\n * Parse YAML board content into a typed Board AST.\n * @param content - Raw YAML string (board file content).\n * @returns Board AST.\n * @throws ParseError when YAML is invalid or required fields are missing/wrong.\n */\nexport function parseBoardFromString(content: string): Board {\n let raw: unknown;\n try {\n raw = parse(content);\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ParseError(`Invalid YAML: ${message}`);\n }\n\n assertObject(raw, \"root\");\n const root = raw as Record<string, unknown>;\n\n // board (required, non-empty string)\n if (!(\"board\" in root)) {\n throw new ParseError(\"Missing required field: board\");\n }\n assertString(root.board, \"board\");\n if (root.board.trim() === \"\") {\n throw new ParseError(\"board must be a non-empty string\");\n }\n\n // columns (required, non-empty array)\n if (!(\"columns\" in root)) {\n throw new ParseError(\"Missing required field: columns\");\n }\n assertArray(root.columns, \"columns\");\n if (root.columns.length === 0) {\n throw new ParseError(\"columns must be a non-empty array\");\n }\n\n const columns: Column[] = root.columns.map((item: unknown, i: number) => {\n const path = `columns[${i}]`;\n if (typeof item === \"string\") {\n if (item.trim() === \"\") {\n throw new ParseError(`${path}: column name must be non-empty`);\n }\n return { name: item };\n }\n if (item !== null && typeof item === \"object\" && !Array.isArray(item)) {\n const obj = item as Record<string, unknown>;\n if (!(\"name\" in obj)) {\n throw new ParseError(`${path}: column object must have \"name\"`);\n }\n assertString(obj.name, `${path}.name`);\n if ((obj.name as string).trim() === \"\") {\n throw new ParseError(`${path}.name: must be non-empty`);\n }\n const col: Column = { name: obj.name as string };\n if (\"limit\" in obj && obj.limit !== undefined) {\n if (typeof obj.limit !== \"number\" || !Number.isInteger(obj.limit) || obj.limit < 1) {\n throw new ParseError(`${path}.limit: must be a positive integer`);\n }\n col.limit = obj.limit;\n }\n return col;\n }\n throw new ParseError(`${path}: expected a string or object with \"name\" (and optional \"limit\")`);\n });\n\n // tasks (required, object; may be empty)\n if (!(\"tasks\" in root)) {\n throw new ParseError(\"Missing required field: tasks\");\n }\n if (root.tasks === null || typeof root.tasks !== \"object\" || Array.isArray(root.tasks)) {\n throw new ParseError(\"tasks must be an object (map of task id to task)\");\n }\n const tasksRaw = root.tasks as Record<string, unknown>;\n const tasks: Record<string, Task> = {};\n\n for (const [id, rawTask] of Object.entries(tasksRaw)) {\n const path = `tasks.${id}`;\n assertObject(rawTask, path);\n const t = rawTask as Record<string, unknown>;\n\n if (!(\"title\" in t)) {\n throw new ParseError(`${path}: missing required field \"title\"`);\n }\n assertString(t.title, `${path}.title`);\n\n if (!(\"status\" in t)) {\n throw new ParseError(`${path}: missing required field \"status\"`);\n }\n assertString(t.status, `${path}.status`);\n\n const task: Task = {\n title: t.title as string,\n status: t.status as string,\n };\n\n if (\"description\" in t && t.description !== undefined) {\n assertString(t.description, `${path}.description`);\n task.description = t.description as string;\n }\n if (\"spec\" in t && t.spec !== undefined) {\n assertString(t.spec, `${path}.spec`);\n task.spec = t.spec as string;\n }\n if (\"owner\" in t && t.owner !== undefined) {\n assertString(t.owner, `${path}.owner`);\n task.owner = t.owner as string;\n }\n if (\"priority\" in t && t.priority !== undefined) {\n assertString(t.priority, `${path}.priority`);\n task.priority = t.priority as string;\n }\n if (\"depends_on\" in t && t.depends_on !== undefined) {\n if (typeof t.depends_on === \"string\") {\n task.depends_on = [t.depends_on];\n } else if (Array.isArray(t.depends_on)) {\n const arr = t.depends_on as unknown[];\n for (let i = 0; i < arr.length; i++) {\n assertString(arr[i], `${path}.depends_on[${i}]`);\n }\n task.depends_on = arr as string[];\n } else {\n throw new ParseError(`${path}.depends_on: expected a string or array of strings`);\n }\n }\n\n tasks[id] = task;\n }\n\n return {\n board: root.board as string,\n columns,\n tasks,\n };\n}\n\n/**\n * Heuristic: treat as file path if no newline and (ends with .yaml/.yml or contains path sep).\n * Otherwise treat as raw YAML content.\n */\nfunction looksLikePath(input: string): boolean {\n if (input.includes(\"\\n\")) return false;\n const trimmed = input.trim();\n if (trimmed.length === 0) return false;\n return (\n trimmed.endsWith(\".yaml\") ||\n trimmed.endsWith(\".yml\") ||\n trimmed.includes(\"/\") ||\n trimmed.includes(path.sep)\n );\n}\n\n/**\n * Parse a board from a file path or raw YAML string.\n * - If `input` looks like a path (no newline, ends with .yaml/.yml or contains path separator),\n * the file is read from disk (relative to cwd) and parsed.\n * - Otherwise `input` is parsed as raw YAML content.\n *\n * @param input - File path (absolute or relative to cwd) or raw YAML string.\n * @returns Board AST.\n * @throws ParseError when YAML is invalid or required fields are missing/wrong.\n * @throws ParseError when input is a path and the file cannot be read (e.g. not found).\n */\nexport function parseBoard(input: string): Board {\n if (looksLikePath(input)) {\n const resolved = path.resolve(input.trim());\n let content: string;\n try {\n content = fs.readFileSync(resolved, \"utf-8\");\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new ParseError(`Cannot read file \"${resolved}\": ${message}`);\n }\n return parseBoardFromString(content);\n }\n return parseBoardFromString(input);\n}\n","import type { Board, Column, Task } from \"./ast.js\";\n\n/** Canonical column set per spec: exact names and order. */\nexport const CANONICAL_COLUMNS = [\"Backlog\", \"Ready\", \"In Progress\", \"Done\"] as const;\n\n/** A single validation error (or warning). */\nexport interface ValidationError {\n /** Human-readable message. */\n message: string;\n /** Optional path (e.g. \"tasks.AUTH-12.status\") for tooling. */\n path?: string;\n /** Optional code (e.g. \"STATUS_INVALID\") for programmatic handling. */\n code?: string;\n}\n\n/** Result of validating a board. Does not throw; caller decides what to do with errors. */\nexport interface ValidationResult {\n /** True if there are no errors. */\n valid: boolean;\n /** List of validation errors (empty if valid). */\n errors: ValidationError[];\n /** Optional warnings (e.g. spec file missing). Empty if none. */\n warnings?: ValidationError[];\n}\n\n/**\n * Run all validation rules on a parsed board. Does not throw; returns a list of errors.\n * @param board - Parsed board AST.\n * @returns List of validation errors (empty if valid).\n */\nexport function validateBoard(board: Board): ValidationError[] {\n const errors: ValidationError[] = [];\n const columnNames = board.columns.map((c) => c.name);\n\n // Canonical columns: must be Backlog, Ready, In Progress, Done in that order\n if (board.columns.length !== CANONICAL_COLUMNS.length) {\n errors.push({\n path: \"columns\",\n message: `Board must have exactly ${CANONICAL_COLUMNS.length} columns: ${CANONICAL_COLUMNS.join(\", \")}. Got ${board.columns.length}.`,\n code: \"COLUMNS_NOT_CANONICAL\",\n });\n } else {\n for (let i = 0; i < CANONICAL_COLUMNS.length; i++) {\n const expected = CANONICAL_COLUMNS[i];\n const actual = board.columns[i]?.name;\n if (actual !== expected) {\n errors.push({\n path: `columns[${i}]`,\n message: `Column at index ${i} must be \"${expected}\". Got \"${actual}\". Canonical order: ${CANONICAL_COLUMNS.join(\", \")}.`,\n code: \"COLUMNS_NOT_CANONICAL\",\n });\n }\n }\n }\n\n // Column names unique (redundant if canonical check passed, but keeps other checks consistent)\n const seen = new Set<string>();\n for (let i = 0; i < board.columns.length; i++) {\n const name = board.columns[i].name;\n if (seen.has(name)) {\n errors.push({\n path: `columns[${i}]`,\n message: `Duplicate column name: \"${name}\"`,\n code: \"DUPLICATE_COLUMN\",\n });\n }\n seen.add(name);\n }\n\n // Task status matches column; depends_on refs exist\n const taskIds = new Set(Object.keys(board.tasks));\n for (const [id, task] of Object.entries(board.tasks)) {\n const taskPath = `tasks.${id}`;\n\n if (!columnNames.includes(task.status)) {\n errors.push({\n path: `${taskPath}.status`,\n message: `Task status \"${task.status}\" does not match any column. Valid columns: ${columnNames.join(\", \")}`,\n code: \"STATUS_INVALID\",\n });\n }\n\n if (task.depends_on) {\n for (const refId of task.depends_on) {\n if (!taskIds.has(refId)) {\n errors.push({\n path: `${taskPath}.depends_on`,\n message: `Task \"${id}\" depends on \"${refId}\", which does not exist`,\n code: \"DEPENDS_ON_INVALID\",\n });\n }\n }\n }\n }\n\n // WIP limit per column\n const countByStatus = new Map<string, number>();\n for (const task of Object.values(board.tasks)) {\n countByStatus.set(task.status, (countByStatus.get(task.status) ?? 0) + 1);\n }\n\n for (let i = 0; i < board.columns.length; i++) {\n const col = board.columns[i];\n if (col.limit == null) continue;\n const count = countByStatus.get(col.name) ?? 0;\n if (count > col.limit) {\n errors.push({\n path: `columns[${i}]`,\n message: `Column \"${col.name}\" has limit ${col.limit} but ${count} task(s) in that status`,\n code: \"WIP_LIMIT_EXCEEDED\",\n });\n }\n }\n\n return errors;\n}\n\n/**\n * Validate a parsed board. Returns a result with `valid` and `errors`; does not throw.\n * Use this as the public API; use `validateBoard` if you only need the error list.\n *\n * @param board - Parsed board AST.\n * @returns ValidationResult with `valid: true` and empty `errors` when valid, otherwise `valid: false` and non-empty `errors`.\n */\nexport function validate(board: Board): ValidationResult {\n const errors = validateBoard(board);\n return {\n valid: errors.length === 0,\n errors,\n warnings: [],\n };\n}\n","import type { Board, Column } from \"./ast.js\";\n\n/**\n * Returns a plain-text summary of the board for terminal output or paste into chat/docs.\n * Format: board name, per-column counts, optional WIP limit lines, task list, optional blocked section.\n * @param board - Parsed board AST.\n * @returns Summary string ending with a single newline.\n */\nexport function summarize(board: Board): string {\n const columnNames = board.columns.map((c: Column) => c.name);\n const lastColumnName = columnNames.length > 0 ? columnNames[columnNames.length - 1] : \"\";\n\n // Per-column task counts\n const countByColumn: Record<string, number> = {};\n for (const name of columnNames) {\n countByColumn[name] = 0;\n }\n for (const task of Object.values(board.tasks)) {\n if (task.status in countByColumn) {\n countByColumn[task.status]++;\n }\n }\n\n const lines: string[] = [];\n\n // Board name\n lines.push(`Board: ${board.board}`);\n lines.push(\"\");\n\n // Columns: name (count), ...\n const columnPart = columnNames.map((name) => `${name} (${countByColumn[name] ?? 0})`).join(\", \");\n lines.push(`Columns: ${columnPart}`);\n\n // WIP limit: for each column with limit and count >= limit\n for (const col of board.columns) {\n if (col.limit != null && col.limit >= 1) {\n const count = countByColumn[col.name] ?? 0;\n if (count >= col.limit) {\n lines.push(` ${col.name} at WIP limit (${count}/${col.limit})`);\n }\n }\n }\n\n lines.push(\"\");\n\n // Tasks: ordered by column order, then by task id within column\n lines.push(\"Tasks:\");\n const taskIds = Object.keys(board.tasks);\n const statusToIndex: Record<string, number> = {};\n columnNames.forEach((name, i) => {\n statusToIndex[name] = i;\n });\n const sortedIds = taskIds.slice().sort((a, b) => {\n const taskA = board.tasks[a];\n const taskB = board.tasks[b];\n const idxA = statusToIndex[taskA.status] ?? 999;\n const idxB = statusToIndex[taskB.status] ?? 999;\n if (idxA !== idxB) return idxA - idxB;\n return a.localeCompare(b);\n });\n if (sortedIds.length === 0) {\n lines.push(\" (none)\");\n } else {\n for (const id of sortedIds) {\n const task = board.tasks[id];\n lines.push(` ${id} [${task.status}] ${task.title}`);\n }\n }\n\n // Blocked: task has depends_on and at least one dependency is not in last column\n const blocked: { id: string; deps: string[] }[] = [];\n for (const id of taskIds) {\n const task = board.tasks[id];\n const deps = task.depends_on ?? [];\n const unmet = deps.filter((depId) => {\n const dep = board.tasks[depId];\n return dep && dep.status !== lastColumnName;\n });\n if (unmet.length > 0) {\n blocked.push({ id, deps: unmet });\n }\n }\n if (blocked.length > 0) {\n lines.push(\"\");\n for (const { id, deps } of blocked) {\n lines.push(`Blocked: ${id} (depends on ${deps.join(\", \")})`);\n }\n }\n\n return lines.join(\"\\n\") + \"\\n\";\n}\n","/**\n * @stcrft/statecraft-core — DSL, AST, validation, CRUS operations.\n * VERSION is injected at build time from package.json (tsup define) so it stays in sync and works in Node and browser bundles.\n */\ndeclare const __STATECRAFT_CORE_VERSION__: string;\nexport const VERSION = __STATECRAFT_CORE_VERSION__;\n\nexport type { Board, Column, Task } from \"./ast.js\";\nexport { parseBoard, parseBoardFromString, ParseError } from \"./parser.js\";\nexport type { ValidationError, ValidationResult } from \"./validation.js\";\nexport { validate, validateBoard } from \"./validation.js\";\nexport { summarize } from \"./summarize.js\";\n"],"mappings":";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,aAAa;AAIf,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,aAAa,OAAgBA,OAAwD;AAC5F,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AACvE,UAAM,IAAI,WAAW,GAAGA,KAAI,sBAAsB;AAAA,EACpD;AACF;AAEA,SAAS,YAAY,OAAgBA,OAA0C;AAC7E,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,UAAM,IAAI,WAAW,GAAGA,KAAI,qBAAqB;AAAA,EACnD;AACF;AAEA,SAAS,aAAa,OAAgBA,OAAuC;AAC3E,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,WAAW,GAAGA,KAAI,qBAAqB;AAAA,EACnD;AACF;AAQO,SAAS,qBAAqB,SAAwB;AAC3D,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,OAAO;AAAA,EACrB,SAAS,KAAK;AACZ,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,UAAM,IAAI,WAAW,iBAAiB,OAAO,EAAE;AAAA,EACjD;AAEA,eAAa,KAAK,MAAM;AACxB,QAAM,OAAO;AAGb,MAAI,EAAE,WAAW,OAAO;AACtB,UAAM,IAAI,WAAW,+BAA+B;AAAA,EACtD;AACA,eAAa,KAAK,OAAO,OAAO;AAChC,MAAI,KAAK,MAAM,KAAK,MAAM,IAAI;AAC5B,UAAM,IAAI,WAAW,kCAAkC;AAAA,EACzD;AAGA,MAAI,EAAE,aAAa,OAAO;AACxB,UAAM,IAAI,WAAW,iCAAiC;AAAA,EACxD;AACA,cAAY,KAAK,SAAS,SAAS;AACnC,MAAI,KAAK,QAAQ,WAAW,GAAG;AAC7B,UAAM,IAAI,WAAW,mCAAmC;AAAA,EAC1D;AAEA,QAAM,UAAoB,KAAK,QAAQ,IAAI,CAAC,MAAe,MAAc;AACvE,UAAMA,QAAO,WAAW,CAAC;AACzB,QAAI,OAAO,SAAS,UAAU;AAC5B,UAAI,KAAK,KAAK,MAAM,IAAI;AACtB,cAAM,IAAI,WAAW,GAAGA,KAAI,iCAAiC;AAAA,MAC/D;AACA,aAAO,EAAE,MAAM,KAAK;AAAA,IACtB;AACA,QAAI,SAAS,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,GAAG;AACrE,YAAM,MAAM;AACZ,UAAI,EAAE,UAAU,MAAM;AACpB,cAAM,IAAI,WAAW,GAAGA,KAAI,kCAAkC;AAAA,MAChE;AACA,mBAAa,IAAI,MAAM,GAAGA,KAAI,OAAO;AACrC,UAAK,IAAI,KAAgB,KAAK,MAAM,IAAI;AACtC,cAAM,IAAI,WAAW,GAAGA,KAAI,0BAA0B;AAAA,MACxD;AACA,YAAM,MAAc,EAAE,MAAM,IAAI,KAAe;AAC/C,UAAI,WAAW,OAAO,IAAI,UAAU,QAAW;AAC7C,YAAI,OAAO,IAAI,UAAU,YAAY,CAAC,OAAO,UAAU,IAAI,KAAK,KAAK,IAAI,QAAQ,GAAG;AAClF,gBAAM,IAAI,WAAW,GAAGA,KAAI,oCAAoC;AAAA,QAClE;AACA,YAAI,QAAQ,IAAI;AAAA,MAClB;AACA,aAAO;AAAA,IACT;AACA,UAAM,IAAI,WAAW,GAAGA,KAAI,kEAAkE;AAAA,EAChG,CAAC;AAGD,MAAI,EAAE,WAAW,OAAO;AACtB,UAAM,IAAI,WAAW,+BAA+B;AAAA,EACtD;AACA,MAAI,KAAK,UAAU,QAAQ,OAAO,KAAK,UAAU,YAAY,MAAM,QAAQ,KAAK,KAAK,GAAG;AACtF,UAAM,IAAI,WAAW,kDAAkD;AAAA,EACzE;AACA,QAAM,WAAW,KAAK;AACtB,QAAM,QAA8B,CAAC;AAErC,aAAW,CAAC,IAAI,OAAO,KAAK,OAAO,QAAQ,QAAQ,GAAG;AACpD,UAAMA,QAAO,SAAS,EAAE;AACxB,iBAAa,SAASA,KAAI;AAC1B,UAAM,IAAI;AAEV,QAAI,EAAE,WAAW,IAAI;AACnB,YAAM,IAAI,WAAW,GAAGA,KAAI,kCAAkC;AAAA,IAChE;AACA,iBAAa,EAAE,OAAO,GAAGA,KAAI,QAAQ;AAErC,QAAI,EAAE,YAAY,IAAI;AACpB,YAAM,IAAI,WAAW,GAAGA,KAAI,mCAAmC;AAAA,IACjE;AACA,iBAAa,EAAE,QAAQ,GAAGA,KAAI,SAAS;AAEvC,UAAM,OAAa;AAAA,MACjB,OAAO,EAAE;AAAA,MACT,QAAQ,EAAE;AAAA,IACZ;AAEA,QAAI,iBAAiB,KAAK,EAAE,gBAAgB,QAAW;AACrD,mBAAa,EAAE,aAAa,GAAGA,KAAI,cAAc;AACjD,WAAK,cAAc,EAAE;AAAA,IACvB;AACA,QAAI,UAAU,KAAK,EAAE,SAAS,QAAW;AACvC,mBAAa,EAAE,MAAM,GAAGA,KAAI,OAAO;AACnC,WAAK,OAAO,EAAE;AAAA,IAChB;AACA,QAAI,WAAW,KAAK,EAAE,UAAU,QAAW;AACzC,mBAAa,EAAE,OAAO,GAAGA,KAAI,QAAQ;AACrC,WAAK,QAAQ,EAAE;AAAA,IACjB;AACA,QAAI,cAAc,KAAK,EAAE,aAAa,QAAW;AAC/C,mBAAa,EAAE,UAAU,GAAGA,KAAI,WAAW;AAC3C,WAAK,WAAW,EAAE;AAAA,IACpB;AACA,QAAI,gBAAgB,KAAK,EAAE,eAAe,QAAW;AACnD,UAAI,OAAO,EAAE,eAAe,UAAU;AACpC,aAAK,aAAa,CAAC,EAAE,UAAU;AAAA,MACjC,WAAW,MAAM,QAAQ,EAAE,UAAU,GAAG;AACtC,cAAM,MAAM,EAAE;AACd,iBAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,uBAAa,IAAI,CAAC,GAAG,GAAGA,KAAI,eAAe,CAAC,GAAG;AAAA,QACjD;AACA,aAAK,aAAa;AAAA,MACpB,OAAO;AACL,cAAM,IAAI,WAAW,GAAGA,KAAI,oDAAoD;AAAA,MAClF;AAAA,IACF;AAEA,UAAM,EAAE,IAAI;AAAA,EACd;AAEA,SAAO;AAAA,IACL,OAAO,KAAK;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AACF;AAMA,SAAS,cAAc,OAAwB;AAC7C,MAAI,MAAM,SAAS,IAAI,EAAG,QAAO;AACjC,QAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,SACE,QAAQ,SAAS,OAAO,KACxB,QAAQ,SAAS,MAAM,KACvB,QAAQ,SAAS,GAAG,KACpB,QAAQ,SAAS,KAAK,GAAG;AAE7B;AAaO,SAAS,WAAW,OAAsB;AAC/C,MAAI,cAAc,KAAK,GAAG;AACxB,UAAM,WAAW,KAAK,QAAQ,MAAM,KAAK,CAAC;AAC1C,QAAI;AACJ,QAAI;AACF,gBAAU,GAAG,aAAa,UAAU,OAAO;AAAA,IAC7C,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC/D,YAAM,IAAI,WAAW,qBAAqB,QAAQ,MAAM,OAAO,EAAE;AAAA,IACnE;AACA,WAAO,qBAAqB,OAAO;AAAA,EACrC;AACA,SAAO,qBAAqB,KAAK;AACnC;;;AC3MO,IAAM,oBAAoB,CAAC,WAAW,SAAS,eAAe,MAAM;AA2BpE,SAAS,cAAc,OAAiC;AAC7D,QAAM,SAA4B,CAAC;AACnC,QAAM,cAAc,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,IAAI;AAGnD,MAAI,MAAM,QAAQ,WAAW,kBAAkB,QAAQ;AACrD,WAAO,KAAK;AAAA,MACV,MAAM;AAAA,MACN,SAAS,2BAA2B,kBAAkB,MAAM,aAAa,kBAAkB,KAAK,IAAI,CAAC,SAAS,MAAM,QAAQ,MAAM;AAAA,MAClI,MAAM;AAAA,IACR,CAAC;AAAA,EACH,OAAO;AACL,aAAS,IAAI,GAAG,IAAI,kBAAkB,QAAQ,KAAK;AACjD,YAAM,WAAW,kBAAkB,CAAC;AACpC,YAAM,SAAS,MAAM,QAAQ,CAAC,GAAG;AACjC,UAAI,WAAW,UAAU;AACvB,eAAO,KAAK;AAAA,UACV,MAAM,WAAW,CAAC;AAAA,UAClB,SAAS,mBAAmB,CAAC,aAAa,QAAQ,WAAW,MAAM,uBAAuB,kBAAkB,KAAK,IAAI,CAAC;AAAA,UACtH,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,QAAM,OAAO,oBAAI,IAAY;AAC7B,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,QAAQ,KAAK;AAC7C,UAAM,OAAO,MAAM,QAAQ,CAAC,EAAE;AAC9B,QAAI,KAAK,IAAI,IAAI,GAAG;AAClB,aAAO,KAAK;AAAA,QACV,MAAM,WAAW,CAAC;AAAA,QAClB,SAAS,2BAA2B,IAAI;AAAA,QACxC,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AACA,SAAK,IAAI,IAAI;AAAA,EACf;AAGA,QAAM,UAAU,IAAI,IAAI,OAAO,KAAK,MAAM,KAAK,CAAC;AAChD,aAAW,CAAC,IAAI,IAAI,KAAK,OAAO,QAAQ,MAAM,KAAK,GAAG;AACpD,UAAM,WAAW,SAAS,EAAE;AAE5B,QAAI,CAAC,YAAY,SAAS,KAAK,MAAM,GAAG;AACtC,aAAO,KAAK;AAAA,QACV,MAAM,GAAG,QAAQ;AAAA,QACjB,SAAS,gBAAgB,KAAK,MAAM,+CAA+C,YAAY,KAAK,IAAI,CAAC;AAAA,QACzG,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAEA,QAAI,KAAK,YAAY;AACnB,iBAAW,SAAS,KAAK,YAAY;AACnC,YAAI,CAAC,QAAQ,IAAI,KAAK,GAAG;AACvB,iBAAO,KAAK;AAAA,YACV,MAAM,GAAG,QAAQ;AAAA,YACjB,SAAS,SAAS,EAAE,iBAAiB,KAAK;AAAA,YAC1C,MAAM;AAAA,UACR,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,gBAAgB,oBAAI,IAAoB;AAC9C,aAAW,QAAQ,OAAO,OAAO,MAAM,KAAK,GAAG;AAC7C,kBAAc,IAAI,KAAK,SAAS,cAAc,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC;AAAA,EAC1E;AAEA,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,QAAQ,KAAK;AAC7C,UAAM,MAAM,MAAM,QAAQ,CAAC;AAC3B,QAAI,IAAI,SAAS,KAAM;AACvB,UAAM,QAAQ,cAAc,IAAI,IAAI,IAAI,KAAK;AAC7C,QAAI,QAAQ,IAAI,OAAO;AACrB,aAAO,KAAK;AAAA,QACV,MAAM,WAAW,CAAC;AAAA,QAClB,SAAS,WAAW,IAAI,IAAI,eAAe,IAAI,KAAK,QAAQ,KAAK;AAAA,QACjE,MAAM;AAAA,MACR,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AASO,SAAS,SAAS,OAAgC;AACvD,QAAM,SAAS,cAAc,KAAK;AAClC,SAAO;AAAA,IACL,OAAO,OAAO,WAAW;AAAA,IACzB;AAAA,IACA,UAAU,CAAC;AAAA,EACb;AACF;;;AC3HO,SAAS,UAAU,OAAsB;AAC9C,QAAM,cAAc,MAAM,QAAQ,IAAI,CAAC,MAAc,EAAE,IAAI;AAC3D,QAAM,iBAAiB,YAAY,SAAS,IAAI,YAAY,YAAY,SAAS,CAAC,IAAI;AAGtF,QAAM,gBAAwC,CAAC;AAC/C,aAAW,QAAQ,aAAa;AAC9B,kBAAc,IAAI,IAAI;AAAA,EACxB;AACA,aAAW,QAAQ,OAAO,OAAO,MAAM,KAAK,GAAG;AAC7C,QAAI,KAAK,UAAU,eAAe;AAChC,oBAAc,KAAK,MAAM;AAAA,IAC3B;AAAA,EACF;AAEA,QAAM,QAAkB,CAAC;AAGzB,QAAM,KAAK,UAAU,MAAM,KAAK,EAAE;AAClC,QAAM,KAAK,EAAE;AAGb,QAAM,aAAa,YAAY,IAAI,CAAC,SAAS,GAAG,IAAI,KAAK,cAAc,IAAI,KAAK,CAAC,GAAG,EAAE,KAAK,IAAI;AAC/F,QAAM,KAAK,YAAY,UAAU,EAAE;AAGnC,aAAW,OAAO,MAAM,SAAS;AAC/B,QAAI,IAAI,SAAS,QAAQ,IAAI,SAAS,GAAG;AACvC,YAAM,QAAQ,cAAc,IAAI,IAAI,KAAK;AACzC,UAAI,SAAS,IAAI,OAAO;AACtB,cAAM,KAAK,KAAK,IAAI,IAAI,kBAAkB,KAAK,IAAI,IAAI,KAAK,GAAG;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,KAAK,EAAE;AAGb,QAAM,KAAK,QAAQ;AACnB,QAAM,UAAU,OAAO,KAAK,MAAM,KAAK;AACvC,QAAM,gBAAwC,CAAC;AAC/C,cAAY,QAAQ,CAAC,MAAM,MAAM;AAC/B,kBAAc,IAAI,IAAI;AAAA,EACxB,CAAC;AACD,QAAM,YAAY,QAAQ,MAAM,EAAE,KAAK,CAAC,GAAG,MAAM;AAC/C,UAAM,QAAQ,MAAM,MAAM,CAAC;AAC3B,UAAM,QAAQ,MAAM,MAAM,CAAC;AAC3B,UAAM,OAAO,cAAc,MAAM,MAAM,KAAK;AAC5C,UAAM,OAAO,cAAc,MAAM,MAAM,KAAK;AAC5C,QAAI,SAAS,KAAM,QAAO,OAAO;AACjC,WAAO,EAAE,cAAc,CAAC;AAAA,EAC1B,CAAC;AACD,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,KAAK,UAAU;AAAA,EACvB,OAAO;AACL,eAAW,MAAM,WAAW;AAC1B,YAAM,OAAO,MAAM,MAAM,EAAE;AAC3B,YAAM,KAAK,KAAK,EAAE,KAAK,KAAK,MAAM,KAAK,KAAK,KAAK,EAAE;AAAA,IACrD;AAAA,EACF;AAGA,QAAM,UAA4C,CAAC;AACnD,aAAW,MAAM,SAAS;AACxB,UAAM,OAAO,MAAM,MAAM,EAAE;AAC3B,UAAM,OAAO,KAAK,cAAc,CAAC;AACjC,UAAM,QAAQ,KAAK,OAAO,CAAC,UAAU;AACnC,YAAM,MAAM,MAAM,MAAM,KAAK;AAC7B,aAAO,OAAO,IAAI,WAAW;AAAA,IAC/B,CAAC;AACD,QAAI,MAAM,SAAS,GAAG;AACpB,cAAQ,KAAK,EAAE,IAAI,MAAM,MAAM,CAAC;AAAA,IAClC;AAAA,EACF;AACA,MAAI,QAAQ,SAAS,GAAG;AACtB,UAAM,KAAK,EAAE;AACb,eAAW,EAAE,IAAI,KAAK,KAAK,SAAS;AAClC,YAAM,KAAK,YAAY,EAAE,gBAAgB,KAAK,KAAK,IAAI,CAAC,GAAG;AAAA,IAC7D;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI,IAAI;AAC5B;;;ACrFO,IAAM,UAAU;","names":["path"]}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@stcrft/statecraft-core",
3
+ "version": "1.1.2",
4
+ "description": "Parser, AST, validation, and summarizer for the Statecraft board DSL",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/KristijanS99/statecraft.git"
9
+ },
10
+ "license": "GPL-3.0-only",
11
+ "engines": {
12
+ "node": ">=20"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "default": "./dist/index.js"
21
+ }
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "test": "vitest run",
26
+ "test:coverage": "vitest run --coverage"
27
+ },
28
+ "devDependencies": {
29
+ "@types/node": "^25.2.0",
30
+ "@vitest/coverage-v8": "^4.0.18",
31
+ "tsup": "^8.3.5",
32
+ "typescript": "^5.7.2",
33
+ "vitest": "^4.0.18"
34
+ },
35
+ "dependencies": {
36
+ "yaml": "^2.8.2"
37
+ }
38
+ }
package/src/ast.ts ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * AST types for a parsed Statecraft board.
3
+ * Parser (YAML → AST) produces these; validation and tools consume them.
4
+ * @see docs/spec.md
5
+ */
6
+
7
+ /** Column: normalized shape. Parser converts string columns to `{ name }`. Optional `limit` is WIP cap (e.g. In Progress). */
8
+ export interface Column {
9
+ name: string;
10
+ limit?: number;
11
+ }
12
+
13
+ /** Task: required `title` and `status`; optional fields per spec. `depends_on` normalized to array by parser. */
14
+ export interface Task {
15
+ title: string;
16
+ status: string;
17
+ description?: string;
18
+ spec?: string;
19
+ owner?: string;
20
+ priority?: string;
21
+ /** Task ids this task depends on (normalized to array by parser). */
22
+ depends_on?: string[];
23
+ }
24
+
25
+ /** Board: one board per file. `columns` in order; `tasks` keyed by task id. */
26
+ export interface Board {
27
+ board: string;
28
+ columns: Column[];
29
+ tasks: Record<string, Task>;
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @stcrft/statecraft-core — DSL, AST, validation, CRUS operations.
3
+ * VERSION is injected at build time from package.json (tsup define) so it stays in sync and works in Node and browser bundles.
4
+ */
5
+ declare const __STATECRAFT_CORE_VERSION__: string;
6
+ export const VERSION = __STATECRAFT_CORE_VERSION__;
7
+
8
+ export type { Board, Column, Task } from "./ast.js";
9
+ export { parseBoard, parseBoardFromString, ParseError } from "./parser.js";
10
+ export type { ValidationError, ValidationResult } from "./validation.js";
11
+ export { validate, validateBoard } from "./validation.js";
12
+ export { summarize } from "./summarize.js";