//!
//! architect — the structure instruments as an EDITOR DEV TOOL: LSP-shaped diagnostics
//! and code actions that scaffold the split.
//!
//! This is the core a rust-analyzer / VS-Code extension calls. Two signals feed it, and
//! the derivation/suggestion distinction between them maps directly onto LSP semantics:
//!
//! - **cohesion** (laws): a decomposable module gets a `Hint` at its `theory!` site and
//!   a quick-fix whose edits create the scaffolded sub-modules. The split creates seam
//!   obligations, so a human ratifies it (`isPreferred: false`).
//! - **placement** (nets, via [`Architect::place`]): a theory whose operators fall into
//!   net-disjoint components gets an `Information` diagnostic and an auto-applicable
//!   extraction (`isPreferred: true`) — the components share nothing, no seam is
//!   created, and the split is lossless, so an editor or an agent loop may apply it
//!   unattended. Placement reads signatures only, so this check is cheap enough for
//!   every keystroke; cohesion needs discovery and is not.
//!
//! Together they answer "where does an agent write code as it goes": into a WORKBENCH —
//! one designated bundle `theory!` the agent appends operators to, making no structure
//! decisions while writing. The architect watches the bundle through `place::<Bundle>`;
//! while new operators share nets with existing ones, nothing fires; the moment a
//! net-disjoint feature accumulates, the extraction crystallises it into its own module
//! file and the workbench shrinks back to one component. Modules precipitate out of the
//! bundle; the boundary is derived, never decided mid-thought. (The shape lock then
//! holds it: see `discover::shape`.)
//!
//! `render_lsp` serialises findings to the JSON an editor consumes; `apply` writes the
//! files, path-confined. The full LSP server (the stdio protocol loop) is a thin shim
//! over this — the analysis-to-action pipeline lives here and is mutation-tested like
//! everything else.

use std::path::Path;

use super::scaffold::Scaffold;
use crate::discover::arithmetic::Arithmetic;
use crate::discover::date::Calendar;
use crate::discover::engine::Theory;
use crate::discover::router::Router;

/// LSP diagnostic severity (the wire integers).
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Severity {
    Error = 1,
    Warning = 2,
    Information = 3,
    Hint = 4,
}

/// A diagnostic at a source location (LSP `Diagnostic`).
pub struct Diagnostic {
    pub file: String,
    pub line: usize,
    pub severity: Severity,
    pub message: String,
}

/// One file the code action creates (an LSP `CreateFile` + insert).
pub struct FileEdit {
    pub path: String,
    pub contents: String,
}

/// An LSP `CodeAction` (kind `refactor.extract`). `preferred` maps to the wire field
/// `isPreferred` and carries the derivation/suggestion distinction into the editor: a
/// PLACEMENT split shares no nets, needs no seams, and is safe to apply unattended
/// (`preferred: true`); a COHESION split creates seam obligations a human ratifies
/// (`preferred: false` — a quick-fix, not an auto-fix).
pub struct CodeAction {
    pub title: String,
    pub preferred: bool,
    pub edits: Vec<FileEdit>,
}

/// A diagnostic together with its fix.
pub struct Finding {
    pub name: String,
    pub diagnostic: Diagnostic,
    pub action: CodeAction,
}

struct Entry {
    name: &'static str,
    /// The source file the theory is declared in (repo-relative).
    file: &'static str,
    /// The directory new sub-modules are written under (repo-relative).
    out_dir: &'static str,
    scaffold: fn() -> Option<Scaffold>,
}

/// The theories the architect analyses (the crate's real discovery domains). No
/// placement checks here, deliberately: the registry's modules are shape-locked — the
/// committed `spec/boundary-spec.shape.spec` already gates their boundaries on every
/// test run, so a per-analysis re-derivation would be a wire that can never carry a
/// signal (its first targeted sweep proved exactly that, with a survivor). Placement
/// findings belong where the shape is still forming: a WORKBENCH, watched directly
/// through [`Architect::place`].
fn registry() -> Vec<Entry> {
    fn s<T: Theory>() -> Option<Scaffold> {
        Scaffold::of::<T>()
    }
    vec![
        Entry {
            name: Arithmetic::name(),
            file: "src/discover/arithmetic.rs",
            out_dir: "src/discover/arithmetic",
            scaffold: s::<Arithmetic>,
        },
        Entry {
            name: Router::name(),
            file: "src/discover/router.rs",
            out_dir: "src/discover/router",
            scaffold: s::<Router>,
        },
        Entry {
            name: Calendar::name(),
            file: "src/discover/date.rs",
            out_dir: "src/discover/date",
            scaffold: s::<Calendar>,
        },
    ]
}

/// The 1-based line of the `theory!` invocation in `file` (so the diagnostic points at the
/// declaration), or 1 if it can't be found.
///
/// Capability: Effectful — reads the source file from disk (a world-read).
fn theory_line(file: &str) -> usize {
    let path = Path::new(env!("CARGO_MANIFEST_DIR")).join(file);
    std::fs::read_to_string(path)
        .ok()
        .and_then(|src| {
            src.lines()
                .position(|l| l.contains("theory!"))
                .map(|i| i + 1)
        })
        .unwrap_or(1)
}

/// The architect tool itself, as a TYPESTATE — the surface its analysis, serialisation, and
/// auto-apply hang off, so the tool's public callables are associated functions of the tool's
/// value object, not loose ones (the no-rats-nest rule: every public callable hangs off a
/// typestate).
pub struct Architect;

impl Architect {
    /// Analyse the registry and produce a finding (diagnostic + scaffold code action) for every module
    /// whose discovered algebra DECOMPOSES — the cohesive ones produce nothing, exactly as an editor
    /// would surface a hint only where there is something to act on. The analysis is an associated
    /// function of the TOOL — the public surface is the typestate, not a loose function (the
    /// no-rats-nest rule: every public callable hangs off a typestate).
    /// The PLACEMENT finding for one theory — the autorouter's editor half, and the
    /// entry point a bundle workbench is watched through (`Architect::place::<Bundle>`
    /// on the file the agent writes into). Returns `None` when the placement is settled.
    /// Cheap enough for every keystroke: placement reads signatures only, never
    /// discovery. The finding's action is `preferred` — the components share no nets,
    /// no seam is created, and the split is lossless, so an editor (or an agent loop)
    /// may apply it unattended.
    pub fn place<T: Theory>(file: &str, out_dir: &str) -> Option<Finding> {
        let sc = Scaffold::placement::<T>()?;
        let n = sc.modules.len();
        let edits = sc
            .modules
            .iter()
            .enumerate()
            .map(|(i, m)| FileEdit {
                path: format!("{out_dir}/module{i}.rs"),
                contents: m.source.clone(),
            })
            .collect();
        Some(Finding {
            name: T::name().to_string(),
            diagnostic: Diagnostic {
                file: file.to_string(),
                line: theory_line(file),
                severity: Severity::Information,
                message: format!(
                    "`{}` places as {n} modules — the components share no nets, so the \
                     split is indisputable and lossless. Extract.",
                    T::name()
                ),
            },
            action: CodeAction {
                title: format!("Extract `{}`'s {n} net-disjoint modules", T::name()),
                preferred: true,
                edits,
            },
        })
    }

    pub fn analyze() -> Vec<Finding> {
        let mut findings = Vec::new();
        for e in registry() {
            let Some(sc) = (e.scaffold)() else {
                continue;
            };
            let edits = sc
                .modules
                .iter()
                .enumerate()
                .map(|(i, m)| FileEdit {
                    path: format!("{}/module{i}.rs", e.out_dir),
                    contents: m.source.clone(),
                })
                .collect();
            let seams: Vec<&str> = sc
                .seams
                .iter()
                .map(|s| match s.kind {
                    super::cohesion::SeamKind::Transport => "transport",
                    super::cohesion::SeamKind::Transform => "transform",
                })
                .collect();
            findings.push(Finding {
                name: e.name.to_string(),
                diagnostic: Diagnostic {
                    file: e.file.to_string(),
                    line: theory_line(e.file),
                    severity: Severity::Hint,
                    message: format!(
                        "`{}` is secretly {} modules — its algebra decomposes (seam: {}). Consider splitting.",
                        e.name,
                        sc.modules.len(),
                        seams.join(", ")
                    ),
                },
                action: CodeAction {
                    title: format!("Split `{}` into {} modules", e.name, sc.modules.len()),
                    preferred: false,
                    edits,
                },
            });
        }
        findings
    }
}

/// Escape a string to its RFC 8259 wire form: `"` and `\` and the three whitespace controls take
/// their short escapes; every OTHER C0 control (U+0000–U+001F, which raw JSON forbids outright)
/// takes the generic `\u00XX` form. Deliberately a PER-CHARACTER map — each char's wire form is
/// independent of its neighbours — so `esc` is a monoid homomorphism over concatenation, exactly
/// the law the `EscapeCodec` theory discovers below.
fn esc(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '"' => out.push_str("\\\""),
            '\\' => out.push_str("\\\\"),
            '\n' => out.push_str("\\n"),
            '\t' => out.push_str("\\t"),
            // ESCAPED, not dropped: `esc` must be INVERTIBLE or the engine refuses the codec
            // round-trip below (a dropping escaper loses `\r` and `unesc(esc(s)) != s`).
            '\r' => out.push_str("\\r"),
            // the REMAINING C0 controls (`\x08`, `\x0c`, ...): RFC 8259 forbids every one of
            // them raw, so each becomes `\u00XX` — still a fixed per-character map.
            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
            _ => out.push(c),
        }
    }
    out
}

/// The inverse of `esc` — decode the wire-escaping back to the original. With `esc` it forms a
/// CODEC: `unesc(esc(s)) = s`, the round-trip the `EscapeCodec` theory discovers below.
fn unesc(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    let mut chars = s.chars();
    while let Some(c) = chars.next() {
        if c == '\\' {
            match chars.next() {
                Some('"') => out.push('"'),
                Some('\\') => out.push('\\'),
                Some('n') => out.push('\n'),
                Some('t') => out.push('\t'),
                Some('r') => out.push('\r'),
                // exactly what `esc` emits for the remaining C0 controls: `\u` then four hex
                // digits. A malformed tail (not `esc` output) is kept literally — `unesc` is
                // total, and on `esc`'s image this arm is the exact inverse of `\u00XX`.
                Some('u') => {
                    let hex: String = chars.by_ref().take(4).collect();
                    match u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
                        Some(decoded) if hex.len() == 4 => out.push(decoded),
                        _ => {
                            out.push_str("\\u");
                            out.push_str(&hex);
                        }
                    }
                }
                Some(other) => out.push(other),
                None => out.push('\\'),
            }
        } else {
            out.push(c);
        }
    }
    out
}

impl Architect {
    /// Serialise the findings to the LSP JSON an editor consumes: a `diagnostics` array and a
    /// `codeActions` array (each action a `refactor.extract` whose `documentChanges` create the files).
    pub fn render_lsp(findings: &[Finding]) -> String {
        let diags: Vec<String> = findings
            .iter()
            .map(|f| {
                let d = &f.diagnostic;
                let line = d.line.saturating_sub(1);
                format!(
                    "{{\"uri\":\"{}\",\"range\":{{\"start\":{{\"line\":{line},\"character\":0}},\
                     \"end\":{{\"line\":{line},\"character\":0}}}},\"severity\":{},\
                     \"source\":\"architect\",\"message\":\"{}\"}}",
                    esc(&d.file),
                    d.severity as u8,
                    esc(&d.message)
                )
            })
            .collect();
        let actions: Vec<String> = findings
            .iter()
            .map(|f| {
                let changes: Vec<String> = f
                    .action
                    .edits
                    .iter()
                    .map(|e| {
                        format!(
                            "{{\"kind\":\"create\",\"uri\":\"{}\",\"text\":\"{}\"}}",
                            esc(&e.path),
                            esc(&e.contents)
                        )
                    })
                    .collect();
                format!(
                    "{{\"title\":\"{}\",\"kind\":\"refactor.extract\",\
                     \"isPreferred\":{},\"edit\":{{\"documentChanges\":[{}]}}}}",
                    esc(&f.action.title),
                    f.action.preferred,
                    changes.join(",")
                )
            })
            .collect();
        format!(
            "{{\"diagnostics\":[{}],\"codeActions\":[{}]}}",
            diags.join(","),
            actions.join(",")
        )
    }
}

/// `apply` is the architect's one EFFECTFUL edge — everything else here (the analysis, the codec)
/// is `Pure`. Declared, not hidden: the top of the capability lattice (`effectful ⊃ stateful ⊃
/// lossy ⊃ pure`). The `std::fs::write` beneath it is the leaf where the world is finally touched.
pub const APPLY_CAPABILITY: crate::boundary::Capability = crate::boundary::Capability::Effectful;

/// True iff `rel` stays UNDER a root when joined — the confinement bound `apply` declares. An
/// absolute path or a `..` component would escape it, so neither is confined.
fn confined(rel: &Path) -> bool {
    !rel.is_absolute()
        && !rel
            .components()
            .any(|c| c == std::path::Component::ParentDir)
}

impl Architect {
    /// Apply a code action: write every created file under `root`. Returns the paths written. This is
    /// the "auto-apply" — the scaffolded skeletons land on disk; the developer (or an agent) then moves
    /// the operator functions in and names the modules.
    ///
    /// The effect is BOUNDED to `root`: an edit whose path escapes it (absolute, or via `..`) is
    /// rejected rather than written, so the declared `Effectful`-confined-to-`root` capability is real,
    /// not a vacuous claim.
    ///
    /// Capability: Effectful — writes files to disk (confined to `root`; see `APPLY_CAPABILITY`).
    pub fn apply(action: &CodeAction, root: &Path) -> std::io::Result<Vec<std::path::PathBuf>> {
        let mut written = Vec::new();
        for e in &action.edits {
            let rel = Path::new(&e.path);
            if !confined(rel) {
                return Err(std::io::Error::new(
                    std::io::ErrorKind::InvalidInput,
                    format!("edit path `{}` escapes the root", e.path),
                ));
            }
            let path = root.join(rel);
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent)?;
            }
            std::fs::write(&path, &e.contents)?;
            written.push(path);
        }
        Ok(written)
    }
}

// ----- the architect's OWN algebra: a report is a join-semilattice ----------
//
// Dogfood: the tool that surfaces each module's algebra is itself written in the algebra format. The
// architect's value object is a REPORT — the set of modules a run flagged for splitting. Reports
// compose by UNION (`merge`), with the empty report as identity: a commutative, idempotent monoid
// (a join-semilattice), discovered by running it like any other domain. Merging real runs (several
// crates, several passes) is exactly this operator, not a contrivance.

use std::collections::BTreeSet;

/// The architect's value object: the set of module names a run flagged.
#[derive(Clone)]
pub struct Report(BTreeSet<String>);

impl Report {
    /// The architect's output AS a value object — the set of flagged modules from one analysis
    /// run. An associated function of the value object it yields (the no-rats-nest rule: every
    /// public callable hangs off a typestate).
    pub fn of() -> Report {
        Report(Architect::analyze().into_iter().map(|f| f.name).collect())
    }

    /// The flagged module names, sorted.
    pub fn flagged(&self) -> Vec<String> {
        self.0.iter().cloned().collect()
    }
}

/// The report theory's single sort.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum RSort {
    Report,
}

fn empty_report(_: &[Report]) -> Option<Report> {
    Some(Report(BTreeSet::new()))
}
fn merge_reports(v: &[Report]) -> Option<Report> {
    let mut merged = v[0].0.clone();
    merged.extend(v[1].0.iter().cloned());
    Some(Report(merged))
}
fn sample_reports() -> Vec<Report> {
    let r = |names: &[&str]| Report(names.iter().map(|s| s.to_string()).collect());
    vec![r(&[]), r(&["x"]), r(&["y"]), r(&["x", "y"]), r(&["z"])]
}

/// The architect's report monoid, as a `Theory` — so the tool's own algebra is discovered, frozen,
/// and ratified exactly like the modules it analyses.
pub struct Reports;
crate::theory! {
    Reports : "architect report", Value = Report, Obs = Vec<String>, Sort = RSort,
    sort_of = |_: &Report| RSort::Report,
    observe = |r: &Report| r.0.iter().cloned().collect(),
    vars { RSort::Report => &["a", "b", "c"], }
    inhabit { RSort::Report => sample_reports(), }
    ops {
        Nullary "Empty" "empty" () -> RSort::Report = empty_report;
        Infix   "Merge" "merge" (RSort::Report, RSort::Report) -> RSort::Report = merge_reports;
    }
}

// ----- the LSP serialization as an in-format TRANSFORM SEAM -----------------
//
// The wire-escaping that carries the scaffolded source (full of quotes and newlines) safely inside
// the JSON payload is modelled as a string theory over `concat`, `esc`, and `unesc`. Running it, the
// engine DISCOVERS the serializer's algebra:
//   - HOMOMORPHISM — `esc(a ++ b) = esc(a) ++ esc(b)` — so the payload assembles and escapes
//     piecewise, identically (exactly how `render_lsp` builds it from fragments).
//   - ROUND-TRIP — `unesc(esc(s)) = s` — so the wire form is a FAITHFUL CODEC (and this is what
//     forced `\r` to be escaped rather than dropped: the round-trip would not hold otherwise).
//
// What the algebra does NOT determine is the ENCODING — which character maps to which escape. That
// is a representation CONVENTION (conformance to the RFC 8259 JSON wire standard), not a law: many
// invertible homomorphisms exist (the identity is one), so the specific arms `'\t' => "\\t"` … and
// the `\u00XX` form for the remaining C0 controls are a LEAF the laws cannot derive. The in-format
// model thus locates the irreducible bit precisely: the codec STRUCTURE is discovered; the encoding
// is an oracle pinned by `esc_conforms_to_the_json_encoding`.

/// The escape codec's single sort.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum ESort {
    Str,
}

fn cat(v: &[String]) -> Option<String> {
    Some(format!("{}{}", v[0], v[1]))
}
fn esc_op(v: &[String]) -> Option<String> {
    Some(esc(&v[0]))
}
fn unesc_op(v: &[String]) -> Option<String> {
    Some(unesc(&v[0]))
}
/// One inhabitant per short-form escape arm (the generic `\u00XX` C0 arm is pinned by the
/// `esc_conforms_to_the_json_encoding` oracle instead — the laws hold either way, so the grid
/// stays minimal), plus plain text AND already-escaped SEQUENCES (`\n` as the two
/// chars backslash-n, a trailing backslash). The sequences matter: without them the grid is too weak
/// and discovery over-fits — `unesc` looks like an involution and a homomorphism (it is neither);
/// the sequences refute both, leaving only the TRUE laws (esc homomorphism, the codec round-trip,
/// concat associativity). A non-invertible mutation still fails the round-trip on its escape arm.
fn escape_inhabitants() -> Vec<String> {
    ["", "\"", "\\", "\n", "\t", "\r", "ab", "\\n", "x\\y"]
        .iter()
        .map(|s| s.to_string())
        .collect()
}

/// The LSP serializer's escaping AS a theory — so the tool's wire format is a discovered algebra
/// (an invertible homomorphism), not a hand-rolled transform.
pub struct EscapeCodec;
crate::theory! {
    EscapeCodec : "lsp escape codec", Value = String, Obs = String, Sort = ESort,
    sort_of = |_: &String| ESort::Str,
    observe = |s: &String| s.clone(),
    vars { ESort::Str => &["a", "b", "c"], }
    inhabit { ESort::Str => escape_inhabitants(), }
    ops {
        Infix  "concat" "++"    (ESort::Str, ESort::Str) -> ESort::Str = cat;
        Prefix "esc"    "esc"   (ESort::Str) -> ESort::Str = esc_op;
        Prefix "unesc"  "unesc" (ESort::Str) -> ESort::Str = unesc_op;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::discover::cohesion::CohesionReport;
    use crate::discover::engine::Engine;

    /// The architect surfaces a finding for every DECOMPOSABLE module and none for the cohesive
    /// ones — exactly the actionable hints an editor would show. Date and arithmetic decompose;
    /// router is cohesive, so it is silent.
    #[test]
    fn it_surfaces_findings_only_for_decomposable_modules() {
        let f = Architect::analyze();
        let named: Vec<&str> = f.iter().map(|x| x.diagnostic.message.as_str()).collect();
        assert!(named
            .iter()
            .any(|m| m.contains("date calculus") && m.contains("transform")));
        assert!(named
            .iter()
            .any(|m| m.contains("interpreter arithmetic") && m.contains("transport")));
        assert!(
            !named.iter().any(|m| m.contains("router")),
            "router is cohesive: {named:?}"
        );
        // every finding is a Hint with a create-the-modules code action.
        for finding in &f {
            assert_eq!(finding.diagnostic.severity, Severity::Hint);
            assert!(finding.action.edits.len() >= 2, "splits into ≥2 files");
            assert!(finding.action.title.starts_with("Split `"));
        }
    }

    /// The diagnostic points at the `theory!` declaration line, not the top of the file.
    #[test]
    fn the_diagnostic_points_at_the_theory_declaration() {
        let date = Architect::analyze()
            .into_iter()
            .find(|f| f.diagnostic.message.contains("date calculus"))
            .expect("date finding");
        assert_eq!(date.diagnostic.file, "src/discover/date.rs");
        // the exact line of `crate::theory! {` in date.rs — pins the `i + 1` line math.
        assert_eq!(
            date.diagnostic.line, 57,
            "should locate the theory! declaration"
        );
    }

    /// DOGFOOD: the LSP wire-escaping is a DISCOVERED algebra, and its WHOLE spec is exactly this —
    /// the serializer's structure found by running `esc`/`unesc`/`concat`: `concat` is associative,
    /// `unesc` undoes `esc` (a faithful codec), and `esc` is a homomorphism over concatenation
    /// (exactly how `render_lsp` assembles the payload). Pinning the EXACT law set + consequence
    /// count kills the under-constrained mutants a loose `any()` check misses: a constant `concat`
    /// would spuriously become commutative (a 4th law), and a collapsed inhabitant grid would change
    /// the consequence count. (Note: the engine finds NO false `unesc` law — the escape-sequence
    /// inhabitants refute the involution/homomorphism a weaker grid would over-fit.)
    #[test]
    fn the_escape_codec_spec_is_exact() {
        let d = Engine::<EscapeCodec>::new().discover();
        let got: Vec<(String, String)> = d
            .laws
            .iter()
            .map(|l| (l.prose.clone(), l.equation.clone()))
            .collect();
        let expected: Vec<(&str, &str)> = vec![
            (
                "With concat, the grouping of three values doesn't matter.",
                "((a ++ b) ++ c) = (a ++ (b ++ c))",
            ),
            (
                "esc turns concat into concat.",
                "esc((a ++ b)) = (esc(a) ++ esc(b))",
            ),
            (
                "unesc is a projection — applying it twice is applying it once.",
                "unesc(unesc(a)) = unesc(a)",
            ),
            (
                "unesc undoes esc — the round trip is the identity.",
                "unesc(esc(a)) = a",
            ),
        ];
        let expected: Vec<(String, String)> = expected
            .into_iter()
            .map(|(p, e)| (p.to_string(), e.to_string()))
            .collect();
        assert_eq!(got, expected, "the escape codec's discovered spec changed");
        assert!(
            d.uncovered_ops.is_empty(),
            "uncovered: {:?}",
            d.uncovered_ops
        );
        assert_eq!(d.consequences, 38, "consequence count changed");
    }

    /// The codec STRUCTURE is discovered (above); the ENCODING is not a law — many invertible
    /// homomorphisms exist, so which character maps to which escape is a representation CONVENTION,
    /// conformance to the RFC 8259 JSON wire standard. This is the irreducible leaf the algebra
    /// cannot derive, pinned here as an oracle (and pinning every `esc` arm against deletion) —
    /// including the standard's blanket rule that EVERY C0 control (U+0000–U+001F), not just the
    /// five with short forms, must be escaped.
    #[test]
    fn esc_conforms_to_the_json_encoding() {
        assert_eq!(esc("a\"b\\c\nd\te\rf"), "a\\\"b\\\\c\\nd\\te\\rf");
        // and it round-trips through `unesc` — invertibility, including the once-dropped `\r`.
        assert_eq!(unesc(&esc("a\"b\\c\nd\te\rf")), "a\"b\\c\nd\te\rf");
        // the controls WITHOUT short forms take the generic `\u00XX` escape (the once-raw leak).
        assert_eq!(
            esc("\u{0}a\u{8}b\u{c}c\u{1f}"),
            "\\u0000a\\u0008b\\u000cc\\u001f"
        );
        // and EVERY C0 control leaves no raw control byte on the wire and round-trips exactly.
        for c in '\u{0}'..='\u{1f}' {
            let s = c.to_string();
            let wire = esc(&s);
            assert!(
                wire.chars().all(|w| w as u32 >= 0x20),
                "raw control {c:?} leaked into the wire form {wire:?}"
            );
            assert_eq!(unesc(&wire), s, "control {c:?} must round-trip");
        }
        // `unesc` is TOTAL off `esc`'s image: a truncated `\u` tail (fewer than four hex
        // digits) is kept literally, never decoded on a partial parse — `\u0a` would
        // otherwise become a raw newline that `esc` never emitted.
        assert_eq!(unesc("\\u0a"), "\\u0a");
        assert_eq!(unesc("\\u00"), "\\u00");
    }

    /// `render_lsp` emits the LSP payloads an editor consumes — a Hint (severity 4) diagnostic and a
    /// `refactor.extract` code action whose documentChange CREATES the scaffolded module file.
    #[test]
    fn it_renders_lsp_json() {
        let json = Architect::render_lsp(&Architect::analyze());
        assert!(json.contains("\"diagnostics\":["));
        assert!(json.contains("\"severity\":4"));
        assert!(json.contains("\"source\":\"architect\""));
        assert!(json.contains("\"kind\":\"refactor.extract\""));
        assert!(json.contains("\"kind\":\"create\""));
        // the scaffolded source rides inside the edit (escaped).
        assert!(json.contains("crate::theory! {"));
        // cohesion findings are quick-fixes, never auto-applied.
        assert!(json.contains("\"isPreferred\":false"));
        assert!(!json.contains("\"isPreferred\":true"));
    }

    // ===== the placer's editor half: the bundle workbench ================================

    // A WORKBENCH bundle: two features written into one theory with no structure
    // decisions — a counter (zero/bump over sort A) and a flag (off/toggle over sort B).
    // They share no sorts, so the placement is 2 net-disjoint components.
    #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
    enum WSort {
        A,
        B,
    }
    struct Workbench;
    fn zero(_: &[(u8, i64)]) -> Option<(u8, i64)> {
        Some((0, 0))
    }
    fn bump(v: &[(u8, i64)]) -> Option<(u8, i64)> {
        Some((0, v[0].1 + 1))
    }
    fn off(_: &[(u8, i64)]) -> Option<(u8, i64)> {
        Some((1, 0))
    }
    fn toggle(v: &[(u8, i64)]) -> Option<(u8, i64)> {
        Some((1, 1 - v[0].1))
    }
    crate::theory! {
        Workbench : "workbench", Value = (u8, i64), Obs = (u8, i64), Sort = WSort,
        sort_of = |v: &(u8, i64)| if v.0 == 0 { WSort::A } else { WSort::B },
        observe = |v: &(u8, i64)| *v,
        vars { WSort::A => &["a", "b"], WSort::B => &["p", "q"], }
        inhabit {
            WSort::A => vec![(0, 0), (0, 1), (0, 2)],
            WSort::B => vec![(1, 0), (1, 1)],
        }
        ops {
            Nullary "zero"   "zero"   () -> WSort::A = zero;
            Prefix  "bump"   "bump"   (WSort::A) -> WSort::A = bump;
            Nullary "off"    "off"    () -> WSort::B = off;
            Prefix  "toggle" "toggle" (WSort::B) -> WSort::B = toggle;
        }
    }

    /// The autorouter's loop, end to end: the workbench bundle produces a PREFERRED
    /// extraction (Information, not Hint — a derivation, not a suggestion), its edits
    /// carry one scaffolded module per net-disjoint component with the right owners,
    /// no seam obligation exists (the components share nothing), and `apply` writes
    /// the files confined to the root.
    #[test]
    fn the_workbench_extraction_is_preferred_and_seamless() {
        let finding = Architect::place::<Workbench>("src/workbench.rs", "src/workbench")
            .expect("two net-disjoint features must place apart");
        assert_eq!(finding.diagnostic.severity, Severity::Information);
        assert_eq!(
            finding.diagnostic.message,
            "`workbench` places as 2 modules — the components share no nets, so the \
             split is indisputable and lossless. Extract."
        );
        assert_eq!(
            finding.action.title,
            "Extract `workbench`'s 2 net-disjoint modules"
        );
        assert!(finding.action.preferred, "safe to apply unattended");

        let scaffold = Scaffold::placement::<Workbench>().expect("unsettled");
        assert!(scaffold.seams.is_empty(), "no nets shared, nothing crosses");
        assert_eq!(scaffold.modules[0].operators, vec!["zero", "bump"]);
        assert_eq!(scaffold.modules[1].operators, vec!["off", "toggle"]);
        assert_eq!(finding.action.edits.len(), 2);
        assert_eq!(finding.action.edits[0].path, "src/workbench/module0.rs");
        assert!(finding.action.edits[0].contents.contains("\"bump\""));
        assert!(!finding.action.edits[0].contents.contains("\"toggle\""));

        // and the preferred flag reaches the wire.
        let json = Architect::render_lsp(&[finding]);
        assert!(json.contains("\"isPreferred\":true"));
        assert!(json.contains("\"severity\":3"));

        let finding = Architect::place::<Workbench>("src/workbench.rs", "src/workbench").unwrap();
        let dir = std::env::temp_dir().join(format!("architect-place-{}", std::process::id()));
        let written = Architect::apply(&finding.action, &dir).expect("write");
        assert!(written.iter().all(|p| p.starts_with(&dir)));
        std::fs::remove_dir_all(&dir).ok();
    }

    /// A settled theory is SILENT to the placer — including the repo's own modules,
    /// every one of which cohesion may still hint about. The two instruments disagree on
    /// arithmetic by design: cohesion suggests a split (laws don't link `<` to `+`);
    /// placement derives keep-whole (`Int` nets them together). The editor shows the
    /// hint and withholds the auto-fix — exactly the derivation/suggestion line. (The
    /// registry runs no placement checks at all: its modules are shape-locked, so
    /// analyze() carries cohesion findings only, none preferred.)
    #[test]
    fn settled_theories_are_silent_to_the_placer() {
        assert!(Architect::place::<Arithmetic>("f", "d").is_none());
        assert!(Architect::place::<Router>("f", "d").is_none());
        assert!(Architect::place::<Calendar>("f", "d").is_none());
        assert!(Scaffold::placement::<Arithmetic>().is_none());
        assert!(Architect::analyze().iter().all(|f| !f.action.preferred));
    }

    /// DOGFOOD: the architect's own domain is a discovered algebra. Its report is a join-semilattice
    /// — `merge` is commutative, associative, and idempotent, with the empty report as identity — and
    /// the engine finds exactly those laws by running it. And `cohesion` on the tool's own algebra
    /// reports COHESIVE: the architect does not itself want splitting.
    #[test]
    fn the_architect_is_itself_a_discovered_algebra() {
        let proses: Vec<String> = Engine::<Reports>::new()
            .discover()
            .laws
            .iter()
            .map(|l| l.prose.clone())
            .collect();
        for shape in [
            "Merge gives the same result in either order.", // commutative
            "Merge of a value with itself gives that value.", // idempotent
            "Merge with empty leaves a value unchanged.",   // identity
        ] {
            assert!(
                proses.iter().any(|p| p == shape),
                "missing `{shape}` in {proses:?}"
            );
        }
        assert!(
            proses.iter().any(|p| p.contains("grouping")),
            "associativity"
        );
        assert!(
            CohesionReport::of::<Reports>().is_cohesive(),
            "the architect itself is one algebra"
        );
        // and `Report::of()` yields the architect's output as that value object.
        assert!(Report::of().flagged().iter().any(|n| n == "date calculus"));
    }

    /// `apply` writes the scaffolded files to disk — the auto-apply — and its effect is CONFINED to
    /// `root`: every written path lands under it (the declared `Effectful`-bounded-to-`root`
    /// capability, made real). Uses the scratch dir.
    #[test]
    fn apply_writes_the_split_files_confined_to_root() {
        let date = Architect::analyze()
            .into_iter()
            .find(|f| f.diagnostic.message.contains("date calculus"))
            .expect("date finding");
        let dir = std::env::temp_dir().join(format!("architect-test-{}", std::process::id()));
        let written = Architect::apply(&date.action, &dir).expect("write");
        assert_eq!(written.len(), date.action.edits.len());
        assert_eq!(APPLY_CAPABILITY, crate::boundary::Capability::Effectful);
        // CONFINEMENT: every write stayed under the root — the effect is bounded as declared.
        assert!(
            written.iter().all(|p| p.starts_with(&dir)),
            "a write escaped the root: {written:?}"
        );
        let body = std::fs::read_to_string(&written[0]).expect("read back");
        assert!(body.contains("crate::theory! {"));
        std::fs::remove_dir_all(&dir).ok();
    }

    /// An edit whose path ESCAPES the root (absolute, or via `..`) is rejected, not written — so the
    /// confinement bound is enforced, not decorative. Pins the `confined` guard.
    #[test]
    fn apply_rejects_paths_that_escape_the_root() {
        let dir = std::env::temp_dir().join(format!("architect-escape-{}", std::process::id()));
        for bad in ["../escape.rs", "/etc/passwd"] {
            let action = CodeAction {
                title: "x".to_string(),
                preferred: false,
                edits: vec![FileEdit {
                    path: bad.to_string(),
                    contents: "x".to_string(),
                }],
            };
            assert!(
                Architect::apply(&action, &dir).is_err(),
                "escaping path `{bad}` should be rejected"
            );
        }
        // a plain relative path is accepted (the guard is not just rejecting everything).
        assert!(confined(Path::new("src/discover/date/module0.rs")));
        std::fs::remove_dir_all(&dir).ok();
    }
}
