package com.correx.infrastructure.workflow

/**
 * Plan-derived diff manifest (BACKLOG §B-§2).
 *
 * The write-guard manifest enforced by the plane-2 `ManifestContainmentRule` was originally a
 * STATIC per-stage `writes = [globs]` set (role-reliability §2). This object DERIVES the
 * allowed-write set for a stage from the confirmed `impl_plan` artifact instead — the file
 * paths/globs a [PlanStage] declares it produces/touches — so the guard tracks the actual locked
 * plan rather than only hand-written globs.
 *
 * The derived set is not a replacement: it AUGMENTS the static baseline. [combine] unions the two,
 * and the resulting set is fed verbatim into `StageConfig.writeManifest`, so plan-derived paths are
 * matched with the SAME glob/containment semantics the static manifest already uses — no parallel
 * matcher is introduced.
 *
 * ## Extraction rule
 * Write targets are read from [PlanStage.writes]: the workspace-relative file paths/globs the
 * plan-stage explicitly declares it will write. `produces`/`kind`/`needs` name artifact *slots*
 * (logical outputs), not filesystem paths, so they are deliberately NOT treated as write targets.
 * Derivation is deterministic and lenient: a stage with no [PlanStage.writes] yields an empty set,
 * blank/whitespace entries are dropped, and duplicates collapse.
 */
object PlanDerivedManifest {

    /**
     * The set of workspace-relative paths/globs [planStage] is permitted to write, derived from its
     * declared [PlanStage.writes]. Lenient: a stage with no declared write targets yields the empty
     * set (the static manifest, if any, still governs). Deterministic for a given input.
     */
    fun deriveAllowedWrites(planStage: PlanStage): Set<String> =
        planStage.writes
            .map { it.trim() }
            .filter { it.isNotEmpty() }
            .toSet()

    /**
     * Unions the static per-stage write globs with the plan-derived set. A write is allowed if it
     * matches EITHER source. Blank entries are dropped so the combined set carries only matchable
     * globs. The static path is never narrowed — the result only ever widens the static baseline by
     * the plan-derived paths.
     */
    fun combine(staticManifest: Collection<String>, planDerived: Set<String>): Set<String> =
        (staticManifest + planDerived)
            .map { it.trim() }
            .filter { it.isNotEmpty() }
            .toSet()
}
