@zhuan-ai/zhuanspec 1.3.0 → 2.0.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/dist/commands/artifact-workflow.js +24 -1
- package/dist/commands/validate.d.ts +9 -0
- package/dist/commands/validate.js +97 -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 +11 -0
- package/dist/core/task-graph/index.js +14 -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/package.json +1 -1
- package/schemas/spec-driven/templates/tasks.md +17 -5
|
@@ -13,6 +13,7 @@ import { createChange, validateChangeName } from '../utils/change-utils.js';
|
|
|
13
13
|
import { discoverSkills } from '../core/skill-discovery.js';
|
|
14
14
|
import { getNewChangeSkillTemplate, getContinueChangeSkillTemplate, getApplyChangeSkillTemplate, getFfChangeSkillTemplate, getSyncSpecsSkillTemplate, getArchiveChangeSkillTemplate, getProposeChangeSkillTemplate, getExploreSkillTemplate, getVerifySkillTemplate, getOpsxNewCommandTemplate, getOpsxContinueCommandTemplate, getOpsxApplyCommandTemplate, getOpsxFfCommandTemplate, getOpsxSyncCommandTemplate, getOpsxArchiveCommandTemplate, getOpsxProposeCommandTemplate, getOpsxExploreCommandTemplate, getOpsxVerifyCommandTemplate } from '../core/templates/skill-templates.js';
|
|
15
15
|
import { FileSystemUtils } from '../utils/file-system.js';
|
|
16
|
+
import { parseTasks, generateExecutionPlan, renderExecutionPlanXml, hasDependsAnnotations, } from '../core/task-graph/index.js';
|
|
16
17
|
const DEFAULT_SCHEMA = 'spec-driven';
|
|
17
18
|
/**
|
|
18
19
|
* Checks if color output is disabled via NO_COLOR env or --no-color flag.
|
|
@@ -408,12 +409,25 @@ async function generateApplyInstructions(projectRoot, changeName, schemaName) {
|
|
|
408
409
|
// Parse tasks if tracking file exists
|
|
409
410
|
let tasks = [];
|
|
410
411
|
let tracksFileExists = false;
|
|
412
|
+
let executionPlanXml;
|
|
413
|
+
let hasDependencies = false;
|
|
411
414
|
if (tracksFile) {
|
|
412
415
|
const tracksPath = path.join(changeDir, tracksFile);
|
|
413
416
|
tracksFileExists = fs.existsSync(tracksPath);
|
|
414
417
|
if (tracksFileExists) {
|
|
415
418
|
const tasksContent = await fs.promises.readFile(tracksPath, 'utf-8');
|
|
416
419
|
tasks = parseTasksFile(tasksContent);
|
|
420
|
+
// Parse tasks using task-graph module for dependency analysis
|
|
421
|
+
const parsedTasks = parseTasks(tasksContent);
|
|
422
|
+
hasDependencies = hasDependsAnnotations(parsedTasks);
|
|
423
|
+
// Only generate execution plan if:
|
|
424
|
+
// 1. Tasks have @depends annotations
|
|
425
|
+
// 2. Not all tasks are complete
|
|
426
|
+
const allTasksComplete = parsedTasks.length > 0 && parsedTasks.every(t => t.completed);
|
|
427
|
+
if (hasDependencies && !allTasksComplete) {
|
|
428
|
+
const executionPlan = generateExecutionPlan(parsedTasks);
|
|
429
|
+
executionPlanXml = renderExecutionPlanXml(executionPlan);
|
|
430
|
+
}
|
|
417
431
|
}
|
|
418
432
|
}
|
|
419
433
|
// Calculate progress
|
|
@@ -480,6 +494,8 @@ See the apply command template for detailed code review workflow instructions.`;
|
|
|
480
494
|
state,
|
|
481
495
|
missingArtifacts: missingArtifacts.length > 0 ? missingArtifacts : undefined,
|
|
482
496
|
instruction,
|
|
497
|
+
executionPlanXml,
|
|
498
|
+
hasDependencies,
|
|
483
499
|
};
|
|
484
500
|
}
|
|
485
501
|
async function applyInstructionsCommand(options) {
|
|
@@ -506,7 +522,7 @@ async function applyInstructionsCommand(options) {
|
|
|
506
522
|
}
|
|
507
523
|
}
|
|
508
524
|
function printApplyInstructionsText(instructions) {
|
|
509
|
-
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction } = instructions;
|
|
525
|
+
const { changeName, schemaName, contextFiles, progress, tasks, state, missingArtifacts, instruction, executionPlanXml } = instructions;
|
|
510
526
|
console.log(`## Apply: ${changeName}`);
|
|
511
527
|
console.log(`Schema: ${schemaName}`);
|
|
512
528
|
console.log();
|
|
@@ -547,6 +563,13 @@ function printApplyInstructionsText(instructions) {
|
|
|
547
563
|
}
|
|
548
564
|
console.log();
|
|
549
565
|
}
|
|
566
|
+
// Execution Plan (only if tasks have dependencies and not all complete)
|
|
567
|
+
if (executionPlanXml) {
|
|
568
|
+
console.log('### Execution Plan');
|
|
569
|
+
console.log();
|
|
570
|
+
console.log(executionPlanXml);
|
|
571
|
+
console.log();
|
|
572
|
+
}
|
|
550
573
|
// Instruction
|
|
551
574
|
console.log('### Instruction');
|
|
552
575
|
console.log(instruction);
|
|
@@ -18,6 +18,15 @@ export declare class ValidateCommand {
|
|
|
18
18
|
private validateByType;
|
|
19
19
|
private printReport;
|
|
20
20
|
private printNextSteps;
|
|
21
|
+
/**
|
|
22
|
+
* Validate task dependencies in tasks.md if present.
|
|
23
|
+
* Returns a report with validation status and execution plan.
|
|
24
|
+
*/
|
|
25
|
+
private validateTaskDependencies;
|
|
26
|
+
/**
|
|
27
|
+
* Print execution plan summary for tasks with dependencies.
|
|
28
|
+
*/
|
|
29
|
+
private printExecutionPlanSummary;
|
|
21
30
|
private runBulkValidation;
|
|
22
31
|
}
|
|
23
32
|
export {};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import ora from 'ora';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
3
4
|
import { Validator } from '../core/validation/validator.js';
|
|
4
5
|
import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
|
|
5
6
|
import { getActiveChangeIds, getSpecIds } from '../utils/item-discovery.js';
|
|
6
7
|
import { nearestMatches } from '../utils/match.js';
|
|
8
|
+
import { parseTasks, TaskGraph, generateExecutionPlan } from '../core/task-graph/index.js';
|
|
7
9
|
export class ValidateCommand {
|
|
8
10
|
async execute(itemName, options = {}) {
|
|
9
11
|
const interactive = isInteractive(options);
|
|
@@ -102,8 +104,15 @@ export class ValidateCommand {
|
|
|
102
104
|
const changeDir = path.join(process.cwd(), 'zhuanspec', 'changes', id);
|
|
103
105
|
const start = Date.now();
|
|
104
106
|
const report = await validator.validateChangeDeltaSpecs(changeDir);
|
|
107
|
+
// Validate tasks.md dependencies if present
|
|
108
|
+
const taskReport = await this.validateTaskDependencies(changeDir);
|
|
109
|
+
// Merge task dependency issues into report
|
|
110
|
+
if (taskReport.issues.length > 0) {
|
|
111
|
+
report.issues.push(...taskReport.issues);
|
|
112
|
+
report.valid = report.valid && taskReport.valid;
|
|
113
|
+
}
|
|
105
114
|
const durationMs = Date.now() - start;
|
|
106
|
-
this.printReport('change', id, report, durationMs, opts.json);
|
|
115
|
+
this.printReport('change', id, report, durationMs, opts.json, taskReport);
|
|
107
116
|
// Non-zero exit if invalid (keeps enriched output test semantics)
|
|
108
117
|
process.exitCode = report.valid ? 0 : 1;
|
|
109
118
|
return;
|
|
@@ -115,14 +124,26 @@ export class ValidateCommand {
|
|
|
115
124
|
this.printReport('spec', id, report, durationMs, opts.json);
|
|
116
125
|
process.exitCode = report.valid ? 0 : 1;
|
|
117
126
|
}
|
|
118
|
-
printReport(type, id, report, durationMs, json) {
|
|
127
|
+
printReport(type, id, report, durationMs, json, taskReport) {
|
|
119
128
|
if (json) {
|
|
120
|
-
const out = {
|
|
129
|
+
const out = {
|
|
130
|
+
items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }],
|
|
131
|
+
summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } },
|
|
132
|
+
version: '1.0'
|
|
133
|
+
};
|
|
134
|
+
// Add execution plan to JSON output if available
|
|
135
|
+
if (taskReport?.hasDependencies && taskReport.executionPlan) {
|
|
136
|
+
out.executionPlan = taskReport.executionPlan;
|
|
137
|
+
}
|
|
121
138
|
console.log(JSON.stringify(out, null, 2));
|
|
122
139
|
return;
|
|
123
140
|
}
|
|
124
141
|
if (report.valid) {
|
|
125
142
|
console.log(`${type === 'change' ? 'Change' : 'Specification'} '${id}' is valid`);
|
|
143
|
+
// Print execution plan summary if available
|
|
144
|
+
if (taskReport?.hasDependencies && taskReport.executionPlan && !taskReport.executionPlan.hasCycle) {
|
|
145
|
+
this.printExecutionPlanSummary(taskReport.executionPlan);
|
|
146
|
+
}
|
|
126
147
|
}
|
|
127
148
|
else {
|
|
128
149
|
console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
|
|
@@ -149,6 +170,79 @@ export class ValidateCommand {
|
|
|
149
170
|
console.error('Next steps:');
|
|
150
171
|
bullets.forEach(b => console.error(` ${b}`));
|
|
151
172
|
}
|
|
173
|
+
/**
|
|
174
|
+
* Validate task dependencies in tasks.md if present.
|
|
175
|
+
* Returns a report with validation status and execution plan.
|
|
176
|
+
*/
|
|
177
|
+
async validateTaskDependencies(changeDir) {
|
|
178
|
+
const tasksFile = path.join(changeDir, 'tasks.md');
|
|
179
|
+
// Check if tasks.md exists
|
|
180
|
+
let content;
|
|
181
|
+
try {
|
|
182
|
+
content = await fs.readFile(tasksFile, 'utf-8');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// No tasks.md file - skip dependency validation
|
|
186
|
+
return { valid: true, hasDependencies: false, issues: [] };
|
|
187
|
+
}
|
|
188
|
+
// Parse tasks
|
|
189
|
+
const tasks = parseTasks(content);
|
|
190
|
+
if (tasks.length === 0) {
|
|
191
|
+
return { valid: true, hasDependencies: false, issues: [] };
|
|
192
|
+
}
|
|
193
|
+
// Check if any task has @depends annotation
|
|
194
|
+
const hasDependencies = tasks.some(t => t.depends.length > 0);
|
|
195
|
+
if (!hasDependencies) {
|
|
196
|
+
// No dependencies - skip validation, no execution plan output
|
|
197
|
+
return { valid: true, hasDependencies: false, issues: [] };
|
|
198
|
+
}
|
|
199
|
+
// Validate task dependencies
|
|
200
|
+
const graph = new TaskGraph(tasks);
|
|
201
|
+
const validation = graph.validate();
|
|
202
|
+
const issues = [];
|
|
203
|
+
if (!validation.valid) {
|
|
204
|
+
for (const error of validation.errors) {
|
|
205
|
+
issues.push({
|
|
206
|
+
level: 'ERROR',
|
|
207
|
+
path: 'tasks.md',
|
|
208
|
+
message: error,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Generate execution plan
|
|
213
|
+
const executionPlan = generateExecutionPlan(tasks);
|
|
214
|
+
// Add cycle-related errors from execution plan
|
|
215
|
+
if (executionPlan.hasCycle && executionPlan.errors.length > 0) {
|
|
216
|
+
for (const error of executionPlan.errors) {
|
|
217
|
+
// Avoid duplicates
|
|
218
|
+
if (!issues.some(i => i.message === error)) {
|
|
219
|
+
issues.push({
|
|
220
|
+
level: 'ERROR',
|
|
221
|
+
path: 'tasks.md',
|
|
222
|
+
message: error,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
valid: issues.filter(i => i.level === 'ERROR').length === 0,
|
|
229
|
+
hasDependencies: true,
|
|
230
|
+
issues,
|
|
231
|
+
executionPlan,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Print execution plan summary for tasks with dependencies.
|
|
236
|
+
*/
|
|
237
|
+
printExecutionPlanSummary(plan) {
|
|
238
|
+
const { waves, totalTasks, parallelTasks, serialTasks } = plan;
|
|
239
|
+
console.log(` Execution Plan: ${waves.length} waves, ${totalTasks} tasks (${parallelTasks} parallel, ${serialTasks} serial)`);
|
|
240
|
+
for (const wave of waves) {
|
|
241
|
+
const taskIds = wave.tasks.map(t => t.id).join(', ');
|
|
242
|
+
const parallelLabel = wave.parallel ? ' (parallel)' : '';
|
|
243
|
+
console.log(` Wave ${wave.wave}: [${taskIds}]${parallelLabel}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
152
246
|
async runBulkValidation(scope, opts) {
|
|
153
247
|
const spinner = !opts.json && !opts.noInteractive ? ora('Validating...').start() : undefined;
|
|
154
248
|
const [changeIds, specIds] = await Promise.all([
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Planner
|
|
3
|
+
*
|
|
4
|
+
* Generates wave-based execution plans from parsed tasks.
|
|
5
|
+
* Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
|
|
6
|
+
*/
|
|
7
|
+
import type { ParsedTask, ExecutionPlan } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates an execution plan from parsed tasks.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm: Iteratively peel off nodes with in-degree 0 to form waves.
|
|
12
|
+
*
|
|
13
|
+
* @param tasks - Array of parsed tasks
|
|
14
|
+
* @returns Execution plan with waves
|
|
15
|
+
*/
|
|
16
|
+
export declare function generateExecutionPlan(tasks: ParsedTask[]): ExecutionPlan;
|
|
17
|
+
//# sourceMappingURL=execution-planner.d.ts.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution Planner
|
|
3
|
+
*
|
|
4
|
+
* Generates wave-based execution plans from parsed tasks.
|
|
5
|
+
* Uses a layer-by-layer approach: each wave contains tasks with in-degree 0.
|
|
6
|
+
*/
|
|
7
|
+
import { TaskGraph } from './task-graph.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generates an execution plan from parsed tasks.
|
|
10
|
+
*
|
|
11
|
+
* Algorithm: Iteratively peel off nodes with in-degree 0 to form waves.
|
|
12
|
+
*
|
|
13
|
+
* @param tasks - Array of parsed tasks
|
|
14
|
+
* @returns Execution plan with waves
|
|
15
|
+
*/
|
|
16
|
+
export function generateExecutionPlan(tasks) {
|
|
17
|
+
// Filter out already completed tasks
|
|
18
|
+
const incompleteTasks = tasks.filter(t => !t.completed);
|
|
19
|
+
// Handle empty case
|
|
20
|
+
if (incompleteTasks.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
waves: [],
|
|
23
|
+
totalTasks: 0,
|
|
24
|
+
parallelTasks: 0,
|
|
25
|
+
serialTasks: 0,
|
|
26
|
+
hasCycle: false,
|
|
27
|
+
errors: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Create graph and validate
|
|
31
|
+
const graph = new TaskGraph(incompleteTasks);
|
|
32
|
+
const validation = graph.validate();
|
|
33
|
+
// Check for cycles
|
|
34
|
+
if (validation.errors.some(e => e.includes('Circular dependency'))) {
|
|
35
|
+
return {
|
|
36
|
+
waves: [],
|
|
37
|
+
totalTasks: incompleteTasks.length,
|
|
38
|
+
parallelTasks: 0,
|
|
39
|
+
serialTasks: 0,
|
|
40
|
+
hasCycle: true,
|
|
41
|
+
errors: validation.errors,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// Collect non-cycle errors
|
|
45
|
+
const errors = validation.errors.filter(e => !e.includes('Circular dependency'));
|
|
46
|
+
// Build waves using modified Kahn's algorithm
|
|
47
|
+
const waves = [];
|
|
48
|
+
const completed = new Set();
|
|
49
|
+
// Track in-degrees locally
|
|
50
|
+
const inDegree = new Map();
|
|
51
|
+
const dependents = new Map();
|
|
52
|
+
// Initialize
|
|
53
|
+
for (const task of incompleteTasks) {
|
|
54
|
+
// Only count dependencies that exist in incomplete tasks
|
|
55
|
+
const validDeps = task.depends.filter(d => incompleteTasks.some(t => t.id === d));
|
|
56
|
+
inDegree.set(task.id, validDeps.length);
|
|
57
|
+
dependents.set(task.id, []);
|
|
58
|
+
}
|
|
59
|
+
// Build reverse adjacency
|
|
60
|
+
for (const task of incompleteTasks) {
|
|
61
|
+
for (const dep of task.depends) {
|
|
62
|
+
if (dependents.has(dep)) {
|
|
63
|
+
dependents.get(dep).push(task.id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let waveNumber = 0;
|
|
68
|
+
let remainingTasks = incompleteTasks.length;
|
|
69
|
+
while (remainingTasks > 0) {
|
|
70
|
+
waveNumber++;
|
|
71
|
+
// Find all tasks with in-degree 0
|
|
72
|
+
const readyIds = [...inDegree.keys()]
|
|
73
|
+
.filter(id => !completed.has(id) && inDegree.get(id) === 0)
|
|
74
|
+
.sort();
|
|
75
|
+
if (readyIds.length === 0) {
|
|
76
|
+
// Shouldn't happen if no cycles, but handle gracefully
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
const readyTasks = readyIds
|
|
80
|
+
.map(id => incompleteTasks.find(t => t.id === id))
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
waves.push({
|
|
83
|
+
wave: waveNumber,
|
|
84
|
+
tasks: readyTasks,
|
|
85
|
+
parallel: readyTasks.length > 1,
|
|
86
|
+
});
|
|
87
|
+
// Mark as completed and update in-degrees
|
|
88
|
+
for (const id of readyIds) {
|
|
89
|
+
completed.add(id);
|
|
90
|
+
remainingTasks--;
|
|
91
|
+
// Decrease in-degree of dependents
|
|
92
|
+
for (const dep of dependents.get(id) || []) {
|
|
93
|
+
if (!completed.has(dep)) {
|
|
94
|
+
inDegree.set(dep, inDegree.get(dep) - 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Calculate statistics
|
|
100
|
+
let parallelTasks = 0;
|
|
101
|
+
let serialTasks = 0;
|
|
102
|
+
for (const wave of waves) {
|
|
103
|
+
if (wave.parallel) {
|
|
104
|
+
parallelTasks += wave.tasks.length;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
serialTasks += wave.tasks.length;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
waves,
|
|
112
|
+
totalTasks: incompleteTasks.length,
|
|
113
|
+
parallelTasks,
|
|
114
|
+
serialTasks,
|
|
115
|
+
hasCycle: false,
|
|
116
|
+
errors,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=execution-planner.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph Module
|
|
3
|
+
*
|
|
4
|
+
* Provides task dependency graph operations for tasks.md processing.
|
|
5
|
+
*/
|
|
6
|
+
export type { ParsedTask, ExecutionWave, ExecutionPlan } from './types.js';
|
|
7
|
+
export { parseTasks } from './task-parser.js';
|
|
8
|
+
export { TaskGraph, type ValidationResult } from './task-graph.js';
|
|
9
|
+
export { generateExecutionPlan } from './execution-planner.js';
|
|
10
|
+
export { renderExecutionPlanXml, hasDependsAnnotations } from './xml-renderer.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task Graph Module
|
|
3
|
+
*
|
|
4
|
+
* Provides task dependency graph operations for tasks.md processing.
|
|
5
|
+
*/
|
|
6
|
+
// Parser
|
|
7
|
+
export { parseTasks } from './task-parser.js';
|
|
8
|
+
// Graph
|
|
9
|
+
export { TaskGraph } from './task-graph.js';
|
|
10
|
+
// Execution planner
|
|
11
|
+
export { generateExecutionPlan } from './execution-planner.js';
|
|
12
|
+
// XML Renderer
|
|
13
|
+
export { renderExecutionPlanXml, hasDependsAnnotations } from './xml-renderer.js';
|
|
14
|
+
//# sourceMappingURL=index.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
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XML Renderer for Execution Plan
|
|
3
|
+
*
|
|
4
|
+
* Renders ExecutionPlan to XML format for injection into apply instructions.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Escapes special XML characters in a string.
|
|
8
|
+
*/
|
|
9
|
+
function escapeXml(str) {
|
|
10
|
+
return str
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"')
|
|
15
|
+
.replace(/'/g, ''');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Renders a single task to XML format.
|
|
19
|
+
*/
|
|
20
|
+
function renderTaskXml(task, indent) {
|
|
21
|
+
const skillsAttr = task.skills.length > 0 ? ` skills="${escapeXml(task.skills.join(','))}"` : '';
|
|
22
|
+
const lines = [];
|
|
23
|
+
lines.push(`${indent}<task id="${escapeXml(task.id)}"${skillsAttr}>`);
|
|
24
|
+
lines.push(`${indent} <description>${escapeXml(task.description)}</description>`);
|
|
25
|
+
lines.push(`${indent} <depends>${task.depends.length > 0 ? escapeXml(task.depends.join(',')) : ''}</depends>`);
|
|
26
|
+
lines.push(`${indent}</task>`);
|
|
27
|
+
return lines.join('\n');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Renders an ExecutionPlan to XML format.
|
|
31
|
+
*
|
|
32
|
+
* @param plan - The execution plan to render
|
|
33
|
+
* @returns XML string representation of the plan
|
|
34
|
+
*/
|
|
35
|
+
export function renderExecutionPlanXml(plan) {
|
|
36
|
+
const lines = [];
|
|
37
|
+
// Handle error cases
|
|
38
|
+
if (plan.hasCycle) {
|
|
39
|
+
lines.push(`<execution_plan error="cycle_detected">`);
|
|
40
|
+
lines.push(' <errors>');
|
|
41
|
+
for (const error of plan.errors) {
|
|
42
|
+
lines.push(` <error>${escapeXml(error)}</error>`);
|
|
43
|
+
}
|
|
44
|
+
lines.push(' </errors>');
|
|
45
|
+
lines.push('</execution_plan>');
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
// Handle empty plan (all tasks complete or no tasks)
|
|
49
|
+
if (plan.waves.length === 0) {
|
|
50
|
+
return ''; // Return empty string - no execution plan needed
|
|
51
|
+
}
|
|
52
|
+
// Render full plan
|
|
53
|
+
lines.push(`<execution_plan total_tasks="${plan.totalTasks}" parallel_tasks="${plan.parallelTasks}" serial_tasks="${plan.serialTasks}">`);
|
|
54
|
+
// Render each wave
|
|
55
|
+
for (const wave of plan.waves) {
|
|
56
|
+
lines.push(` <wave number="${wave.wave}" parallel="${wave.parallel}">`);
|
|
57
|
+
for (const task of wave.tasks) {
|
|
58
|
+
lines.push(renderTaskXml(task, ' '));
|
|
59
|
+
}
|
|
60
|
+
lines.push(' </wave>');
|
|
61
|
+
}
|
|
62
|
+
// Add orchestration strategy
|
|
63
|
+
lines.push('');
|
|
64
|
+
lines.push(' <orchestration_strategy>');
|
|
65
|
+
lines.push(' 每个 wave 内的任务通过独立 subagent 并行执行。');
|
|
66
|
+
lines.push(' Wave 之间严格串行,前一 wave 全部完成后才启动下一 wave。');
|
|
67
|
+
lines.push(' 每个 subagent 仅加载其 task 关联的 @skill,实现上下文隔离。');
|
|
68
|
+
lines.push(' </orchestration_strategy>');
|
|
69
|
+
lines.push('</execution_plan>');
|
|
70
|
+
return lines.join('\n');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Checks if any task in the parsed tasks has @depends annotation.
|
|
74
|
+
*
|
|
75
|
+
* @param tasks - Array of parsed tasks
|
|
76
|
+
* @returns True if at least one task has dependencies
|
|
77
|
+
*/
|
|
78
|
+
export function hasDependsAnnotations(tasks) {
|
|
79
|
+
return tasks.some(task => task.depends.length > 0);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=xml-renderer.js.map
|
package/package.json
CHANGED
|
@@ -10,16 +10,28 @@
|
|
|
10
10
|
- 避免过度匹配:简单的代码修改(如增删枚举值、修改常量)只需通用编码规范 skill
|
|
11
11
|
- 避免关键词匹配:不要仅因文件名或路径包含某些词就匹配相关 skill
|
|
12
12
|
|
|
13
|
+
@depends 标注指南:
|
|
14
|
+
1. 分析任务间的依赖关系,识别哪些任务必须在其他任务完成后才能开始
|
|
15
|
+
2. 使用 @depends:taskId 标注依赖,支持多依赖 @depends:taskId1,taskId2
|
|
16
|
+
3. 无 @depends 标注的任务默认无依赖,可与其他无依赖任务并行执行
|
|
17
|
+
4. 依赖可以跨任务组(如 2.1 依赖 1.2)
|
|
18
|
+
5. 不允许循环依赖
|
|
19
|
+
6. 标注原则:
|
|
20
|
+
- 仅标注直接依赖:如 A→B→C,C 只需标注 @depends:B,无需标注 A
|
|
21
|
+
- 数据依赖:后续任务使用前序任务创建的表/接口/类时,必须标注依赖
|
|
22
|
+
- 独立任务:无数据或逻辑依赖的任务不要添加 @depends,以最大化并行度
|
|
23
|
+
|
|
13
24
|
示例:
|
|
14
|
-
- [ ] 1.1 新增 AssBusinessDataEnum 枚举值 ASSEMBLY_MACHINE_FLAG
|
|
15
|
-
- [ ] 1.2
|
|
16
|
-
- [ ] 1.3
|
|
25
|
+
- [ ] 1.1 新增 AssBusinessDataEnum 枚举值 ASSEMBLY_MACHINE_FLAG @skill:kf-backend-coding-standards
|
|
26
|
+
- [ ] 1.2 创建数据库表 ass_order @skill:java-db-schema-standards
|
|
27
|
+
- [ ] 1.3 编写 DAO 层代码 @skill:java-dao-standards @depends:1.2
|
|
28
|
+
- [ ] 1.4 定义 SCF 接口 IAssQueryService @skill:java-scf-rpc-usage-skill @depends:1.3
|
|
17
29
|
-->
|
|
18
30
|
|
|
19
31
|
- [ ] 1.1 <!-- 任务描述 --> <!-- @skill:real-skill-name -->
|
|
20
|
-
- [ ] 1.2 <!-- 任务描述 -->
|
|
32
|
+
- [ ] 1.2 <!-- 任务描述 --> <!-- @skill:real-skill-name @depends:1.1 -->
|
|
21
33
|
|
|
22
34
|
## 2. <!-- 任务组名称 -->
|
|
23
35
|
|
|
24
36
|
- [ ] 2.1 <!-- 任务描述 -->
|
|
25
|
-
- [ ] 2.2 <!-- 任务描述 -->
|
|
37
|
+
- [ ] 2.2 <!-- 任务描述 --> <!-- @depends:1.2,2.1 -->
|