Files
Do Siki f918e300a6 feat(plane): add Plane-TODO sync script
Add plane-sync.js one-way (Plane -> TODO.md) synchronization for the
MITHOME project. Config reads from .mcp.json with .env override. CLI:
--list, --dry-run, --yes, --project, --verbose, --help. Interactive
discrepancy resolution (status mismatches, orphans, new issues), backup
before write, rate limiting. Pure functions covered by node:test unit
tests. Includes PLANE-SYNC-GUIDE.md and TODO.md changelog entry.
2026-08-17 12:18:55 +02:00

75 lines
2.3 KiB
JavaScript

// WHY: single source of truth for group -> section, cancelled is reported not placed
function groupToSection(group) {
switch (group) {
case 'completed':
return 'completed';
case 'started':
return 'inProgress';
case 'unstarted':
return 'todo';
case 'backlog':
return 'backlog';
default:
return null;
}
}
function buildDiscrepancies(planeIssues, todoRows, identifier) {
const todoById = new Map(todoRows.map(row => [row.id, row]));
const planeById = new Map();
const cancelled = [];
for (const issue of planeIssues) {
const id = `${identifier}-${issue.sequence_id}`;
const group = (issue.state && issue.state.group) || 'backlog';
const section = groupToSection(group);
if (!section) {
cancelled.push({ id, name: issue.name });
continue;
}
planeById.set(id, { id, name: issue.name, section, group });
}
const mismatches = [];
const newIssues = [];
const orphans = [];
for (const [id, p] of planeById) {
const t = todoById.get(id);
if (t && t.section !== p.section) mismatches.push({ id, fromSection: t.section, toSection: p.section, name: p.name });
if (!t) newIssues.push({ id, section: p.section, name: p.name });
}
for (const [id, t] of todoById) {
if (!planeById.has(id)) orphans.push({ id, section: t.section, name: t.name });
}
return { mismatches, newIssues, orphans, cancelled, planeById, todoById };
}
// WHY: Plane is authoritative, so plane rows always win; decisions only move/add local rows
function buildFinalSections(buildData, decisions) {
const final = { completed: [], todo: [], inProgress: [], backlog: [] };
const { planeById, todoById, mismatches } = buildData;
for (const [id, p] of planeById) {
const t = todoById.get(id);
const note = t ? t.note : '';
const isMismatch = mismatches.some(m => m.id === id);
if (isMismatch && !decisions.moveToPlane.has(id)) {
final[t.section].push({ id, name: p.name, note });
} else {
final[p.section].push({ id, name: p.name, note });
}
}
for (const orphan of buildData.orphans) {
if (decisions.keepOrphan.has(orphan.id)) {
final[orphan.section].push({ id: orphan.id, name: orphan.name, note: '' });
}
}
return final;
}
module.exports = { groupToSection, buildDiscrepancies, buildFinalSections };