// @crash-shield-ignore-file — config/cache reader for Planu-controlled JSON; writer is this codebase, shape guaranteed by build/seed.
// tools/create-spec/spec-builder.ts — Builds the Spec object and estimation (SPEC-461: lean format).
// Extracted from create-spec.ts to keep file sizes within limits.
import { hashProjectPath, knowledgeStore, specStore, globalStore } from '../../storage/index.js';
import { getCurrentTier } from '../license-gate.js';
import { checkLimits } from '../../engine/license-validator.js';
import { inferDifficulty, calculateEstimation } from '../../engine/estimator.js';
import { analyzeSplit, formatSplitSuggestion } from '../../engine/spec-splitter.js';
import { isLegacyLocation, getSpecFileNames } from '../../engine/planu-config-writer.js';
import { slugify, generateSpecId, generateBranchName } from '../create-spec-helpers.js';
import { join, resolve } from 'node:path';
import { readdir, readFile } from 'node:fs/promises';
import { createHash } from 'node:crypto';
/** Builds the Spec object, calculates estimation, generates diagrams, and scope filters. */
export async function buildSpecContext(params) {
    const { title, description, projectPath, type = 'feature', scope = 'feature', target = 'fullstack', tags = [], feature, } = params;
    // SPEC-509: projectPath is now optional in CreateSpecInput (auto-detected upstream)
    // buildSpecContext is only called after resolution so this guard is a safety net.
    if (!projectPath) {
        return {
            ok: false,
            errorMessage: 'projectPath is required. Run init_project or provide projectPath.',
        };
    }
    const projectId = hashProjectPath(projectPath);
    const knowledge = await knowledgeStore.getKnowledge(projectId);
    const existingSpecs = await specStore.listSpecs(projectId);
    // License: check active spec limit (exclude completed specs)
    const activeSpecs = existingSpecs.filter((s) => s.status !== 'done' && s.status !== 'discarded');
    const tier = await getCurrentTier();
    const specLimit = checkLimits(tier, activeSpecs.length, 'maxActiveSpecs');
    /* v8 ignore start -- license limit requires paid tier test env */
    if (!specLimit.allowed) {
        return { ok: false, errorMessage: specLimit.message };
    }
    /* v8 ignore stop */
    // Generate spec ID: combine store IDs + filesystem IDs for global max
    const fsSpecIds = await scanFilesystemSpecIds(projectPath);
    const allSpecs = [...existingSpecs, ...fsSpecIds.map((id) => ({ id }))];
    const specId = generateSpecId(allSpecs);
    const slug = slugify(title);
    // Check for duplicate title
    const duplicate = existingSpecs.find((s) => slugify(s.title) === slug);
    // Infer difficulty and calculate estimation
    const difficulty = inferDifficulty(type, scope);
    const globalConfig = await globalStore.getGlobalConfig();
    const estimation = calculateEstimation(type, scope, difficulty, globalConfig);
    // Determine risk level from difficulty and scope
    const risk = difficulty >= 4 || scope === 'architectural'
        ? 'high'
        : difficulty >= 3 || scope === 'cross-module'
            ? 'medium'
            : 'low';
    // SPEC-697: Build agent team plan from spec tags (best-effort, fire-and-forget data)
    const agentTeamPlanResult = await import('../../engine/agent-team-planner/index.js')
        .then(({ buildAgentTeamPlan }) => buildAgentTeamPlan({
        specId,
        specTitle: title,
        specDescription: description,
        projectPath,
        stackSignals: tags,
        specScope: scope,
    }))
        .catch(() => null);
    // Build spec location with naming convention detection
    const specLocation = knowledge?.specLocation ?? 'planu/specs';
    const naming = isLegacyLocation(specLocation) ? 'legacy' : 'modern';
    const fileNames = getSpecFileNames(naming);
    // Flat structure — no subcategories (feature stored as tag instead)
    // Always resolve to absolute path to prevent CWD-relative writes
    const specDir = resolve(join(projectPath, specLocation, `${specId}-${slug}`));
    // Safety: abort if the specDir already exists (ID collision)
    try {
        await readdir(specDir);
        return {
            ok: false,
            errorMessage: `Directory already exists: ${specDir}. Spec ID ${specId} collides with filesystem. Delete the conflicting directory or check planu.json specLocation.`,
        };
    }
    catch {
        // Expected: directory doesn't exist yet — proceed
    }
    const specPath = join(specDir, fileNames.spec);
    const technicalPath = specPath;
    // Generate git branch name
    const gitBranch = generateBranchName(specId, slug, type);
    // SPEC-770: derive idempotency key from sha256(title + projectPath) to prevent duplicate specs
    const idempotencyKey = createHash('sha256').update(`${title}::${projectPath}`).digest('hex');
    // Build the Spec object
    const now = new Date().toISOString();
    const spec = {
        id: specId,
        uuid: crypto.randomUUID(),
        title,
        slug,
        feature: feature ?? undefined,
        type,
        scope,
        status: 'draft',
        difficulty,
        risk,
        projectId,
        createdAt: now,
        updatedAt: now,
        specPath,
        technicalPath,
        estimation,
        actuals: null,
        target,
        tags: buildTags(tags, feature, title, description, type),
        dependencies: [],
        blockedBy: [],
        gitBranch,
        impactAnalysis: null,
        idempotencyKey,
    };
    // Analyze if spec should be split (lean: still useful for large specs)
    const splitSuggestion = analyzeSplit({
        criteriaCount: 4,
        moduleCount: scope === 'cross-module' ? 5 : scope === 'architectural' ? 6 : 2,
        estimatedHours: estimation.devHours,
        description,
        type,
        scope,
        target,
    }, specId);
    // SPEC-461: Lean format — no diagrams, scope filters, legal, MCP, or doc criteria
    return {
        ok: true,
        context: {
            spec,
            specDir,
            specPath,
            technicalPath,
            progressPath: '', // SPEC-461: lean format — no progress.md
            diagrams: [],
            estimation,
            splitSuggestion,
            scopeFilters: { scope, filters: [], allowManualOverride: false },
            duplicate,
            projectId,
            legalSection: null,
            legalCriteria: [],
            mcpLayerSection: null,
            docCriteria: [],
            agentTeamPlan: agentTeamPlanResult,
        },
    };
}
/** Builds the split suggestion result object if applicable. */
export function buildSplitResult(splitSuggestion, experienceLevel) {
    if (!splitSuggestion) {
        return undefined;
    }
    const level = experienceLevel ?? 'intermediate';
    return {
        shouldSplit: true,
        reason: splitSuggestion.reason,
        subSpecs: splitSuggestion.suggestedSpecs,
        formatted: formatSplitSuggestion(splitSuggestion, level === 'beginner' ? 'beginner' : 'expert'),
    };
}
const EN_STOPWORDS = new Set([
    'the',
    'a',
    'an',
    'is',
    'are',
    'with',
    'and',
    'or',
    'for',
    'to',
    'of',
    'in',
    'on',
    'at',
    'by',
    'from',
    'that',
    'this',
    'it',
    'be',
    'as',
    'spec',
    'create',
    'workflow',
    'files',
    'file',
    'example',
    'avoid',
    'planu',
    'feature',
]);
/** Infer meaningful tags from title and description when no tags were provided. */
export function inferTagsFromContent(title, description, type) {
    const words = `${title} ${description}`
        .toLowerCase()
        .split(/[\s\W]+/)
        .filter((w) => w.length > 3 && !EN_STOPWORDS.has(w));
    const seen = new Set();
    const tags = [];
    for (const word of words) {
        if (!seen.has(word)) {
            seen.add(word);
            tags.push(word);
        }
        if (tags.length >= 5) {
            break;
        }
    }
    // Pad to minimum 3 tags
    if (tags.length < 3 && type) {
        if (!seen.has(type)) {
            tags.push(type);
        }
    }
    if (tags.length < 3) {
        if (!seen.has('feature')) {
            tags.push('feature');
        }
    }
    return tags;
}
/** Build the final tags array from explicit user/tool input only. */
function buildTags(tags, feature, _title, _description, _type) {
    const explicitTags = tags
        .map((tag) => slugify(tag))
        .filter((tag) => tag.length > 0 && !EN_STOPWORDS.has(tag));
    const result = feature ? [...new Set([...explicitTags, slugify(feature)])] : explicitTags;
    return [...new Set(result)];
}
/* v8 ignore start -- filesystem scanning is best-effort, tested via integration */
/** Scan filesystem for SPEC-XXX directories to find max ID globally. */
async function scanFilesystemSpecIds(projectPath) {
    const ids = [];
    try {
        const configPath = join(projectPath, 'planu.json');
        let specLocation = 'planu/specs';
        try {
            const config = JSON.parse(await readFile(configPath, 'utf-8'));
            if (typeof config.specLocation === 'string') {
                specLocation = config.specLocation;
            }
        }
        catch {
            /* no planu.json — use default */
        }
        const specsRoot = join(projectPath, specLocation);
        await scanSpecDir(specsRoot, ids);
        // Also scan one level deeper for subcategory folders (e.g., planu/specs/category/SPEC-xxx/)
        const topEntries = await readdir(specsRoot, { withFileTypes: true }).catch(() => []);
        for (const top of topEntries) {
            if (top.isDirectory() && !/^SPEC-\d+/.test(top.name)) {
                await scanSpecDir(join(specsRoot, top.name), ids);
            }
        }
        // Also check legacy location
        const legacyRoot = join(projectPath, 'docs/sdd/specs');
        const legacyEntries = await readdir(legacyRoot, { withFileTypes: true }).catch(() => []);
        for (const entry of legacyEntries) {
            const match = /^(SPEC-\d+)/.exec(entry.name);
            if (match?.[1]) {
                ids.push(match[1]);
            }
        }
    }
    catch (err) {
        /* filesystem scan is best-effort — log for diagnostics */
        console.error('[planu] scanFilesystemSpecIds failed:', err.message);
    }
    return ids;
}
/** Scan a single directory for SPEC-XXX folders or slug folders with spec.md. */
async function scanSpecDir(dir, ids) {
    const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
    for (const entry of entries) {
        if (!entry.isDirectory()) {
            continue;
        }
        // 1. Try directory name (e.g. SPEC-042-feature-name/)
        const dirMatch = /^(SPEC-\d+)/.exec(entry.name);
        if (dirMatch?.[1]) {
            ids.push(dirMatch[1]);
            continue;
        }
        // 2. Fallback: read spec.md for ID (slug-only dirs)
        try {
            const specFile = join(dir, entry.name, 'spec.md');
            const content = await readFile(specFile, 'utf-8');
            const first200 = content.slice(0, 200);
            // Try heading: # SPEC-001
            const headingMatch = /^#\s+(SPEC-\d+)/m.exec(first200);
            if (headingMatch?.[1]) {
                ids.push(headingMatch[1]);
            }
            else {
                // Try YAML frontmatter: id: SPEC-001
                const yamlMatch = /^id:\s*(SPEC-\d+)/m.exec(first200);
                if (yamlMatch?.[1]) {
                    ids.push(yamlMatch[1]);
                }
            }
        }
        catch {
            /* no spec.md or unreadable — skip */
        }
    }
}
/* v8 ignore stop */
//# sourceMappingURL=spec-builder.js.map