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;