@danalexilewis/taskgraph 0.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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/cli/block.js +114 -0
  4. package/dist/cli/context.js +139 -0
  5. package/dist/cli/done.js +98 -0
  6. package/dist/cli/edge.js +99 -0
  7. package/dist/cli/export.js +97 -0
  8. package/dist/cli/import.js +123 -0
  9. package/dist/cli/index.js +78 -0
  10. package/dist/cli/init.js +106 -0
  11. package/dist/cli/next.js +97 -0
  12. package/dist/cli/note.js +72 -0
  13. package/dist/cli/plan.js +108 -0
  14. package/dist/cli/portfolio.js +159 -0
  15. package/dist/cli/setup.js +142 -0
  16. package/dist/cli/show.js +142 -0
  17. package/dist/cli/split.js +191 -0
  18. package/dist/cli/start.js +94 -0
  19. package/dist/cli/status.js +149 -0
  20. package/dist/cli/task.js +92 -0
  21. package/dist/cli/utils.js +74 -0
  22. package/dist/db/commit.js +18 -0
  23. package/dist/db/connection.js +22 -0
  24. package/dist/db/escape.js +6 -0
  25. package/dist/db/migrate.js +159 -0
  26. package/dist/db/query.js +102 -0
  27. package/dist/domain/errors.js +33 -0
  28. package/dist/domain/invariants.js +103 -0
  29. package/dist/domain/types.js +120 -0
  30. package/dist/export/dot.js +21 -0
  31. package/dist/export/graph-data.js +41 -0
  32. package/dist/export/markdown.js +108 -0
  33. package/dist/export/mermaid.js +27 -0
  34. package/dist/plan-import/importer.js +155 -0
  35. package/dist/plan-import/parser.js +213 -0
  36. package/dist/template/.cursor/memory.md +14 -0
  37. package/dist/template/.cursor/rules/memory.mdc +11 -0
  38. package/dist/template/.cursor/rules/plan-authoring.mdc +42 -0
  39. package/dist/template/.cursor/rules/session-start.mdc +18 -0
  40. package/dist/template/.cursor/rules/taskgraph-workflow.mdc +35 -0
  41. package/dist/template/AGENT.md +73 -0
  42. package/dist/template/docs/backend.md +33 -0
  43. package/dist/template/docs/frontend.md +31 -0
  44. package/dist/template/docs/infra.md +26 -0
  45. package/dist/template/docs/skills/README.md +14 -0
  46. package/dist/template/docs/skills/plan-authoring.md +38 -0
  47. package/dist/template/docs/skills/refactoring-safely.md +21 -0
  48. package/dist/template/docs/skills/taskgraph-lifecycle-execution.md +23 -0
  49. package/package.json +47 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.jsonObj = jsonObj;
4
+ exports.now = now;
5
+ exports.query = query;
6
+ const connection_1 = require("./connection");
7
+ const escape_1 = require("./escape");
8
+ function jsonObj(value) {
9
+ return { _type: "json", value };
10
+ }
11
+ function now() {
12
+ return new Date().toISOString().slice(0, 19).replace("T", " ");
13
+ }
14
+ function formatValue(value) {
15
+ if (value === null) {
16
+ return "NULL";
17
+ }
18
+ if (typeof value === "boolean" || typeof value === "number") {
19
+ return String(value);
20
+ }
21
+ if (typeof value === "string") {
22
+ return `'${(0, escape_1.sqlEscape)(value)}'`;
23
+ }
24
+ if (value._type === "json") {
25
+ const jsonPairs = Object.entries(value.value)
26
+ .map(([key, val]) => {
27
+ return `'${(0, escape_1.sqlEscape)(key)}', '${(0, escape_1.sqlEscape)(JSON.stringify(val))}'`;
28
+ })
29
+ .join(", ");
30
+ return `JSON_OBJECT(${jsonPairs})`;
31
+ }
32
+ // Should not happen with SqlValue type
33
+ return `'${(0, escape_1.sqlEscape)(String(value))}'`;
34
+ }
35
+ function backtickWrap(name) {
36
+ return `\`${name}\``;
37
+ }
38
+ function buildWhereClause(where) {
39
+ const parts = [];
40
+ for (const [key, val] of Object.entries(where)) {
41
+ if (typeof val === "object" && val !== null && "op" in val) {
42
+ parts.push(`${backtickWrap(key)} ${val.op} ${formatValue(val.value)}`);
43
+ }
44
+ else {
45
+ parts.push(`${backtickWrap(key)} = ${formatValue(val)}`);
46
+ }
47
+ }
48
+ return parts.join(" AND ");
49
+ }
50
+ function query(repoPath) {
51
+ return {
52
+ insert: (table, data) => {
53
+ const cols = Object.keys(data).map(backtickWrap).join(", ");
54
+ const vals = Object.values(data).map(formatValue).join(", ");
55
+ const sql = `INSERT INTO ${backtickWrap(table)} (${cols}) VALUES (${vals})`;
56
+ return (0, connection_1.doltSql)(sql, repoPath).map((res) => res);
57
+ },
58
+ update: (table, data, where) => {
59
+ const setParts = Object.entries(data)
60
+ .map(([key, val]) => `${backtickWrap(key)} = ${formatValue(val)}`)
61
+ .join(", ");
62
+ const whereClause = buildWhereClause(where);
63
+ const sql = `UPDATE ${backtickWrap(table)} SET ${setParts} WHERE ${whereClause}`;
64
+ return (0, connection_1.doltSql)(sql, repoPath).map((res) => res);
65
+ },
66
+ select: (table, options) => {
67
+ let sql = `SELECT ${options?.columns?.map(backtickWrap).join(", ") ?? "*"} FROM ${backtickWrap(table)}`;
68
+ const whereClause = options?.where && Object.keys(options.where).length > 0
69
+ ? buildWhereClause(options.where)
70
+ : "";
71
+ if (whereClause) {
72
+ sql += ` WHERE ${whereClause}`;
73
+ }
74
+ if (options?.groupBy && options.groupBy.length > 0) {
75
+ sql += ` GROUP BY ${options.groupBy.map(backtickWrap).join(", ")}`;
76
+ }
77
+ if (options?.having) {
78
+ sql += ` HAVING ${options.having}`;
79
+ }
80
+ if (options?.orderBy) {
81
+ sql += ` ORDER BY ${options.orderBy}`; // orderBy expected to be already escaped/formatted
82
+ }
83
+ if (options?.limit) {
84
+ sql += ` LIMIT ${options.limit}`;
85
+ }
86
+ if (options?.offset) {
87
+ sql += ` OFFSET ${options.offset}`;
88
+ }
89
+ return (0, connection_1.doltSql)(sql, repoPath).map((res) => res);
90
+ },
91
+ count: (table, where) => {
92
+ let sql = `SELECT COUNT(*) AS count FROM ${backtickWrap(table)}`;
93
+ if (where) {
94
+ sql += ` WHERE ${buildWhereClause(where)}`;
95
+ }
96
+ return (0, connection_1.doltSql)(sql, repoPath).map((res) => res[0]?.count ?? 0);
97
+ },
98
+ raw: (sql) => {
99
+ return (0, connection_1.doltSql)(sql, repoPath).map((res) => res);
100
+ },
101
+ };
102
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildError = exports.ErrorCode = void 0;
4
+ var ErrorCode;
5
+ (function (ErrorCode) {
6
+ // DB errors
7
+ ErrorCode["DB_QUERY_FAILED"] = "DB_QUERY_FAILED";
8
+ ErrorCode["DB_COMMIT_FAILED"] = "DB_COMMIT_FAILED";
9
+ ErrorCode["DB_PARSE_FAILED"] = "DB_PARSE_FAILED";
10
+ // Domain errors
11
+ ErrorCode["TASK_NOT_FOUND"] = "TASK_NOT_FOUND";
12
+ ErrorCode["TASK_ALREADY_CLAIMED"] = "TASK_ALREADY_CLAIMED";
13
+ ErrorCode["PLAN_NOT_FOUND"] = "PLAN_NOT_FOUND";
14
+ ErrorCode["INVALID_TRANSITION"] = "INVALID_TRANSITION";
15
+ ErrorCode["TASK_NOT_RUNNABLE"] = "TASK_NOT_RUNNABLE";
16
+ ErrorCode["CYCLE_DETECTED"] = "CYCLE_DETECTED";
17
+ ErrorCode["EDGE_EXISTS"] = "EDGE_EXISTS";
18
+ // Config errors
19
+ ErrorCode["CONFIG_NOT_FOUND"] = "CONFIG_NOT_FOUND";
20
+ ErrorCode["CONFIG_PARSE_FAILED"] = "CONFIG_PARSE_FAILED";
21
+ // Import errors
22
+ ErrorCode["FILE_READ_FAILED"] = "FILE_READ_FAILED";
23
+ ErrorCode["PARSE_FAILED"] = "PARSE_FAILED";
24
+ // Validation
25
+ ErrorCode["VALIDATION_FAILED"] = "VALIDATION_FAILED";
26
+ ErrorCode["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
27
+ })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
28
+ const buildError = (code, message, cause) => ({
29
+ code,
30
+ message,
31
+ cause,
32
+ });
33
+ exports.buildError = buildError;
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkNoBlockerCycle = checkNoBlockerCycle;
4
+ exports.checkRunnable = checkRunnable;
5
+ exports.checkValidTransition = checkValidTransition;
6
+ const neverthrow_1 = require("neverthrow");
7
+ const neverthrow_2 = require("neverthrow");
8
+ const errors_1 = require("./errors");
9
+ const query_1 = require("../db/query");
10
+ const escape_1 = require("../db/escape");
11
+ // Helper for cycle detection (DFS)
12
+ function hasCycleDFS(start, current, graph, visited, recursionStack) {
13
+ visited.add(current);
14
+ recursionStack.add(current);
15
+ const neighbors = graph.get(current) || [];
16
+ for (const neighbor of neighbors) {
17
+ if (!visited.has(neighbor)) {
18
+ if (hasCycleDFS(start, neighbor, graph, visited, recursionStack)) {
19
+ return true;
20
+ }
21
+ }
22
+ else if (recursionStack.has(neighbor)) {
23
+ return true; // Cycle detected
24
+ }
25
+ }
26
+ recursionStack.delete(current);
27
+ return false;
28
+ }
29
+ function checkNoBlockerCycle(fromTaskId, toTaskId, existingEdges) {
30
+ const allEdges = [
31
+ ...existingEdges,
32
+ {
33
+ from_task_id: fromTaskId,
34
+ to_task_id: toTaskId,
35
+ type: "blocks",
36
+ reason: null,
37
+ },
38
+ ];
39
+ const graph = new Map();
40
+ for (const edge of allEdges) {
41
+ if (edge.type === "blocks") {
42
+ if (!graph.has(edge.from_task_id)) {
43
+ graph.set(edge.from_task_id, []);
44
+ }
45
+ graph.get(edge.from_task_id)?.push(edge.to_task_id);
46
+ }
47
+ }
48
+ // Check for cycles starting from each node
49
+ for (const node of graph.keys()) {
50
+ const visited = new Set();
51
+ const recursionStack = new Set();
52
+ if (hasCycleDFS(node, node, graph, visited, recursionStack)) {
53
+ return (0, neverthrow_1.err)((0, errors_1.buildError)(errors_1.ErrorCode.CYCLE_DETECTED, `Blocking edge from ${fromTaskId} to ${toTaskId} would create a cycle.`));
54
+ }
55
+ }
56
+ return (0, neverthrow_1.ok)(undefined);
57
+ }
58
+ function checkRunnable(taskId, repoPath) {
59
+ const q = (0, query_1.query)(repoPath);
60
+ return q
61
+ .select("task", {
62
+ columns: ["status"],
63
+ where: { task_id: taskId },
64
+ })
65
+ .andThen((taskResult) => {
66
+ if (taskResult.length === 0) {
67
+ return (0, neverthrow_2.errAsync)((0, errors_1.buildError)(errors_1.ErrorCode.TASK_NOT_FOUND, `Task with ID ${taskId} not found.`));
68
+ }
69
+ const taskStatus = taskResult[0].status;
70
+ if (taskStatus !== "todo") {
71
+ return (0, neverthrow_2.errAsync)((0, errors_1.buildError)(errors_1.ErrorCode.INVALID_TRANSITION, `Task ${taskId} is not in 'todo' status. Current status: ${taskStatus}.`));
72
+ }
73
+ const unmetBlockersQuery = `
74
+ SELECT COUNT(*)
75
+ FROM \`edge\` e
76
+ JOIN \`task\` bt ON e.from_task_id = bt.task_id
77
+ WHERE e.to_task_id = '${(0, escape_1.sqlEscape)(taskId)}'
78
+ AND e.type = 'blocks'
79
+ AND bt.status NOT IN ('done','canceled');
80
+ `;
81
+ return q.raw(unmetBlockersQuery);
82
+ })
83
+ .andThen((blockerCountResult) => {
84
+ const unmetBlockers = blockerCountResult[0]["COUNT(*)"];
85
+ if (unmetBlockers > 0) {
86
+ return (0, neverthrow_2.errAsync)((0, errors_1.buildError)(errors_1.ErrorCode.TASK_NOT_RUNNABLE, `Task ${taskId} has ${unmetBlockers} unmet blockers and is not runnable.`));
87
+ }
88
+ return (0, neverthrow_2.okAsync)(undefined);
89
+ });
90
+ }
91
+ function checkValidTransition(currentStatus, nextStatus) {
92
+ const validTransitions = {
93
+ todo: ["doing", "blocked", "canceled"],
94
+ doing: ["done", "blocked", "canceled"],
95
+ blocked: ["todo", "canceled"], // Can only go to todo when unblocked, or canceled
96
+ done: [], // Terminal state
97
+ canceled: [], // Terminal state
98
+ };
99
+ if (!validTransitions[currentStatus].includes(nextStatus)) {
100
+ return (0, neverthrow_1.err)((0, errors_1.buildError)(errors_1.ErrorCode.INVALID_TRANSITION, `Invalid task status transition from '${currentStatus}' to '${nextStatus}'.`));
101
+ }
102
+ return (0, neverthrow_1.ok)(undefined);
103
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DecisionSchema = exports.EventSchema = exports.EdgeSchema = exports.TaskSkillSchema = exports.TaskDomainSchema = exports.TaskSchema = exports.PlanSchema = exports.PlanRiskEntrySchema = exports.ActorSchema = exports.EventKindSchema = exports.EdgeTypeSchema = exports.ChangeTypeSchema = exports.RiskSchema = exports.OwnerSchema = exports.TaskStatusSchema = exports.PlanStatusSchema = void 0;
4
+ const zod_1 = require("zod");
5
+ // Enums
6
+ exports.PlanStatusSchema = zod_1.z.enum([
7
+ "draft",
8
+ "active",
9
+ "paused",
10
+ "done",
11
+ "abandoned",
12
+ ]);
13
+ exports.TaskStatusSchema = zod_1.z.enum([
14
+ "todo",
15
+ "doing",
16
+ "blocked",
17
+ "done",
18
+ "canceled",
19
+ ]);
20
+ exports.OwnerSchema = zod_1.z.enum(["human", "agent"]);
21
+ exports.RiskSchema = zod_1.z.enum(["low", "medium", "high"]);
22
+ exports.ChangeTypeSchema = zod_1.z.enum([
23
+ "create",
24
+ "modify",
25
+ "refactor",
26
+ "fix",
27
+ "investigate",
28
+ "test",
29
+ "document",
30
+ ]);
31
+ exports.EdgeTypeSchema = zod_1.z.enum(["blocks", "relates"]);
32
+ exports.EventKindSchema = zod_1.z.enum([
33
+ "created",
34
+ "started",
35
+ "progress",
36
+ "blocked",
37
+ "unblocked",
38
+ "done",
39
+ "split",
40
+ "decision_needed",
41
+ "note",
42
+ ]);
43
+ exports.ActorSchema = zod_1.z.enum(["human", "agent"]);
44
+ // Risk entry for plan.risks (rich planning)
45
+ exports.PlanRiskEntrySchema = zod_1.z.object({
46
+ description: zod_1.z.string(),
47
+ severity: exports.RiskSchema,
48
+ mitigation: zod_1.z.string(),
49
+ });
50
+ // Schemas
51
+ exports.PlanSchema = zod_1.z.object({
52
+ plan_id: zod_1.z.string().uuid(),
53
+ title: zod_1.z.string().max(255),
54
+ intent: zod_1.z.string(),
55
+ status: exports.PlanStatusSchema.default("draft"),
56
+ priority: zod_1.z.number().int().default(0),
57
+ source_path: zod_1.z.string().max(512).nullable(),
58
+ source_commit: zod_1.z.string().max(64).nullable(),
59
+ created_at: zod_1.z.string().datetime(),
60
+ updated_at: zod_1.z.string().datetime(),
61
+ file_tree: zod_1.z.string().nullable(),
62
+ risks: zod_1.z.array(exports.PlanRiskEntrySchema).nullable(),
63
+ tests: zod_1.z.array(zod_1.z.string()).nullable(),
64
+ });
65
+ exports.TaskSchema = zod_1.z.object({
66
+ task_id: zod_1.z.string().uuid(),
67
+ plan_id: zod_1.z.string().uuid(),
68
+ feature_key: zod_1.z.string().max(64).nullable(),
69
+ title: zod_1.z.string().max(255),
70
+ intent: zod_1.z.string().nullable(),
71
+ scope_in: zod_1.z.string().nullable(),
72
+ scope_out: zod_1.z.string().nullable(),
73
+ acceptance: zod_1.z.array(zod_1.z.string()).nullable(), // Assuming acceptance is an array of strings
74
+ status: exports.TaskStatusSchema.default("todo"),
75
+ owner: exports.OwnerSchema.default("agent"),
76
+ area: zod_1.z.string().max(64).nullable(),
77
+ risk: exports.RiskSchema.default("low"),
78
+ estimate_mins: zod_1.z.number().int().nullable(),
79
+ created_at: zod_1.z.string().datetime(),
80
+ updated_at: zod_1.z.string().datetime(),
81
+ external_key: zod_1.z.string().max(128).nullable(),
82
+ change_type: exports.ChangeTypeSchema.nullable(),
83
+ suggested_changes: zod_1.z.string().nullable(),
84
+ });
85
+ /** Junction: task_id + domain (task can have many domains). */
86
+ exports.TaskDomainSchema = zod_1.z.object({
87
+ task_id: zod_1.z.string().uuid(),
88
+ domain: zod_1.z.string().max(64),
89
+ });
90
+ /** Junction: task_id + skill (task can have many skills). */
91
+ exports.TaskSkillSchema = zod_1.z.object({
92
+ task_id: zod_1.z.string().uuid(),
93
+ skill: zod_1.z.string().max(64),
94
+ });
95
+ exports.EdgeSchema = zod_1.z.object({
96
+ from_task_id: zod_1.z.string().uuid(),
97
+ to_task_id: zod_1.z.string().uuid(),
98
+ type: exports.EdgeTypeSchema.default("blocks"),
99
+ reason: zod_1.z.string().nullable(),
100
+ });
101
+ exports.EventSchema = zod_1.z.object({
102
+ event_id: zod_1.z.string().uuid(),
103
+ task_id: zod_1.z.string().uuid(),
104
+ kind: exports.EventKindSchema,
105
+ body: zod_1.z.record(zod_1.z.any()), // JSON type in Dolt maps to a generic record
106
+ actor: exports.ActorSchema.default("agent"),
107
+ created_at: zod_1.z.string().datetime(),
108
+ });
109
+ exports.DecisionSchema = zod_1.z.object({
110
+ decision_id: zod_1.z.string().uuid(),
111
+ plan_id: zod_1.z.string().uuid(),
112
+ task_id: zod_1.z.string().uuid().nullable(),
113
+ summary: zod_1.z.string().max(255),
114
+ context: zod_1.z.string(),
115
+ options: zod_1.z.array(zod_1.z.string()).nullable(), // Assuming options is an array of strings
116
+ decision: zod_1.z.string(),
117
+ consequences: zod_1.z.string().nullable(),
118
+ source_ref: zod_1.z.string().max(512).nullable(),
119
+ created_at: zod_1.z.string().datetime(),
120
+ });
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatDotGraph = formatDotGraph;
4
+ exports.generateDotGraph = generateDotGraph;
5
+ const graph_data_1 = require("./graph-data");
6
+ function formatDotGraph(nodes, edges) {
7
+ let dot = "digraph TaskGraph {\n";
8
+ dot += " rankdir=LR;\n";
9
+ dot += " node [shape=box];\n";
10
+ nodes.forEach((node) => {
11
+ dot += ` \"${node.id}\" [label=\"${node.label}\"];\n`;
12
+ });
13
+ edges.forEach((edge) => {
14
+ dot += ` \"${edge.from}\" -> \"${edge.to}\" [label=\"${edge.type}\"];\n`;
15
+ });
16
+ dot += "}\n";
17
+ return dot;
18
+ }
19
+ function generateDotGraph(planId, featureKey, basePath) {
20
+ return (0, graph_data_1.getGraphData)(planId, featureKey, basePath).map(({ nodes, edges }) => formatDotGraph(nodes, edges));
21
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getGraphData = getGraphData;
4
+ const utils_1 = require("../cli/utils");
5
+ const query_1 = require("../db/query");
6
+ function getGraphData(planId, featureKey, basePath) {
7
+ return (0, utils_1.readConfig)(basePath).asyncAndThen((config) => {
8
+ // Passed basePath to readConfig
9
+ const q = (0, query_1.query)(config.doltRepoPath);
10
+ const whereClause = {};
11
+ if (planId) {
12
+ whereClause.plan_id = planId;
13
+ }
14
+ if (featureKey) {
15
+ whereClause.feature_key = featureKey;
16
+ }
17
+ return q
18
+ .select("task", {
19
+ columns: ["task_id", "title", "status"],
20
+ where: whereClause,
21
+ })
22
+ .andThen((tasksResult) => {
23
+ const tasks = tasksResult;
24
+ const edgesQuery = `SELECT from_task_id, to_task_id, type FROM \`edge\`;`;
25
+ return q.raw(edgesQuery).map((edgesResult) => {
26
+ const edges = edgesResult;
27
+ const nodes = tasks.map((task) => ({
28
+ id: task.task_id,
29
+ label: `${task.title} (${task.status})`,
30
+ status: task.status,
31
+ }));
32
+ const graphEdges = edges.map((edge) => ({
33
+ from: edge.from_task_id,
34
+ to: edge.to_task_id,
35
+ type: edge.type,
36
+ }));
37
+ return { nodes, edges: graphEdges };
38
+ });
39
+ });
40
+ });
41
+ }
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateMarkdown = generateMarkdown;
7
+ const js_yaml_1 = __importDefault(require("js-yaml"));
8
+ const utils_1 = require("../cli/utils");
9
+ const neverthrow_1 = require("neverthrow");
10
+ const errors_1 = require("../domain/errors");
11
+ const query_1 = require("../db/query");
12
+ const escape_1 = require("../db/escape");
13
+ /** Generates Cursor-format markdown from plan and tasks. */
14
+ function generateMarkdown(planId, basePath) {
15
+ return (0, utils_1.readConfig)(basePath).asyncAndThen((config) => {
16
+ const q = (0, query_1.query)(config.doltRepoPath);
17
+ return q
18
+ .select("plan", {
19
+ columns: ["plan_id", "title", "intent"],
20
+ where: { plan_id: planId },
21
+ })
22
+ .andThen((plans) => {
23
+ if (plans.length === 0) {
24
+ return (0, neverthrow_1.errAsync)((0, errors_1.buildError)(errors_1.ErrorCode.PLAN_NOT_FOUND, `Plan ${planId} not found`));
25
+ }
26
+ const plan = plans[0];
27
+ return q
28
+ .select("task", {
29
+ columns: ["task_id", "external_key", "title", "status"],
30
+ where: { plan_id: planId },
31
+ })
32
+ .andThen((tasks) => {
33
+ const taskIds = tasks.map((t) => t.task_id);
34
+ return q
35
+ .raw(taskIds.length > 0
36
+ ? `SELECT task_id, domain FROM \`task_domain\` WHERE task_id IN (${taskIds.map((id) => `'${(0, escape_1.sqlEscape)(id)}'`).join(",")})`
37
+ : "SELECT task_id, domain FROM `task_domain` WHERE 1=0")
38
+ .andThen((domainRows) => q
39
+ .raw(taskIds.length > 0 ? `SELECT task_id, skill FROM \`task_skill\` WHERE task_id IN (${taskIds.map((id) => `'${(0, escape_1.sqlEscape)(id)}'`).join(",")})` : "SELECT task_id, skill FROM `task_skill` WHERE 1=0")
40
+ .map((skillRows) => {
41
+ const domainsByTask = new Map();
42
+ domainRows.forEach((r) => {
43
+ const arr = domainsByTask.get(r.task_id) ?? [];
44
+ arr.push(r.domain);
45
+ domainsByTask.set(r.task_id, arr);
46
+ });
47
+ const skillsByTask = new Map();
48
+ skillRows.forEach((r) => {
49
+ const arr = skillsByTask.get(r.task_id) ?? [];
50
+ arr.push(r.skill);
51
+ skillsByTask.set(r.task_id, arr);
52
+ });
53
+ return { domainsByTask, skillsByTask };
54
+ }))
55
+ .andThen(({ domainsByTask, skillsByTask }) => q
56
+ .raw("SELECT from_task_id, to_task_id, type FROM `edge` WHERE type = 'blocks'")
57
+ .map((edges) => {
58
+ const taskIdToKey = new Map();
59
+ tasks.forEach((t) => {
60
+ if (t.external_key) {
61
+ taskIdToKey.set(t.task_id, t.external_key);
62
+ }
63
+ });
64
+ const blockedByMap = new Map();
65
+ edges.forEach((e) => {
66
+ if (e.to_task_id &&
67
+ taskIdToKey.has(e.from_task_id) &&
68
+ taskIdToKey.has(e.to_task_id)) {
69
+ const key = taskIdToKey.get(e.to_task_id);
70
+ const blockerKey = taskIdToKey.get(e.from_task_id);
71
+ if (!blockedByMap.has(key)) {
72
+ blockedByMap.set(key, []);
73
+ }
74
+ blockedByMap.get(key).push(blockerKey);
75
+ }
76
+ });
77
+ const todos = tasks
78
+ .filter((t) => t.external_key)
79
+ .map((t) => {
80
+ const status = t.status === "done" ? "completed" : "pending";
81
+ const blockedBy = blockedByMap.get(t.external_key) ?? [];
82
+ const domains = domainsByTask.get(t.task_id);
83
+ const skills = skillsByTask.get(t.task_id);
84
+ return {
85
+ id: t.external_key,
86
+ content: t.title,
87
+ status,
88
+ ...(blockedBy.length > 0 && { blockedBy }),
89
+ ...(domains &&
90
+ domains.length > 0 && { domain: domains }),
91
+ ...(skills && skills.length > 0 && { skill: skills }),
92
+ };
93
+ });
94
+ const frontmatter = {
95
+ name: plan.title,
96
+ overview: plan.intent || "",
97
+ todos,
98
+ isProject: false,
99
+ };
100
+ const yamlStr = js_yaml_1.default.dump(frontmatter, { lineWidth: -1 });
101
+ return `---
102
+ ${yamlStr}---
103
+ `;
104
+ }));
105
+ });
106
+ });
107
+ });
108
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatMermaidGraph = formatMermaidGraph;
4
+ exports.generateMermaidGraph = generateMermaidGraph;
5
+ const graph_data_1 = require("./graph-data");
6
+ function formatMermaidGraph(nodes, edges) {
7
+ let mermaid = "graph TD\n";
8
+ nodes.forEach((node) => {
9
+ // Node IDs in Mermaid typically cannot have special characters or spaces
10
+ const nodeId = node.id.replace(/[^a-zA-Z0-9]/g, "");
11
+ mermaid += ` ${nodeId}[\"${node.label}\"]\n`;
12
+ });
13
+ edges.forEach((edge) => {
14
+ const fromNodeId = edge.from.replace(/[^a-zA-Z0-9]/g, "");
15
+ const toNodeId = edge.to.replace(/[^a-zA-Z0-9]/g, "");
16
+ if (edge.type === "blocks") {
17
+ mermaid += ` ${fromNodeId} --> ${toNodeId}\n`;
18
+ }
19
+ else if (edge.type === "relates") {
20
+ mermaid += ` ${fromNodeId} --- ${toNodeId}\n`;
21
+ }
22
+ });
23
+ return mermaid;
24
+ }
25
+ function generateMermaidGraph(planId, featureKey, basePath) {
26
+ return (0, graph_data_1.getGraphData)(planId, featureKey, basePath).map(({ nodes, edges }) => formatMermaidGraph(nodes, edges));
27
+ }