@ginkoai/cli 2.0.5 → 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/dist/commands/epic/index.d.ts +29 -0
- package/dist/commands/epic/index.d.ts.map +1 -0
- package/dist/commands/epic/index.js +94 -0
- package/dist/commands/epic/index.js.map +1 -0
- package/dist/commands/epic/status.d.ts +40 -0
- package/dist/commands/epic/status.d.ts.map +1 -0
- package/dist/commands/epic/status.js +244 -0
- package/dist/commands/epic/status.js.map +1 -0
- package/dist/commands/graph/api-client.d.ts +209 -0
- package/dist/commands/graph/api-client.d.ts.map +1 -1
- package/dist/commands/graph/api-client.js +125 -0
- package/dist/commands/graph/api-client.js.map +1 -1
- package/dist/commands/graph/load.d.ts.map +1 -1
- package/dist/commands/graph/load.js +40 -2
- package/dist/commands/graph/load.js.map +1 -1
- package/dist/commands/migrate/index.d.ts +27 -0
- package/dist/commands/migrate/index.d.ts.map +1 -0
- package/dist/commands/migrate/index.js +76 -0
- package/dist/commands/migrate/index.js.map +1 -0
- package/dist/commands/migrate/status-migration.d.ts +58 -0
- package/dist/commands/migrate/status-migration.d.ts.map +1 -0
- package/dist/commands/migrate/status-migration.js +323 -0
- package/dist/commands/migrate/status-migration.js.map +1 -0
- package/dist/commands/sprint/index.d.ts.map +1 -1
- package/dist/commands/sprint/index.js +4 -0
- package/dist/commands/sprint/index.js.map +1 -1
- package/dist/commands/sprint/status.d.ts +42 -0
- package/dist/commands/sprint/status.d.ts.map +1 -0
- package/dist/commands/sprint/status.js +278 -0
- package/dist/commands/sprint/status.js.map +1 -0
- package/dist/commands/start/start-reflection.d.ts +39 -0
- package/dist/commands/start/start-reflection.d.ts.map +1 -1
- package/dist/commands/start/start-reflection.js +311 -91
- package/dist/commands/start/start-reflection.js.map +1 -1
- package/dist/commands/sync/sprint-syncer.d.ts +19 -12
- package/dist/commands/sync/sprint-syncer.d.ts.map +1 -1
- package/dist/commands/sync/sprint-syncer.js +58 -140
- package/dist/commands/sync/sprint-syncer.js.map +1 -1
- package/dist/commands/sync/sync-command.d.ts.map +1 -1
- package/dist/commands/sync/sync-command.js +6 -18
- package/dist/commands/sync/sync-command.js.map +1 -1
- package/dist/commands/task/index.d.ts +25 -0
- package/dist/commands/task/index.d.ts.map +1 -0
- package/dist/commands/task/index.js +100 -0
- package/dist/commands/task/index.js.map +1 -0
- package/dist/commands/task/status.d.ts +43 -0
- package/dist/commands/task/status.d.ts.map +1 -0
- package/dist/commands/task/status.js +301 -0
- package/dist/commands/task/status.js.map +1 -0
- package/dist/index.js +12 -29
- package/dist/index.js.map +1 -1
- package/dist/lib/context-loader-events.d.ts +1 -0
- package/dist/lib/context-loader-events.d.ts.map +1 -1
- package/dist/lib/context-loader-events.js +28 -41
- package/dist/lib/context-loader-events.js.map +1 -1
- package/dist/lib/output-formatter.d.ts +12 -4
- package/dist/lib/output-formatter.d.ts.map +1 -1
- package/dist/lib/output-formatter.js +186 -14
- package/dist/lib/output-formatter.js.map +1 -1
- package/dist/lib/pending-updates.d.ts +148 -0
- package/dist/lib/pending-updates.d.ts.map +1 -0
- package/dist/lib/pending-updates.js +301 -0
- package/dist/lib/pending-updates.js.map +1 -0
- package/dist/lib/sprint-loader.d.ts +86 -14
- package/dist/lib/sprint-loader.d.ts.map +1 -1
- package/dist/lib/sprint-loader.js +293 -98
- package/dist/lib/sprint-loader.js.map +1 -1
- package/dist/lib/state-cache.d.ts +142 -0
- package/dist/lib/state-cache.d.ts.map +1 -0
- package/dist/lib/state-cache.js +259 -0
- package/dist/lib/state-cache.js.map +1 -0
- package/dist/lib/task-graph-sync.d.ts +105 -0
- package/dist/lib/task-graph-sync.d.ts.map +1 -0
- package/dist/lib/task-graph-sync.js +178 -0
- package/dist/lib/task-graph-sync.js.map +1 -0
- package/dist/lib/task-parser.d.ts +107 -0
- package/dist/lib/task-parser.d.ts.map +1 -0
- package/dist/lib/task-parser.js +384 -0
- package/dist/lib/task-parser.js.map +1 -0
- package/dist/templates/ai-instructions-template.d.ts.map +1 -1
- package/dist/templates/ai-instructions-template.js +7 -5
- package/dist/templates/ai-instructions-template.js.map +1 -1
- package/dist/templates/epic-template.md +0 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +7 -5
- package/dist/types/config.js.map +1 -1
- package/dist/utils/synthesis.d.ts +4 -0
- package/dist/utils/synthesis.d.ts.map +1 -1
- package/dist/utils/synthesis.js +12 -18
- package/dist/utils/synthesis.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-01-19
|
|
5
|
+
* @tags: [task-sync, graph, neo4j, epic-015, sprint-0a]
|
|
6
|
+
* @related: [task-parser.ts, ../commands/graph/api-client.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: medium
|
|
9
|
+
* @dependencies: [api-client]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Task Graph Sync (EPIC-015 Sprint 0a Tasks 2-3)
|
|
13
|
+
*
|
|
14
|
+
* Syncs parsed tasks to Neo4j graph via the dashboard API.
|
|
15
|
+
* Creates Task nodes and BELONGS_TO relationships.
|
|
16
|
+
*
|
|
17
|
+
* Key principle (ADR-060): Content from Git, State from Graph.
|
|
18
|
+
* - On CREATE: Uses initial_status from markdown
|
|
19
|
+
* - On UPDATE: Preserves existing status (graph-authoritative)
|
|
20
|
+
*/
|
|
21
|
+
import { GraphApiClient } from '../commands/graph/api-client.js';
|
|
22
|
+
import { loadGraphConfig, isGraphInitialized } from '../commands/graph/config.js';
|
|
23
|
+
/**
|
|
24
|
+
* Sync tasks to graph via API
|
|
25
|
+
*
|
|
26
|
+
* @param tasks - Array of parsed tasks
|
|
27
|
+
* @param graphId - Graph namespace ID
|
|
28
|
+
* @param client - GraphApiClient instance
|
|
29
|
+
* @param options - Sync options
|
|
30
|
+
* @returns TaskSyncResponse
|
|
31
|
+
*/
|
|
32
|
+
export async function syncTasksToGraph(tasks, graphId, client, options = {}) {
|
|
33
|
+
const { createRelationships = true, batchSize = 50, onProgress } = options;
|
|
34
|
+
if (tasks.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
success: true,
|
|
37
|
+
created: 0,
|
|
38
|
+
updated: 0,
|
|
39
|
+
relationships: 0,
|
|
40
|
+
tasks: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Process in batches
|
|
44
|
+
const batches = [];
|
|
45
|
+
for (let i = 0; i < tasks.length; i += batchSize) {
|
|
46
|
+
batches.push(tasks.slice(i, i + batchSize));
|
|
47
|
+
}
|
|
48
|
+
let totalCreated = 0;
|
|
49
|
+
let totalUpdated = 0;
|
|
50
|
+
let totalRelationships = 0;
|
|
51
|
+
const allTaskIds = [];
|
|
52
|
+
for (let i = 0; i < batches.length; i++) {
|
|
53
|
+
const batch = batches[i];
|
|
54
|
+
const response = await client.syncTasks(graphId, batch, createRelationships);
|
|
55
|
+
totalCreated += response.created;
|
|
56
|
+
totalUpdated += response.updated;
|
|
57
|
+
totalRelationships += response.relationships;
|
|
58
|
+
allTaskIds.push(...response.tasks);
|
|
59
|
+
if (onProgress) {
|
|
60
|
+
const synced = Math.min((i + 1) * batchSize, tasks.length);
|
|
61
|
+
onProgress(synced, tasks.length);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
created: totalCreated,
|
|
67
|
+
updated: totalUpdated,
|
|
68
|
+
relationships: totalRelationships,
|
|
69
|
+
tasks: allTaskIds,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Sync a single sprint's tasks to graph
|
|
74
|
+
*
|
|
75
|
+
* @param sprintResult - Parsed sprint with tasks
|
|
76
|
+
* @param client - GraphApiClient instance (optional, will create if not provided)
|
|
77
|
+
* @param options - Sync options
|
|
78
|
+
* @returns TaskSyncResponse
|
|
79
|
+
*/
|
|
80
|
+
export async function syncSprintTasksToGraph(sprintResult, client, options = {}) {
|
|
81
|
+
// Check if graph is initialized
|
|
82
|
+
if (!await isGraphInitialized()) {
|
|
83
|
+
throw new Error('Graph not initialized. Run "ginko graph init" first.');
|
|
84
|
+
}
|
|
85
|
+
// Load config to get graphId
|
|
86
|
+
const config = await loadGraphConfig();
|
|
87
|
+
if (!config) {
|
|
88
|
+
throw new Error('Failed to load graph configuration');
|
|
89
|
+
}
|
|
90
|
+
// Create client if not provided
|
|
91
|
+
const apiClient = client || new GraphApiClient(config.apiEndpoint);
|
|
92
|
+
return syncTasksToGraph(sprintResult.tasks, config.graphId, apiClient, options);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Sync multiple sprints' tasks to graph
|
|
96
|
+
*
|
|
97
|
+
* @param sprintResults - Array of parsed sprints with tasks
|
|
98
|
+
* @param options - Sync options
|
|
99
|
+
* @returns BatchSyncResult
|
|
100
|
+
*/
|
|
101
|
+
export async function syncAllSprintTasksToGraph(sprintResults, options = {}) {
|
|
102
|
+
// Check if graph is initialized
|
|
103
|
+
if (!await isGraphInitialized()) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
totalTasks: 0,
|
|
107
|
+
created: 0,
|
|
108
|
+
updated: 0,
|
|
109
|
+
relationships: 0,
|
|
110
|
+
errors: ['Graph not initialized. Run "ginko graph init" first.'],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Load config
|
|
114
|
+
const config = await loadGraphConfig();
|
|
115
|
+
if (!config) {
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
totalTasks: 0,
|
|
119
|
+
created: 0,
|
|
120
|
+
updated: 0,
|
|
121
|
+
relationships: 0,
|
|
122
|
+
errors: ['Failed to load graph configuration'],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// Collect all tasks
|
|
126
|
+
const allTasks = [];
|
|
127
|
+
for (const result of sprintResults) {
|
|
128
|
+
allTasks.push(...result.tasks);
|
|
129
|
+
}
|
|
130
|
+
if (allTasks.length === 0) {
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
totalTasks: 0,
|
|
134
|
+
created: 0,
|
|
135
|
+
updated: 0,
|
|
136
|
+
relationships: 0,
|
|
137
|
+
errors: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// Create client
|
|
141
|
+
const client = new GraphApiClient(config.apiEndpoint);
|
|
142
|
+
const errors = [];
|
|
143
|
+
try {
|
|
144
|
+
const response = await syncTasksToGraph(allTasks, config.graphId, client, options);
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
totalTasks: allTasks.length,
|
|
148
|
+
created: response.created,
|
|
149
|
+
updated: response.updated,
|
|
150
|
+
relationships: response.relationships,
|
|
151
|
+
errors,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
errors.push(error instanceof Error ? error.message : 'Unknown error during sync');
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
totalTasks: allTasks.length,
|
|
159
|
+
created: 0,
|
|
160
|
+
updated: 0,
|
|
161
|
+
relationships: 0,
|
|
162
|
+
errors,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Get task sync status from graph
|
|
168
|
+
*
|
|
169
|
+
* @param graphId - Graph namespace ID
|
|
170
|
+
* @param sprintId - Optional sprint ID filter
|
|
171
|
+
* @param epicId - Optional epic ID filter
|
|
172
|
+
* @param client - GraphApiClient instance
|
|
173
|
+
* @returns Array of task status objects
|
|
174
|
+
*/
|
|
175
|
+
export async function getTasksFromGraph(graphId, client, filters) {
|
|
176
|
+
return client.getTasks(graphId, filters);
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=task-graph-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-graph-sync.js","sourceRoot":"","sources":["../../src/lib/task-graph-sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-01-19
|
|
5
|
+
* @tags: [task-parser, sprint, epic-015, sprint-0a]
|
|
6
|
+
* @related: [sprint-parser.ts, task-graph-sync.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: medium
|
|
9
|
+
* @dependencies: [fs-extra]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Task status values (aligned with Status API)
|
|
13
|
+
*/
|
|
14
|
+
export type TaskStatus = 'not_started' | 'in_progress' | 'blocked' | 'complete' | 'paused';
|
|
15
|
+
/**
|
|
16
|
+
* Parsed task from sprint markdown
|
|
17
|
+
*/
|
|
18
|
+
export interface ParsedTask {
|
|
19
|
+
/** Task ID (e.g., e015_s00a_t01, TASK-1, adhoc_260119_s01_t01) */
|
|
20
|
+
id: string;
|
|
21
|
+
/** Derived sprint ID (e.g., e015_s00a, adhoc_260119_s01) */
|
|
22
|
+
sprint_id: string;
|
|
23
|
+
/** Derived epic ID (e.g., e015, adhoc_260119) */
|
|
24
|
+
epic_id: string;
|
|
25
|
+
/** Task title */
|
|
26
|
+
title: string;
|
|
27
|
+
/** Estimated effort (e.g., "3h", "4-6h") */
|
|
28
|
+
estimate: string | null;
|
|
29
|
+
/** Priority level */
|
|
30
|
+
priority: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW';
|
|
31
|
+
/** Assignee email or null */
|
|
32
|
+
assignee: string | null;
|
|
33
|
+
/** Initial status from checkbox (used only on CREATE) */
|
|
34
|
+
initial_status: TaskStatus;
|
|
35
|
+
/** Task goal/description */
|
|
36
|
+
goal: string | null;
|
|
37
|
+
/** Acceptance criteria list */
|
|
38
|
+
acceptance_criteria: string[];
|
|
39
|
+
/** Referenced files */
|
|
40
|
+
files: string[];
|
|
41
|
+
/** Related ADR references */
|
|
42
|
+
related_adrs: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Sprint metadata extracted alongside tasks
|
|
46
|
+
*/
|
|
47
|
+
export interface ParsedSprint {
|
|
48
|
+
/** Sprint ID (derived from filename or content) */
|
|
49
|
+
id: string;
|
|
50
|
+
/** Sprint name */
|
|
51
|
+
name: string;
|
|
52
|
+
/** Epic ID */
|
|
53
|
+
epic_id: string;
|
|
54
|
+
/** Sprint file path */
|
|
55
|
+
file_path: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Result of parsing a sprint file
|
|
59
|
+
*/
|
|
60
|
+
export interface SprintParseResult {
|
|
61
|
+
sprint: ParsedSprint;
|
|
62
|
+
tasks: ParsedTask[];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse task hierarchy from task ID
|
|
66
|
+
*
|
|
67
|
+
* @param taskId - Task ID in various formats
|
|
68
|
+
* @returns Object with sprint_id and epic_id, or null if invalid
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseTaskHierarchy(taskId: string): {
|
|
71
|
+
sprint_id: string;
|
|
72
|
+
epic_id: string;
|
|
73
|
+
} | null;
|
|
74
|
+
/**
|
|
75
|
+
* Parse a single task block
|
|
76
|
+
*
|
|
77
|
+
* @param blockText - Raw markdown text for one task
|
|
78
|
+
* @param sprintContext - Sprint context for legacy TASK-N format
|
|
79
|
+
* @returns ParsedTask or null if invalid
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseTaskBlock(blockText: string, sprintContext?: {
|
|
82
|
+
sprint_id: string;
|
|
83
|
+
epic_id: string;
|
|
84
|
+
}): ParsedTask | null;
|
|
85
|
+
/**
|
|
86
|
+
* Parse sprint markdown file to extract all tasks
|
|
87
|
+
*
|
|
88
|
+
* @param content - Raw sprint markdown content
|
|
89
|
+
* @param filePath - Path to sprint file (for metadata extraction)
|
|
90
|
+
* @returns SprintParseResult with sprint metadata and parsed tasks
|
|
91
|
+
*/
|
|
92
|
+
export declare function parseSprintTasks(content: string, filePath: string): SprintParseResult;
|
|
93
|
+
/**
|
|
94
|
+
* Parse sprint file from filesystem
|
|
95
|
+
*
|
|
96
|
+
* @param filePath - Absolute path to sprint markdown file
|
|
97
|
+
* @returns SprintParseResult or null if file not found
|
|
98
|
+
*/
|
|
99
|
+
export declare function parseSprintFile(filePath: string): Promise<SprintParseResult | null>;
|
|
100
|
+
/**
|
|
101
|
+
* Parse all sprint files in a directory
|
|
102
|
+
*
|
|
103
|
+
* @param sprintsDir - Path to sprints directory
|
|
104
|
+
* @returns Array of SprintParseResult
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseAllSprints(sprintsDir: string): Promise<SprintParseResult[]>;
|
|
107
|
+
//# sourceMappingURL=task-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-parser.d.ts","sourceRoot":"","sources":["../../src/lib/task-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAmBH;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE3F;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,kEAAkE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,SAAS,EAAE,MAAM,CAAC;IAClB,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,4CAA4C;IAC5C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,qBAAqB;IACrB,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,6BAA6B;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yDAAyD;IACzD,cAAc,EAAE,UAAU,CAAC;IAC3B,4BAA4B;IAC5B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,+BAA+B;IAC/B,mBAAmB,EAAE,MAAM,EAAE,CAAC;IAC9B,uBAAuB;IACvB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,6BAA6B;IAC7B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,EAAE,EAAE,MAAM,CAAC;IACX,kBAAkB;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,cAAc;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,UAAU,EAAE,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA8BhG;AA4GD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EACjB,aAAa,CAAC,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrD,UAAU,GAAG,IAAI,CAgGnB;AAgFD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAwBrF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAazF;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAyBtF"}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileType: utility
|
|
3
|
+
* @status: current
|
|
4
|
+
* @updated: 2026-01-19
|
|
5
|
+
* @tags: [task-parser, sprint, epic-015, sprint-0a]
|
|
6
|
+
* @related: [sprint-parser.ts, task-graph-sync.ts]
|
|
7
|
+
* @priority: high
|
|
8
|
+
* @complexity: medium
|
|
9
|
+
* @dependencies: [fs-extra]
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Task Parser for Sprint Markdown (EPIC-015 Sprint 0a Task 1)
|
|
13
|
+
*
|
|
14
|
+
* Parses task definitions from sprint markdown files into structured data
|
|
15
|
+
* for syncing to Neo4j graph. Handles multiple task ID formats:
|
|
16
|
+
* - Standard: e{NNN}_s{NN}_t{NN} (e.g., e015_s00a_t01)
|
|
17
|
+
* - Legacy: TASK-N (e.g., TASK-1)
|
|
18
|
+
* - Ad-hoc: adhoc_{YYMMDD}_s{NN}_t{NN} (e.g., adhoc_260119_s01_t01)
|
|
19
|
+
*
|
|
20
|
+
* Key principle (ADR-060): Content from Git, State from Graph.
|
|
21
|
+
* Parser extracts CONTENT fields only (title, goal, priority, estimate).
|
|
22
|
+
* Status in markdown is only used for initial creation, not updates.
|
|
23
|
+
*/
|
|
24
|
+
import fs from 'fs-extra';
|
|
25
|
+
import path from 'path';
|
|
26
|
+
/**
|
|
27
|
+
* Parse task hierarchy from task ID
|
|
28
|
+
*
|
|
29
|
+
* @param taskId - Task ID in various formats
|
|
30
|
+
* @returns Object with sprint_id and epic_id, or null if invalid
|
|
31
|
+
*/
|
|
32
|
+
export function parseTaskHierarchy(taskId) {
|
|
33
|
+
// Standard format: e015_s00a_t01 or e015_s00_t01
|
|
34
|
+
const standardMatch = taskId.match(/^(e\d{3})_(s\d{2}[a-z]?)_(t\d{2})$/i);
|
|
35
|
+
if (standardMatch) {
|
|
36
|
+
const epicId = standardMatch[1].toLowerCase();
|
|
37
|
+
const sprintSuffix = standardMatch[2].toLowerCase();
|
|
38
|
+
return {
|
|
39
|
+
sprint_id: `${epicId}_${sprintSuffix}`,
|
|
40
|
+
epic_id: epicId,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Ad-hoc format: adhoc_260119_s01_t01
|
|
44
|
+
const adhocMatch = taskId.match(/^(adhoc_\d{6})_(s\d{2})_(t\d{2})$/i);
|
|
45
|
+
if (adhocMatch) {
|
|
46
|
+
const adhocId = adhocMatch[1].toLowerCase();
|
|
47
|
+
const sprintSuffix = adhocMatch[2].toLowerCase();
|
|
48
|
+
return {
|
|
49
|
+
sprint_id: `${adhocId}_${sprintSuffix}`,
|
|
50
|
+
epic_id: adhocId,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
// Legacy TASK-N format - derive from context (requires sprint info)
|
|
54
|
+
// For legacy format, we cannot derive hierarchy without sprint context
|
|
55
|
+
if (taskId.match(/^TASK-\d+$/i)) {
|
|
56
|
+
return null; // Caller must provide sprint context
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Map checkbox character to task status
|
|
62
|
+
*
|
|
63
|
+
* @param checkbox - Single character from checkbox ([x], [@], [ ], [Z])
|
|
64
|
+
* @returns TaskStatus value
|
|
65
|
+
*/
|
|
66
|
+
function mapCheckboxToStatus(checkbox) {
|
|
67
|
+
if (!checkbox)
|
|
68
|
+
return 'not_started';
|
|
69
|
+
const char = checkbox.trim().toLowerCase();
|
|
70
|
+
switch (char) {
|
|
71
|
+
case 'x':
|
|
72
|
+
return 'complete';
|
|
73
|
+
case '@':
|
|
74
|
+
return 'in_progress';
|
|
75
|
+
case 'z':
|
|
76
|
+
return 'paused';
|
|
77
|
+
case ' ':
|
|
78
|
+
default:
|
|
79
|
+
return 'not_started';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Normalize priority value
|
|
84
|
+
*/
|
|
85
|
+
function normalizePriority(priority) {
|
|
86
|
+
if (!priority)
|
|
87
|
+
return 'MEDIUM';
|
|
88
|
+
const upper = priority.trim().toUpperCase();
|
|
89
|
+
if (['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'].includes(upper)) {
|
|
90
|
+
return upper;
|
|
91
|
+
}
|
|
92
|
+
return 'MEDIUM';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extract acceptance criteria from task block
|
|
96
|
+
*/
|
|
97
|
+
function extractAcceptanceCriteria(blockText) {
|
|
98
|
+
const criteria = [];
|
|
99
|
+
// Find acceptance criteria section
|
|
100
|
+
const sectionMatch = blockText.match(/\*\*Acceptance Criteria:\*\*\s*([\s\S]*?)(?=\n\*\*|\n###|\n---|\n##|$)/i);
|
|
101
|
+
if (!sectionMatch)
|
|
102
|
+
return criteria;
|
|
103
|
+
// Extract checkbox items: - [ ] or - [x]
|
|
104
|
+
const checkboxMatches = sectionMatch[1].matchAll(/^-\s+\[.\]\s+(.+?)$/gm);
|
|
105
|
+
for (const match of checkboxMatches) {
|
|
106
|
+
criteria.push(match[1].trim());
|
|
107
|
+
}
|
|
108
|
+
// Also extract plain bullets if no checkboxes found
|
|
109
|
+
if (criteria.length === 0) {
|
|
110
|
+
const bulletMatches = sectionMatch[1].matchAll(/^-\s+(.+?)$/gm);
|
|
111
|
+
for (const match of bulletMatches) {
|
|
112
|
+
criteria.push(match[1].trim());
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return criteria;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Extract file paths from task block
|
|
119
|
+
*/
|
|
120
|
+
function extractFiles(blockText) {
|
|
121
|
+
const files = new Set();
|
|
122
|
+
// Pattern: **Files:** section with bullet list
|
|
123
|
+
const filesSection = blockText.match(/\*\*Files(?:\sto\s(?:Create|Modify))?:\*\*\s*([\s\S]*?)(?=\n\*\*|\n###|\n---|\n##|$)/i);
|
|
124
|
+
if (filesSection) {
|
|
125
|
+
// Match: - Create: `path/to/file.ts` or - Modify: `path/to/file.ts` or just `path`
|
|
126
|
+
const pathMatches = filesSection[1].matchAll(/`([^`]+\.[a-z]+)`/gi);
|
|
127
|
+
for (const match of pathMatches) {
|
|
128
|
+
files.add(match[1]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Pattern: inline code paths that look like file paths
|
|
132
|
+
const inlineMatches = blockText.matchAll(/`((?:packages|dashboard|src|docs)\/[^`]+\.[a-z]+)`/gi);
|
|
133
|
+
for (const match of inlineMatches) {
|
|
134
|
+
files.add(match[1]);
|
|
135
|
+
}
|
|
136
|
+
return Array.from(files).sort();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Extract related ADR references
|
|
140
|
+
*/
|
|
141
|
+
function extractRelatedADRs(blockText) {
|
|
142
|
+
const adrs = new Set();
|
|
143
|
+
// Match ADR-XXX patterns
|
|
144
|
+
const adrMatches = blockText.matchAll(/ADR-(\d+)/gi);
|
|
145
|
+
for (const match of adrMatches) {
|
|
146
|
+
adrs.add(`ADR-${match[1]}`);
|
|
147
|
+
}
|
|
148
|
+
return Array.from(adrs).sort();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse a single task block
|
|
152
|
+
*
|
|
153
|
+
* @param blockText - Raw markdown text for one task
|
|
154
|
+
* @param sprintContext - Sprint context for legacy TASK-N format
|
|
155
|
+
* @returns ParsedTask or null if invalid
|
|
156
|
+
*/
|
|
157
|
+
export function parseTaskBlock(blockText, sprintContext) {
|
|
158
|
+
// Extract task header - multiple formats supported
|
|
159
|
+
// Standard: ### e015_s00a_t01: Title (3h)
|
|
160
|
+
// Legacy: ### TASK-1: Title (4-6h)
|
|
161
|
+
// Ad-hoc: ### adhoc_260119_s01_t01 - Title
|
|
162
|
+
// Without time: ### e015_s00a_t01: Title
|
|
163
|
+
const headerPatterns = [
|
|
164
|
+
// Standard with colon and optional time
|
|
165
|
+
/^###\s+([a-z0-9_]+):\s+(.+?)\s*(?:\(([0-9]+(?:-[0-9]+)?h?)\))?$/im,
|
|
166
|
+
// With dash separator (ad-hoc style)
|
|
167
|
+
/^###\s+([a-z0-9_]+)\s+-\s+(.+?)$/im,
|
|
168
|
+
// Legacy TASK-N format
|
|
169
|
+
/^###\s+(TASK-\d+):\s+(.+?)\s*(?:\(([0-9]+(?:-[0-9]+)?h?)\))?$/im,
|
|
170
|
+
];
|
|
171
|
+
let taskId = null;
|
|
172
|
+
let title = null;
|
|
173
|
+
let estimate = null;
|
|
174
|
+
for (const pattern of headerPatterns) {
|
|
175
|
+
const match = blockText.match(pattern);
|
|
176
|
+
if (match) {
|
|
177
|
+
taskId = match[1];
|
|
178
|
+
title = match[2].trim();
|
|
179
|
+
estimate = match[3] || null;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!taskId || !title) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
// Parse hierarchy
|
|
187
|
+
let hierarchy = parseTaskHierarchy(taskId);
|
|
188
|
+
// For legacy TASK-N format, use sprint context
|
|
189
|
+
if (!hierarchy && sprintContext) {
|
|
190
|
+
hierarchy = {
|
|
191
|
+
sprint_id: sprintContext.sprint_id,
|
|
192
|
+
epic_id: sprintContext.epic_id,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
if (!hierarchy) {
|
|
196
|
+
// Cannot determine hierarchy, skip task
|
|
197
|
+
console.warn(`Cannot determine hierarchy for task: ${taskId}`);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
// Extract status from checkbox: **Status:** [x]
|
|
201
|
+
const statusMatch = blockText.match(/\*\*Status:\*\*\s+\[(.)\]/i);
|
|
202
|
+
const initialStatus = mapCheckboxToStatus(statusMatch?.[1]);
|
|
203
|
+
// Extract priority
|
|
204
|
+
const priorityMatch = blockText.match(/\*\*Priority:\*\*\s+([A-Z_]+)/i);
|
|
205
|
+
const priority = normalizePriority(priorityMatch?.[1]);
|
|
206
|
+
// Extract assignee (accepts both Assignee and Owner)
|
|
207
|
+
const assigneeMatch = blockText.match(/\*\*(?:Assignee|Owner):\*\*\s+([^\n]+)/i);
|
|
208
|
+
let assignee = null;
|
|
209
|
+
if (assigneeMatch) {
|
|
210
|
+
const value = assigneeMatch[1].trim();
|
|
211
|
+
// Filter out "TBD", "None", empty values
|
|
212
|
+
if (value && !['tbd', 'none', 'n/a', '-'].includes(value.toLowerCase())) {
|
|
213
|
+
assignee = value;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Extract goal
|
|
217
|
+
const goalMatch = blockText.match(/\*\*Goal:\*\*\s+([^\n]+)/i);
|
|
218
|
+
const goal = goalMatch ? goalMatch[1].trim() : null;
|
|
219
|
+
// Extract acceptance criteria
|
|
220
|
+
const acceptanceCriteria = extractAcceptanceCriteria(blockText);
|
|
221
|
+
// Extract files
|
|
222
|
+
const files = extractFiles(blockText);
|
|
223
|
+
// Extract related ADRs
|
|
224
|
+
const relatedADRs = extractRelatedADRs(blockText);
|
|
225
|
+
return {
|
|
226
|
+
id: taskId.toLowerCase(),
|
|
227
|
+
sprint_id: hierarchy.sprint_id,
|
|
228
|
+
epic_id: hierarchy.epic_id,
|
|
229
|
+
title,
|
|
230
|
+
estimate,
|
|
231
|
+
priority,
|
|
232
|
+
assignee,
|
|
233
|
+
initial_status: initialStatus,
|
|
234
|
+
goal,
|
|
235
|
+
acceptance_criteria: acceptanceCriteria,
|
|
236
|
+
files,
|
|
237
|
+
related_adrs: relatedADRs,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Extract sprint metadata from sprint file
|
|
242
|
+
*
|
|
243
|
+
* @param content - Sprint file content
|
|
244
|
+
* @param filePath - Path to sprint file
|
|
245
|
+
* @returns ParsedSprint metadata
|
|
246
|
+
*/
|
|
247
|
+
function extractSprintMetadata(content, filePath) {
|
|
248
|
+
const filename = path.basename(filePath, '.md');
|
|
249
|
+
// Try to extract sprint ID from filename
|
|
250
|
+
// Pattern: SPRINT-2026-01-e015-s00a-... → e015_s00a
|
|
251
|
+
const filenameMatch = filename.match(/SPRINT-\d{4}-\d{2}-(e\d{3})-(s\d{2}[a-z]?)-/i);
|
|
252
|
+
if (filenameMatch) {
|
|
253
|
+
const epicId = filenameMatch[1].toLowerCase();
|
|
254
|
+
const sprintSuffix = filenameMatch[2].toLowerCase();
|
|
255
|
+
const sprintId = `${epicId}_${sprintSuffix}`;
|
|
256
|
+
// Extract sprint name from title
|
|
257
|
+
const titleMatch = content.match(/^#\s+(?:SPRINT:\s+)?(.+?)(?:\s+\(|$)/m);
|
|
258
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
259
|
+
return {
|
|
260
|
+
id: sprintId,
|
|
261
|
+
name,
|
|
262
|
+
epic_id: epicId,
|
|
263
|
+
file_path: filePath,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
// Ad-hoc pattern: SPRINT-adhoc_260119-...
|
|
267
|
+
const adhocMatch = filename.match(/SPRINT-(adhoc_\d{6})-/i);
|
|
268
|
+
if (adhocMatch) {
|
|
269
|
+
const adhocId = adhocMatch[1].toLowerCase();
|
|
270
|
+
const sprintId = `${adhocId}_s01`; // Default to s01 for adhoc
|
|
271
|
+
const titleMatch = content.match(/^#\s+(.+?)(?:\s+\(|$)/m);
|
|
272
|
+
const name = titleMatch ? titleMatch[1].trim() : filename;
|
|
273
|
+
return {
|
|
274
|
+
id: sprintId,
|
|
275
|
+
name,
|
|
276
|
+
epic_id: adhocId,
|
|
277
|
+
file_path: filePath,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// Legacy pattern from content: # SPRINT: Name (EPIC-XXX Sprint N)
|
|
281
|
+
const legacyMatch = content.match(/^#\s+SPRINT:\s+(.+?)\s+\(EPIC-(\d+)\s+Sprint\s+(\d+)\)/m);
|
|
282
|
+
if (legacyMatch) {
|
|
283
|
+
const name = legacyMatch[1].trim();
|
|
284
|
+
const epicNum = legacyMatch[2];
|
|
285
|
+
const sprintNum = legacyMatch[3];
|
|
286
|
+
const epicId = `e${epicNum.padStart(3, '0')}`;
|
|
287
|
+
const sprintId = `${epicId}_s${sprintNum.padStart(2, '0')}`;
|
|
288
|
+
return {
|
|
289
|
+
id: sprintId,
|
|
290
|
+
name,
|
|
291
|
+
epic_id: epicId,
|
|
292
|
+
file_path: filePath,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// Fallback: generate from filename
|
|
296
|
+
const fallbackId = filename
|
|
297
|
+
.toLowerCase()
|
|
298
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
299
|
+
.replace(/^sprint_/, '');
|
|
300
|
+
return {
|
|
301
|
+
id: fallbackId,
|
|
302
|
+
name: filename,
|
|
303
|
+
epic_id: 'unknown',
|
|
304
|
+
file_path: filePath,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Parse sprint markdown file to extract all tasks
|
|
309
|
+
*
|
|
310
|
+
* @param content - Raw sprint markdown content
|
|
311
|
+
* @param filePath - Path to sprint file (for metadata extraction)
|
|
312
|
+
* @returns SprintParseResult with sprint metadata and parsed tasks
|
|
313
|
+
*/
|
|
314
|
+
export function parseSprintTasks(content, filePath) {
|
|
315
|
+
// Extract sprint metadata
|
|
316
|
+
const sprint = extractSprintMetadata(content, filePath);
|
|
317
|
+
// Split content by task headers (### followed by task ID pattern)
|
|
318
|
+
// Match: ### e015_s00a_t01:, ### TASK-1:, ### adhoc_..._t01
|
|
319
|
+
const taskSections = content.split(/(?=^###\s+(?:e\d{3}_s\d{2}[a-z]?_t\d{2}|TASK-\d+|adhoc_\d{6}_s\d{2}_t\d{2})[\s:-])/im);
|
|
320
|
+
const tasks = [];
|
|
321
|
+
const sprintContext = {
|
|
322
|
+
sprint_id: sprint.id,
|
|
323
|
+
epic_id: sprint.epic_id,
|
|
324
|
+
};
|
|
325
|
+
for (const section of taskSections) {
|
|
326
|
+
if (!section.trim().startsWith('###'))
|
|
327
|
+
continue;
|
|
328
|
+
const task = parseTaskBlock(section, sprintContext);
|
|
329
|
+
if (task) {
|
|
330
|
+
tasks.push(task);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return { sprint, tasks };
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse sprint file from filesystem
|
|
337
|
+
*
|
|
338
|
+
* @param filePath - Absolute path to sprint markdown file
|
|
339
|
+
* @returns SprintParseResult or null if file not found
|
|
340
|
+
*/
|
|
341
|
+
export async function parseSprintFile(filePath) {
|
|
342
|
+
try {
|
|
343
|
+
if (!await fs.pathExists(filePath)) {
|
|
344
|
+
console.warn(`Sprint file not found: ${filePath}`);
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
348
|
+
return parseSprintTasks(content, filePath);
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
console.error(`Failed to parse sprint file ${filePath}:`, error);
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Parse all sprint files in a directory
|
|
357
|
+
*
|
|
358
|
+
* @param sprintsDir - Path to sprints directory
|
|
359
|
+
* @returns Array of SprintParseResult
|
|
360
|
+
*/
|
|
361
|
+
export async function parseAllSprints(sprintsDir) {
|
|
362
|
+
const results = [];
|
|
363
|
+
try {
|
|
364
|
+
if (!await fs.pathExists(sprintsDir)) {
|
|
365
|
+
console.warn(`Sprints directory not found: ${sprintsDir}`);
|
|
366
|
+
return results;
|
|
367
|
+
}
|
|
368
|
+
const files = await fs.readdir(sprintsDir);
|
|
369
|
+
const sprintFiles = files.filter(f => f.startsWith('SPRINT-') && f.endsWith('.md'));
|
|
370
|
+
for (const file of sprintFiles) {
|
|
371
|
+
const filePath = path.join(sprintsDir, file);
|
|
372
|
+
const result = await parseSprintFile(filePath);
|
|
373
|
+
if (result) {
|
|
374
|
+
results.push(result);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return results;
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
console.error(`Failed to parse sprints directory ${sprintsDir}:`, error);
|
|
381
|
+
return results;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=task-parser.js.map
|