/**
 * design/product-context.mjs — loader + normalizer for .aperant/product-context.json (C44).
 *
 * Pure function: `loadProductContext(projectDir)` reads the on-disk
 * product-context file (written by the Aperant app — framework CLI is
 * read-only) and returns a structured envelope describing what was found.
 *
 * Missing file is NOT an error — it's the common case before a user has
 * completed product-onboarding, so the loader returns `{ present: false }`
 * and lets callers fall back to the generic drafting path. Parse errors and
 * schema violations return `errors[]` populated.
 *
 * The canonical shape mirrors `WizardProductContext`
 * (packages/ui/src/design-system/wizard/types.ts) so the file written by
 * the wizard (project scope, out-of-scope for this task) and the file read
 * by the framework share a single line of code for the contract.
 *
 * No network, no subprocess, no LLM — deterministic. CI-friendly. No API
 * keys required. The LLM call site is `design/synthesize.mjs`, which is
 * explicitly dependency-injected for tests.
 */
import { existsSync, readFileSync } from 'node:fs';
import { resolve as resolvePath } from 'node:path';
import { z } from 'zod';
const NonEmptyString = z.string().min(1);
const FeatureSchema = z
    .object({
    id: NonEmptyString,
    name: NonEmptyString,
    description: z.string().optional(),
})
    .passthrough();
const MetaSchema = z
    .object({
    source: z.string().optional(),
    updatedAt: z.string().optional(),
})
    .passthrough();
export const ProductContextSchema = z
    .object({
    productName: NonEmptyString,
    productDescription: z.string(),
    targetAudience: z.string(),
    features: z.array(FeatureSchema),
    _meta: MetaSchema.optional(),
})
    .passthrough();
/**
 * @typedef {Object} LoadedFeature
 * @property {string} id
 * @property {string} name
 * @property {string} description
 */
/**
 * @typedef {Object} LoadedProductContext
 * @property {string} productName
 * @property {string} productDescription
 * @property {string} targetAudience
 * @property {LoadedFeature[]} features
 * @property {{ source?: string, updatedAt?: string }=} _meta
 */
/**
 * @typedef {Object} LoadResult
 * @property {boolean} present
 * @property {'aperant-file' | 'none'} source
 * @property {LoadedProductContext | null} productContext
 * @property {string[]} errors
 * @property {string[]} warnings
 * @property {string} path   absolute path the loader resolved (may not exist)
 */
/**
 * Resolve the canonical location of product-context.json for a project dir.
 *
 * @param {string} projectDir
 * @returns {string}
 */
export function productContextPath(projectDir) {
    return resolvePath(projectDir, '.aperant', 'product-context.json');
}
/**
 * Load + normalize product-context from disk.
 *
 * - Missing file → `{ present: false, source: 'none', errors: [] }` (NOT an error).
 * - Parse error or schema violation → `errors[]` populated, `present: false`.
 * - Valid file → normalized `productContext` + `present: true`.
 *
 * @param {string} projectDir  absolute or cwd-relative path to the project root
 * @returns {LoadResult}
 */
export function loadProductContext(projectDir) {
    const path = productContextPath(projectDir);
    const result = {
        present: false,
        source: /** @type {'aperant-file' | 'none'} */ ('none'),
        productContext: /** @type {LoadedProductContext | null} */ (null),
        errors: /** @type {string[]} */ ([]),
        warnings: /** @type {string[]} */ ([]),
        path,
    };
    if (!existsSync(path)) {
        return result;
    }
    let raw;
    try {
        raw = readFileSync(path, 'utf8');
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        result.errors.push(`failed to read ${path}: ${msg}`);
        return result;
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        result.errors.push(`product-context.json: invalid JSON — ${msg}`);
        return result;
    }
    if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
        result.errors.push('product-context.json: top-level value must be an object');
        return result;
    }
    const validation = ProductContextSchema.safeParse(parsed);
    if (!validation.success) {
        for (const issue of validation.error.issues) {
            const pathStr = issue.path && issue.path.length > 0 ? issue.path.join('.') : '(root)';
            result.errors.push(`product-context.${pathStr}: ${issue.message}`);
        }
        return result;
    }
    const value = /** @type {LoadedProductContext} */ (validation.data);
    // Normalize features: ensure description is a string (fill empties).
    const normFeatures = value.features.map((f) => ({
        id: f.id,
        name: f.name,
        description: typeof f.description === 'string' ? f.description : '',
    }));
    result.present = true;
    result.source = 'aperant-file';
    result.productContext = {
        productName: value.productName,
        productDescription: value.productDescription,
        targetAudience: value.targetAudience,
        features: normFeatures,
        ..._meta(value),
    };
    return result;
}
/**
 * @param {LoadedProductContext} value
 * @returns {{ _meta?: { source?: string, updatedAt?: string } }}
 */
function _meta(value) {
    if (!value._meta || typeof value._meta !== 'object')
        return {};
    return { _meta: value._meta };
}
//# sourceMappingURL=product-context.mjs.map