Skip to main content

lean_ctx/core/
rule_artifacts.rs

1//! The committed `LEAN-CTX.md` rule artifacts as `(relative_path, content)`
2//! pairs — the single source shared by the regenerator (`gen_rules` example)
3//! and the drift gate (`tests/rules_drift.rs`) so the project copy and the
4//! `rust/` copy can never disagree.
5//!
6//! Content is forced to the default profile (non-shadow, compression `Off`) so
7//! the committed bytes are independent of the developer's local lean-ctx config
8//! and stay deterministic (#498). The live writer
9//! (`hooks::ensure_project_agents_integration`) renders with the *user's* config
10//! instead — that is each user's own copy, not this repo's checked-in artifact.
11
12use crate::core::config::CompressionLevel;
13use crate::core::rules_canonical::{self, Wrapper};
14
15/// Project-relative paths of every committed dedicated-rules artifact. Add new
16/// real rule artifacts here — not docs examples or templates.
17pub const ARTIFACT_PATHS: &[&str] = &["LEAN-CTX.md", "rust/LEAN-CTX.md"];
18
19/// Canonical body of a project `LEAN-CTX.md`: the owner banner, the long-form
20/// rules block (non-shadow, compression `Off`), and a trailing newline.
21/// Inverse of what the drift gate reads back. Longform because `LEAN-CTX.md`
22/// is opened on demand (AGENTS.md pointer), never auto-loaded — it can afford
23/// the verbose teaching sections the injected profiles fold away (#578).
24#[must_use]
25pub fn canonical_body() -> String {
26    format!(
27        "{}\n{}\n",
28        rules_canonical::PROJECT_LEAN_CTX_OWNED_MARKER,
29        rules_canonical::render(
30            false,
31            Wrapper::Longform,
32            CompressionLevel::Off,
33            &super::tool_profiles::ToolProfile::Power,
34        )
35    )
36}
37
38/// The repo-root skill (installed by `skills/lean-ctx/scripts/install.sh` and
39/// skill marketplaces) is a verbatim copy of the template `lean-ctx setup`
40/// installs, so the two can never drift apart again.
41pub const SKILL_COPY_PATH: &str = "skills/lean-ctx/SKILL.md";
42
43/// The Claude Code mod's development workspace (`claude plugin test`, `tsc`,
44/// `--plugin-dir`) mirrors the sources the binary embeds and installs
45/// (`templates/claude_mod/`), so what is tested is exactly what ships.
46pub const CLAUDE_MOD_COPIES: &[(&str, &str)] = &[
47    (
48        "integrations/claude-code-mod/hooks/register.ts",
49        crate::core::claude_host::REGISTER_TS,
50    ),
51    (
52        "integrations/claude-code-mod/hooks/hooks.json",
53        crate::core::claude_host::HOOKS_JSON,
54    ),
55    (
56        "integrations/claude-code-mod/.claude-plugin/plugin.json",
57        crate::core::claude_host::PLUGIN_JSON,
58    ),
59    (
60        "integrations/claude-code-mod/hooks/cockpit.tsx",
61        crate::core::claude_host::COCKPIT_TSX,
62    ),
63    (
64        "integrations/claude-code-mod/hooks/render.tsx",
65        crate::core::claude_host::RENDER_TSX,
66    ),
67    (
68        "integrations/claude-code-mod/types/index.d.ts",
69        crate::core::claude_host::TYPES_DTS,
70    ),
71];
72
73/// `(relative_path, content)` for every artifact the generator writes: the
74/// versioned rule artifacts (one canonical body), the skill copy, and the
75/// Claude Code mod workspace copies.
76#[must_use]
77pub fn artifacts() -> Vec<(&'static str, String)> {
78    let body = canonical_body();
79    let mut arts: Vec<(&'static str, String)> =
80        ARTIFACT_PATHS.iter().map(|p| (*p, body.clone())).collect();
81    arts.push((
82        SKILL_COPY_PATH,
83        include_str!("../templates/SKILL.md").to_string(),
84    ));
85    arts.extend(
86        CLAUDE_MOD_COPIES
87            .iter()
88            .map(|(path, content)| (*path, (*content).to_string())),
89    );
90    arts
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn canonical_body_is_owned_versioned_and_current() {
99        let body = canonical_body();
100        assert!(body.starts_with(rules_canonical::PROJECT_LEAN_CTX_OWNED_MARKER));
101        assert!(body.contains(&format!(
102            "<!-- version: {} -->",
103            rules_canonical::RULES_VERSION
104        )));
105        // The body must carry the v3 guidance it exists to ship.
106        assert!(body.contains("AGENT LOOP"));
107        assert!(body.contains("NAVIGATION PARADOX"));
108        assert!(body.ends_with('\n'));
109    }
110
111    #[test]
112    fn artifacts_cover_every_declared_path() {
113        let arts = artifacts();
114        for declared in ARTIFACT_PATHS.iter().chain([&SKILL_COPY_PATH]) {
115            assert!(
116                arts.iter().any(|(p, b)| p == declared && !b.is_empty()),
117                "{declared} missing from the generator output"
118            );
119        }
120    }
121}