Skip to main content

release_kit/
embedded.rs

1//! The compile-time payload: everything the binary serves or lands.
2//!
3//! `include_dir!` embeds each authored root at compile time, so the binary
4//! and the canon it carries cannot drift. Which roots exist is declared
5//! once, in [`crate::payload_roots`], read here, by `build.rs` for change
6//! tracking, and by the packaging test; a test below holds this module to
7//! that inventory.
8
9use include_dir::{Dir, include_dir};
10
11pub use crate::payload_roots::PAYLOAD_ROOTS;
12
13/// The technology-agnostic method chapters.
14pub static METHOD: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/method");
15
16/// The per-technology bindings.
17pub static BINDINGS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/bindings");
18
19/// The human-facing runbooks `rk guide` renders.
20pub static RUNBOOKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/runbooks");
21
22/// The per-forge documents answering the fifth axis.
23pub static FORGES: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/forges");
24
25/// The setup scripts, one subtree per forge, executed by `rk setup` and
26/// landed nowhere.
27pub static SETUP: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/setup");
28
29/// The deterministic files `rk init` lands, one subtree per technology,
30/// laid out exactly as they land in a target repository.
31pub static SNIPPETS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/snippets");
32
33/// The whole texts the binary writes outside `snippets/`.
34///
35/// The spliced blocks and the host-side hook body, authored as files so
36/// no human-faced artifact lives as a source literal; the readers in
37/// `src/projection.rs` and `src/setup/branch_reminder.rs` embed each file
38/// by name.
39pub static BLOCKS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/blocks");
40
41/// The agent skills, one directory per skill.
42pub static SKILLS: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skills");
43
44/// The artifacts every skill shares, installed once outside the skill roots.
45pub static SKILL_SHARED: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/skill-shared");
46
47/// The pinned-tool registry.
48pub static VERSIONS: &str = include_str!("../versions.toml");
49
50/// What this release's bundle needs beyond the payload schema.
51pub static COMPATIBILITY: &str = include_str!("../compatibility.toml");
52
53/// One file per release that needs an operator step, filtered to a
54/// target by the planner.
55pub static GUIDANCE: Dir<'static> = include_dir!("$CARGO_MANIFEST_DIR/guidance");
56
57/// The release history, carried whole for the stage's reference tree.
58///
59/// Generated by release-plz, so a stage can hand an agent the changelog of
60/// the exact binary that staged it. Like the licenses it sits beside the
61/// payload inventory rather than in it: history is crate metadata, not
62/// authored payload, and it names the repository that publishes it.
63pub static CHANGELOG: &str = include_str!("../CHANGELOG.md");
64
65/// The root license statement naming both halves.
66pub static LICENSE: &str = include_str!("../LICENSE");
67
68/// The MIT text covering the distribution.
69pub static LICENSE_MIT: &str = include_str!("../LICENSE-MIT");
70
71/// The CC BY 4.0 text covering the method.
72pub static LICENSE_CC_BY: &str = include_str!("../LICENSE-CC-BY-4.0");
73
74/// The sentinel marker a landed file may carry; `rk init --apply` reports
75/// every line holding one so nothing lands half-configured silently.
76pub const SENTINEL: &str = "TODO(release-kit)";
77
78/// Collect every file under `dir`, depth-first, as `(path, contents)` with
79/// the path relative to the embedded root, sorted by path.
80pub(crate) fn walk<'a>(dir: &Dir<'a>) -> Vec<(String, &'a [u8])> {
81    let mut out = Vec::new();
82    for file in dir.files() {
83        out.push((file.path().to_string_lossy().into_owned(), file.contents()));
84    }
85    for sub in dir.dirs() {
86        out.extend(walk(sub));
87    }
88    out.sort_by(|a, b| a.0.cmp(&b.0));
89    out
90}
91
92/// The files one payload root carries, as `(path, bytes)` with the path
93/// carrying the root as its first segment, or `None` for a name the
94/// inventory does not declare.
95#[must_use]
96pub fn root_files(root: &str) -> Option<Vec<(String, &'static [u8])>> {
97    let dir = match root {
98        "method" => &METHOD,
99        "bindings" => &BINDINGS,
100        "runbooks" => &RUNBOOKS,
101        "forges" => &FORGES,
102        "snippets" => &SNIPPETS,
103        "blocks" => &BLOCKS,
104        "setup" => &SETUP,
105        "skills" => &SKILLS,
106        "skill-shared" => &SKILL_SHARED,
107        "guidance" => &GUIDANCE,
108        "versions.toml" => return Some(vec![(root.to_owned(), VERSIONS.as_bytes())]),
109        "compatibility.toml" => return Some(vec![(root.to_owned(), COMPATIBILITY.as_bytes())]),
110        _ => return None,
111    };
112    Some(
113        walk(dir)
114            .into_iter()
115            .map(|(path, bytes)| (format!("{root}/{path}"), bytes))
116            .collect(),
117    )
118}
119
120/// Every artifact the payload carries, root by root in inventory order,
121/// sorted by path within each root.
122///
123/// The license files are deliberately absent: they are crate metadata the
124/// registry requires, not authored payload, and `rk license` serves them.
125#[must_use]
126pub fn artifacts() -> Vec<(String, &'static [u8])> {
127    PAYLOAD_ROOTS
128        .iter()
129        .filter_map(|root| root_files(root))
130        .flatten()
131        .collect()
132}
133
134#[cfg(test)]
135mod tests {
136    use super::{PAYLOAD_ROOTS, artifacts, root_files};
137
138    /// The inventory and this module must name the same roots: a root
139    /// embedded here but absent from the inventory would be served without
140    /// change tracking, and a development build would then carry stale
141    /// bytes; a root declared but not embedded is a name `rk payload`
142    /// would report and nothing would serve.
143    #[test]
144    fn the_inventory_and_the_embed_declare_the_same_roots() {
145        let source = include_str!("embedded.rs");
146        let mut embedded: Vec<String> = source
147            .lines()
148            .filter_map(|line| {
149                let (_, rest) = line.split_once("include_dir!(\"$CARGO_MANIFEST_DIR/")?;
150                let (root, _) = rest.split_once('"')?;
151                Some(root.to_owned())
152            })
153            .collect();
154        embedded.extend(source.lines().filter_map(|line| {
155            let (_, rest) = line.split_once("include_str!(\"../")?;
156            let (name, _) = rest.split_once('"')?;
157            (!name.starts_with("LICENSE") && name != "CHANGELOG.md").then(|| name.to_owned())
158        }));
159        embedded.sort();
160        let mut declared: Vec<String> = PAYLOAD_ROOTS.iter().map(ToString::to_string).collect();
161        declared.sort();
162        assert_eq!(
163            embedded, declared,
164            "src/embedded.rs and src/payload_roots.rs disagree on the payload roots"
165        );
166    }
167
168    #[test]
169    fn every_declared_root_serves_at_least_one_file() {
170        for root in PAYLOAD_ROOTS {
171            let files = root_files(root).expect("a declared root resolves");
172            assert!(!files.is_empty(), "{root}: the root carries no file");
173            for (path, _) in &files {
174                assert!(
175                    path == root || path.starts_with(&format!("{root}/")),
176                    "{path}: an artifact path must carry its root"
177                );
178            }
179        }
180        assert!(root_files("no-such-root").is_none());
181    }
182
183    /// Every authored block ends in exactly one newline — the one the
184    /// repository's hooks enforce and the readers strip — so the bytes a
185    /// reader composes are identical to what the authored file holds
186    /// above that newline, and no landed target reads as drift.
187    #[test]
188    fn every_block_is_authored_with_one_final_newline() {
189        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("blocks");
190        for file in super::BLOCKS.files() {
191            let name = file.path().to_string_lossy().into_owned();
192            let disk = std::fs::read(root.join(&name)).expect("an embedded block exists on disk");
193            assert_eq!(disk, file.contents(), "{name}: embed and disk disagree");
194            let text = std::str::from_utf8(file.contents()).expect("a block is UTF-8");
195            assert!(text.ends_with('\n'), "{name}: a block ends in a newline");
196            assert!(
197                !text.ends_with("\n\n"),
198                "{name}: a block ends in exactly one newline"
199            );
200        }
201    }
202
203    /// No whole human-faced artifact lives as a Rust literal: every text
204    /// the binary writes into a target or host is authored under
205    /// `blocks/`, per `distribution:a-human-faced-artifact-is-authored-text`.
206    /// Two nets, both over production code only — everything above a
207    /// file's first `#[cfg(test)]`: a structural one that fails any
208    /// string literal spanning three or more source lines, whatever its
209    /// name, because a whole artifact body is multi-line and a message is
210    /// not; and a needle list holding the retired const names out and
211    /// pinning the one-line artifact signatures the structural net cannot
212    /// tell from a message.
213    #[test]
214    fn no_artifact_body_lives_as_a_source_literal() {
215        let needles = [
216            "## Releases",
217            "Installed by rk setup step branch-reminder",
218            "This project works in worktrees:",
219            "Branches are worked in the main checkout",
220            "stages: [commit-msg]",
221            "ROUTING_BLOCK",
222            "ROUTING_WORKTREE_LINE",
223            "ROUTING_BRANCHES_LINE",
224            "HOOKS_BLOCK",
225            "WORKTREE_GUARD_ENTRY",
226            "HOOK_BODY",
227            "use flake",
228            "rk self-depend sync --apply",
229            "release-kit.packages.",
230            "inputs.nixpkgs.follows",
231        ];
232        let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
233        let mut offenders = Vec::new();
234        scan(&src, &needles, &mut offenders);
235        assert!(
236            offenders.is_empty(),
237            "an artifact body belongs under blocks/, not in the sources: {offenders:?}"
238        );
239    }
240
241    fn scan(dir: &std::path::Path, needles: &[&str], offenders: &mut Vec<String>) {
242        for entry in std::fs::read_dir(dir).expect("the source tree is readable") {
243            let entry = entry.expect("a directory entry is readable");
244            let path = entry.path();
245            if path.is_dir() {
246                scan(&path, needles, offenders);
247                continue;
248            }
249            if path.extension().is_none_or(|ext| ext != "rs") {
250                continue;
251            }
252            let text = std::fs::read_to_string(&path).expect("a source file is UTF-8");
253            let production = text.split("#[cfg(test)]").next().unwrap_or("");
254            for (index, line) in production.lines().enumerate() {
255                if line.trim_start().starts_with("//") {
256                    continue;
257                }
258                for needle in needles {
259                    if line.contains(needle) {
260                        offenders.push(format!("{}:{}: {needle}", path.display(), index + 1));
261                    }
262                }
263            }
264            for (line, span) in multiline_literals(production) {
265                offenders.push(format!(
266                    "{}:{line}: a string literal spanning {span} lines",
267                    path.display()
268                ));
269            }
270        }
271    }
272
273    /// The interpolation glues the decoded-break net exempts, by their
274    /// exact source text: the two splice compositions in
275    /// `src/projection.rs` and the header block in `src/setup/app_jwt.rs`.
276    /// Growing this list is a reviewed act; a whole artifact body never
277    /// belongs on it.
278    const GLUE: [&str; 3] = [
279        "{}\\n\\n{block}\\n",
280        "{HOOK_TYPES_LINE}\\n\\nrepos:\\n{block}\\n",
281        concat!(
282            "Authorization: Bearer {jwt}\\nAccept: application/vnd.github+json\\n",
283            "X-GitHub-Api-Version: 2022-11-28\\n"
284        ),
285    ];
286
287    /// Every string literal in `text` whose decoded value spans three or
288    /// more lines, as `(starting line, decoded line count)`. A hand
289    /// scanner over the token stream: line comments are skipped, raw
290    /// literals end at their matching quote-and-hashes delimiter however
291    /// many hashes open them, and quoted literals honor backslash
292    /// escapes, so an artifact written on one source line as `\n`
293    /// escapes counts by what it decodes to, not by how it is typed.
294    fn multiline_literals(text: &str) -> Vec<(usize, usize)> {
295        let bytes = text.as_bytes();
296        let mut spans = Vec::new();
297        let mut line = 1;
298        let mut i = 0;
299        while i < bytes.len() {
300            match bytes[i] {
301                b'\n' => {
302                    line += 1;
303                    i += 1;
304                }
305                b'/' if bytes.get(i + 1) == Some(&b'/') => {
306                    while i < bytes.len() && bytes[i] != b'\n' {
307                        i += 1;
308                    }
309                }
310                b'r' if matches!(bytes.get(i + 1), Some(&b'#' | &b'"')) => {
311                    let hashes = bytes[i + 1..]
312                        .iter()
313                        .take_while(|byte| **byte == b'#')
314                        .count();
315                    if bytes.get(i + 1 + hashes) != Some(&b'"') {
316                        i += 1;
317                        continue;
318                    }
319                    let body = i + hashes + 2;
320                    let close = format!("\"{}", "#".repeat(hashes));
321                    let end = text[body..]
322                        .find(&close)
323                        .map_or(bytes.len(), |at| body + at);
324                    let physical = text[i..end].matches('\n').count();
325                    if physical >= 2 {
326                        spans.push((line, physical + 1));
327                    }
328                    line += physical;
329                    i = (end + close.len()).min(bytes.len());
330                }
331                b'"' => {
332                    let mut j = i + 1;
333                    while j < bytes.len() && bytes[j] != b'"' {
334                        j += if bytes[j] == b'\\' { 2 } else { 1 };
335                    }
336                    let segment = &text[i + 1..j.min(bytes.len())];
337                    let physical = segment.matches('\n').count();
338                    let decoded = physical + segment.matches("\\n").count();
339                    // A literal spanning source lines is judged whole. A
340                    // one-source-line literal is judged by its decoded
341                    // breaks, with the few known interpolation glues
342                    // allowlisted by their exact source text: a whole
343                    // artifact is static authored text, and anything new
344                    // that decodes to three lines answers here.
345                    if physical >= 2 || (decoded >= 2 && !GLUE.contains(&segment)) {
346                        spans.push((line, decoded + 1));
347                    }
348                    line += physical;
349                    i = j + 1;
350                }
351                _ => i += 1,
352            }
353        }
354        spans
355    }
356
357    #[test]
358    fn the_artifact_list_is_stable_and_complete() {
359        let listed = artifacts();
360        let total: usize = PAYLOAD_ROOTS
361            .iter()
362            .map(|root| root_files(root).expect("a declared root resolves").len())
363            .sum();
364        assert_eq!(listed.len(), total);
365        assert!(
366            listed.iter().any(|(path, _)| path == "versions.toml"),
367            "the single-file root must appear as itself"
368        );
369    }
370}