"use strict";
/**
 * Kanban bridge — maps taskStore tasks to a 3-column Kanban view
 * (triage / doing / done) and promotes any task that lands in the
 * triage column into the autonomy loop's IMPLEMENTATION_PLAN.md so
 * the next loop iteration picks it up.
 *
 * Mapping convention:
 *   Column "triage" → tag "kanban:triage" OR status "pending"
 *   Column "doing"  → tag "kanban:doing"  OR status in {assigned, in_progress, review}
 *   Column "done"   → tag "kanban:done"   OR status in {done, cancelled, failed}
 *
 * An explicit "kanban:*" tag always wins over the status-based default.
 * That lets the UI move a card without touching its lifecycle state.
 *
 * Promotion contract (triage → plan):
 *   - Idempotent: a task whose id already appears in the plan is not
 *     duplicated.
 *   - The resulting plan task uses the same id as the source task
 *     (slugified for safety), the task title verbatim, and a `kind:`
 *     inferred from task tags ("kind:code"/"kind:research"/"kind:external"
 *     wins; otherwise default code).
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.KANBAN_TAGS = void 0;
exports.taskToColumn = taskToColumn;
exports.groupTasksByColumn = groupTasksByColumn;
exports.withKanbanTag = withKanbanTag;
exports.inferKindFromTags = inferKindFromTags;
exports.taskToPlanTask = taskToPlanTask;
exports.promoteTriageToPlan = promoteTriageToPlan;
const fs = __importStar(require("node:fs/promises"));
const path = __importStar(require("node:path"));
const goalExpander_1 = require("./goalExpander");
exports.KANBAN_TAGS = {
    triage: 'kanban:triage',
    doing: 'kanban:doing',
    done: 'kanban:done',
};
const DOING_STATUSES = new Set(['assigned', 'in_progress', 'review', 'blocked']);
const DONE_STATUSES = new Set(['done', 'cancelled', 'failed']);
/**
 * Decide which Kanban column a task belongs to. Explicit `kanban:*`
 * tags always win — that's how the UI moves a card without touching
 * the task's lifecycle state.
 */
function taskToColumn(task) {
    const tags = task.tags ?? [];
    if (tags.includes(exports.KANBAN_TAGS.done))
        return 'done';
    if (tags.includes(exports.KANBAN_TAGS.doing))
        return 'doing';
    if (tags.includes(exports.KANBAN_TAGS.triage))
        return 'triage';
    if (DONE_STATUSES.has(task.status))
        return 'done';
    if (DOING_STATUSES.has(task.status))
        return 'doing';
    return 'triage';
}
/**
 * Group an arbitrary list of tasks into the 3-column board view.
 * Order within each column preserves caller order.
 */
function groupTasksByColumn(tasks) {
    const board = { triage: [], doing: [], done: [] };
    for (const t of tasks)
        board[taskToColumn(t)].push(t);
    return board;
}
/**
 * Replace the kanban:* tag on a task. Returns the new tag set with at
 * most one kanban tag. Other tags (including `kind:*`) are preserved.
 */
function withKanbanTag(currentTags, column) {
    const filtered = (currentTags ?? []).filter((t) => !t.startsWith('kanban:'));
    filtered.push(exports.KANBAN_TAGS[column]);
    return filtered;
}
/**
 * Extract a TaskKind from tag list (`kind:code`, `kind:research`,
 * `kind:external`). Returns undefined when no recognised tag is set.
 */
function inferKindFromTags(tags) {
    for (const tag of tags ?? []) {
        if (tag === 'kind:code')
            return 'code';
        if (tag === 'kind:research')
            return 'research';
        if (tag === 'kind:external')
            return 'external';
    }
    return undefined;
}
function slugify(input) {
    return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'task';
}
/**
 * Map a taskStore Task into a plan-shaped task. Uses the task id when
 * it's already slug-shaped; otherwise slugifies the title.
 */
function taskToPlanTask(task) {
    const id = /^[a-z0-9-]+$/.test(task.id) ? task.id : slugify(task.title || task.id);
    const kind = inferKindFromTags(task.tags);
    const anchors = (task.tags ?? [])
        .filter((t) => t.startsWith('anchor:'))
        .map((t) => t.slice('anchor:'.length))
        .filter(Boolean);
    return { id, title: task.title, kind, anchors: anchors.length ? anchors : undefined };
}
const defaultIo = {
    readFile: (p) => fs.readFile(p, 'utf-8'),
    writeFile: (p, data) => fs.writeFile(p, data, 'utf-8'),
    appendFile: (p, data) => fs.appendFile(p, data, 'utf-8'),
    access: (p) => fs.access(p),
};
/**
 * Read the plan file and return the set of task ids it already declares.
 * Returns an empty set if the file does not exist.
 */
async function readPlanIds(planPath, io) {
    try {
        await io.access(planPath);
    }
    catch {
        return new Set();
    }
    let text;
    try {
        text = await io.readFile(planPath);
    }
    catch {
        return new Set();
    }
    const ids = new Set();
    for (const line of text.split(/\r?\n/)) {
        const m = line.match(/^- \[.\] (\S+)\s+[—-]/);
        if (m)
            ids.add(m[1]);
    }
    return ids;
}
/**
 * Append any "triage"-column tasks that aren't already in the plan.
 * Idempotent — running twice is a no-op for the second call.
 */
async function promoteTriageToPlan(tasks, options) {
    const io = options.io ?? defaultIo;
    const planPath = path.resolve(options.projectDir, options.planPath ?? 'IMPLEMENTATION_PLAN.md');
    const triage = tasks.filter((t) => taskToColumn(t) === 'triage');
    if (triage.length === 0)
        return { appended: [], skipped: [], mutated: false };
    const existingIds = await readPlanIds(planPath, io);
    const appended = [];
    const skipped = [];
    for (const t of triage) {
        const planTask = taskToPlanTask(t);
        if (existingIds.has(planTask.id)) {
            skipped.push(planTask);
            continue;
        }
        appended.push(planTask);
        existingIds.add(planTask.id);
    }
    if (appended.length === 0)
        return { appended, skipped, mutated: false };
    const block = (0, goalExpander_1.renderTasksAsPlanMarkdown)(appended);
    let exists = true;
    try {
        await io.access(planPath);
    }
    catch {
        exists = false;
    }
    if (exists) {
        const current = await io.readFile(planPath);
        const sep = current.endsWith('\n') ? '' : '\n';
        await io.appendFile(planPath, sep + block);
    }
    else {
        await io.writeFile(planPath, '# Implementation Plan\n\n' + block);
    }
    return { appended, skipped, mutated: true };
}
//# sourceMappingURL=kanbanBridge.js.map