Skip to main content

git_policy_cli/
policy_registry.rs

1//! What:
2//!  The fixed registry of policies compiled into the native wrapper.
3//! Why:
4//!  Repository JSONC can only tune shipped policies;
5//!  it can never name code to run.
6//!
7//! In TS you'd write (pseudocode):
8//! ```ts
9//! // const POLICY_REGISTRY = [{ name: 'require-root', defaultSeverity: 'error', ... }] as const;
10//! ```
11
12/// What:
13///  `enum Severity` is a closed set of three named values.
14///       `#[derive(...)]` asks the compiler to generate copying,
15///  printing and `==`.
16/// Why:
17///   A closed set makes an unhandled severity a compile error,
18///  unlike a free string.
19///
20/// In TS you'd write (pseudocode):
21/// ```ts
22/// type Severity = 'off' | 'warn' | 'error';
23/// ```
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum Severity {
26    /// The policy does not run.
27    Off,
28    /// Findings are reported without blocking the Git command.
29    Warn,
30    /// Findings block the Git command.
31    Error,
32}
33
34/// What:
35///  One variant per shipped policy;
36///  the variant is the policy's identity in Rust.
37/// Why:
38///   Typed identities let options and settings be looked up without string comparison.
39///
40/// In TS you'd write (pseudocode):
41/// ```ts
42/// type PolicyId = 'require-root' | 'linked-worktree-only' | ... | 'security/forbidden-strings';
43/// ```
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum PolicyId {
46    /// `require-root`:
47    ///  repository-root working directory requirement.
48    RequireRoot,
49    /// `linked-worktree-only`:
50    ///  destructive commands stay in linked worktrees.
51    LinkedWorktreeOnly,
52    /// `branch-worktree-only`:
53    ///  branch creation goes through `git worktree add`.
54    BranchWorktreeOnly,
55    /// `add-explicit`:
56    ///  bulk staging patterns are rejected.
57    AddExplicit,
58    /// `final-newline`:
59    ///  text candidates end with one line feed.
60    FinalNewline,
61    /// `markdown/autofix`:
62    ///  commit-time Markdown normalization.
63    MarkdownAutofix,
64    /// `mono/forbidden-root-context`:
65    ///  no root `CONTEXT.md` enters a commit.
66    ForbiddenRootContext,
67    /// `mono/dependent-version-bump`:
68    ///  dependent package versions follow a bumped package.
69    DependentVersionBump,
70    /// `security/forbidden-strings`:
71    ///  candidate bytes are scanned by the bundled scanner.
72    ForbiddenStrings,
73}
74
75/// What:
76///  `struct PolicyDescriptor` is one registry row.
77///       `&'static str` is a borrowed string baked into the executable for its whole run
78///       (`'static` is that "lives forever" lifetime).
79///  Sibling:
80///  `String`,
81///  an owned copy.
82///       `bool` is `true`/`false`,
83///  exactly TS `boolean`.
84/// Why:
85///   Registry text is compiled in and never changes,
86///  so borrowing forever is free,
87///       while `String` would allocate at startup for no gain.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// type PolicyDescriptor = { id: PolicyId; name: string; defaultSeverity: Severity;
92///   warnSafe: boolean; acceptsOptions: boolean; offUnlessListed: boolean };
93/// ```
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub struct PolicyDescriptor {
96    /// Typed identity used by Rust callers.
97    pub id: PolicyId,
98    /// Stable configuration key and JSONL `policyId`.
99    pub name: &'static str,
100    /// The incumbent policy definition's `defaultSeverity`.
101    ///  A built-in policy the
102    /// repository configuration does not name runs at this severity.
103    pub default_severity: Severity,
104    /// Whether `warn` keeps the policy's protection (an unsafe `warn` earns a warning event).
105    pub warn_safe: bool,
106    /// Whether the `["severity", { ... }]` form is accepted for this policy.
107    pub accepts_options: bool,
108    /// Whether the policy runs only where `cli-git.config.jsonc` names it.
109    ///  The four
110    /// policies that plugins used to provide are off everywhere else,
111    ///  with or without
112    /// a configuration file (decided 2026-10-05).
113    pub off_unless_listed: bool,
114}
115
116/// What:
117///  `pub const POLICY_REGISTRY: &[PolicyDescriptor]` is a compiled-in,
118///  read-only list.
119///       `&[T]` is a borrowed view of an array;
120///  siblings are `Vec<T>` (owned,
121///  growable)
122///       and `[T; N]` (fixed length in the type).
123/// Why:
124///   Execution and configuration order must be identical on every run;
125///  a borrowed
126///       constant needs no allocation and cannot be reordered at runtime.
127///       Every default is the incumbent policy definition's own `defaultSeverity`.
128///       The four formerly plugin-provided policies are additionally off unless the
129///       repository configuration names them,
130///  because the incumbent only ran them in
131///       repositories whose configuration registered their plugin.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// export const POLICY_REGISTRY: readonly PolicyDescriptor[] = [ ... ];
136/// ```
137pub const POLICY_REGISTRY: &[PolicyDescriptor] = &[
138    PolicyDescriptor {
139        id: PolicyId::RequireRoot,
140        name: "require-root",
141        default_severity: Severity::Error,
142        warn_safe: false,
143        accepts_options: false,
144        off_unless_listed: false,
145    },
146    PolicyDescriptor {
147        id: PolicyId::LinkedWorktreeOnly,
148        name: "linked-worktree-only",
149        default_severity: Severity::Error,
150        warn_safe: false,
151        accepts_options: false,
152        off_unless_listed: false,
153    },
154    PolicyDescriptor {
155        id: PolicyId::BranchWorktreeOnly,
156        name: "branch-worktree-only",
157        default_severity: Severity::Error,
158        warn_safe: true,
159        accepts_options: false,
160        off_unless_listed: false,
161    },
162    PolicyDescriptor {
163        id: PolicyId::AddExplicit,
164        name: "add-explicit",
165        default_severity: Severity::Error,
166        warn_safe: false,
167        accepts_options: false,
168        off_unless_listed: false,
169    },
170    PolicyDescriptor {
171        id: PolicyId::FinalNewline,
172        name: "final-newline",
173        default_severity: Severity::Warn,
174        warn_safe: true,
175        accepts_options: false,
176        off_unless_listed: false,
177    },
178    PolicyDescriptor {
179        id: PolicyId::MarkdownAutofix,
180        name: "markdown/autofix",
181        default_severity: Severity::Warn,
182        warn_safe: true,
183        accepts_options: true,
184        off_unless_listed: true,
185    },
186    PolicyDescriptor {
187        id: PolicyId::ForbiddenRootContext,
188        name: "mono/forbidden-root-context",
189        default_severity: Severity::Error,
190        warn_safe: true,
191        accepts_options: false,
192        off_unless_listed: true,
193    },
194    PolicyDescriptor {
195        id: PolicyId::DependentVersionBump,
196        name: "mono/dependent-version-bump",
197        default_severity: Severity::Error,
198        warn_safe: false,
199        accepts_options: false,
200        off_unless_listed: true,
201    },
202    PolicyDescriptor {
203        id: PolicyId::ForbiddenStrings,
204        name: "security/forbidden-strings",
205        default_severity: Severity::Error,
206        warn_safe: false,
207        accepts_options: true,
208        off_unless_listed: true,
209    },
210];
211
212/// What:
213///  Find a registry row by its configuration key.
214///       `Option<T>` is Rust's "value or nothing":
215///  `Some(row)` or `None`.
216///       `&'static PolicyDescriptor` borrows a row that lives as long as the program.
217/// Why:
218///   An unknown policy ID must be reported,
219///  never treated as a new policy.
220///
221/// In TS you'd write (pseudocode):
222/// ```ts
223/// function policyByName(name: string): PolicyDescriptor | undefined;
224/// ```
225pub fn policy_by_name(name: &str) -> Option<&'static PolicyDescriptor> {
226    // What: `usize` is the index type of every Rust list (siblings `u32`, `i64`);
227    //       `&POLICY_REGISTRY[index]` borrows one row without copying it.
228    // Why:  Nine rows make a linear scan cheaper than building a map, and an indexed
229    //       `while` loop needs no callback to stop at the first match.
230    //
231    // In TS you'd write (pseudocode):
232    // ```ts
233    // for (let index = 0; index < POLICY_REGISTRY.length; index += 1) { ... }
234    // ```
235    let mut index: usize = 0;
236    while index < POLICY_REGISTRY.len() {
237        let descriptor: &PolicyDescriptor = &POLICY_REGISTRY[index];
238        index += 1;
239        if descriptor.name == name {
240            // What: `Some(descriptor)` wraps the found row in the "present" variant.
241            // Why:  The caller must handle absence explicitly; there is no `undefined`.
242            //
243            // In TS you'd write (pseudocode):
244            // ```ts
245            // return descriptor;
246            // ```
247            return Some(descriptor);
248        }
249    }
250    // `None` is the "absent" variant: no shipped policy has this name.
251    return None;
252}
253
254/// What:
255///  Find the registry row for a typed identity.
256/// Why:
257///   Callers holding a `PolicyId` need its name,
258///  default and warn-safety.
259///
260/// In TS you'd write (pseudocode):
261/// ```ts
262/// function policyDescriptor(id: PolicyId): PolicyDescriptor;
263/// ```
264pub fn policy_descriptor(id: PolicyId) -> &'static PolicyDescriptor {
265    // `for descriptor in POLICY_REGISTRY` borrows each row in order; no index is needed
266    // because the row itself is returned, not wrapped in `Some(...)`.
267    for descriptor in POLICY_REGISTRY {
268        if descriptor.id == id {
269            return descriptor;
270        }
271    }
272    // What: `unreachable!` stops the program with this message if ever executed.
273    //       The `!` marks a macro (compiler-expanded code), not negation.
274    // Why:  Every `PolicyId` variant has a row; the registry test proves it, so
275    //       reaching here means the registry itself was edited incorrectly.
276    //
277    // In TS you'd write (pseudocode):
278    // ```ts
279    // throw new Error('unreachable: every PolicyId has a registry row');
280    // ```
281    unreachable!("every PolicyId variant has a POLICY_REGISTRY row");
282}
283
284/// What:
285///  Translate a configuration word into a typed severity.
286/// Why:
287///   Only the three documented spellings are settings;
288///  anything else is an error
289///       the caller reports with the offending key.
290///
291/// In TS you'd write (pseudocode):
292/// ```ts
293/// function severityFromName(name: string): Severity | undefined;
294/// ```
295pub fn severity_from_name(name: &str) -> Option<Severity> {
296    if name == "off" {
297        return Some(Severity::Off);
298    }
299    if name == "warn" {
300        return Some(Severity::Warn);
301    }
302    if name == "error" {
303        return Some(Severity::Error);
304    }
305    return None;
306}
307
308/// What:
309///  Translate a typed severity back to its configuration and JSONL spelling.
310/// Why:
311///   Events and diagnostics print the same words the configuration accepts.
312///
313/// In TS you'd write (pseudocode):
314/// ```ts
315/// function severityName(severity: Severity): string;
316/// ```
317pub fn severity_name(severity: Severity) -> &'static str {
318    if severity == Severity::Off {
319        return "off";
320    }
321    if severity == Severity::Warn {
322        return "warn";
323    }
324    return "error";
325}
326
327/// What:
328///  `#[cfg(test)]` compiles the next item only for `cargo test`;
329///       `#[path = "..."]` names the sibling file holding the module body.
330/// Why:
331///   Registry controls stay out of the release executable.
332///
333/// In TS you'd write (pseudocode):
334/// ```ts
335/// // policy_registry.unit.test.ts lives beside this file.
336/// ```
337#[cfg(test)]
338#[path = "policy_registry_tests.rs"]
339mod tests;