/**
 * sync.mjs — in-process roadmap partial-commit helper (C29).
 */
import { execFileSync, execSync } from 'node:child_process';
import { findOutputsViolations } from '../task/outputs-guard.mjs';
import { isTeamEnabled } from '../team/team.mjs';
/**
 * Partial-commit helper: stages ONLY the dirty tracked `.aperant/roadmap/**`
 * files and commits them via `git commit --only -- <paths>`. Never touches
 * the rest of the index — the user's pre-existing staged work is invisible
 * to this call by construction, which fixes the old `git add` + global
 * `git diff --cached` + `git reset HEAD --` unstaging bug.
 *
 * Used by:
 *   - `apt-tools roadmap sync` subcommand (external callers keep their JSON shape)
 *   - `apt-tools effects drain` (in-process after a phase transition)
 *
 * opts:
 *   - cwd: optional git cwd for all git calls. Defaults to `targetDir`. When the
 *     caller is a lifecycle handler running on behalf of a worktree task, the
 *     worktree path is passed here so the commit lands on the worktree's
 *     feature branch, not the main repo's branch (R7 — fixes the C29 leak).
 *     State-coordination paths (staged file list, allowlists) continue to
 *     resolve against `targetDir` because shared `.aperant/` always lives in
 *     the main repo.
 *
 * Returns: { committed: boolean, files: string[], sha: string, reason: string, skipped?: string }
 * Throws on git errors (let caller decide how to surface them).
 */
export function syncRoadmapInProcess(targetDir, reason, opts = {}) {
    const gitCwd = opts.cwd ?? targetDir;
    if (!isTeamEnabled(targetDir)) {
        return { committed: false, files: [], sha: '', reason, skipped: 'team mode disabled' };
    }
    let tracked = '';
    try {
        tracked = execSync('git ls-files -- .aperant/roadmap 2>/dev/null', {
            cwd: gitCwd,
            encoding: 'utf-8',
        });
    }
    catch {
        return { committed: false, files: [], sha: '', reason, skipped: 'no tracked roadmap files' };
    }
    const trackedSet = new Set(tracked
        .split('\n')
        .map((s) => s.trim())
        .filter((f) => f && (f.endsWith('.md') || f.endsWith('.json'))));
    let porcelain = '';
    try {
        porcelain = execSync('git status --porcelain -- .aperant/roadmap 2>/dev/null', {
            cwd: gitCwd,
            encoding: 'utf-8',
        });
    }
    catch {
        return { committed: false, files: [], sha: '', reason, skipped: 'git status failed' };
    }
    const dirty = [];
    for (const rawLine of porcelain.split('\n')) {
        if (!rawLine)
            continue;
        const line = rawLine.slice(3);
        const arrowIdx = line.indexOf(' -> ');
        const path = arrowIdx >= 0 ? line.slice(arrowIdx + 4) : line;
        const clean = path.replace(/^"(.*)"$/, '$1').trim();
        if (trackedSet.has(clean))
            dirty.push(clean);
    }
    if (dirty.length === 0) {
        return { committed: false, files: [], sha: '', reason, skipped: 'roadmap clean' };
    }
    // R11 outputs enforcement: if the caller passed a task outputs allowlist,
    // every dirty path must match it. Otherwise we refuse to stage. This is
    // the belt to R7's suspenders — a forgotten cwd can't leak into another
    // repo because the guard blocks paths outside the declared task output.
    if (Array.isArray(opts.outputs)) {
        const violations = findOutputsViolations(opts.outputs, dirty);
        if (violations.length > 0) {
            const err = new Error(`outputs-violation: roadmap sync attempted to commit ${violations.length} path(s) outside task outputs: ${violations.slice(0, 5).join(', ')}`);
            err.code = 'outputs-violation';
            err.violations = violations;
            err.outputs = opts.outputs;
            throw err;
        }
    }
    const message = `chore(aperant): roadmap sync (${reason})\n\nCo-Authored-By: Aperant Framework <noreply@aperant.dev>\n`;
    // `git commit --only -- <paths>` performs a partial commit: it snapshots the
    // working-tree versions of <paths> into the commit WITHOUT touching the
    // rest of the index. Pre-existing staged files outside <paths> are not
    // read, not modified, and not committed. This is the fix for the
    // unstaging regression that the old `git add` + `git reset HEAD` dance caused.
    try {
        execFileSync('git', ['commit', '--only', '-m', message, '--', ...dirty], {
            cwd: gitCwd,
            stdio: 'pipe',
        });
    }
    catch (err) {
        throw new Error(`git commit failed: ${err.message}`);
    }
    let sha = '';
    try {
        sha = execSync('git rev-parse --short HEAD', {
            cwd: gitCwd,
            encoding: 'utf-8',
        }).trim();
    }
    catch {
        /* ignore */
    }
    return { committed: true, files: dirty, sha, reason };
}
//# sourceMappingURL=sync.mjs.map