docs(audit): align documentation with post-remediation backend reality
- 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/)
This commit is contained in:
217
scripts/export-taskmaster-to-obsidian.mjs
Normal file
217
scripts/export-taskmaster-to-obsidian.mjs
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/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, " ");
|
||||
}
|
||||
Reference in New Issue
Block a user