@zhuan-ai/zhuanspec 1.3.0 → 2.1.0
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.zh.md +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/commands/artifact-workflow.js +45 -2
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.js +160 -4
- package/dist/core/skill-discovery.d.ts +2 -2
- package/dist/core/skill-discovery.js +16 -3
- package/dist/core/task-graph/execution-planner.d.ts +17 -0
- package/dist/core/task-graph/execution-planner.js +119 -0
- package/dist/core/task-graph/index.d.ts +13 -0
- package/dist/core/task-graph/index.js +16 -0
- package/dist/core/task-graph/mermaid-renderer.d.ts +22 -0
- package/dist/core/task-graph/mermaid-renderer.js +128 -0
- package/dist/core/task-graph/task-graph.d.ts +74 -0
- package/dist/core/task-graph/task-graph.js +219 -0
- package/dist/core/task-graph/task-parser.d.ts +14 -0
- package/dist/core/task-graph/task-parser.js +111 -0
- package/dist/core/task-graph/types.d.ts +53 -0
- package/dist/core/task-graph/types.js +7 -0
- package/dist/core/task-graph/xml-renderer.d.ts +21 -0
- package/dist/core/task-graph/xml-renderer.js +81 -0
- package/dist/core/templates/agents-template.d.ts +1 -1
- package/dist/core/templates/agents-template.js +23 -0
- package/dist/core/templates/skill-templates.js +42 -0
- package/dist/core/templates/slash-command-templates.js +25 -2
- package/dist/core/templates/tasks-template.d.ts +23 -0
- package/dist/core/templates/tasks-template.js +79 -0
- package/dist/core/templates/tdd-tasks-template.d.ts +24 -0
- package/dist/core/templates/tdd-tasks-template.js +116 -0
- package/dist/core/validation/strict-rules.d.ts +60 -0
- package/dist/core/validation/strict-rules.js +287 -0
- package/dist/core/validation/types.d.ts +10 -0
- package/dist/core/validation/validator.d.ts +5 -0
- package/dist/core/validation/validator.js +103 -1
- package/package.json +22 -20
- package/schemas/spec-driven/templates/tasks.md +17 -5
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid Renderer for Execution Plan
|
|
3
|
+
*
|
|
4
|
+
* Renders ExecutionPlan to Mermaid flowchart format for visualization in tasks.md.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Escapes special Mermaid characters in a string.
|
|
8
|
+
* Mermaid uses quotes, brackets, and other characters that need escaping.
|
|
9
|
+
*/
|
|
10
|
+
function escapeMermaid(str) {
|
|
11
|
+
return str
|
|
12
|
+
.replace(/"/g, '#quot;')
|
|
13
|
+
.replace(/\[/g, '#91;')
|
|
14
|
+
.replace(/\]/g, '#93;')
|
|
15
|
+
.replace(/\(/g, '#40;')
|
|
16
|
+
.replace(/\)/g, '#41;')
|
|
17
|
+
.replace(/</g, '#lt;')
|
|
18
|
+
.replace(/>/g, '#gt;')
|
|
19
|
+
.replace(/\{/g, '#123;')
|
|
20
|
+
.replace(/\}/g, '#125;')
|
|
21
|
+
.replace(/\|/g, '#124;');
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts a task ID to a valid Mermaid node ID.
|
|
25
|
+
* Mermaid node IDs cannot contain dots or hyphens.
|
|
26
|
+
*
|
|
27
|
+
* Example: "1.1" -> "T1_1", "2.3" -> "T2_3"
|
|
28
|
+
*/
|
|
29
|
+
function toNodeId(taskId) {
|
|
30
|
+
return 'T' + taskId.replace(/\./g, '_').replace(/-/g, '_');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Truncates a string to a maximum length, appending "..." if truncated.
|
|
34
|
+
*/
|
|
35
|
+
function truncate(str, maxLength) {
|
|
36
|
+
if (str.length <= maxLength) {
|
|
37
|
+
return str;
|
|
38
|
+
}
|
|
39
|
+
return str.slice(0, maxLength) + '...';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Generates the wave subtitle based on task count and max parallelism.
|
|
43
|
+
*/
|
|
44
|
+
function getWaveSubtitle(taskCount, maxParallelism) {
|
|
45
|
+
if (taskCount === 1) {
|
|
46
|
+
return '串行';
|
|
47
|
+
}
|
|
48
|
+
if (taskCount <= maxParallelism) {
|
|
49
|
+
return `并行度: ${taskCount}`;
|
|
50
|
+
}
|
|
51
|
+
// Calculate batch count: ceil(taskCount / maxParallelism)
|
|
52
|
+
const batchCount = Math.ceil(taskCount / maxParallelism);
|
|
53
|
+
return `并行度: ${taskCount}, 建议分批: ${batchCount}批`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Renders a single task node label.
|
|
57
|
+
*/
|
|
58
|
+
function renderNodeLabel(task, showSkills) {
|
|
59
|
+
const truncatedDesc = truncate(task.description, 30);
|
|
60
|
+
let label = `${task.id} ${truncatedDesc}`;
|
|
61
|
+
if (showSkills && task.skills.length > 0) {
|
|
62
|
+
// Show first skill
|
|
63
|
+
label += `\\n@skill:${task.skills[0]}`;
|
|
64
|
+
}
|
|
65
|
+
return escapeMermaid(label);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Renders an ExecutionPlan to Mermaid flowchart format.
|
|
69
|
+
*
|
|
70
|
+
* @param plan - The execution plan to render
|
|
71
|
+
* @param tasks - The parsed tasks (used for task details)
|
|
72
|
+
* @param options - Render options
|
|
73
|
+
* @returns Complete markdown section with Mermaid diagram
|
|
74
|
+
*/
|
|
75
|
+
export function renderMermaidDiagram(plan, tasks, options) {
|
|
76
|
+
const maxParallelism = options?.maxParallelism ?? 3;
|
|
77
|
+
const showSkills = options?.showSkills ?? true;
|
|
78
|
+
// Handle error cases - return empty string for cycle
|
|
79
|
+
if (plan.hasCycle) {
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
// Handle empty plan (all tasks complete or no tasks)
|
|
83
|
+
if (plan.waves.length === 0) {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
// Build task map for quick lookup
|
|
87
|
+
const taskMap = new Map();
|
|
88
|
+
for (const task of tasks) {
|
|
89
|
+
taskMap.set(task.id, task);
|
|
90
|
+
}
|
|
91
|
+
const lines = [];
|
|
92
|
+
// Markdown header with auto-generated notice
|
|
93
|
+
lines.push('## Workflow Diagram');
|
|
94
|
+
lines.push('');
|
|
95
|
+
lines.push('<!-- Auto-generated by zhuanspec validate. DO NOT edit manually. -->');
|
|
96
|
+
lines.push('');
|
|
97
|
+
lines.push('```mermaid');
|
|
98
|
+
lines.push('graph TD');
|
|
99
|
+
// Render each wave as a subgraph
|
|
100
|
+
for (const wave of plan.waves) {
|
|
101
|
+
const subtitle = getWaveSubtitle(wave.tasks.length, maxParallelism);
|
|
102
|
+
lines.push(` subgraph Wave${wave.wave}["Wave ${wave.wave} (${subtitle})"]`);
|
|
103
|
+
for (const task of wave.tasks) {
|
|
104
|
+
const nodeId = toNodeId(task.id);
|
|
105
|
+
const label = renderNodeLabel(task, showSkills);
|
|
106
|
+
lines.push(` ${nodeId}["${label}"]`);
|
|
107
|
+
}
|
|
108
|
+
lines.push(' end');
|
|
109
|
+
}
|
|
110
|
+
// Render dependency arrows after all subgraphs
|
|
111
|
+
// Arrow direction: from dependency -> to dependent (被依赖者 -> 依赖者)
|
|
112
|
+
for (const wave of plan.waves) {
|
|
113
|
+
for (const task of wave.tasks) {
|
|
114
|
+
for (const depId of task.depends) {
|
|
115
|
+
// Check if dependency exists in the plan (might be completed already)
|
|
116
|
+
const depTask = taskMap.get(depId);
|
|
117
|
+
if (depTask && !depTask.completed) {
|
|
118
|
+
const fromNode = toNodeId(depId);
|
|
119
|
+
const toNode = toNodeId(task.id);
|
|
120
|
+
lines.push(` ${fromNode} --> ${toNode}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
lines.push('```');
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=mermaid-renderer.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph
|
|
3
|
+
*
|
|
4
|
+
* Builds a DAG from parsed tasks and provides graph operations.
|
|
5
|
+
* Uses Kahn's algorithm for topological sorting.
|
|
6
|
+
*/
|
|
7
|
+
import type { ParsedTask } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Validation result for the task graph
|
|
10
|
+
*/
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
errors: string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Represents a task dependency graph.
|
|
17
|
+
* Provides methods for querying build order, ready tasks, and completion status.
|
|
18
|
+
*/
|
|
19
|
+
export declare class TaskGraph {
|
|
20
|
+
private tasks;
|
|
21
|
+
private inDegree;
|
|
22
|
+
private dependents;
|
|
23
|
+
private dependencies;
|
|
24
|
+
private completedTasks;
|
|
25
|
+
constructor(tasks: ParsedTask[]);
|
|
26
|
+
/**
|
|
27
|
+
* Validates the task graph.
|
|
28
|
+
*
|
|
29
|
+
* Checks:
|
|
30
|
+
* - All depends references exist
|
|
31
|
+
* - No self-dependencies
|
|
32
|
+
* - No circular dependencies
|
|
33
|
+
*/
|
|
34
|
+
validate(): ValidationResult;
|
|
35
|
+
/**
|
|
36
|
+
* Performs topological sort using Kahn's algorithm.
|
|
37
|
+
* Returns tasks in execution order.
|
|
38
|
+
*/
|
|
39
|
+
topologicalSort(): ParsedTask[];
|
|
40
|
+
/**
|
|
41
|
+
* Detects circular dependencies in the graph.
|
|
42
|
+
* Returns an array of cycle paths.
|
|
43
|
+
*/
|
|
44
|
+
detectCycles(): string[][];
|
|
45
|
+
/**
|
|
46
|
+
* Gets tasks that are ready to execute (in-degree 0 and not completed).
|
|
47
|
+
*/
|
|
48
|
+
getReady(): ParsedTask[];
|
|
49
|
+
/**
|
|
50
|
+
* Marks a task as completed and updates graph state.
|
|
51
|
+
*/
|
|
52
|
+
markCompleted(taskId: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Gets the direct dependencies of a task (tasks it depends on).
|
|
55
|
+
*/
|
|
56
|
+
getDependencies(taskId: string): ParsedTask[];
|
|
57
|
+
/**
|
|
58
|
+
* Gets the direct dependents of a task (tasks that depend on it).
|
|
59
|
+
*/
|
|
60
|
+
getDependents(taskId: string): ParsedTask[];
|
|
61
|
+
/**
|
|
62
|
+
* Gets a task by ID.
|
|
63
|
+
*/
|
|
64
|
+
getTask(taskId: string): ParsedTask | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Gets all tasks in the graph.
|
|
67
|
+
*/
|
|
68
|
+
getAllTasks(): ParsedTask[];
|
|
69
|
+
/**
|
|
70
|
+
* Checks if all tasks are completed.
|
|
71
|
+
*/
|
|
72
|
+
isComplete(): boolean;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=task-graph.d.ts.map
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph
|
|
3
|
+
*
|
|
4
|
+
* Builds a DAG from parsed tasks and provides graph operations.
|
|
5
|
+
* Uses Kahn's algorithm for topological sorting.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Represents a task dependency graph.
|
|
9
|
+
* Provides methods for querying build order, ready tasks, and completion status.
|
|
10
|
+
*/
|
|
11
|
+
export class TaskGraph {
|
|
12
|
+
tasks;
|
|
13
|
+
inDegree;
|
|
14
|
+
dependents; // task -> tasks that depend on it
|
|
15
|
+
dependencies; // task -> tasks it depends on
|
|
16
|
+
completedTasks;
|
|
17
|
+
constructor(tasks) {
|
|
18
|
+
this.tasks = new Map(tasks.map(t => [t.id, t]));
|
|
19
|
+
this.inDegree = new Map();
|
|
20
|
+
this.dependents = new Map();
|
|
21
|
+
this.dependencies = new Map();
|
|
22
|
+
this.completedTasks = new Set();
|
|
23
|
+
// Initialize data structures
|
|
24
|
+
for (const task of tasks) {
|
|
25
|
+
this.inDegree.set(task.id, 0);
|
|
26
|
+
this.dependents.set(task.id, []);
|
|
27
|
+
this.dependencies.set(task.id, []);
|
|
28
|
+
// Mark already completed tasks
|
|
29
|
+
if (task.completed) {
|
|
30
|
+
this.completedTasks.add(task.id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Build graph edges
|
|
34
|
+
for (const task of tasks) {
|
|
35
|
+
// Filter depends to only include existing tasks
|
|
36
|
+
const validDepends = task.depends.filter(dep => this.tasks.has(dep));
|
|
37
|
+
this.dependencies.set(task.id, validDepends);
|
|
38
|
+
this.inDegree.set(task.id, validDepends.length);
|
|
39
|
+
for (const dep of validDepends) {
|
|
40
|
+
this.dependents.get(dep).push(task.id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Validates the task graph.
|
|
46
|
+
*
|
|
47
|
+
* Checks:
|
|
48
|
+
* - All depends references exist
|
|
49
|
+
* - No self-dependencies
|
|
50
|
+
* - No circular dependencies
|
|
51
|
+
*/
|
|
52
|
+
validate() {
|
|
53
|
+
const errors = [];
|
|
54
|
+
// Check for missing dependencies and self-dependencies
|
|
55
|
+
for (const task of this.tasks.values()) {
|
|
56
|
+
for (const dep of task.depends) {
|
|
57
|
+
if (!this.tasks.has(dep)) {
|
|
58
|
+
errors.push(`Task ${task.id}: depends on non-existent task "${dep}"`);
|
|
59
|
+
}
|
|
60
|
+
if (dep === task.id) {
|
|
61
|
+
errors.push(`Task ${task.id}: self-dependency not allowed`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Check for duplicate dependencies (warning, not error)
|
|
65
|
+
const uniqueDeps = new Set(task.depends);
|
|
66
|
+
if (uniqueDeps.size < task.depends.length) {
|
|
67
|
+
const duplicates = task.depends.filter((d, i) => task.depends.indexOf(d) !== i);
|
|
68
|
+
errors.push(`Task ${task.id}: duplicate dependencies detected: ${[...new Set(duplicates)].join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Check for cycles
|
|
72
|
+
const cycles = this.detectCycles();
|
|
73
|
+
if (cycles.length > 0) {
|
|
74
|
+
for (const cycle of cycles) {
|
|
75
|
+
errors.push(`Circular dependency detected: ${cycle.join(' → ')}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
valid: errors.length === 0,
|
|
80
|
+
errors,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Performs topological sort using Kahn's algorithm.
|
|
85
|
+
* Returns tasks in execution order.
|
|
86
|
+
*/
|
|
87
|
+
topologicalSort() {
|
|
88
|
+
// Create a copy of in-degrees for this operation
|
|
89
|
+
const inDegree = new Map();
|
|
90
|
+
for (const [id, degree] of this.inDegree) {
|
|
91
|
+
inDegree.set(id, degree);
|
|
92
|
+
}
|
|
93
|
+
// Start with roots (in-degree 0), sorted for determinism
|
|
94
|
+
const queue = [...this.tasks.keys()]
|
|
95
|
+
.filter(id => inDegree.get(id) === 0)
|
|
96
|
+
.sort();
|
|
97
|
+
const result = [];
|
|
98
|
+
while (queue.length > 0) {
|
|
99
|
+
const current = queue.shift();
|
|
100
|
+
result.push(this.tasks.get(current));
|
|
101
|
+
// Collect newly ready tasks, then sort before adding
|
|
102
|
+
const newlyReady = [];
|
|
103
|
+
for (const dep of this.dependents.get(current)) {
|
|
104
|
+
const newDegree = inDegree.get(dep) - 1;
|
|
105
|
+
inDegree.set(dep, newDegree);
|
|
106
|
+
if (newDegree === 0) {
|
|
107
|
+
newlyReady.push(dep);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
queue.push(...newlyReady.sort());
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Detects circular dependencies in the graph.
|
|
116
|
+
* Returns an array of cycle paths.
|
|
117
|
+
*/
|
|
118
|
+
detectCycles() {
|
|
119
|
+
const cycles = [];
|
|
120
|
+
const visited = new Set();
|
|
121
|
+
const recStack = new Set();
|
|
122
|
+
const path = [];
|
|
123
|
+
const dfs = (taskId) => {
|
|
124
|
+
visited.add(taskId);
|
|
125
|
+
recStack.add(taskId);
|
|
126
|
+
path.push(taskId);
|
|
127
|
+
const deps = this.dependencies.get(taskId) || [];
|
|
128
|
+
for (const dep of deps) {
|
|
129
|
+
if (!visited.has(dep)) {
|
|
130
|
+
if (dfs(dep)) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
else if (recStack.has(dep)) {
|
|
135
|
+
// Found a cycle - extract the cycle path
|
|
136
|
+
const cycleStart = path.indexOf(dep);
|
|
137
|
+
const cyclePath = [...path.slice(cycleStart), dep];
|
|
138
|
+
cycles.push(cyclePath);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
path.pop();
|
|
143
|
+
recStack.delete(taskId);
|
|
144
|
+
return false;
|
|
145
|
+
};
|
|
146
|
+
for (const taskId of this.tasks.keys()) {
|
|
147
|
+
if (!visited.has(taskId)) {
|
|
148
|
+
dfs(taskId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return cycles;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Gets tasks that are ready to execute (in-degree 0 and not completed).
|
|
155
|
+
*/
|
|
156
|
+
getReady() {
|
|
157
|
+
const ready = [];
|
|
158
|
+
for (const task of this.tasks.values()) {
|
|
159
|
+
if (this.completedTasks.has(task.id)) {
|
|
160
|
+
continue; // Already completed
|
|
161
|
+
}
|
|
162
|
+
// Check if all dependencies are completed
|
|
163
|
+
const deps = this.dependencies.get(task.id) || [];
|
|
164
|
+
const allDepsCompleted = deps.every(dep => this.completedTasks.has(dep));
|
|
165
|
+
if (allDepsCompleted) {
|
|
166
|
+
ready.push(task);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// Sort for deterministic ordering
|
|
170
|
+
return ready.sort((a, b) => a.id.localeCompare(b.id));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Marks a task as completed and updates graph state.
|
|
174
|
+
*/
|
|
175
|
+
markCompleted(taskId) {
|
|
176
|
+
if (!this.tasks.has(taskId)) {
|
|
177
|
+
throw new Error(`Task "${taskId}" not found in graph`);
|
|
178
|
+
}
|
|
179
|
+
this.completedTasks.add(taskId);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Gets the direct dependencies of a task (tasks it depends on).
|
|
183
|
+
*/
|
|
184
|
+
getDependencies(taskId) {
|
|
185
|
+
const deps = this.dependencies.get(taskId) || [];
|
|
186
|
+
return deps.map(id => this.tasks.get(id)).filter(Boolean);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Gets the direct dependents of a task (tasks that depend on it).
|
|
190
|
+
*/
|
|
191
|
+
getDependents(taskId) {
|
|
192
|
+
const deps = this.dependents.get(taskId) || [];
|
|
193
|
+
return deps.map(id => this.tasks.get(id)).filter(Boolean);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Gets a task by ID.
|
|
197
|
+
*/
|
|
198
|
+
getTask(taskId) {
|
|
199
|
+
return this.tasks.get(taskId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Gets all tasks in the graph.
|
|
203
|
+
*/
|
|
204
|
+
getAllTasks() {
|
|
205
|
+
return Array.from(this.tasks.values());
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Checks if all tasks are completed.
|
|
209
|
+
*/
|
|
210
|
+
isComplete() {
|
|
211
|
+
for (const task of this.tasks.values()) {
|
|
212
|
+
if (!this.completedTasks.has(task.id)) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=task-graph.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.md content into structured ParsedTask array.
|
|
5
|
+
*/
|
|
6
|
+
import type { ParsedTask } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Parse tasks.md content into an array of ParsedTask objects.
|
|
9
|
+
*
|
|
10
|
+
* @param content - The raw content of tasks.md
|
|
11
|
+
* @returns Array of parsed tasks
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseTasks(content: string): ParsedTask[];
|
|
14
|
+
//# sourceMappingURL=task-parser.d.ts.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses tasks.md content into structured ParsedTask array.
|
|
5
|
+
*/
|
|
6
|
+
// Regex patterns for parsing
|
|
7
|
+
// Group header: ## N. Group Name
|
|
8
|
+
const GROUP_HEADER_REGEX = /^##\s+(\d+)\.\s+(.+)$/;
|
|
9
|
+
// Task line: - [ ] N.M description @skill:name1,name2 @depends:id1,id2
|
|
10
|
+
// or: - [x] N.M description ...
|
|
11
|
+
const TASK_LINE_REGEX = /^-\s+\[([ xX])\]\s+(\d+\.\d+)\s+(.*)$/;
|
|
12
|
+
// Annotations (can be inside or outside HTML comments)
|
|
13
|
+
const SKILL_REGEX = /@skill:([^\s@]+)/g;
|
|
14
|
+
const DEPENDS_REGEX = /@depends:([^\s@]+)/g;
|
|
15
|
+
// HTML comment pattern to extract content
|
|
16
|
+
const HTML_COMMENT_REGEX = /<!--\s*(.*?)\s*-->/g;
|
|
17
|
+
/**
|
|
18
|
+
* Extract annotations from text (handles both plain text and HTML comments)
|
|
19
|
+
*/
|
|
20
|
+
function extractAnnotations(text) {
|
|
21
|
+
const skills = [];
|
|
22
|
+
const depends = [];
|
|
23
|
+
// Collect all text to search (including content inside HTML comments)
|
|
24
|
+
let searchText = text;
|
|
25
|
+
// Extract content from HTML comments and add to search text
|
|
26
|
+
let match;
|
|
27
|
+
while ((match = HTML_COMMENT_REGEX.exec(text)) !== null) {
|
|
28
|
+
searchText += ' ' + match[1];
|
|
29
|
+
}
|
|
30
|
+
// Reset lastIndex for subsequent regex operations
|
|
31
|
+
HTML_COMMENT_REGEX.lastIndex = 0;
|
|
32
|
+
// Extract skills
|
|
33
|
+
let skillMatch;
|
|
34
|
+
while ((skillMatch = SKILL_REGEX.exec(searchText)) !== null) {
|
|
35
|
+
const skillList = skillMatch[1].split(',').filter(s => s.trim());
|
|
36
|
+
skills.push(...skillList);
|
|
37
|
+
}
|
|
38
|
+
SKILL_REGEX.lastIndex = 0;
|
|
39
|
+
// Extract depends
|
|
40
|
+
let dependsMatch;
|
|
41
|
+
while ((dependsMatch = DEPENDS_REGEX.exec(searchText)) !== null) {
|
|
42
|
+
const dependsList = dependsMatch[1].split(',').filter(d => d.trim());
|
|
43
|
+
depends.push(...dependsList);
|
|
44
|
+
}
|
|
45
|
+
DEPENDS_REGEX.lastIndex = 0;
|
|
46
|
+
return {
|
|
47
|
+
skills: [...new Set(skills)], // dedupe
|
|
48
|
+
depends: [...new Set(depends)], // dedupe
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Clean description by removing annotations
|
|
53
|
+
*/
|
|
54
|
+
function cleanDescription(text) {
|
|
55
|
+
let cleaned = text
|
|
56
|
+
// Remove HTML comments with annotations
|
|
57
|
+
.replace(/<!--\s*@(?:skill|depends):[^\s]*(?:\s+@(?:skill|depends):[^\s]*)?\s*-->/g, '')
|
|
58
|
+
// Remove plain annotations
|
|
59
|
+
.replace(/@skill:[^\s@]+/g, '')
|
|
60
|
+
.replace(/@depends:[^\s@]+/g, '')
|
|
61
|
+
.trim();
|
|
62
|
+
return cleaned;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse tasks.md content into an array of ParsedTask objects.
|
|
66
|
+
*
|
|
67
|
+
* @param content - The raw content of tasks.md
|
|
68
|
+
* @returns Array of parsed tasks
|
|
69
|
+
*/
|
|
70
|
+
export function parseTasks(content) {
|
|
71
|
+
if (!content || !content.trim()) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
const tasks = [];
|
|
76
|
+
let currentGroup = '';
|
|
77
|
+
let currentGroupName = '';
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
// Check for group header
|
|
80
|
+
const groupMatch = line.match(GROUP_HEADER_REGEX);
|
|
81
|
+
if (groupMatch) {
|
|
82
|
+
currentGroup = groupMatch[1];
|
|
83
|
+
currentGroupName = groupMatch[2].trim();
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// Check for task line
|
|
87
|
+
const taskMatch = line.match(TASK_LINE_REGEX);
|
|
88
|
+
if (taskMatch) {
|
|
89
|
+
const checkboxState = taskMatch[1];
|
|
90
|
+
const taskId = taskMatch[2];
|
|
91
|
+
const rest = taskMatch[3];
|
|
92
|
+
// Extract annotations
|
|
93
|
+
const { skills, depends } = extractAnnotations(rest);
|
|
94
|
+
// Clean description
|
|
95
|
+
const description = cleanDescription(rest);
|
|
96
|
+
// Parse group from task ID if not already set
|
|
97
|
+
const taskGroup = taskId.split('.')[0];
|
|
98
|
+
tasks.push({
|
|
99
|
+
id: taskId,
|
|
100
|
+
group: currentGroup || taskGroup,
|
|
101
|
+
groupName: currentGroupName,
|
|
102
|
+
description,
|
|
103
|
+
skills,
|
|
104
|
+
depends,
|
|
105
|
+
completed: checkboxState.toLowerCase() === 'x',
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return tasks;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=task-parser.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for task dependency graph module.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Represents a parsed task from tasks.md
|
|
8
|
+
*/
|
|
9
|
+
export interface ParsedTask {
|
|
10
|
+
/** Task ID, e.g., "1.1", "2.3" */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Group number, e.g., "1", "2" */
|
|
13
|
+
group: string;
|
|
14
|
+
/** Group name */
|
|
15
|
+
groupName: string;
|
|
16
|
+
/** Task description text */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Skills required, e.g., ["java-dao-standards"] */
|
|
19
|
+
skills: string[];
|
|
20
|
+
/** Task IDs this task depends on, e.g., ["1.1", "2.1"] */
|
|
21
|
+
depends: string[];
|
|
22
|
+
/** Whether the task is completed (checkbox state [x] vs [ ]) */
|
|
23
|
+
completed: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Represents a wave of tasks that can be executed together
|
|
27
|
+
*/
|
|
28
|
+
export interface ExecutionWave {
|
|
29
|
+
/** Wave number (starting from 1) */
|
|
30
|
+
wave: number;
|
|
31
|
+
/** Tasks in this wave */
|
|
32
|
+
tasks: ParsedTask[];
|
|
33
|
+
/** Whether tasks can run in parallel (tasks.length > 1) */
|
|
34
|
+
parallel: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Represents the complete execution plan
|
|
38
|
+
*/
|
|
39
|
+
export interface ExecutionPlan {
|
|
40
|
+
/** Waves of tasks to execute */
|
|
41
|
+
waves: ExecutionWave[];
|
|
42
|
+
/** Total number of tasks */
|
|
43
|
+
totalTasks: number;
|
|
44
|
+
/** Number of tasks that can be executed in parallel (tasks in multi-task waves) */
|
|
45
|
+
parallelTasks: number;
|
|
46
|
+
/** Number of tasks that must be executed serially (tasks in single-task waves) */
|
|
47
|
+
serialTasks: number;
|
|
48
|
+
/** Whether there is a cycle in the dependency graph */
|
|
49
|
+
hasCycle: boolean;
|
|
50
|
+
/** Validation errors */
|
|
51
|
+
errors: string[];
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML Renderer for Execution Plan
|
|
3
|
+
*
|
|
4
|
+
* Renders ExecutionPlan to XML format for injection into apply instructions.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExecutionPlan, ParsedTask } from './types.js';
|
|
7
|
+
/**
|
|
8
|
+
* Renders an ExecutionPlan to XML format.
|
|
9
|
+
*
|
|
10
|
+
* @param plan - The execution plan to render
|
|
11
|
+
* @returns XML string representation of the plan
|
|
12
|
+
*/
|
|
13
|
+
export declare function renderExecutionPlanXml(plan: ExecutionPlan): string;
|
|
14
|
+
/**
|
|
15
|
+
* Checks if any task in the parsed tasks has @depends annotation.
|
|
16
|
+
*
|
|
17
|
+
* @param tasks - Array of parsed tasks
|
|
18
|
+
* @returns True if at least one task has dependencies
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasDependsAnnotations(tasks: ParsedTask[]): boolean;
|
|
21
|
+
//# sourceMappingURL=xml-renderer.d.ts.map
|