@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.
@@ -0,0 +1,103 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseBoardFromString } from "../src/parser.js";
3
+ import { summarize } from "../src/summarize.js";
4
+
5
+ describe("summarize", () => {
6
+ it("includes board name, column counts, and task lines", () => {
7
+ const yaml = `
8
+ board: "Summary Test"
9
+ columns: [Backlog, Ready, In Progress, Done]
10
+ tasks:
11
+ T1:
12
+ title: "First task"
13
+ status: Backlog
14
+ T2:
15
+ title: "Second task"
16
+ status: Done
17
+ `;
18
+ const board = parseBoardFromString(yaml);
19
+ const out = summarize(board);
20
+ expect(out).toContain("Board: Summary Test");
21
+ expect(out).toContain("Columns:");
22
+ expect(out).toContain("Backlog");
23
+ expect(out).toContain("Done");
24
+ expect(out).toContain("Tasks:");
25
+ expect(out).toContain("T1");
26
+ expect(out).toContain("T2");
27
+ expect(out).toContain("First task");
28
+ expect(out).toContain("Second task");
29
+ });
30
+
31
+ it("ends with a single newline", () => {
32
+ const yaml = `
33
+ board: "X"
34
+ columns: [Backlog, Ready, In Progress, Done]
35
+ tasks: {}
36
+ `;
37
+ const board = parseBoardFromString(yaml);
38
+ const out = summarize(board);
39
+ expect(out).toMatch(/\n$/);
40
+ expect(out).not.toMatch(/\n\n$/);
41
+ });
42
+
43
+ it("includes WIP limit line when column is at limit", () => {
44
+ const yaml = `
45
+ board: "WIP"
46
+ columns:
47
+ - Backlog
48
+ - Ready
49
+ - name: In Progress
50
+ limit: 2
51
+ - Done
52
+ tasks:
53
+ t1: { title: "A", status: In Progress }
54
+ t2: { title: "B", status: In Progress }
55
+ `;
56
+ const board = parseBoardFromString(yaml);
57
+ const out = summarize(board);
58
+ expect(out).toContain("at WIP limit");
59
+ expect(out).toContain("2/2");
60
+ });
61
+
62
+ it("includes Blocked section when a task depends on non-done task", () => {
63
+ const yaml = `
64
+ board: "Blocked"
65
+ columns: [Backlog, Ready, In Progress, Done]
66
+ tasks:
67
+ t1: { title: "First", status: Backlog }
68
+ t2: { title: "Second", status: Backlog, depends_on: [t1] }
69
+ `;
70
+ const board = parseBoardFromString(yaml);
71
+ const out = summarize(board);
72
+ expect(out).toContain("Blocked:");
73
+ expect(out).toContain("t2");
74
+ expect(out).toContain("depends on");
75
+ expect(out).toContain("t1");
76
+ });
77
+
78
+ it("omits Blocked section when all dependencies are in last column", () => {
79
+ const yaml = `
80
+ board: "Unblocked"
81
+ columns: [Backlog, Ready, In Progress, Done]
82
+ tasks:
83
+ t1: { title: "First", status: Done }
84
+ t2: { title: "Second", status: Backlog, depends_on: [t1] }
85
+ `;
86
+ const board = parseBoardFromString(yaml);
87
+ const out = summarize(board);
88
+ expect(out).not.toContain("Blocked:");
89
+ });
90
+
91
+ it("shows (none) for empty tasks", () => {
92
+ const yaml = `
93
+ board: "Empty"
94
+ columns: [Backlog, Ready, In Progress, Done]
95
+ tasks: {}
96
+ `;
97
+ const board = parseBoardFromString(yaml);
98
+ const out = summarize(board);
99
+ expect(out).toContain("Board: Empty");
100
+ expect(out).toContain("Tasks:");
101
+ expect(out).toContain("(none)");
102
+ });
103
+ });
@@ -0,0 +1,153 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseBoardFromString } from "../src/parser.js";
3
+ import { validate } from "../src/validation.js";
4
+
5
+ const validBoardYaml = `
6
+ board: "Valid"
7
+ columns: [Backlog, Ready, In Progress, Done]
8
+ tasks:
9
+ t1: { title: "One", status: Backlog }
10
+ t2: { title: "Two", status: Ready }
11
+ t3: { title: "Three", status: In Progress }
12
+ t4: { title: "Four", status: Done }
13
+ `;
14
+
15
+ describe("validate", () => {
16
+ it("returns valid and empty errors for a board with canonical columns", () => {
17
+ const board = parseBoardFromString(validBoardYaml);
18
+ const result = validate(board);
19
+ expect(result.valid).toBe(true);
20
+ expect(result.errors).toHaveLength(0);
21
+ expect(result.warnings).toEqual([]);
22
+ });
23
+
24
+ it("returns error when columns do not match canonical set", () => {
25
+ const yaml = `
26
+ board: "Test"
27
+ columns: [To Do, Done]
28
+ tasks:
29
+ t1: { title: "Task", status: "To Do" }
30
+ `;
31
+ const board = parseBoardFromString(yaml);
32
+ const result = validate(board);
33
+ expect(result.valid).toBe(false);
34
+ const canonicalError = result.errors.find((e) => e.code === "COLUMNS_NOT_CANONICAL");
35
+ expect(canonicalError).toBeDefined();
36
+ expect(canonicalError!.message).toMatch(/Backlog.*Ready.*In Progress.*Done/);
37
+ });
38
+
39
+ it("returns error when task status does not match any column", () => {
40
+ const yaml = `
41
+ board: "Test"
42
+ columns: [Backlog, Ready, In Progress, Done]
43
+ tasks:
44
+ t1:
45
+ title: "Task"
46
+ status: Unknown Column
47
+ `;
48
+ const board = parseBoardFromString(yaml);
49
+ const result = validate(board);
50
+ expect(result.valid).toBe(false);
51
+ expect(result.errors.length).toBeGreaterThanOrEqual(1);
52
+ const statusError = result.errors.find((e) => e.code === "STATUS_INVALID");
53
+ expect(statusError).toBeDefined();
54
+ expect(statusError!.message).toMatch(/Unknown Column/);
55
+ expect(statusError!.path).toBe("tasks.t1.status");
56
+ });
57
+
58
+ it("returns error when depends_on references non-existent task", () => {
59
+ const yaml = `
60
+ board: "Test"
61
+ columns: [Backlog, Ready, In Progress, Done]
62
+ tasks:
63
+ t1: { title: "One", status: Backlog }
64
+ t2:
65
+ title: "Two"
66
+ status: Done
67
+ depends_on: [NONEXISTENT]
68
+ `;
69
+ const board = parseBoardFromString(yaml);
70
+ const result = validate(board);
71
+ expect(result.valid).toBe(false);
72
+ const refError = result.errors.find((e) => e.code === "DEPENDS_ON_INVALID");
73
+ expect(refError).toBeDefined();
74
+ expect(refError!.message).toMatch(/NONEXISTENT/);
75
+ expect(refError!.path).toBe("tasks.t2.depends_on");
76
+ });
77
+
78
+ it("returns error when WIP limit exceeded", () => {
79
+ const yaml = `
80
+ board: "Test"
81
+ columns:
82
+ - Backlog
83
+ - Ready
84
+ - name: In Progress
85
+ limit: 1
86
+ - Done
87
+ tasks:
88
+ t1: { title: "One", status: In Progress }
89
+ t2: { title: "Two", status: In Progress }
90
+ `;
91
+ const board = parseBoardFromString(yaml);
92
+ const result = validate(board);
93
+ expect(result.valid).toBe(false);
94
+ const wipError = result.errors.find((e) => e.code === "WIP_LIMIT_EXCEEDED");
95
+ expect(wipError).toBeDefined();
96
+ expect(wipError!.message).toMatch(/limit 1.*2 task/);
97
+ expect(wipError!.path).toMatch(/columns/);
98
+ });
99
+
100
+ it("returns error when column names are duplicated", () => {
101
+ const yaml = `
102
+ board: "Test"
103
+ columns: [Backlog, Backlog, In Progress, Done]
104
+ tasks: {}
105
+ `;
106
+ const board = parseBoardFromString(yaml);
107
+ const result = validate(board);
108
+ expect(result.valid).toBe(false);
109
+ const dupError = result.errors.find((e) => e.code === "DUPLICATE_COLUMN");
110
+ const canonicalError = result.errors.find((e) => e.code === "COLUMNS_NOT_CANONICAL");
111
+ expect(dupError ?? canonicalError).toBeDefined();
112
+ if (dupError) {
113
+ expect(dupError.message).toMatch(/Duplicate column name.*"Backlog"/);
114
+ expect(dupError.path).toBe("columns[1]");
115
+ }
116
+ });
117
+
118
+ it("each error has message and optional path/code", () => {
119
+ const yaml = `
120
+ board: "Test"
121
+ columns: [Backlog, Ready, In Progress, Done]
122
+ tasks:
123
+ t1: { title: "One", status: Bad }
124
+ `;
125
+ const board = parseBoardFromString(yaml);
126
+ const result = validate(board);
127
+ expect(result.errors.length).toBeGreaterThanOrEqual(1);
128
+ for (const err of result.errors) {
129
+ expect(err).toHaveProperty("message");
130
+ expect(typeof err.message).toBe("string");
131
+ expect(err.message.length).toBeGreaterThan(0);
132
+ if (err.path !== undefined) expect(typeof err.path).toBe("string");
133
+ if (err.code !== undefined) expect(typeof err.code).toBe("string");
134
+ }
135
+ });
136
+
137
+ it("multiple violations produce multiple errors", () => {
138
+ const yaml = `
139
+ board: "Test"
140
+ columns: [Backlog, Ready, In Progress, Done]
141
+ tasks:
142
+ t1: { title: "One", status: Unknown }
143
+ t2: { title: "Two", status: Backlog, depends_on: [MISSING] }
144
+ `;
145
+ const board = parseBoardFromString(yaml);
146
+ const result = validate(board);
147
+ expect(result.valid).toBe(false);
148
+ expect(result.errors.length).toBeGreaterThanOrEqual(2);
149
+ const codes = result.errors.map((e) => e.code);
150
+ expect(codes).toContain("STATUS_INVALID");
151
+ expect(codes).toContain("DEPENDS_ON_INVALID");
152
+ });
153
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "noEmit": false
7
+ },
8
+ "include": ["src"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,19 @@
1
+ /// <reference types="node" />
2
+ import { readFileSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { defineConfig } from "tsup";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const pkg = JSON.parse(readFileSync(join(__dirname, "package.json"), "utf-8")) as { version: string };
9
+
10
+ export default defineConfig({
11
+ entry: ["src/index.ts"],
12
+ format: ["esm"],
13
+ dts: true,
14
+ sourcemap: true,
15
+ clean: true,
16
+ esbuildOptions(options) {
17
+ options.define = { ...options.define, __STATECRAFT_CORE_VERSION__: JSON.stringify(pkg.version) };
18
+ },
19
+ });
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { resolve } from "path";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ include: ["test/**/*.test.ts"],
7
+ globals: false,
8
+ coverage: {
9
+ provider: "v8",
10
+ reporter: ["text", "text-summary", "html"],
11
+ include: ["src/**/*.ts"],
12
+ exclude: ["src/**/*.test.ts", "test/**", "node_modules", "dist"],
13
+ },
14
+ },
15
+ resolve: {
16
+ alias: {
17
+ "@": resolve(__dirname, "./src"),
18
+ },
19
+ },
20
+ });