/**
 * Shared pure helpers for scenario rules: lifecycle-path parsing and
 * create-payload synthesis. No filesystem / network / mutable state.
 */
/**
 * Parse a model's `lifecycles:` block into one LifecyclePath per
 * declared lifecycle. Handles both the shorthand flow form
 * (`flow: "draft -> open -> closed"`) and the structured form
 * (`states: [...]`, `transitions: { name: {from,to} } | [{from,to}]`).
 */
export function parseLifecyclePaths(model) {
    const lifecycles = model.lifecycles;
    if (!lifecycles)
        return [];
    const paths = [];
    for (const [field, def] of Object.entries(lifecycles)) {
        if (!def)
            continue;
        // Shorthand: flow: "draft -> open -> closed"
        if (typeof def.flow === 'string') {
            const states = def.flow.split('->').map((s) => s.trim()).filter(Boolean);
            if (states.length < 2)
                continue;
            const transitions = [];
            for (let i = 0; i < states.length - 1; i++) {
                transitions.push({ from: states[i], to: states[i + 1] });
            }
            paths.push({ field, states, transitions, initial: states[0] });
            continue;
        }
        // Structured: states[] + transitions ({} keyed or [] of {from,to})
        const statesRaw = Array.isArray(def.states) ? def.states : [];
        const states = statesRaw
            .map((s) => (typeof s === 'string' ? s : s?.name))
            .filter(Boolean);
        if (states.length < 2)
            continue;
        let transitions = [];
        const t = def.transitions;
        if (Array.isArray(t)) {
            transitions = t
                .map((x) => ({ from: x?.from, to: x?.to }))
                .filter((x) => x.from && x.to);
        }
        else if (t && typeof t === 'object') {
            transitions = Object.values(t)
                .map((x) => ({ from: x?.from, to: x?.to }))
                .filter((x) => x.from && x.to);
        }
        const initial = typeof def.initial === 'string' ? def.initial : states[0];
        paths.push({ field, states, transitions, initial });
    }
    return paths;
}
/**
 * Find an undeclared single-hop transition from the initial state — a
 * jump the lifecycle does NOT permit — to drive the negative arm of the
 * walkable rule. Returns null if every other state is directly reachable
 * (e.g. a 2-state lifecycle has no undeclared jump to test).
 */
export function findUndeclaredJump(path) {
    const reachable = new Set(path.transitions.filter(t => t.from === path.initial).map(t => t.to));
    const target = path.states.find(s => s !== path.initial && !reachable.has(s));
    return target ? { from: path.initial, to: target } : null;
}
function hasRequiredBelongsTo(model) {
    return (model.relationships || []).some(r => /belongsTo/i.test(String(r.type || '')));
}
/** Required business attribute values, marker-tagged. */
function fillRequired(model, marker) {
    const data = {};
    for (const attr of model.attributes || []) {
        if (attr.category === 'metadata' || attr.category === 'relationship')
            continue;
        if (!attr.required)
            continue;
        data[attr.name] = sampleValueFor(attr, marker);
    }
    return data;
}
/** Every lifecycle field set to its initial state. */
function lifecycleInitials(model) {
    const out = {};
    for (const path of parseLifecyclePaths(model))
        out[path.field] = path.initial;
    return out;
}
/**
 * Build a create payload for a TOP-LEVEL model (no required belongsTo):
 * required business fields + every lifecycle field in its initial state.
 * Returns null if the model needs a parent seeded first (out of v1 scope).
 */
export function buildTopLevelPayload(model, marker) {
    if (hasRequiredBelongsTo(model))
        return null;
    return { ...fillRequired(model, marker), ...lifecycleInitials(model) };
}
/**
 * Build a create payload for a CHILD model, overlaying foreign-key values
 * (e.g. `{ pollId: '$expr:p.id' }`). Unlike buildTopLevelPayload it does
 * not bail on belongsTo — the caller supplies the parent ids.
 */
export function buildChildPayload(model, marker, fkValues) {
    return { ...fillRequired(model, marker), ...lifecycleInitials(model), ...fkValues };
}
/** The FK column a belongsTo relationship maps to (e.g. `poll` → `pollId`). */
export function belongsToFk(relName) {
    return `${relName}Id`;
}
/**
 * Whether a CURVED op is available on the controller for a model. Mirrors
 * realize: a model controller with an explicit `cured:` block exposes only
 * the listed ops; one with no explicit block gets the full inferred CURVED
 * set. Returns false if no controller owns the model (no API surface).
 */
export function opAvailable(spec, modelName, op) {
    const ctrl = (spec.controllers || []).find(c => c.model === modelName);
    if (!ctrl)
        return false;
    const cured = ctrl.cured;
    if (cured && Object.keys(cured).length > 0)
        return !!cured[op];
    return true; // no explicit cured → full inferred CURVED
}
function sampleValueFor(attr, marker) {
    const type = (attr.type || 'String').trim();
    if (/^Integer$|^Int$|^Number$|^Float$/i.test(type))
        return 1;
    if (/^Boolean$/i.test(type))
        return false;
    if (/^DateTime$|^Date$/i.test(type))
        return new Date().toISOString();
    return `${marker}-${attr.name}`;
}
//# sourceMappingURL=_shared.js.map