@pi-unipi/kanboard 2.0.8 → 2.0.10

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,386 @@
1
+ /**
2
+ * @pi-unipi/kanboard — Remaining Parsers
3
+ *
4
+ * Parsers for quick-work, debug, fix, chore, and review document types.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { DocParser, ParsedDoc, ParsedItem } from "../types.js";
10
+
11
+ /** Parse frontmatter from markdown file */
12
+ function parseFrontmatter(content: string): {
13
+ metadata: Record<string, string>;
14
+ bodyStart: number;
15
+ } {
16
+ const metadata: Record<string, string> = {};
17
+ const lines = content.split("\n");
18
+
19
+ if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
20
+
21
+ for (let i = 1; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ if (line.trim() === "---") {
24
+ return { metadata, bodyStart: i + 1 };
25
+ }
26
+ const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
27
+ if (match) {
28
+ metadata[match[1]] = match[2].trim();
29
+ }
30
+ }
31
+
32
+ return { metadata, bodyStart: 0 };
33
+ }
34
+
35
+ /** Quick-work parser — extracts summary and checklist items */
36
+ export class QuickWorkParser implements DocParser {
37
+ canParse(filePath: string): boolean {
38
+ return /\/quick-work\//.test(filePath) && filePath.endsWith(".md");
39
+ }
40
+
41
+ parse(filePath: string): ParsedDoc {
42
+ const warnings: string[] = [];
43
+ const items: ParsedItem[] = [];
44
+ let content: string;
45
+
46
+ try {
47
+ content = fs.readFileSync(filePath, "utf-8");
48
+ } catch (err: unknown) {
49
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
50
+ return this.emptyDoc(filePath, warnings);
51
+ }
52
+
53
+ const { metadata, bodyStart } = parseFrontmatter(content);
54
+ const lines = content.split("\n");
55
+ const fileName = path.basename(filePath);
56
+
57
+ // Extract checklist items if present
58
+ for (let i = bodyStart; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ const lineNum = i + 1;
61
+
62
+ const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
63
+ if (checkboxMatch) {
64
+ const checked = checkboxMatch[1].toLowerCase() === "x";
65
+ const text = checkboxMatch[2].trim();
66
+ if (text) {
67
+ items.push({
68
+ text,
69
+ status: checked ? "done" : "todo",
70
+ lineNumber: lineNum,
71
+ sourceFile: fileName,
72
+ command: `/unipi:quick-work`,
73
+ });
74
+ }
75
+ }
76
+ }
77
+
78
+ return {
79
+ type: "quick-work",
80
+ title: metadata.title ?? fileName.replace(/\.md$/, ""),
81
+ filePath,
82
+ items,
83
+ metadata,
84
+ warnings,
85
+ };
86
+ }
87
+
88
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
89
+ return {
90
+ type: "quick-work",
91
+ title: path.basename(filePath).replace(/\.md$/, ""),
92
+ filePath,
93
+ items: [],
94
+ metadata: {},
95
+ warnings,
96
+ };
97
+ }
98
+ }
99
+
100
+ /** Debug parser — extracts bug description and status */
101
+ export class DebugParser implements DocParser {
102
+ canParse(filePath: string): boolean {
103
+ return /\/debug\//.test(filePath) && filePath.endsWith(".md");
104
+ }
105
+
106
+ parse(filePath: string): ParsedDoc {
107
+ const warnings: string[] = [];
108
+ const items: ParsedItem[] = [];
109
+ let content: string;
110
+
111
+ try {
112
+ content = fs.readFileSync(filePath, "utf-8");
113
+ } catch (err: unknown) {
114
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
115
+ return this.emptyDoc(filePath, warnings);
116
+ }
117
+
118
+ const { metadata, bodyStart } = parseFrontmatter(content);
119
+ const lines = content.split("\n");
120
+ const fileName = path.basename(filePath);
121
+
122
+ // Extract sections as items
123
+ for (let i = bodyStart; i < lines.length; i++) {
124
+ const line = lines[i];
125
+ const lineNum = i + 1;
126
+
127
+ // Match ## headers as items
128
+ const headerMatch = line.match(/^##\s+(.+)$/);
129
+ if (headerMatch) {
130
+ const text = headerMatch[1].trim();
131
+ items.push({
132
+ text,
133
+ status: "todo",
134
+ lineNumber: lineNum,
135
+ sourceFile: fileName,
136
+ command: `/unipi:fix debug:${fileName}`,
137
+ });
138
+ }
139
+
140
+ // Match checklist items
141
+ const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
142
+ if (checkboxMatch) {
143
+ const checked = checkboxMatch[1].toLowerCase() === "x";
144
+ const text = checkboxMatch[2].trim();
145
+ if (text) {
146
+ items.push({
147
+ text,
148
+ status: checked ? "done" : "todo",
149
+ lineNumber: lineNum,
150
+ sourceFile: fileName,
151
+ command: `/unipi:fix debug:${fileName}`,
152
+ });
153
+ }
154
+ }
155
+ }
156
+
157
+ return {
158
+ type: "debug",
159
+ title: metadata.title ?? fileName.replace(/\.md$/, ""),
160
+ filePath,
161
+ items,
162
+ metadata,
163
+ warnings,
164
+ };
165
+ }
166
+
167
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
168
+ return {
169
+ type: "debug",
170
+ title: path.basename(filePath).replace(/\.md$/, ""),
171
+ filePath,
172
+ items: [],
173
+ metadata: {},
174
+ warnings,
175
+ };
176
+ }
177
+ }
178
+
179
+ /** Fix parser — extracts what was fixed */
180
+ export class FixParser implements DocParser {
181
+ canParse(filePath: string): boolean {
182
+ return /\/fix\//.test(filePath) && filePath.endsWith(".md");
183
+ }
184
+
185
+ parse(filePath: string): ParsedDoc {
186
+ const warnings: string[] = [];
187
+ const items: ParsedItem[] = [];
188
+ let content: string;
189
+
190
+ try {
191
+ content = fs.readFileSync(filePath, "utf-8");
192
+ } catch (err: unknown) {
193
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
194
+ return this.emptyDoc(filePath, warnings);
195
+ }
196
+
197
+ const { metadata, bodyStart } = parseFrontmatter(content);
198
+ const lines = content.split("\n");
199
+ const fileName = path.basename(filePath);
200
+
201
+ // Extract sections and checklist items
202
+ for (let i = bodyStart; i < lines.length; i++) {
203
+ const line = lines[i];
204
+ const lineNum = i + 1;
205
+
206
+ const headerMatch = line.match(/^##\s+(.+)$/);
207
+ if (headerMatch) {
208
+ items.push({
209
+ text: headerMatch[1].trim(),
210
+ status: "done",
211
+ lineNumber: lineNum,
212
+ sourceFile: fileName,
213
+ command: `/unipi:fix`,
214
+ });
215
+ }
216
+
217
+ const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
218
+ if (checkboxMatch) {
219
+ const checked = checkboxMatch[1].toLowerCase() === "x";
220
+ const text = checkboxMatch[2].trim();
221
+ if (text) {
222
+ items.push({
223
+ text,
224
+ status: checked ? "done" : "todo",
225
+ lineNumber: lineNum,
226
+ sourceFile: fileName,
227
+ command: `/unipi:fix`,
228
+ });
229
+ }
230
+ }
231
+ }
232
+
233
+ // Extract related debug reference
234
+ const related = metadata.related_debug ?? metadata.debug ?? "";
235
+
236
+ return {
237
+ type: "fix",
238
+ title: metadata.title ?? fileName.replace(/\.md$/, ""),
239
+ filePath,
240
+ items,
241
+ metadata: { ...metadata, related_debug: related },
242
+ warnings,
243
+ };
244
+ }
245
+
246
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
247
+ return {
248
+ type: "fix",
249
+ title: path.basename(filePath).replace(/\.md$/, ""),
250
+ filePath,
251
+ items: [],
252
+ metadata: {},
253
+ warnings,
254
+ };
255
+ }
256
+ }
257
+
258
+ /** Chore parser — extracts chore name and steps */
259
+ export class ChoreParser implements DocParser {
260
+ canParse(filePath: string): boolean {
261
+ return /\/chore\//.test(filePath) && filePath.endsWith(".md");
262
+ }
263
+
264
+ parse(filePath: string): ParsedDoc {
265
+ const warnings: string[] = [];
266
+ const items: ParsedItem[] = [];
267
+ let content: string;
268
+
269
+ try {
270
+ content = fs.readFileSync(filePath, "utf-8");
271
+ } catch (err: unknown) {
272
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
273
+ return this.emptyDoc(filePath, warnings);
274
+ }
275
+
276
+ const { metadata, bodyStart } = parseFrontmatter(content);
277
+ const lines = content.split("\n");
278
+ const fileName = path.basename(filePath);
279
+
280
+ // Extract checklist items as steps
281
+ for (let i = bodyStart; i < lines.length; i++) {
282
+ const line = lines[i];
283
+ const lineNum = i + 1;
284
+
285
+ const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
286
+ if (checkboxMatch) {
287
+ const checked = checkboxMatch[1].toLowerCase() === "x";
288
+ const text = checkboxMatch[2].trim();
289
+ if (text) {
290
+ items.push({
291
+ text,
292
+ status: checked ? "done" : "todo",
293
+ lineNumber: lineNum,
294
+ sourceFile: fileName,
295
+ command: `/unipi:chore-execute chore:${fileName}`,
296
+ });
297
+ }
298
+ }
299
+ }
300
+
301
+ return {
302
+ type: "chore",
303
+ title: metadata.title ?? metadata.name ?? fileName.replace(/\.md$/, ""),
304
+ filePath,
305
+ items,
306
+ metadata,
307
+ warnings,
308
+ };
309
+ }
310
+
311
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
312
+ return {
313
+ type: "chore",
314
+ title: path.basename(filePath).replace(/\.md$/, ""),
315
+ filePath,
316
+ items: [],
317
+ metadata: {},
318
+ warnings,
319
+ };
320
+ }
321
+ }
322
+
323
+ /** Review parser — extracts review remarks and status */
324
+ export class ReviewParser implements DocParser {
325
+ canParse(filePath: string): boolean {
326
+ return /\/reviews\//.test(filePath) && filePath.endsWith(".md");
327
+ }
328
+
329
+ parse(filePath: string): ParsedDoc {
330
+ const warnings: string[] = [];
331
+ const items: ParsedItem[] = [];
332
+ let content: string;
333
+
334
+ try {
335
+ content = fs.readFileSync(filePath, "utf-8");
336
+ } catch (err: unknown) {
337
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
338
+ return this.emptyDoc(filePath, warnings);
339
+ }
340
+
341
+ const { metadata, bodyStart } = parseFrontmatter(content);
342
+ const lines = content.split("\n");
343
+ const fileName = path.basename(filePath);
344
+
345
+ // Extract checklist items (remarks)
346
+ for (let i = bodyStart; i < lines.length; i++) {
347
+ const line = lines[i];
348
+ const lineNum = i + 1;
349
+
350
+ const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
351
+ if (checkboxMatch) {
352
+ const checked = checkboxMatch[1].toLowerCase() === "x";
353
+ const text = checkboxMatch[2].trim();
354
+ if (text) {
355
+ items.push({
356
+ text,
357
+ status: checked ? "done" : "todo",
358
+ lineNumber: lineNum,
359
+ sourceFile: fileName,
360
+ command: `/unipi:review-work`,
361
+ });
362
+ }
363
+ }
364
+ }
365
+
366
+ return {
367
+ type: "review",
368
+ title: metadata.title ?? fileName.replace(/\.md$/, ""),
369
+ filePath,
370
+ items,
371
+ metadata,
372
+ warnings,
373
+ };
374
+ }
375
+
376
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
377
+ return {
378
+ type: "review",
379
+ title: path.basename(filePath).replace(/\.md$/, ""),
380
+ filePath,
381
+ items: [],
382
+ metadata: {},
383
+ warnings,
384
+ };
385
+ }
386
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @pi-unipi/kanboard — Spec Parser
3
+ *
4
+ * Parses brainstorm specs for `- [ ]` / `- [x]` checklist items.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import type { DocParser, ParsedDoc, ParsedItem } from "../types.js";
10
+
11
+ /** Parse frontmatter from markdown file */
12
+ function parseFrontmatter(content: string): {
13
+ metadata: Record<string, string>;
14
+ bodyStart: number;
15
+ } {
16
+ const metadata: Record<string, string> = {};
17
+ const lines = content.split("\n");
18
+
19
+ if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
20
+
21
+ for (let i = 1; i < lines.length; i++) {
22
+ const line = lines[i];
23
+ if (line.trim() === "---") {
24
+ return { metadata, bodyStart: i + 1 };
25
+ }
26
+ const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
27
+ if (match) {
28
+ metadata[match[1]] = match[2].trim();
29
+ }
30
+ }
31
+
32
+ return { metadata, bodyStart: 0 };
33
+ }
34
+
35
+ /** Spec parser — extracts checklist items from brainstorm specs */
36
+ export class SpecParser implements DocParser {
37
+ canParse(filePath: string): boolean {
38
+ return /\/specs\//.test(filePath) && filePath.endsWith(".md");
39
+ }
40
+
41
+ parse(filePath: string): ParsedDoc {
42
+ const warnings: string[] = [];
43
+ const items: ParsedItem[] = [];
44
+ let content: string;
45
+
46
+ try {
47
+ content = fs.readFileSync(filePath, "utf-8");
48
+ } catch (err: unknown) {
49
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
50
+ return this.emptyDoc(filePath, warnings);
51
+ }
52
+
53
+ const { metadata, bodyStart } = parseFrontmatter(content);
54
+ const lines = content.split("\n");
55
+ const fileName = path.basename(filePath);
56
+
57
+ for (let i = bodyStart; i < lines.length; i++) {
58
+ const line = lines[i];
59
+ const lineNum = i + 1; // 1-indexed
60
+
61
+ // Match `- [ ]` and `- [x]` patterns
62
+ const checkboxMatch = line.match(/^(\s*)-\s*\[([ xX])\]\s*(.*)$/);
63
+ if (checkboxMatch) {
64
+ const checked = checkboxMatch[2].toLowerCase() === "x";
65
+ const text = checkboxMatch[3].trim();
66
+
67
+ if (!text) {
68
+ warnings.push(`Line ${lineNum}: Empty checkbox text`);
69
+ continue;
70
+ }
71
+
72
+ items.push({
73
+ text,
74
+ status: checked ? "done" : "todo",
75
+ lineNumber: lineNum,
76
+ sourceFile: fileName,
77
+ command: `/unipi:plan specs:${fileName}`,
78
+ });
79
+ }
80
+ }
81
+
82
+ return {
83
+ type: "spec",
84
+ title: metadata.title ?? fileName.replace(/\.md$/, ""),
85
+ filePath,
86
+ items,
87
+ metadata,
88
+ warnings,
89
+ };
90
+ }
91
+
92
+ private emptyDoc(
93
+ filePath: string,
94
+ warnings: string[],
95
+ ): ParsedDoc {
96
+ return {
97
+ type: "spec",
98
+ title: path.basename(filePath).replace(/\.md$/, ""),
99
+ filePath,
100
+ items: [],
101
+ metadata: {},
102
+ warnings,
103
+ };
104
+ }
105
+ }