minco-project-view 1.4.0

Bounded, schema-versioned read models for Minco project architecture and evidence
Documentation
use minco_project_view::{
    EvidenceLane, SemanticStatus, ViewLimits, load_project_view, load_project_view_with_limits,
};
use sha2::{Digest, Sha256};
use std::{fmt::Write as _, fs, path::Path};

fn write(root: &Path, relative: &str, source: &str) {
    let destination = root.join(relative);
    fs::create_dir_all(destination.parent().expect("fixture parent")).expect("create parent");
    fs::write(destination, source).expect("write fixture");
}

fn fixture() -> tempfile::TempDir {
    let fixture = tempfile::tempdir().expect("fixture root");
    write(
        fixture.path(),
        "minco.toml",
        r#"schema = 1
name = "fixture"
contract = "openapi.yaml"
generated = "generated.rs"
deployment_config = "deployment.toml"
roadmap = "roadmap.yaml"
tasks = "tasks"
plugin_catalog = "plugins.toml"
quality = "quality.toml"

[configuration]
root = "config"
default_file = "default.toml"
local_override = ".local.toml"
environment_prefix = "FIXTURE__"

[[configuration.fields]]
key = "application.name"
kind = "string"
required = true
secret = false
description = "Application name"
default = "fixture"

[architecture]
domain_roots = ["domain"]
application_roots = ["application"]
api_roots = ["api"]

[plugins]
enabled = ["health", "feedback"]
disabled = []

[migrations]
roots = []

[seeds]
roots = []

[operations.createFeedback]
contract = "plugins/feedback/openapi.yaml"
handler = "plugins/feedback/src/http.rs#create_feedback"
application = "plugins/feedback/src/service.rs#create"
adapters = ["memory"]
tests = ["plugins/feedback/src/http.rs"]
"#,
    );
    write(
        fixture.path(),
        "openapi.yaml",
        r"openapi: 3.1.0
info:
  title: Fixture API
  version: 1.0.0
paths:
  /health:
    get:
      operationId: healthLive
      responses:
        '200':
          description: healthy
",
    );
    write(
        fixture.path(),
        "deployment.toml",
        r#"schema_version = 1
application = "fixture"
environment = "local"
region = "local"
runtime = "local_native"
ingress = "local_tcp"
allowed_origins = ["http://127.0.0.1:5173"]
allowed_headers = ["content-type"]
log_retention_days = 1
scheduled_wakeups = []
uses_nat_gateway = false

[auth]
kind = "development_headers"

[database]
kind = "sqlite_persistent_host"
host_monthly_usd = 0.0
backup_monthly_usd = 0.0

[[functions]]
name = "api"
artifact_path = "target/debug/fixture"
memory_mb = 128
timeout_seconds = 5
reserved_concurrency = 1
provisioned_concurrency = 0
database_connections_per_instance = 1
"#,
    );
    write(fixture.path(), "generated.rs", "// generated fixture\n");
    write(
        fixture.path(),
        "roadmap.yaml",
        r"schema: 1
product: Fixture
milestones:
  - id: M1
    name: Foundation
    status: active
    depends_on: []
    outcome: A bounded fixture
    exit_criteria: []
",
    );
    write(
        fixture.path(),
        "tasks/M1/M1-T01.md",
        r#"---
id: M1-T01
title: Build the fixture
milestone: M1
status: ready
priority: high
area: test
depends_on: []
operations: [healthLive]
owned_paths: ["api/**"]
checks: ["cargo test"]
---

Fixture task.
"#,
    );
    write(
        fixture.path(),
        "plugins.toml",
        r#"schema = 1

[[plugin]]
id = "health"
crate = "fixture-health"
kind = "plugin"
feature = "plugin-health"
default_enabled = true
stability = "stable"
description = "Fixture health capability."

[[plugin]]
id = "feedback"
crate = "fixture-feedback"
path = "plugins/feedback"
kind = "plugin"
feature = "plugin-feedback"
default_enabled = false
stability = "stable"
description = "Fixture feedback capability."
"#,
    );
    write(
        fixture.path(),
        "plugins/feedback/openapi.yaml",
        r"openapi: 3.1.0
info:
  title: Fixture Feedback API
  version: 1.0.0
paths:
  /feedback:
    post:
      operationId: createFeedback
      responses:
        '201':
          description: created
",
    );
    write(fixture.path(), "quality.toml", "schema = 1\n");
    write(
        fixture.path(),
        "verification/static-validation.json",
        r#"{"schema_version":1,"status":"ok","errors":0,"warnings":0}"#,
    );
    for directory in ["config", "domain", "application", "api", "plugins/health"] {
        fs::create_dir_all(fixture.path().join(directory)).expect("fixture directory");
    }
    fixture
}

#[test]
fn source_digest_and_source_lane_cover_every_file_that_was_read() {
    let fixture = fixture();
    let canonical = fixture.path().canonicalize().expect("canonical fixture");

    let view = load_project_view(&canonical).expect("bounded project view");
    let mut canonical_provenance = String::new();
    for source in &view.provenance {
        writeln!(
            &mut canonical_provenance,
            "{}\0{}",
            source.path.display(),
            source.sha256
        )
        .expect("writing to a String is infallible");
    }
    let expected_digest = hex::encode(Sha256::digest(canonical_provenance));

    assert_eq!(view.project.source_digest, expected_digest);
    assert_eq!(
        view.evidence[&EvidenceLane::Source].len(),
        view.provenance.len()
    );
}

#[test]
fn includes_plugin_owned_operations_from_only_their_declared_contracts() {
    let fixture = fixture();
    let canonical = fixture.path().canonicalize().expect("canonical fixture");

    let view = load_project_view(&canonical).expect("bounded project view");

    assert!(view.operation("createFeedback").is_some());
    assert_eq!(view.feedback.operation_ids, ["createFeedback"]);
    assert!(
        view.provenance
            .iter()
            .any(|source| { source.path == Path::new("plugins/feedback/openapi.yaml") })
    );
}

#[test]
fn applies_the_text_limit_to_configuration_projection_with_a_diagnostic() {
    let fixture = fixture();
    let canonical = fixture.path().canonicalize().expect("canonical fixture");
    let limits = ViewLimits {
        max_text_bytes: 8,
        ..ViewLimits::default()
    };

    let view = load_project_view_with_limits(&canonical, limits).expect("bounded project view");

    assert!(
        view.configuration
            .iter()
            .all(|field| field.description.len() <= limits.max_text_bytes)
    );
    assert!(
        view.diagnostics
            .iter()
            .any(|diagnostic| diagnostic.code == "PROJECT_VIEW_TEXT_TRUNCATED")
    );
}

#[test]
fn projects_raw_repository_state_without_merging_evidence_lanes() {
    let fixture = fixture();
    let canonical = fixture.path().canonicalize().expect("canonical fixture");

    let view = load_project_view(&canonical).expect("bounded project view");

    assert_eq!(view.schema_version, 1);
    assert_eq!(view.project.name, "fixture");
    let registry_plugin = view
        .nodes
        .iter()
        .find(|node| node.id == "feature:health")
        .expect("registry plugin node");
    assert_eq!(registry_plugin.properties["path"], serde_json::Value::Null);
    let workspace_plugin = view
        .nodes
        .iter()
        .find(|node| node.id == "feature:feedback")
        .expect("workspace plugin node");
    assert_eq!(
        workspace_plugin.properties["path"],
        serde_json::json!("plugins/feedback")
    );
    let task = view
        .nodes
        .iter()
        .find(|node| node.id == "task:M1-T01")
        .expect("task node");
    assert_eq!(task.raw_status.as_deref(), Some("ready"));
    assert_eq!(task.semantic_status, Some(SemanticStatus::NotStarted));
    assert!(view.evidence.contains_key(&EvidenceLane::Source));
    assert!(view.evidence.contains_key(&EvidenceLane::LocalVerification));
    assert!(
        view.evidence
            .contains_key(&EvidenceLane::HostedVerification)
    );
    assert!(view.evidence.contains_key(&EvidenceLane::Deployment));
    assert!(view.evidence.contains_key(&EvidenceLane::Runtime));
    assert!(view.evidence.contains_key(&EvidenceLane::Review));
    assert!(view.summary.derived);
    assert_eq!(view.summary.ready_task_ids, ["M1-T01"]);
    assert!(
        view.provenance
            .iter()
            .all(|source| !source.path.is_absolute())
    );
}

#[cfg(unix)]
#[test]
fn rejects_a_dangling_symlink_at_an_optional_evidence_boundary() {
    let fixture = fixture();
    let evidence = fixture.path().join("verification/static-validation.json");
    fs::remove_file(&evidence).expect("remove evidence fixture");
    std::os::unix::fs::symlink(fixture.path().join("missing-outside-file"), &evidence)
        .expect("dangling evidence symlink");
    let canonical = fixture.path().canonicalize().expect("canonical fixture");

    let error = load_project_view(&canonical).expect_err("dangling symlink must fail closed");

    assert!(matches!(
        error,
        minco_project_view::ProjectViewError::SymbolicLink(_)
    ));
}

#[test]
fn directory_scans_are_bounded_even_when_entries_do_not_match_the_requested_extension() {
    let fixture = fixture();
    for index in 0..32 {
        write(
            fixture.path(),
            &format!("tasks/M1/ignored-{index:02}.txt"),
            "ignored",
        );
    }
    let canonical = fixture.path().canonicalize().expect("canonical fixture");
    let limits = ViewLimits {
        max_files: 20,
        ..ViewLimits::default()
    };

    let error = load_project_view_with_limits(&canonical, limits)
        .expect_err("directory entry budget must be enforced");

    assert!(matches!(
        error,
        minco_project_view::ProjectViewError::LimitExceeded {
            limit_name: "max_files",
            ..
        }
    ));
}

#[test]
fn secret_defaults_are_redacted_from_the_serialized_view() {
    let fixture = fixture();
    let manifest_path = fixture.path().join("minco.toml");
    let manifest = fs::read_to_string(&manifest_path).expect("read fixture manifest");
    fs::write(
        &manifest_path,
        manifest.replace(
            "[architecture]",
            r#"[[configuration.fields]]
key = "provider.token"
kind = "string"
required = true
secret = true
description = "Provider token"
default = "must-never-appear"

[architecture]"#,
        ),
    )
    .expect("write secret fixture");
    let canonical = fixture.path().canonicalize().expect("canonical fixture");

    let view = load_project_view(&canonical).expect("bounded project view");
    let serialized = serde_json::to_string(&view).expect("serialize ProjectView");

    assert!(!serialized.contains("must-never-appear"));
    assert!(serialized.contains("provider.token"));
    assert!(serialized.contains("redacted"));
}