- Update data model enums to match backend models - Update API reference auth requirements - Add dispute module references and warning blocks - Add 2026-05-24 audit remediation callout to Overview - Generate task breakdowns and audit artifacts - Add doc alignment report (.taskmaster/reports/)
218 lines
5.6 KiB
JavaScript
218 lines
5.6 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const taskmasterPath = path.join(repoRoot, ".taskmaster", "tasks", "tasks.json");
|
|
const outputDir = path.join(repoRoot, "Taskmaster");
|
|
const taskDir = path.join(outputDir, "Tasks");
|
|
|
|
const STATUS_CHECKBOX = {
|
|
done: "x",
|
|
completed: "x",
|
|
};
|
|
|
|
const PRIORITY_TAG = {
|
|
high: "#priority/high",
|
|
medium: "#priority/medium",
|
|
low: "#priority/low",
|
|
};
|
|
|
|
const PRIORITY_EMOJI = {
|
|
highest: "🔺",
|
|
high: "⏫",
|
|
medium: "🔼",
|
|
low: "🔽",
|
|
lowest: "⏬",
|
|
};
|
|
|
|
const source = JSON.parse(await readFile(taskmasterPath, "utf8"));
|
|
const tasks = source.master?.tasks ?? source.tasks ?? [];
|
|
const generatedAt = new Date().toISOString();
|
|
|
|
await rm(outputDir, { recursive: true, force: true });
|
|
await mkdir(taskDir, { recursive: true });
|
|
|
|
const flatTasks = tasks.flatMap((task) => {
|
|
const parent = normalizeTask(task);
|
|
const subtasks = (task.subtasks ?? []).map((subtask) =>
|
|
normalizeTask({
|
|
...subtask,
|
|
id: `${task.id}.${subtask.id}`,
|
|
parentId: task.id,
|
|
parentTitle: task.title,
|
|
priority: subtask.priority ?? task.priority,
|
|
}),
|
|
);
|
|
return [parent, ...subtasks];
|
|
});
|
|
|
|
for (const task of flatTasks) {
|
|
await writeFile(path.join(taskDir, `${slugTaskId(task.id)}.md`), renderTaskNote(task));
|
|
}
|
|
|
|
await writeFile(path.join(outputDir, "README.md"), renderDashboard(flatTasks));
|
|
await writeFile(path.join(outputDir, "tasks.md"), renderTasksFile(flatTasks));
|
|
|
|
console.log(`Exported ${flatTasks.length} Taskmaster tasks to ${path.relative(repoRoot, outputDir)}`);
|
|
|
|
function normalizeTask(task) {
|
|
return {
|
|
id: String(task.id),
|
|
title: task.title ?? "Untitled task",
|
|
description: task.description ?? "",
|
|
details: task.details ?? "",
|
|
testStrategy: task.testStrategy ?? "",
|
|
status: task.status ?? "pending",
|
|
priority: task.priority ?? "medium",
|
|
dependencies: (task.dependencies ?? []).map(String),
|
|
parentId: task.parentId && task.parentId !== "undefined" ? String(task.parentId) : "",
|
|
parentTitle: task.parentTitle ?? "",
|
|
};
|
|
}
|
|
|
|
function renderTaskNote(task) {
|
|
return `---
|
|
taskmaster_id: "${yamlEscape(task.id)}"
|
|
status: "${yamlEscape(task.status)}"
|
|
priority: "${yamlEscape(task.priority)}"
|
|
depends_on: [${task.dependencies.map((id) => `"${yamlEscape(id)}"`).join(", ")}]
|
|
parent_id: "${yamlEscape(task.parentId)}"
|
|
source: "taskmaster"
|
|
generated_at: "${generatedAt}"
|
|
---
|
|
|
|
# ${task.id} - ${task.title}
|
|
|
|
${renderObsidianTaskLine(task)}
|
|
|
|
## Metadata
|
|
|
|
| Field | Value |
|
|
| --- | --- |
|
|
| Taskmaster ID | ${task.id} |
|
|
| Status | ${task.status} |
|
|
| Priority | ${task.priority} |
|
|
| Dependencies | ${task.dependencies.length ? task.dependencies.join(", ") : "None"} |
|
|
| Parent | ${task.parentId ? `${task.parentId} - ${task.parentTitle}` : "None"} |
|
|
|
|
## Description
|
|
|
|
${task.description || "_No description._"}
|
|
|
|
## Details
|
|
|
|
${task.details || "_No details._"}
|
|
|
|
## Verification
|
|
|
|
${task.testStrategy || "_No verification strategy._"}
|
|
`;
|
|
}
|
|
|
|
function renderDashboard(tasks) {
|
|
const rows = tasks
|
|
.map((task) => `| [[Tasks/${slugTaskId(task.id)}|${task.id}]] | ${escapeTable(task.title)} | ${task.status} | ${task.priority} | ${task.dependencies.length ? task.dependencies.join(", ") : "None"} |`)
|
|
.join("\n");
|
|
|
|
return `# Taskmaster Dashboard
|
|
|
|
Generated from \`.taskmaster/tasks/tasks.json\` at ${generatedAt}.
|
|
|
|
Taskmaster remains the canonical source of truth. Re-run:
|
|
|
|
\`\`\`sh
|
|
node scripts/export-taskmaster-to-obsidian.mjs
|
|
\`\`\`
|
|
|
|
## Status Summary
|
|
|
|
${renderStatusSummary(tasks)}
|
|
|
|
## Task Index
|
|
|
|
| ID | Title | Status | Priority | Dependencies |
|
|
| --- | --- | --- | --- | --- |
|
|
${rows}
|
|
|
|
## Obsidian Tasks Query
|
|
|
|
\`\`\`tasks
|
|
not done
|
|
tag includes #taskmaster
|
|
sort by priority
|
|
sort by description
|
|
\`\`\`
|
|
`;
|
|
}
|
|
|
|
function renderTasksFile(tasks) {
|
|
const lines = tasks.map(renderObsidianTaskLine).join("\n");
|
|
|
|
return `# Taskmaster Tasks
|
|
|
|
Generated from \`.taskmaster/tasks/tasks.json\` at ${generatedAt}.
|
|
|
|
These lines use the Obsidian Tasks emoji format:
|
|
|
|
- standard Markdown checkbox syntax
|
|
- \`#taskmaster\` tag for filtering
|
|
- priority emoji where available
|
|
- \`🆔\` task IDs
|
|
- \`⛔\` dependency IDs
|
|
|
|
${lines}
|
|
`;
|
|
}
|
|
|
|
function renderObsidianTaskLine(task) {
|
|
const checkbox = STATUS_CHECKBOX[task.status] ?? " ";
|
|
const priorityTag = PRIORITY_TAG[task.priority] ?? "#priority/none";
|
|
const priorityEmoji = PRIORITY_EMOJI[task.priority] ?? "";
|
|
const dependencyMarkers = task.dependencies.map((id) => `⛔ tm-${safeId(id)}`).join(" ");
|
|
const fields = [
|
|
"#taskmaster",
|
|
priorityTag,
|
|
`#status/${tagValue(task.status)}`,
|
|
priorityEmoji,
|
|
`🆔 tm-${safeId(task.id)}`,
|
|
dependencyMarkers,
|
|
].filter(Boolean);
|
|
|
|
return `- [${checkbox}] ${task.id} - ${task.title} ${fields.join(" ")}`;
|
|
}
|
|
|
|
function renderStatusSummary(tasks) {
|
|
const counts = tasks.reduce((acc, task) => {
|
|
acc[task.status] = (acc[task.status] ?? 0) + 1;
|
|
return acc;
|
|
}, {});
|
|
|
|
return Object.entries(counts)
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
.map(([status, count]) => `- ${status}: ${count}`)
|
|
.join("\n");
|
|
}
|
|
|
|
function slugTaskId(id) {
|
|
return `task-${safeId(id)}`;
|
|
}
|
|
|
|
function safeId(id) {
|
|
return String(id).replace(/[^A-Za-z0-9_-]+/g, "-");
|
|
}
|
|
|
|
function tagValue(value) {
|
|
return String(value || "none").toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
}
|
|
|
|
function yamlEscape(value) {
|
|
return String(value ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
}
|
|
|
|
function escapeTable(value) {
|
|
return String(value ?? "").replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
}
|