Skip to main content

git_policy_cli/
policy_root_context.rs

1//! What:
2//!  The optional `mono/forbidden-root-context` policy:
3//!  a `CONTEXT.md` file at the
4//!       repository's top level must not enter the index or be checked.
5//! Why:
6//!  This repository keeps no context files;
7//!  agents read the source on every probe
8//!      (`doc/agent/domain.md`).
9//!  The rule and its finding are the installed wrapper's
10//!      (`package/git-policy/repository/src/index.ts`):
11//!  only the top-level pathname
12//!      counts,
13//!  and removing the file is always allowed.
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! // if (candidates.some(c => c.path === 'CONTEXT.md' && c.change !== 'deleted')) return [finding];
18//! ```
19
20/// Import the candidate change kinds.
21use super::candidate_record::CandidateChange;
22/// Import the version type.
23use super::candidate_version::CandidateVersion;
24/// Import the event form of a pathname.
25use super::event_path::EventPath;
26/// Import the lifecycle's candidates.
27use super::policy_content::{ContentState, LifecycleContent};
28/// Import the finding and outcome types of a check.
29use super::policy_engine::{PolicyFinding, PolicyOutcome};
30/// Import the facts interface that prepares candidates.
31use super::repository_facts::RepositoryFacts;
32/// `Rc<T>` is a shared,
33///  read-only handle.
34use std::rc::Rc;
35
36/// The one pathname the policy forbids,
37///  relative to the repository's top level.
38pub const ROOT_CONTEXT_PATH: &str = "CONTEXT.md";
39
40/// The policy-local code of the finding.
41pub const ROOT_CONTEXT_CODE: &str = "root-context-forbidden";
42
43/// The message of the finding,
44///  as the installed wrapper words it.
45pub const ROOT_CONTEXT_MESSAGE: &str = "Root CONTEXT.md is forbidden; read source code directly.";
46
47/// What:
48///  Check the lifecycle's candidates for a top-level `CONTEXT.md` that is not a deletion.
49///       `<F: RepositoryFacts>` accepts any facts provider.
50/// Why:
51///   Only pathnames and change kinds are read,
52///  never content;
53///  at most one finding.
54///
55/// In TS you'd write (pseudocode):
56/// ```ts
57/// async function checkRootContext(content, lifecycle, facts): Promise<PolicyOutcome>;
58/// ```
59pub fn check_root_context<F: RepositoryFacts>(
60    content: &mut ContentState,
61    lifecycle: &LifecycleContent,
62    facts: &mut F,
63) -> PolicyOutcome {
64    let version: Rc<CandidateVersion> = match content.version(lifecycle, facts) {
65        Ok(Some(found)) => found,
66        // `Vec::new()` is the empty list: a lifecycle without candidates has nothing to report.
67        Ok(None) => return PolicyOutcome::Findings(Vec::new()),
68        Err(outcome) => return outcome,
69    };
70    // `if let Some(candidate) = ...` runs only when the version lists the pathname.
71    if let Some(candidate) = version.candidate_at_path(ROOT_CONTEXT_PATH.as_bytes())
72        && candidate.change != CandidateChange::Deleted
73    {
74        // `vec![...]` builds the one-finding list.
75        return PolicyOutcome::Findings(vec![PolicyFinding {
76            code: ROOT_CONTEXT_CODE,
77            message: String::from(ROOT_CONTEXT_MESSAGE),
78            // The fixed name is UTF-8, so it is its own exact form.
79            path: Some(EventPath::from_git_bytes(ROOT_CONTEXT_PATH.as_bytes())),
80            location: None,
81            fix_available: false,
82        }]);
83    }
84    return PolicyOutcome::Findings(Vec::new());
85}
86
87/// Check controls stay out of the release executable.
88#[cfg(test)]
89#[path = "policy_root_context_tests.rs"]
90mod tests;