keel-harness 0.3.2

A gated harness for AI-assisted delivery: auditable stopping conditions and durable memory across coding agents.
//! Assembling a projection body from store sections, inside a hard line budget.
//!
//! When everything fits, everything is included. When it does not, budget is
//! allocated by declared priority using a max-min fill: high-priority sections
//! are satisfied first, and whatever they do not need flows down. Every trimmed
//! section keeps a pointer to the full text, so nothing becomes unreachable —
//! it becomes *deferred*, which is the whole point of progressive disclosure.

use crate::config::{Adapter, Config};
use crate::paths::Paths;
use crate::store::{self, StoreDoc};
use anyhow::Result;
use std::path::PathBuf;

pub struct Section {
    pub title: String,
    pub source: String,
    pub body: String,
}

impl Section {
    fn lines(&self) -> usize { self.body.lines().count() }
}

/// Build all sections an adapter asked for, in its declared order.
fn collect(paths: &Paths, cfg: &Config, adapter: &Adapter) -> Result<Vec<Section>> {
    let mut out = Vec::new();
    for id in &adapter.sections {
        let section = match id.as_str() {
            "product" => doc_section(paths, id, "What this is", paths.product())?,
            "tech" => doc_section(paths, id, "Stack and constraints", paths.tech())?,
            "conventions" => conventions_section(paths, cfg)?,
            "structure" => doc_section(paths, id, "Repository map", paths.structure())?,
            "lessons" => lessons_section(paths, cfg)?,
            _ => None,
        };
        if let Some(s) = section
            && !s.body.trim().is_empty()
        {
            out.push(s);
        }
    }
    Ok(out)
}

fn doc_section(paths: &Paths, id: &str, title: &str, path: PathBuf) -> Result<Option<Section>> {
    let Some(doc) = StoreDoc::read_optional(&path)? else { return Ok(None) };
    let _ = id;
    Ok(Some(Section {
        title: title.to_string(),
        source: paths.rel(&path).to_string_lossy().replace('\\', "/"),
        body: demote_headings(&strip_generated_notice(doc.body_without_title())).trim().to_string(),
    }))
}

/// Push every heading in an embedded document down one level, so a store
/// document's own `## Sections` nest under the projection's section heading
/// instead of appearing as its siblings. Headings inside fenced code blocks are
/// shell comments or markdown examples, and are left alone.
fn demote_headings(body: &str) -> String {
    let mut out = Vec::with_capacity(body.lines().count());
    let mut in_fence = false;
    for line in body.lines() {
        if line.trim_start().starts_with("```") {
            in_fence = !in_fence;
            out.push(line.to_string());
            continue;
        }
        let hashes = line.chars().take_while(|c| *c == '#').count();
        let is_heading = !in_fence
            && hashes > 0
            && hashes < 6
            && line.chars().nth(hashes) == Some(' ');
        if is_heading {
            out.push(format!("#{line}"));
        } else {
            out.push(line.to_string());
        }
    }
    out.join("\n")
}

/// The "do not edit" comment is for humans reading the store, not for agents
/// reading the projection — it is pure budget cost downstream.
fn strip_generated_notice(body: &str) -> String {
    body.lines()
        .filter(|l| !l.trim_start().starts_with("<!-- generated by `keel map`"))
        .collect::<Vec<_>>()
        .join("\n")
}

/// House rules: shared stores first, then this repository's.
///
/// Order is the precedence statement. Platform rules read as the ground the
/// local ones are added to, and a local rule that contradicts one above it is
/// visible as a contradiction rather than hidden by a merge.
fn conventions_section(paths: &Paths, cfg: &Config) -> Result<Option<Section>> {
    let mut body = String::new();
    let mut sources: Vec<String> = Vec::new();

    for sh in store::shared(paths, cfg) {
        if sh.missing {
            // Loud, in the projection itself: an agent reading this must know a
            // rule set it was supposed to be held to did not load.
            body.push_str(&format!(
                "> **Shared store `{}` did not load** from `{}`. Rules it carries are \
                 NOT in force in this render.\n\n",
                sh.id,
                sh.root.display()
            ));
            continue;
        }
        let Some(doc) = StoreDoc::read_optional(&sh.conventions())? else { continue };
        let text = doc.body_without_title().trim().to_string();
        if text.is_empty() { continue; }
        body.push_str(&format!("### From `{}` (shared)\n\n{}\n\n", sh.id, text));
        sources.push(sh.conventions().to_string_lossy().to_string());
    }

    if let Some(doc) = StoreDoc::read_optional(&paths.conventions())? {
        let text = strip_generated_notice(doc.body_without_title());
        if !text.trim().is_empty() {
            if !sources.is_empty() {
                body.push_str("### This repository\n\n");
            }
            body.push_str(text.trim());
            body.push('\n');
        }
        sources.push(paths.rel(&paths.conventions()).to_string_lossy().to_string());
    }

    if body.trim().is_empty() { return Ok(None); }
    Ok(Some(Section {
        title: "House rules".to_string(),
        source: sources.join(", "),
        body: demote_headings(&body).trim().to_string(),
    }))
}

fn lessons_section(paths: &Paths, cfg: &Config) -> Result<Option<Section>> {
    let lessons = crate::lesson::list_all(paths, cfg)?;
    if lessons.is_empty() { return Ok(None); }
    let mut body = String::new();
    for entry in &lessons {
        let l = &entry.lesson;
        let origin = match &entry.from {
            Some(id) => format!("{}, shared:{id}", l.front.scope),
            None => l.front.scope.clone(),
        };
        let rule = l.rule().unwrap_or_else(|| first_prose_line(&l.body));
        body.push_str(&format!("- **{}** ({origin}) — {rule}\n", l.front.id));
    }
    Ok(Some(Section {
        title: "Lessons in force".into(),
        source: ".keel/store/lessons/".into(),
        body: body.trim_end().to_string(),
    }))
}

fn first_prose_line(body: &str) -> String {
    body.lines()
        .map(|l| l.trim())
        .find(|l| !l.is_empty() && !l.starts_with('#') && !l.starts_with("---"))
        .unwrap_or("(no rule stated)")
        .to_string()
}

pub fn render_builtin(paths: &Paths, cfg: &Config, adapter: &Adapter) -> Result<(String, bool)> {
    let sections = collect(paths, cfg, adapter)?;

    let preamble = preamble(adapter);
    // Per section: `## Title` + a blank line before and after.
    let per_section_overhead = 3;
    let fixed = preamble.lines().count() + sections.len() * per_section_overhead;
    let available = adapter.budget.saturating_sub(fixed);

    let allocations = allocate(&sections, available);
    let mut trimmed_any = false;

    let mut out = preamble;
    for (section, alloc) in sections.iter().zip(allocations.iter()) {
        out.push_str(&format!("\n## {}\n\n", section.title));
        if section.lines() <= *alloc {
            out.push_str(section.body.trim_end());
            out.push('\n');
        } else {
            trimmed_any = true;
            out.push_str(&trim_to(&section.body, *alloc, &section.source));
        }
    }
    // The budget is an invariant. Allocation gets us there in every ordinary
    // case; this makes it true even for a budget too small for the preamble.
    let out = hard_clip(out, adapter.budget);
    Ok((out, trimmed_any))
}

/// Final enforcement of the line budget, stating the overrun rather than
/// hiding it.
fn hard_clip(s: String, budget: usize) -> String {
    let total = s.lines().count();
    if total <= budget { return s; }
    if budget == 0 { return String::new(); }
    let keep = budget - 1;
    let mut out: String = s.lines().take(keep).collect::<Vec<_>>().join("\n");
    if !out.is_empty() { out.push('\n'); }
    out.push_str(&format!("_… {} lines cut at the budget; see `.keel/store/`._\n", total - keep));
    out
}

fn preamble(adapter: &Adapter) -> String {
    format!(
        "# Project context\n\n\
         Generated by keel for `{}`. The canonical source is `.keel/store/`; \
         do not edit this file. Anything trimmed below is deferred, not deleted — \
         open the cited path when you need it.\n",
        adapter.id
    )
}

/// Max-min fill by declared priority. Sections earlier in the list are
/// satisfied first; unused allocation flows to the ones still short.
fn allocate(sections: &[Section], available: usize) -> Vec<usize> {
    let n = sections.len();
    let mut alloc = vec![0usize; n];
    if n == 0 { return alloc; }

    let mut unsatisfied: Vec<usize> = (0..n).collect();
    let mut remaining = available;

    while !unsatisfied.is_empty() && remaining > 0 {
        // Weight by position: first section gets the largest share.
        let weights: Vec<f64> = unsatisfied
            .iter()
            .enumerate()
            .map(|(rank, _)| 1.0 / (rank as f64 + 1.5))
            .collect();
        let total: f64 = weights.iter().sum();

        // Satisfy every section whose natural size fits inside its share.
        let mut progressed = false;
        for (k, &i) in unsatisfied.iter().enumerate() {
            let share = ((remaining as f64) * weights[k] / total).floor() as usize;
            if sections[i].lines() <= share {
                alloc[i] = sections[i].lines();
                progressed = true;
            }
        }
        if progressed {
            let satisfied: Vec<usize> = unsatisfied.iter().copied().filter(|i| alloc[*i] > 0).collect();
            for i in &satisfied {
                remaining = remaining.saturating_sub(alloc[*i]);
            }
            unsatisfied.retain(|i| alloc[*i] == 0);
            continue;
        }

        // Nobody fits: hand out the shares as truncation budgets and stop.
        for (k, &i) in unsatisfied.iter().enumerate() {
            alloc[i] = ((remaining as f64) * weights[k] / total).floor() as usize;
        }
        break;
    }
    alloc
}

/// Truncate a markdown body to **exactly at most `budget` lines**, without
/// leaving an unterminated code fence and without dropping the pointer to the
/// full text. The pointer costs lines, so it is paid for out of the budget —
/// an off-by-one here is how a "hard" budget quietly becomes a suggestion.
fn trim_to(body: &str, budget: usize, source: &str) -> String {
    let pointer = format!("_… more in `{source}`._\n");
    if budget == 0 {
        return String::new();
    }
    if budget == 1 {
        return pointer;
    }
    // Reserve: one blank separator + one pointer line.
    let mut content = budget - 2;
    let mut taken: Vec<&str> = body.lines().take(content).collect();
    let unbalanced = |ls: &[&str]| ls.iter().filter(|l| l.trim_start().starts_with("```")).count() % 2 == 1;
    if unbalanced(&taken) {
        // Closing the fence costs a line, so one fewer line of content.
        content = content.saturating_sub(1);
        taken = body.lines().take(content).collect();
    }
    let omitted = body.lines().count().saturating_sub(content);
    let mut out = taken.join("\n");
    if !out.is_empty() { out.push('\n'); }
    if unbalanced(&taken) {
        out.push_str("```\n");
    }
    out.push('\n');
    out.push_str(&format!("_… {omitted} more lines in `{source}`._\n"));
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    fn sections(sizes: &[usize]) -> Vec<Section> {
        sizes
            .iter()
            .enumerate()
            .map(|(i, n)| Section {
                title: format!("S{i}"),
                source: format!("store/s{i}.md"),
                body: (0..*n).map(|j| format!("line {j}")).collect::<Vec<_>>().join("\n"),
            })
            .collect()
    }

    #[test]
    fn headings_are_demoted_but_not_inside_fences() {
        let body = "# Title\n## Sub\ntext\n```sh\n# not a heading\n```\n### Deep\n";
        let out = demote_headings(body);
        assert!(out.contains("\n### Sub"), "{out}");
        assert!(out.starts_with("## Title"), "{out}");
        assert!(out.contains("\n# not a heading"), "fence content was demoted:\n{out}");
        assert!(out.contains("\n#### Deep"), "{out}");
    }

    #[test]
    fn demotion_stops_at_the_deepest_level() {
        let out = demote_headings("###### Deepest\n");
        assert_eq!(out.trim(), "###### Deepest");
    }

    #[test]
    fn everything_fits_when_budget_is_ample() {
        let s = sections(&[3, 4, 5]);
        let a = allocate(&s, 100);
        assert_eq!(a, vec![3, 4, 5]);
    }

    #[test]
    fn allocation_never_exceeds_available() {
        for available in [0usize, 1, 5, 9, 20, 60] {
            let s = sections(&[40, 30, 20, 10]);
            let a = allocate(&s, available);
            let total: usize = a.iter().sum();
            assert!(total <= available, "allocated {total} of {available}: {a:?}");
        }
    }

    #[test]
    fn priority_wins_under_pressure() {
        let s = sections(&[40, 40, 40]);
        let a = allocate(&s, 12);
        assert!(a[0] > a[2], "first section should get the most: {a:?}");
    }

    #[test]
    fn small_low_priority_sections_still_survive() {
        // A big first section must not starve a tiny last one.
        let s = sections(&[200, 2]);
        let a = allocate(&s, 30);
        assert!(a[1] >= 2, "tiny section was starved: {a:?}");
    }

    #[test]
    fn trimming_closes_open_code_fences() {
        let body = "text\n```rust\nlet x = 1;\nlet y = 2;\n```\nmore\n";
        let out = trim_to(body, 5, "store/x.md");
        assert_eq!(out.matches("```").count() % 2, 0, "unbalanced fences in:\n{out}");
        assert!(out.contains("more lines in `store/x.md`"));
    }

    #[test]
    fn trimming_never_exceeds_its_budget() {
        let plain: String = (0..50).map(|i| format!("line {i}\n")).collect();
        let fenced = "intro\n```rust\nlet a = 1;\nlet b = 2;\nlet c = 3;\n```\ntail\n";
        for body in [plain.as_str(), fenced] {
            for budget in 0..12usize {
                let out = trim_to(body, budget, "store/x.md");
                assert!(
                    out.lines().count() <= budget,
                    "budget {budget} produced {} lines:\n{out}", out.lines().count()
                );
            }
        }
    }

    #[test]
    fn a_budget_of_one_still_points_at_the_source() {
        let out = trim_to("a\nb\n", 1, "store/x.md");
        assert_eq!(out.lines().count(), 1);
        assert!(out.contains("store/x.md"));
    }

    #[test]
    fn hard_clip_is_exact() {
        let s: String = (0..40).map(|i| format!("l{i}\n")).collect();
        for budget in 0..20usize {
            let out = hard_clip(s.clone(), budget);
            assert!(out.lines().count() <= budget, "budget {budget}");
        }
    }
}