@sudocode-ai/integration-openspec 0.1.14
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/dist/id-generator.d.ts +114 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +165 -0
- package/dist/id-generator.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +692 -0
- package/dist/index.js.map +1 -0
- package/dist/parser/change-parser.d.ts +164 -0
- package/dist/parser/change-parser.d.ts.map +1 -0
- package/dist/parser/change-parser.js +339 -0
- package/dist/parser/change-parser.js.map +1 -0
- package/dist/parser/markdown-utils.d.ts +138 -0
- package/dist/parser/markdown-utils.d.ts.map +1 -0
- package/dist/parser/markdown-utils.js +283 -0
- package/dist/parser/markdown-utils.js.map +1 -0
- package/dist/parser/spec-parser.d.ts +116 -0
- package/dist/parser/spec-parser.d.ts.map +1 -0
- package/dist/parser/spec-parser.js +204 -0
- package/dist/parser/spec-parser.js.map +1 -0
- package/dist/parser/tasks-parser.d.ts +120 -0
- package/dist/parser/tasks-parser.d.ts.map +1 -0
- package/dist/parser/tasks-parser.js +176 -0
- package/dist/parser/tasks-parser.js.map +1 -0
- package/dist/watcher.d.ts +160 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +614 -0
- package/dist/watcher.js.map +1 -0
- package/dist/writer/index.d.ts +9 -0
- package/dist/writer/index.d.ts.map +1 -0
- package/dist/writer/index.js +9 -0
- package/dist/writer/index.js.map +1 -0
- package/dist/writer/spec-writer.d.ts +24 -0
- package/dist/writer/spec-writer.d.ts.map +1 -0
- package/dist/writer/spec-writer.js +75 -0
- package/dist/writer/spec-writer.js.map +1 -0
- package/dist/writer/tasks-writer.d.ts +33 -0
- package/dist/writer/tasks-writer.d.ts.map +1 -0
- package/dist/writer/tasks-writer.js +144 -0
- package/dist/writer/tasks-writer.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenSpec Spec Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses OpenSpec specification files from `openspec/specs/[capability]/spec.md`.
|
|
5
|
+
*
|
|
6
|
+
* OpenSpec spec format:
|
|
7
|
+
* - Title from H1 heading: `# Title`
|
|
8
|
+
* - Purpose section: `## Purpose`
|
|
9
|
+
* - Requirements: `### Requirement: Name`
|
|
10
|
+
* - Scenarios: `#### Scenario: Description` with GIVEN/WHEN/THEN steps
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { extractTitle, extractSection } from "./markdown-utils.js";
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Regex Patterns
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* OpenSpec-specific patterns for parsing spec files
|
|
20
|
+
*/
|
|
21
|
+
export const SPEC_PATTERNS = {
|
|
22
|
+
/** Match requirement header: ### Requirement: Name */
|
|
23
|
+
REQUIREMENT: /^###\s+Requirement:\s*(.+)$/m,
|
|
24
|
+
/** Match scenario header: #### Scenario: Description */
|
|
25
|
+
SCENARIO: /^####\s+Scenario:\s*(.+)$/m,
|
|
26
|
+
/** Match GIVEN step: - **GIVEN** content or **GIVEN**: content */
|
|
27
|
+
GIVEN: /^-?\s*\*\*GIVEN\*\*:?\s*(.+)$/i,
|
|
28
|
+
/** Match WHEN step: - **WHEN** content or **WHEN**: content */
|
|
29
|
+
WHEN: /^-?\s*\*\*WHEN\*\*:?\s*(.+)$/i,
|
|
30
|
+
/** Match THEN step: - **THEN** content or **THEN**: content */
|
|
31
|
+
THEN: /^-?\s*\*\*THEN\*\*:?\s*(.+)$/i,
|
|
32
|
+
};
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// Parser Functions
|
|
35
|
+
// ============================================================================
|
|
36
|
+
/**
|
|
37
|
+
* Parse an OpenSpec spec file
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - Absolute path to the spec.md file
|
|
40
|
+
* @returns Parsed spec object
|
|
41
|
+
* @throws Error if file cannot be read
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const spec = parseSpecFile("/path/to/openspec/specs/cli-init/spec.md");
|
|
45
|
+
* console.log(spec.title); // "CLI Init Specification"
|
|
46
|
+
* console.log(spec.capability); // "cli-init"
|
|
47
|
+
* console.log(spec.requirements); // Array of parsed requirements
|
|
48
|
+
*/
|
|
49
|
+
export function parseSpecFile(filePath) {
|
|
50
|
+
const rawContent = readFileSync(filePath, "utf-8");
|
|
51
|
+
const lines = rawContent.split("\n");
|
|
52
|
+
// Extract capability from directory name
|
|
53
|
+
const capability = extractCapability(filePath);
|
|
54
|
+
// Extract title from H1 heading
|
|
55
|
+
const title = extractTitle(lines) || capability;
|
|
56
|
+
// Extract purpose section
|
|
57
|
+
const purposeLines = extractSection(lines, "Purpose", 2);
|
|
58
|
+
const purpose = purposeLines ? purposeLines.join("\n").trim() : undefined;
|
|
59
|
+
// Parse requirements
|
|
60
|
+
const requirements = parseRequirements(rawContent);
|
|
61
|
+
return {
|
|
62
|
+
capability,
|
|
63
|
+
title,
|
|
64
|
+
purpose,
|
|
65
|
+
requirements,
|
|
66
|
+
rawContent,
|
|
67
|
+
filePath,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract the capability name from the file path
|
|
72
|
+
*
|
|
73
|
+
* Assumes path structure: .../specs/[capability]/spec.md
|
|
74
|
+
*
|
|
75
|
+
* @param filePath - Path to the spec file
|
|
76
|
+
* @returns Capability directory name
|
|
77
|
+
*/
|
|
78
|
+
export function extractCapability(filePath) {
|
|
79
|
+
const parts = filePath.split(path.sep);
|
|
80
|
+
const specIndex = parts.lastIndexOf("spec.md");
|
|
81
|
+
if (specIndex > 0) {
|
|
82
|
+
return parts[specIndex - 1];
|
|
83
|
+
}
|
|
84
|
+
// Fallback: use parent directory name
|
|
85
|
+
return path.basename(path.dirname(filePath));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse all requirements from spec content
|
|
89
|
+
*
|
|
90
|
+
* @param content - Full markdown content
|
|
91
|
+
* @returns Array of parsed requirements
|
|
92
|
+
*/
|
|
93
|
+
export function parseRequirements(content) {
|
|
94
|
+
const requirements = [];
|
|
95
|
+
const lines = content.split("\n");
|
|
96
|
+
// Find all requirement header positions
|
|
97
|
+
const requirementPositions = [];
|
|
98
|
+
for (let i = 0; i < lines.length; i++) {
|
|
99
|
+
const match = lines[i].match(SPEC_PATTERNS.REQUIREMENT);
|
|
100
|
+
if (match) {
|
|
101
|
+
requirementPositions.push({
|
|
102
|
+
name: match[1].trim(),
|
|
103
|
+
lineIndex: i,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Extract content for each requirement
|
|
108
|
+
for (let i = 0; i < requirementPositions.length; i++) {
|
|
109
|
+
const req = requirementPositions[i];
|
|
110
|
+
const startLine = req.lineIndex + 1;
|
|
111
|
+
// End at next requirement or end of file
|
|
112
|
+
const endLine = i < requirementPositions.length - 1
|
|
113
|
+
? requirementPositions[i + 1].lineIndex
|
|
114
|
+
: lines.length;
|
|
115
|
+
// Get requirement content (excluding the header line)
|
|
116
|
+
const requirementLines = lines.slice(startLine, endLine);
|
|
117
|
+
const requirementContent = requirementLines.join("\n").trim();
|
|
118
|
+
// Parse scenarios within this requirement
|
|
119
|
+
const scenarios = parseScenarios(requirementContent);
|
|
120
|
+
requirements.push({
|
|
121
|
+
name: req.name,
|
|
122
|
+
content: requirementContent,
|
|
123
|
+
scenarios,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return requirements;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Parse scenarios from requirement content
|
|
130
|
+
*
|
|
131
|
+
* @param content - Requirement section content
|
|
132
|
+
* @returns Array of parsed scenarios
|
|
133
|
+
*/
|
|
134
|
+
export function parseScenarios(content) {
|
|
135
|
+
const scenarios = [];
|
|
136
|
+
const lines = content.split("\n");
|
|
137
|
+
// Find all scenario header positions
|
|
138
|
+
const scenarioPositions = [];
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const match = lines[i].match(SPEC_PATTERNS.SCENARIO);
|
|
141
|
+
if (match) {
|
|
142
|
+
scenarioPositions.push({
|
|
143
|
+
description: match[1].trim(),
|
|
144
|
+
lineIndex: i,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Extract content for each scenario
|
|
149
|
+
for (let i = 0; i < scenarioPositions.length; i++) {
|
|
150
|
+
const scenario = scenarioPositions[i];
|
|
151
|
+
const startLine = scenario.lineIndex + 1;
|
|
152
|
+
// End at next scenario or end of content
|
|
153
|
+
const endLine = i < scenarioPositions.length - 1
|
|
154
|
+
? scenarioPositions[i + 1].lineIndex
|
|
155
|
+
: lines.length;
|
|
156
|
+
// Get scenario content
|
|
157
|
+
const scenarioLines = lines.slice(startLine, endLine);
|
|
158
|
+
// Parse GIVEN/WHEN/THEN steps
|
|
159
|
+
const { given, when, then } = parseGivenWhenThen(scenarioLines);
|
|
160
|
+
scenarios.push({
|
|
161
|
+
description: scenario.description,
|
|
162
|
+
given: given.length > 0 ? given : undefined,
|
|
163
|
+
when: when.length > 0 ? when : undefined,
|
|
164
|
+
then: then.length > 0 ? then : undefined,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return scenarios;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Parse GIVEN/WHEN/THEN steps from scenario lines
|
|
171
|
+
*
|
|
172
|
+
* @param lines - Lines within a scenario section
|
|
173
|
+
* @returns Object with given, when, then arrays
|
|
174
|
+
*/
|
|
175
|
+
export function parseGivenWhenThen(lines) {
|
|
176
|
+
const given = [];
|
|
177
|
+
const when = [];
|
|
178
|
+
const then = [];
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (!trimmed)
|
|
182
|
+
continue;
|
|
183
|
+
// Check for GIVEN
|
|
184
|
+
const givenMatch = trimmed.match(SPEC_PATTERNS.GIVEN);
|
|
185
|
+
if (givenMatch) {
|
|
186
|
+
given.push(givenMatch[1].trim());
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Check for WHEN
|
|
190
|
+
const whenMatch = trimmed.match(SPEC_PATTERNS.WHEN);
|
|
191
|
+
if (whenMatch) {
|
|
192
|
+
when.push(whenMatch[1].trim());
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
// Check for THEN
|
|
196
|
+
const thenMatch = trimmed.match(SPEC_PATTERNS.THEN);
|
|
197
|
+
if (thenMatch) {
|
|
198
|
+
then.push(thenMatch[1].trim());
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return { given, when, then };
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=spec-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec-parser.js","sourceRoot":"","sources":["../../src/parser/spec-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks Parser for OpenSpec Integration
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.md files from OpenSpec change directories and extracts tasks.
|
|
5
|
+
*
|
|
6
|
+
* OpenSpec tasks.md format (simpler than spec-kit):
|
|
7
|
+
* ```markdown
|
|
8
|
+
* # Tasks
|
|
9
|
+
*
|
|
10
|
+
* - [ ] First task to complete
|
|
11
|
+
* - [x] Completed task
|
|
12
|
+
* - [ ] Another incomplete task
|
|
13
|
+
* - [ ] Nested subtask (if any)
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Task format: `- [ ] Description`
|
|
17
|
+
* - Checkbox: `[ ]` (incomplete) or `[x]` (complete)
|
|
18
|
+
* - Description: Rest of the line
|
|
19
|
+
* - Indentation: Optional nesting via leading spaces
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Parsed task from an OpenSpec tasks.md file
|
|
23
|
+
*/
|
|
24
|
+
export interface ParsedTask {
|
|
25
|
+
/** Task description */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Whether the task is completed */
|
|
28
|
+
completed: boolean;
|
|
29
|
+
/** Line number in the source file (1-indexed) */
|
|
30
|
+
lineNumber: number;
|
|
31
|
+
/** Indentation level (for nested tasks) */
|
|
32
|
+
indentLevel: number;
|
|
33
|
+
/** Raw line from the file */
|
|
34
|
+
rawLine: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parsed result from an OpenSpec tasks.md file
|
|
38
|
+
*/
|
|
39
|
+
export interface ParsedTasksFile {
|
|
40
|
+
/** Array of parsed tasks */
|
|
41
|
+
tasks: ParsedTask[];
|
|
42
|
+
/** Source file path */
|
|
43
|
+
filePath: string;
|
|
44
|
+
/** Summary statistics */
|
|
45
|
+
stats: {
|
|
46
|
+
total: number;
|
|
47
|
+
completed: number;
|
|
48
|
+
incomplete: number;
|
|
49
|
+
};
|
|
50
|
+
/** Completion percentage (0-100) */
|
|
51
|
+
completionPercentage: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Regex patterns for OpenSpec task parsing
|
|
55
|
+
*/
|
|
56
|
+
export declare const TASK_PATTERNS: {
|
|
57
|
+
/** Match task line: - [ ] Description or - [x] Description (requires space after hyphen) */
|
|
58
|
+
TASK_LINE: RegExp;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Parse an OpenSpec tasks.md file and extract all tasks
|
|
62
|
+
*
|
|
63
|
+
* @param filePath - Absolute path to the tasks.md file
|
|
64
|
+
* @returns Parsed tasks data or null if file doesn't exist
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* const tasksFile = parseTasks("/project/openspec/changes/add-feature/tasks.md");
|
|
68
|
+
* console.log(tasksFile?.tasks.length); // 5
|
|
69
|
+
* console.log(tasksFile?.completionPercentage); // 40
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseTasks(filePath: string): ParsedTasksFile | null;
|
|
72
|
+
/**
|
|
73
|
+
* Parse tasks content from a string (for testing or in-memory parsing)
|
|
74
|
+
*
|
|
75
|
+
* @param content - Markdown content string
|
|
76
|
+
* @param filePath - Optional file path for reference
|
|
77
|
+
* @returns Parsed tasks data
|
|
78
|
+
*/
|
|
79
|
+
export declare function parseTasksContent(content: string, filePath?: string): ParsedTasksFile;
|
|
80
|
+
/**
|
|
81
|
+
* Get all tasks from a file as a simple array
|
|
82
|
+
*
|
|
83
|
+
* @param filePath - Path to the tasks file
|
|
84
|
+
* @returns Array of parsed tasks or empty array
|
|
85
|
+
*/
|
|
86
|
+
export declare function getAllTasks(filePath: string): ParsedTask[];
|
|
87
|
+
/**
|
|
88
|
+
* Get incomplete tasks from a file
|
|
89
|
+
*
|
|
90
|
+
* @param filePath - Path to the tasks file
|
|
91
|
+
* @returns Array of incomplete tasks
|
|
92
|
+
*/
|
|
93
|
+
export declare function getIncompleteTasks(filePath: string): ParsedTask[];
|
|
94
|
+
/**
|
|
95
|
+
* Get task completion statistics
|
|
96
|
+
*
|
|
97
|
+
* @param filePath - Path to the tasks file
|
|
98
|
+
* @returns Statistics object or null
|
|
99
|
+
*/
|
|
100
|
+
export declare function getTaskStats(filePath: string): {
|
|
101
|
+
total: number;
|
|
102
|
+
completed: number;
|
|
103
|
+
incomplete: number;
|
|
104
|
+
completionPercentage: number;
|
|
105
|
+
} | null;
|
|
106
|
+
/**
|
|
107
|
+
* Calculate completion percentage from tasks array
|
|
108
|
+
*
|
|
109
|
+
* @param tasks - Array of parsed tasks
|
|
110
|
+
* @returns Completion percentage (0-100)
|
|
111
|
+
*/
|
|
112
|
+
export declare function calculateCompletionPercentage(tasks: ParsedTask[]): number;
|
|
113
|
+
/**
|
|
114
|
+
* Check if a file appears to be a valid tasks.md file
|
|
115
|
+
*
|
|
116
|
+
* @param filePath - Path to check
|
|
117
|
+
* @returns true if the file looks like a tasks file
|
|
118
|
+
*/
|
|
119
|
+
export declare function isTasksFile(filePath: string): boolean;
|
|
120
|
+
//# sourceMappingURL=tasks-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-parser.d.ts","sourceRoot":"","sources":["../../src/parser/tasks-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,uBAAuB;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,oCAAoC;IACpC,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,6BAA6B;IAC7B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,4BAA4B;IAC5B,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,uBAAuB;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,yBAAyB;IACzB,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IACF,oCAAoC;IACpC,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,4FAA4F;;CAE7F,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CAYnE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAmB,GAC5B,eAAe,CAkCjB;AAwBD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAG1D;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,EAAE,CAGjE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,oBAAoB,EAAE,MAAM,CAAC;CAC9B,GAAG,IAAI,CAQP;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAIzE;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAoBrD"}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tasks Parser for OpenSpec Integration
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.md files from OpenSpec change directories and extracts tasks.
|
|
5
|
+
*
|
|
6
|
+
* OpenSpec tasks.md format (simpler than spec-kit):
|
|
7
|
+
* ```markdown
|
|
8
|
+
* # Tasks
|
|
9
|
+
*
|
|
10
|
+
* - [ ] First task to complete
|
|
11
|
+
* - [x] Completed task
|
|
12
|
+
* - [ ] Another incomplete task
|
|
13
|
+
* - [ ] Nested subtask (if any)
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Task format: `- [ ] Description`
|
|
17
|
+
* - Checkbox: `[ ]` (incomplete) or `[x]` (complete)
|
|
18
|
+
* - Description: Rest of the line
|
|
19
|
+
* - Indentation: Optional nesting via leading spaces
|
|
20
|
+
*/
|
|
21
|
+
import { readFileSync, existsSync } from "fs";
|
|
22
|
+
/**
|
|
23
|
+
* Regex patterns for OpenSpec task parsing
|
|
24
|
+
*/
|
|
25
|
+
export const TASK_PATTERNS = {
|
|
26
|
+
/** Match task line: - [ ] Description or - [x] Description (requires space after hyphen) */
|
|
27
|
+
TASK_LINE: /^(\s*)-\s+\[([ xX])\]\s*(.+)$/,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Parse an OpenSpec tasks.md file and extract all tasks
|
|
31
|
+
*
|
|
32
|
+
* @param filePath - Absolute path to the tasks.md file
|
|
33
|
+
* @returns Parsed tasks data or null if file doesn't exist
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const tasksFile = parseTasks("/project/openspec/changes/add-feature/tasks.md");
|
|
37
|
+
* console.log(tasksFile?.tasks.length); // 5
|
|
38
|
+
* console.log(tasksFile?.completionPercentage); // 40
|
|
39
|
+
*/
|
|
40
|
+
export function parseTasks(filePath) {
|
|
41
|
+
if (!existsSync(filePath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const content = readFileSync(filePath, "utf-8");
|
|
46
|
+
return parseTasksContent(content, filePath);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`[tasks-parser] Failed to parse ${filePath}:`, error);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Parse tasks content from a string (for testing or in-memory parsing)
|
|
55
|
+
*
|
|
56
|
+
* @param content - Markdown content string
|
|
57
|
+
* @param filePath - Optional file path for reference
|
|
58
|
+
* @returns Parsed tasks data
|
|
59
|
+
*/
|
|
60
|
+
export function parseTasksContent(content, filePath = "<string>") {
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
const tasks = [];
|
|
63
|
+
// Parse each line
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const line = lines[i];
|
|
66
|
+
const lineNumber = i + 1; // 1-indexed
|
|
67
|
+
// Check for task line
|
|
68
|
+
const taskMatch = line.match(TASK_PATTERNS.TASK_LINE);
|
|
69
|
+
if (taskMatch) {
|
|
70
|
+
const task = parseTaskLine(line, taskMatch, lineNumber);
|
|
71
|
+
tasks.push(task);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Calculate stats
|
|
75
|
+
const stats = {
|
|
76
|
+
total: tasks.length,
|
|
77
|
+
completed: tasks.filter((t) => t.completed).length,
|
|
78
|
+
incomplete: tasks.filter((t) => !t.completed).length,
|
|
79
|
+
};
|
|
80
|
+
// Calculate completion percentage
|
|
81
|
+
const completionPercentage = stats.total > 0 ? Math.round((stats.completed / stats.total) * 100) : 0;
|
|
82
|
+
return {
|
|
83
|
+
tasks,
|
|
84
|
+
filePath,
|
|
85
|
+
stats,
|
|
86
|
+
completionPercentage,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parse a single task line
|
|
91
|
+
*/
|
|
92
|
+
function parseTaskLine(line, match, lineNumber) {
|
|
93
|
+
const [, leadingSpace, checkbox, description] = match;
|
|
94
|
+
// Calculate indent level (2 spaces = 1 level)
|
|
95
|
+
const indentLevel = Math.floor(leadingSpace.length / 2);
|
|
96
|
+
return {
|
|
97
|
+
description: description.trim(),
|
|
98
|
+
completed: checkbox.toLowerCase() === "x",
|
|
99
|
+
lineNumber,
|
|
100
|
+
indentLevel,
|
|
101
|
+
rawLine: line,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get all tasks from a file as a simple array
|
|
106
|
+
*
|
|
107
|
+
* @param filePath - Path to the tasks file
|
|
108
|
+
* @returns Array of parsed tasks or empty array
|
|
109
|
+
*/
|
|
110
|
+
export function getAllTasks(filePath) {
|
|
111
|
+
const result = parseTasks(filePath);
|
|
112
|
+
return result?.tasks || [];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get incomplete tasks from a file
|
|
116
|
+
*
|
|
117
|
+
* @param filePath - Path to the tasks file
|
|
118
|
+
* @returns Array of incomplete tasks
|
|
119
|
+
*/
|
|
120
|
+
export function getIncompleteTasks(filePath) {
|
|
121
|
+
const tasks = getAllTasks(filePath);
|
|
122
|
+
return tasks.filter((t) => !t.completed);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get task completion statistics
|
|
126
|
+
*
|
|
127
|
+
* @param filePath - Path to the tasks file
|
|
128
|
+
* @returns Statistics object or null
|
|
129
|
+
*/
|
|
130
|
+
export function getTaskStats(filePath) {
|
|
131
|
+
const result = parseTasks(filePath);
|
|
132
|
+
if (!result)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
...result.stats,
|
|
136
|
+
completionPercentage: result.completionPercentage,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Calculate completion percentage from tasks array
|
|
141
|
+
*
|
|
142
|
+
* @param tasks - Array of parsed tasks
|
|
143
|
+
* @returns Completion percentage (0-100)
|
|
144
|
+
*/
|
|
145
|
+
export function calculateCompletionPercentage(tasks) {
|
|
146
|
+
if (tasks.length === 0)
|
|
147
|
+
return 0;
|
|
148
|
+
const completed = tasks.filter((t) => t.completed).length;
|
|
149
|
+
return Math.round((completed / tasks.length) * 100);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if a file appears to be a valid tasks.md file
|
|
153
|
+
*
|
|
154
|
+
* @param filePath - Path to check
|
|
155
|
+
* @returns true if the file looks like a tasks file
|
|
156
|
+
*/
|
|
157
|
+
export function isTasksFile(filePath) {
|
|
158
|
+
if (!existsSync(filePath)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const content = readFileSync(filePath, "utf-8");
|
|
163
|
+
const lines = content.split("\n").slice(0, 30); // Check first 30 lines
|
|
164
|
+
// Look for task-like lines
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
if (TASK_PATTERNS.TASK_LINE.test(line)) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=tasks-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tasks-parser.js","sourceRoot":"","sources":["../../src/parser/tasks-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher for OpenSpec integration
|
|
3
|
+
*
|
|
4
|
+
* Watches the OpenSpec directory for changes to spec files and change directories.
|
|
5
|
+
* Detects which entities were created, updated, or deleted.
|
|
6
|
+
*
|
|
7
|
+
* Watch patterns:
|
|
8
|
+
* - specs/ directory (all .md files) - Spec file changes
|
|
9
|
+
* - changes/<name>/proposal.md - Change proposal updates
|
|
10
|
+
* - changes/<name>/tasks.md - Change task updates
|
|
11
|
+
* - changes/archive/<name>/ - Archived change detection
|
|
12
|
+
* - changes/<name>/specs/<cap>/ - Delta directory changes
|
|
13
|
+
*/
|
|
14
|
+
import type { ExternalChange, ExternalEntity } from "@sudocode-ai/types";
|
|
15
|
+
/**
|
|
16
|
+
* Callback type for change notifications
|
|
17
|
+
*/
|
|
18
|
+
export type ChangeCallback = (changes: ExternalChange[]) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Options for the OpenSpecWatcher
|
|
21
|
+
*/
|
|
22
|
+
export interface OpenSpecWatcherOptions {
|
|
23
|
+
/** Path to the OpenSpec directory */
|
|
24
|
+
openspecPath: string;
|
|
25
|
+
/** Prefix for spec IDs (default: "os") */
|
|
26
|
+
specPrefix?: string;
|
|
27
|
+
/** Prefix for change IDs (default: "osc") */
|
|
28
|
+
changePrefix?: string;
|
|
29
|
+
/** Include archived changes in tracking (default: true) */
|
|
30
|
+
trackArchived?: boolean;
|
|
31
|
+
/** Debounce interval in milliseconds (default: 100) */
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* OpenSpecWatcher monitors the OpenSpec directory for changes
|
|
36
|
+
*
|
|
37
|
+
* Uses content hashing to detect actual changes vs just file touches.
|
|
38
|
+
* This prevents false positives from atomic writes and other file operations.
|
|
39
|
+
*/
|
|
40
|
+
export declare class OpenSpecWatcher {
|
|
41
|
+
private watcher;
|
|
42
|
+
private entityHashes;
|
|
43
|
+
private callback;
|
|
44
|
+
private isProcessing;
|
|
45
|
+
private debounceTimer;
|
|
46
|
+
private isRelevantFile;
|
|
47
|
+
private pendingArchiveMoves;
|
|
48
|
+
private readonly ARCHIVE_MOVE_WINDOW_MS;
|
|
49
|
+
private readonly openspecPath;
|
|
50
|
+
private readonly specPrefix;
|
|
51
|
+
private readonly changePrefix;
|
|
52
|
+
private readonly trackArchived;
|
|
53
|
+
private readonly debounceMs;
|
|
54
|
+
constructor(options: OpenSpecWatcherOptions);
|
|
55
|
+
/**
|
|
56
|
+
* Update the cached hash for a specific entity after we wrote to it.
|
|
57
|
+
* This prevents the watcher from detecting our own writes as changes.
|
|
58
|
+
*/
|
|
59
|
+
updateEntityHash(entityId: string, hash: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Remove an entity from the hash cache (after deletion)
|
|
62
|
+
*/
|
|
63
|
+
removeEntityHash(entityId: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Start watching for changes
|
|
66
|
+
*
|
|
67
|
+
* @param callback - Function to call when changes are detected
|
|
68
|
+
*/
|
|
69
|
+
start(callback: ChangeCallback): void;
|
|
70
|
+
/**
|
|
71
|
+
* Stop watching for changes
|
|
72
|
+
*/
|
|
73
|
+
stop(): void;
|
|
74
|
+
/**
|
|
75
|
+
* Check if watcher is active
|
|
76
|
+
*/
|
|
77
|
+
isWatching(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Handle file change event
|
|
80
|
+
*/
|
|
81
|
+
private handleFileChange;
|
|
82
|
+
/**
|
|
83
|
+
* Handle file deleted event
|
|
84
|
+
*/
|
|
85
|
+
private handleFileDeleted;
|
|
86
|
+
/**
|
|
87
|
+
* Handle directory added event
|
|
88
|
+
* Used to detect archive moves (change moved to archive/)
|
|
89
|
+
*/
|
|
90
|
+
private handleDirectoryAdded;
|
|
91
|
+
/**
|
|
92
|
+
* Handle directory removed event
|
|
93
|
+
* Used to detect archive moves (change moved from changes/)
|
|
94
|
+
*/
|
|
95
|
+
private handleDirectoryRemoved;
|
|
96
|
+
/**
|
|
97
|
+
* Extract change name from an archive directory path
|
|
98
|
+
* Archive pattern: changes/archive/YYYY-MM-DD-name/ or changes/archive/name/
|
|
99
|
+
*/
|
|
100
|
+
private extractChangeNameFromArchive;
|
|
101
|
+
/**
|
|
102
|
+
* Clean up old pending archive move entries
|
|
103
|
+
*/
|
|
104
|
+
private cleanupPendingArchiveMoves;
|
|
105
|
+
/**
|
|
106
|
+
* Schedule change processing with debouncing
|
|
107
|
+
*/
|
|
108
|
+
private scheduleProcessChanges;
|
|
109
|
+
/**
|
|
110
|
+
* Process changes by comparing current state to cached hashes
|
|
111
|
+
*/
|
|
112
|
+
private processChanges;
|
|
113
|
+
/**
|
|
114
|
+
* Capture current state (entity hashes) for comparison
|
|
115
|
+
*/
|
|
116
|
+
captureState(): void;
|
|
117
|
+
/**
|
|
118
|
+
* Detect changes by comparing current state to cached state
|
|
119
|
+
*/
|
|
120
|
+
private detectChanges;
|
|
121
|
+
/**
|
|
122
|
+
* Scan all entities in the OpenSpec directory
|
|
123
|
+
* IMPORTANT: Returns specs FIRST, then issues to ensure proper relationship resolution
|
|
124
|
+
*/
|
|
125
|
+
private scanAllEntities;
|
|
126
|
+
/**
|
|
127
|
+
* Compute a hash for an entity to detect changes
|
|
128
|
+
*/
|
|
129
|
+
computeEntityHash(entity: ExternalEntity): string;
|
|
130
|
+
/**
|
|
131
|
+
* Convert a parsed OpenSpec spec to ExternalEntity
|
|
132
|
+
*/
|
|
133
|
+
private specToExternalEntity;
|
|
134
|
+
/**
|
|
135
|
+
* Convert a proposed spec (from changes/[name]/specs/) to ExternalEntity
|
|
136
|
+
*
|
|
137
|
+
* Proposed specs are NEW specs that don't exist in openspec/specs/ yet.
|
|
138
|
+
* They are marked with isProposed: true in the raw data.
|
|
139
|
+
*/
|
|
140
|
+
private proposedSpecToExternalEntity;
|
|
141
|
+
/**
|
|
142
|
+
* Convert a parsed OpenSpec change to ExternalEntity (as issue)
|
|
143
|
+
*
|
|
144
|
+
* Changes map to sudocode Issues:
|
|
145
|
+
* - Archived changes → status: "closed"
|
|
146
|
+
* - Active changes with 100% task completion → status: "needs_review"
|
|
147
|
+
* - Active changes with progress → status: "in_progress"
|
|
148
|
+
* - Active changes with no progress → status: "open"
|
|
149
|
+
*/
|
|
150
|
+
private changeToExternalEntity;
|
|
151
|
+
/**
|
|
152
|
+
* Get current cached hashes (for testing/debugging)
|
|
153
|
+
*/
|
|
154
|
+
getEntityHashes(): Map<string, string>;
|
|
155
|
+
/**
|
|
156
|
+
* Force refresh of cached state (useful after external sync)
|
|
157
|
+
*/
|
|
158
|
+
refreshState(): void;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAgBzE;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,YAAY,CAAkC;IACtD,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,aAAa,CAA+B;IACpD,OAAO,CAAC,cAAc,CAAgD;IAGtE,OAAO,CAAC,mBAAmB,CAGb;IACd,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAO;IAE9C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAU;IACxC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;gBAExB,OAAO,EAAE,sBAAsB;IAQ3C;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAOtD;;OAEG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAOxC;;;;OAIG;IACH,KAAK,CAAC,QAAQ,EAAE,cAAc,GAAG,IAAI;IAyGrC;;OAEG;IACH,IAAI,IAAI,IAAI;IAcZ;;OAEG;IACH,UAAU,IAAI,OAAO;IAIrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IASxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IASzB;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAgC5B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAgC9B;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IAapC;;OAEG;IACH,OAAO,CAAC,0BAA0B;IASlC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,OAAO,CAAC,cAAc;IAiCtB;;OAEG;IACH,YAAY,IAAI,IAAI;IAcpB;;OAEG;IACH,OAAO,CAAC,aAAa;IAuDrB;;;OAGG;IACH,OAAO,CAAC,eAAe;IA2GvB;;OAEG;IACH,iBAAiB,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;IAYjD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA2B5B;;;;;OAKG;IACH,OAAO,CAAC,4BAA4B;IA8BpC;;;;;;;;OAQG;IACH,OAAO,CAAC,sBAAsB;IAqE9B;;OAEG;IACH,eAAe,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;IAItC;;OAEG;IACH,YAAY,IAAI,IAAI;CAGrB"}
|