Skip to main content

git_policy_cli/
policy_events.rs

1//! What:
2//!  The events one policy pass produces,
3//!  and their JSON Lines rendering.
4//! Why:
5//!  Agents and scripts read these lines from wrapper standard error and from
6//!      `git cli-git check|fix` standard output.
7//!  Each event is one compact JSON object with
8//!      the incumbent's field names and field order,
9//!  so existing readers parse native
10//!      events unchanged.
11//!  Events are numbered when rendered,
12//!  so a list of events always
13//!      prints with consecutive numbers in emission order.
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! // process.stderr.write(renderPolicyEvents(result.events));
18//! ```
19
20/// What:
21///  `use` brings names from sibling files into this file;
22///  `super::` means "the parent
23///       module",
24///  where every sibling file of this crate is declared.
25/// Why:
26///   JSON string escaping,
27///  the failure-code spellings and the schema version already
28///       live in `diagnostics.rs`;
29///  policy and severity names live in the registry.
30///
31/// In TS you'd write (pseudocode):
32/// ```ts
33/// import { jsonString, SCHEMA_VERSION } from './diagnostics.ts';
34/// ```
35use super::diagnostics::{
36    EngineFailureCode, SCHEMA_VERSION, engine_failure_code_name, json_string,
37};
38/// A pathname as events report it,
39///  and the encoding of its exact bytes.
40use super::event_hints::{
41    POST_COMMIT_BLOCKED_HINT, REPLAY_HEADERS_DROPPED_HINT, WARN_UNSAFE_HINT, engine_failure_hint, finding_hint,
42    push_hint_field,
43};
44use super::event_path::{EventPath, base64_standard};
45use super::policy_registry::{PolicyId, Severity, policy_descriptor, severity_name};
46use super::policy_trigger::{Trigger, trigger_name};
47
48/// The code of the warning for a `warn` severity on a policy whose protection `warn` removes.
49pub const WARN_UNSAFE_CODE: &str = "warn-unsafe";
50
51/// What:
52///  A byte range inside a candidate.
53///  A `struct` is a record with named fields;
54///  `u64`
55///       is an unsigned 64-bit integer (siblings `u32`,
56///  `usize`).
57/// Why:
58///   Content policies point at the exact bytes of a finding.
59///  `u64` holds any file
60///       offset on every platform,
61///  where `usize` would be 32 bits on a 32-bit system.
62///
63/// In TS you'd write (pseudocode):
64/// ```ts
65/// type FindingLocation = { byteStart: number; byteEnd: number };
66/// ```
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
68pub struct FindingLocation {
69    /// Offset of the first byte.
70    pub byte_start: u64,
71    /// Offset one past the last byte.
72    pub byte_end: u64,
73}
74
75/// What:
76///  One policy finding as it is reported.
77///  `String` is owned UTF-8 text (sibling `&str`
78///       borrows);
79///  `Option<T>` is "a value or nothing";
80///  `&'static str` is text baked into
81///       the program.
82/// Why:
83///   The record owns its message and path because events outlive the check that made
84///       them.
85///  The code is compiled-in text,
86///  so a shipped policy cannot emit an empty or
87///       misspelled code at run time.
88///
89/// In TS you'd write (pseudocode):
90/// ```ts
91/// type FindingEvent = { trigger; policyId; severity: 'warn' | 'error'; code; message; path?; pathBytes?; location?; fix: 'none' | 'available' };
92/// ```
93#[derive(Clone, Debug, Eq, PartialEq)]
94pub struct FindingEvent {
95    /// The lifecycle point that ran the policy.
96    pub trigger: Trigger,
97    /// The policy that reported.
98    pub policy: PolicyId,
99    /// The effective severity,
100    ///  `Warn` or `Error`.
101    pub severity: Severity,
102    /// The policy-local code;
103    ///  rendering prefixes the policy name.
104    pub code: &'static str,
105    /// The explanation for the person who ran the command.
106    pub message: String,
107    /// The repository path the finding is about,
108    ///  when it has one.
109    pub path: Option<EventPath>,
110    /// The byte range the finding is about,
111    ///  when it has one.
112    pub location: Option<FindingLocation>,
113    /// Whether the engine holds a correction for this finding.
114    pub fix_available: bool,
115}
116
117/// What:
118///  Every event a policy pass can produce.
119///  An `enum` is a closed set of named
120///       alternatives;
121///  each variant carries its own fields.
122///  `Vec<EventPath>` is an owned
123///       list of pathnames in their event form.
124/// Why:
125///   One list of events is rendered in order,
126///  whatever mixture of kinds it holds.
127///
128/// In TS you'd write (pseudocode):
129/// ```ts
130/// type PolicyEvent = FindingEvent | ConfigurationWarningEvent | CoreFindingEvent | EngineFailureEvent | FixSummaryEvent;
131/// ```
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub enum PolicyEvent {
134    /// A policy reported something.
135    Finding(FindingEvent),
136    /// A policy that `warn` does not protect is configured as `warn`.
137    WarnUnsafe {
138        /// The lifecycle point that ran the policy.
139        trigger: Trigger,
140        /// The policy configured as `warn`.
141        policy: PolicyId,
142    },
143    /// A fixed transform rejected the command;
144    ///  it has no severity and cannot be configured.
145    CoreFinding {
146        /// The transform,
147        ///  for example `commit-only`.
148        core_id: &'static str,
149        /// The transform-local code;
150        ///  rendering prefixes the transform name.
151        code: &'static str,
152        /// The explanation for the person who ran the command.
153        message: String,
154    },
155    /// The pass could not complete.
156    EngineFailure {
157        /// The stable cause.
158        code: EngineFailureCode,
159        /// The explanation for the person who ran the command.
160        message: String,
161        /// The lifecycle point,
162        ///  when the failure belongs to one.
163        trigger: Option<Trigger>,
164        /// The policy that could not complete,
165        ///  when one is responsible.
166        policy: Option<PolicyId>,
167        /// The repository path responsible,
168        ///  when the failure is about one path.
169        path: Option<EventPath>,
170    },
171    /// Corrections were applied.
172    FixSummary {
173        /// The fixing lifecycle point.
174        trigger: Trigger,
175        /// How many passes changed candidate content before it settled.
176        passes: u64,
177        /// The changed paths,
178        ///  each once,
179        ///  in Git's byte order.
180        changed_paths: Vec<EventPath>,
181    },
182    /// The landed commit whose automatic backup the post-commit gate stopped.
183    ///  The commit stays local; the event names it so the user can push it deliberately.
184    CommitLanded {
185        /// The exact landed commit object ID.
186        oid: String,
187    },
188    /// A landing found the target moved (`landing-race-lost`).
189    LandingRaceLost {
190        /// This transaction's lost races so far, from 1.
191        attempt: u64,
192        /// The target value that won.
193        winning_oid: String,
194    },
195    /// A commit that lost enough races holds the next landing slot (`landing-reserved`).
196    LandingReserved {
197        /// Lost races when the reservation was granted.
198        lost_races: u64,
199    },
200    /// A prepared commit was replayed onto the moved target (`commit-replayed`).
201    CommitReplayed {
202        /// The prepared commit.
203        prepared_oid: String,
204        /// The preparation base, absent when the branch was unborn.
205        from_base: Option<String>,
206        /// The target value the replay used as parent.
207        onto: String,
208        /// The replayed commit, after revalidation.
209        oid: String,
210    },
211    /// A signed replay could not carry some custom headers (`replay-headers-dropped`).
212    ReplayHeadersDropped {
213        /// The prepared commit carrying the headers.
214        prepared_oid: String,
215        /// The re-signed commit lacking them.
216        oid: String,
217        /// The dropped header names in prepared-commit order, each once.
218        headers: Vec<String>,
219    },
220    /// The core finding `concurrent-commit/replay-conflict`, with the data a person needs to resolve it.
221    ReplayConflict {
222        /// The target ref the commit aimed at.
223        target_ref: String,
224        /// The conflicting paths in Git path byte order, as the exact bytes Git reported.
225        paths: Vec<EventPath>,
226        /// The earliest target commit touching a conflicting path.
227        winning_oid: String,
228        /// The prepared commit kept for cherry-picking.
229        prepared_oid: String,
230    },
231}
232
233/// What:
234///  Whether an event blocks the command.
235///  `&PolicyEvent` borrows the event read-only.
236/// Why:
237///   An error finding and a fixed-transform rejection keep Git from running;
238///  a
239///       warning finding and a configuration warning do not.
240///
241/// In TS you'd write (pseudocode):
242/// ```ts
243/// const blocks = event.type === 'core-finding' || (event.type === 'finding' && event.severity === 'error');
244/// ```
245pub fn event_blocks(event: &PolicyEvent) -> bool {
246    // `match` picks one arm per variant; `{ .. }` ignores the fields a variant carries.
247    match event {
248        PolicyEvent::Finding(finding) => return finding.severity == Severity::Error,
249        PolicyEvent::CoreFinding { .. } | PolicyEvent::ReplayConflict { .. } => return true,
250        PolicyEvent::WarnUnsafe { .. }
251        | PolicyEvent::EngineFailure { .. }
252        | PolicyEvent::FixSummary { .. }
253        | PolicyEvent::CommitLanded { .. }
254        | PolicyEvent::LandingRaceLost { .. }
255        | PolicyEvent::LandingReserved { .. }
256        | PolicyEvent::CommitReplayed { .. }
257        | PolicyEvent::ReplayHeadersDropped { .. } => return false,
258    }
259}
260
261/// What:
262///  The opening every event shares:
263///  schema version,
264///  sequence and type.
265/// Why:
266///   Readers branch on these three fields first,
267///  and they always come first.
268///
269/// In TS you'd write (pseudocode):
270/// ```ts
271/// const head = `{"schemaVersion":1,"sequence":${sequence},"type":${JSON.stringify(type)}`;
272/// ```
273fn event_head(sequence: u64, event_type: &str) -> String {
274    // `format!` builds owned text; `{{` is a literal brace.
275    return format!(
276        "{{\"schemaVersion\":{SCHEMA_VERSION},\"sequence\":{sequence},\"type\":{}",
277        json_string(event_type)
278    );
279}
280
281/// What:
282///  Append `,"name":"value"` to a line.
283///  `&mut String` lends the line for appending.
284/// Why:
285///   Every text field is encoded at this final step,
286///  so no value can end its JSON
287///       string or its line.
288///
289/// In TS you'd write (pseudocode):
290/// ```ts
291/// line += `,${JSON.stringify(name)}:${JSON.stringify(value)}`;
292/// ```
293fn push_text_field(line: &mut String, name: &str, value: &str) {
294    // `.as_str()` lends the owned text to `push_str`.
295    line.push_str(format!(",\"{name}\":{}", json_string(value)).as_str());
296}
297
298/// What:
299///  Append `,"path":"..."` and,
300///  for a name that is not UTF-8,
301///  `,"pathBytes":"..."`.
302///       `&EventPath` borrows the pathname's event form.
303/// Why:
304///   `path` keeps its meaning for every existing reader;
305///  `pathBytes` is the optional
306///       field schema version 1 allows (`SPEC.md`,
307///  "Unknown fields may be added only in a
308///       backward-compatible schema revision"),
309///  present only when `path` is not exact.
310///
311/// In TS you'd write (pseudocode):
312/// ```ts
313/// line.path = path.text; if (path.exact) line.pathBytes = base64(path.exact);
314/// ```
315fn push_path_field(line: &mut String, path: &EventPath) {
316    push_text_field(line, "path", path.text());
317    // `if let Some(exact) = ...` runs only for a name that kept its bytes.
318    if let Some(exact) = path.exact() {
319        push_text_field(line, "pathBytes", base64_standard(exact).as_str());
320    }
321}
322
323/// What:
324///  Render a finding's fields after the shared opening.
325/// Why:
326///   The order is the incumbent's:
327///  trigger,
328///  policy,
329///  severity,
330///  code,
331///  message,
332///  then the
333///       optional path (with its exact bytes) and location,
334///  then the fix state.
335///
336/// In TS you'd write (pseudocode):
337/// ```ts
338/// JSON.stringify({ trigger, policyId, severity, code: `${policyId}/${code}`, message, path, location, fix })
339/// ```
340fn push_finding(line: &mut String, finding: &FindingEvent) {
341    let policy_name: &str = policy_descriptor(finding.policy).name;
342    push_text_field(line, "trigger", trigger_name(finding.trigger));
343    push_text_field(line, "policyId", policy_name);
344    push_text_field(line, "severity", severity_name(finding.severity));
345    push_text_field(
346        line,
347        "code",
348        format!("{policy_name}/{}", finding.code).as_str(),
349    );
350    push_text_field(line, "message", finding.message.as_str());
351    push_hint_field(line, finding_hint(policy_name, finding.code, finding.fix_available));
352    // `if let Some(path) = &finding.path` borrows the path only when one is present.
353    if let Some(path) = &finding.path {
354        push_path_field(line, path);
355    }
356    if let Some(location) = finding.location {
357        line.push_str(
358            format!(
359                ",\"location\":{{\"byteStart\":{},\"byteEnd\":{}}}",
360                location.byte_start, location.byte_end
361            )
362            .as_str(),
363        );
364    }
365    let fix: &str = if finding.fix_available {
366        "available"
367    } else {
368        "none"
369    };
370    push_text_field(line, "fix", fix);
371}
372
373/// What:
374///  Render one JSON string per changed path:
375///  its text,
376///  or the base64 of its bytes.
377///       `fn(&EventPath) -> String` is a plain function pointer choosing what each entry holds.
378/// Why:
379///   A fix summary lists every path it changed,
380///  and its byte list lines up with that
381///       list entry for entry.
382///
383/// In TS you'd write (pseudocode):
384/// ```ts
385/// JSON.stringify(changedPaths.map(entry))
386/// ```
387fn path_array(paths: &[EventPath], entry: fn(&EventPath) -> String) -> String {
388    // `String::from` copies the borrowed text into an owned, growable line.
389    let mut array: String = String::from("[");
390    // `.iter().enumerate()` yields `(index, path)` pairs in order.
391    for (index, path) in paths.iter().enumerate() {
392        if index > 0 {
393            array.push(',');
394        }
395        array.push_str(json_string(entry(path).as_str()).as_str());
396    }
397    array.push(']');
398    return array;
399}
400
401/// What:
402///  A JSON array of strings.
403/// Why:
404///   The replay events carry lists of names in the incumbent's `JSON.stringify` form.
405///
406/// In TS you'd write (pseudocode):
407/// ```ts
408/// JSON.stringify(names);
409/// ```
410fn string_array(names: &[String]) -> String {
411    let mut array: String = String::from("[");
412    for (index, name) in names.iter().enumerate() {
413        if index > 0 {
414            array.push(',');
415        }
416        array.push_str(json_string(name.as_str()).as_str());
417    }
418    array.push(']');
419    return array;
420}
421
422/// What:
423///  The readable text of one changed path,
424///  as a `changedPaths` entry.
425/// Why:
426///   A named function for `path_array`,
427///  because the repository bans anonymous functions.
428///
429/// In TS you'd write (pseudocode):
430/// ```ts
431/// const pathText = (path: EventPath) => path.text;
432/// ```
433fn path_text(path: &EventPath) -> String {
434    return String::from(path.text());
435}
436
437/// What:
438///  The base64 of one changed path's bytes,
439///  as a `changedPathBytes` entry.
440/// Why:
441///   Names that are UTF-8 are listed too,
442///  so index `n` of both lists is the same file.
443///
444/// In TS you'd write (pseudocode):
445/// ```ts
446/// const pathBytes = (path: EventPath) => base64(path.bytes);
447/// ```
448fn path_bytes(path: &EventPath) -> String {
449    return base64_standard(path.bytes());
450}
451
452/// What:
453///  Append a list of paths as `,"<text_key>":[...]` and,
454///  when any of them is not UTF-8,
455///       `,"<bytes_key>":[...]` with one base64 entry per path.
456/// Why:
457///   An array cannot hold an absent entry without JSON `null`,
458///  which events never
459///       carry,
460///  so the byte list names every path once it is needed at all,
461///  and is
462///       absent while every name is exact.
463///
464/// In TS you'd write (pseudocode):
465/// ```ts
466/// line[textKey] = paths.map(p => p.text); if (paths.some(p => p.exact)) line[bytesKey] = paths.map(p => base64(p.bytes));
467/// ```
468fn push_path_list(line: &mut String, text_key: &str, bytes_key: &str, paths: &[EventPath]) {
469    line.push_str(format!(",\"{text_key}\":{}", path_array(paths, path_text)).as_str());
470    for path in paths {
471        if path.exact().is_some() {
472            line.push_str(format!(",\"{bytes_key}\":{}", path_array(paths, path_bytes)).as_str());
473            return;
474        }
475    }
476}
477
478/// What:
479///  The wire spelling of an event's kind.
480/// Why:
481///   The `type` field is the third field of every line and decides how a reader
482///       interprets the rest.
483///
484/// In TS you'd write (pseudocode):
485/// ```ts
486/// const type = event.type;
487/// ```
488fn event_type(event: &PolicyEvent) -> &'static str {
489    match event {
490        PolicyEvent::Finding(_) => return "finding",
491        PolicyEvent::WarnUnsafe { .. } => return "configuration-warning",
492        PolicyEvent::CoreFinding { .. } => return "core-finding",
493        PolicyEvent::EngineFailure { .. } => return "engine-failure",
494        PolicyEvent::FixSummary { .. } => return "fix-summary",
495        PolicyEvent::CommitLanded { .. } => return "commit-landed",
496        PolicyEvent::LandingRaceLost { .. } => return "landing-race-lost",
497        PolicyEvent::LandingReserved { .. } => return "landing-reserved",
498        PolicyEvent::CommitReplayed { .. } => return "commit-replayed",
499        PolicyEvent::ReplayHeadersDropped { .. } => return "replay-headers-dropped",
500        PolicyEvent::ReplayConflict { .. } => return "core-finding",
501    }
502}
503
504/// What:
505///  Render one event as a line-terminated JSON object.
506///  `&PolicyEvent` borrows the
507///       event;
508///  the result is owned text.
509/// Why:
510///   One function owns every field order,
511///  so two events of one kind can never differ
512///       in shape.
513///
514/// In TS you'd write (pseudocode):
515/// ```ts
516/// function renderPolicyEvent(sequence: number, event: PolicyEvent): string { return JSON.stringify({ ...event, sequence }) + '\n'; }
517/// ```
518pub fn render_policy_event(sequence: u64, event: &PolicyEvent) -> String {
519    // `mut` allows appending the fields of the matched variant.
520    let mut line: String = event_head(sequence, event_type(event));
521    match event {
522        // `&mut line` lends the line for appending.
523        PolicyEvent::Finding(finding) => push_finding(&mut line, finding),
524        PolicyEvent::WarnUnsafe { trigger, policy } => {
525            let policy_name: &str = policy_descriptor(*policy).name;
526            // `*trigger` copies the small value out of the borrowed event.
527            push_text_field(&mut line, "trigger", trigger_name(*trigger));
528            push_text_field(&mut line, "policyId", policy_name);
529            push_text_field(&mut line, "code", WARN_UNSAFE_CODE);
530            push_text_field(
531                &mut line,
532                "message",
533                format!("Policy {policy_name} is warn-unsafe but configured as warn.").as_str(),
534            );
535            push_hint_field(&mut line, Some(WARN_UNSAFE_HINT));
536        }
537        PolicyEvent::CoreFinding {
538            core_id,
539            code,
540            message,
541        } => {
542            push_text_field(&mut line, "trigger", trigger_name(Trigger::PreForward));
543            push_text_field(&mut line, "coreId", core_id);
544            push_text_field(&mut line, "code", format!("{core_id}/{code}").as_str());
545            push_text_field(&mut line, "message", message.as_str());
546        }
547        PolicyEvent::EngineFailure {
548            code,
549            message,
550            trigger,
551            policy,
552            path,
553        } => {
554            let code_name: &str = engine_failure_code_name(*code);
555            push_text_field(&mut line, "code", code_name);
556            push_text_field(&mut line, "message", message.as_str());
557            push_hint_field(&mut line, engine_failure_hint(code_name));
558            if let Some(found) = trigger {
559                push_text_field(&mut line, "trigger", trigger_name(*found));
560            }
561            if let Some(found) = policy {
562                push_text_field(&mut line, "policyId", policy_descriptor(*found).name);
563            }
564            if let Some(found) = path {
565                push_path_field(&mut line, found);
566            }
567        }
568        PolicyEvent::FixSummary {
569            trigger,
570            passes,
571            changed_paths,
572        } => {
573            push_text_field(&mut line, "trigger", trigger_name(*trigger));
574            line.push_str(format!(",\"passes\":{passes}").as_str());
575            push_path_list(&mut line, "changedPaths", "changedPathBytes", changed_paths.as_slice());
576        }
577        PolicyEvent::CommitLanded { oid } => {
578            push_text_field(&mut line, "oid", oid);
579            push_text_field(&mut line, "outcome", "post-commit-blocked");
580            push_text_field(
581                &mut line,
582                "message",
583                format!("Commit {oid} remains local; post-commit gate blocked automatic backup.")
584                    .as_str(),
585            );
586            push_hint_field(&mut line, Some(POST_COMMIT_BLOCKED_HINT));
587        }
588        PolicyEvent::LandingRaceLost { attempt, winning_oid } => {
589            line.push_str(format!(",\"attempt\":{attempt}").as_str());
590            push_text_field(&mut line, "winningOid", winning_oid);
591        }
592        PolicyEvent::LandingReserved { lost_races } => {
593            line.push_str(format!(",\"lostRaces\":{lost_races}").as_str());
594        }
595        PolicyEvent::CommitReplayed {
596            prepared_oid,
597            from_base,
598            onto,
599            oid,
600        } => {
601            push_text_field(&mut line, "preparedOid", prepared_oid);
602            if let Some(base) = from_base {
603                push_text_field(&mut line, "fromBase", base);
604            }
605            push_text_field(&mut line, "onto", onto);
606            push_text_field(&mut line, "oid", oid);
607        }
608        PolicyEvent::ReplayHeadersDropped {
609            prepared_oid,
610            oid,
611            headers,
612        } => {
613            push_text_field(&mut line, "preparedOid", prepared_oid);
614            push_text_field(&mut line, "oid", oid);
615            line.push_str(format!(",\"headers\":{}", string_array(headers.as_slice())).as_str());
616            let plural: &str = if headers.len() == 1 { "" } else { "s" };
617            push_text_field(
618                &mut line,
619                "message",
620                format!(
621                    "Replaying signed commit {prepared_oid} as {oid} dropped its {} header{plural}, because Git can re-sign only the headers git commit-tree writes.",
622                    headers.join(", ")
623                )
624                .as_str(),
625            );
626            push_hint_field(&mut line, Some(REPLAY_HEADERS_DROPPED_HINT));
627        }
628        PolicyEvent::ReplayConflict {
629            target_ref,
630            paths,
631            winning_oid,
632            prepared_oid,
633        } => {
634            push_text_field(&mut line, "trigger", trigger_name(Trigger::PreForward));
635            push_text_field(&mut line, "coreId", "concurrent-commit");
636            push_text_field(&mut line, "code", "concurrent-commit/replay-conflict");
637            push_text_field(
638                &mut line,
639                "message",
640                format!(
641                    "{target_ref} moved while this commit was prepared, and replaying it conflicts with {winning_oid} in {}; nothing landed. The prepared commit {prepared_oid} stays in the object store until gc expires it: resolve with git cherry-pick {prepared_oid}.",
642                    paths.iter().map(EventPath::text).collect::<Vec<&str>>().join(", ")
643                )
644                .as_str(),
645            );
646            push_path_list(&mut line, "paths", "pathsBytes", paths.as_slice());
647            push_text_field(&mut line, "winningOid", winning_oid);
648            push_text_field(&mut line, "preparedOid", prepared_oid);
649        }
650    }
651    line.push_str("}\n");
652    return line;
653}
654
655/// What:
656///  Render a list of events,
657///  numbered consecutively from `first_sequence`.
658///       `&[PolicyEvent]` borrows the list.
659/// Why:
660///   An invocation numbers its events once:
661///  events printed before the pass (a legacy
662///       configuration notice) take the first numbers and the pass continues after them.
663///
664/// In TS you'd write (pseudocode):
665/// ```ts
666/// function renderPolicyEvents(firstSequence: number, events: PolicyEvent[]): string;
667/// ```
668pub fn render_policy_events(first_sequence: u64, events: &[PolicyEvent]) -> String {
669    // `String::new()` is empty owned text.
670    let mut rendered: String = String::new();
671    for (index, event) in events.iter().enumerate() {
672        // `as u64` widens the list index to the event number type.
673        rendered.push_str(render_policy_event(first_sequence + index as u64, event).as_str());
674    }
675    return rendered;
676}
677
678/// Field-order,
679///  escaping and numbering controls stay out of the release executable.
680#[cfg(test)]
681#[path = "policy_events_tests.rs"]
682mod tests;