Skip to main content

lean_ctx/core/
rules_canonical.rs

1//! Canonical rules source — single source of truth for all lean-ctx guidance.
2//!
3//! All content is declared as `pub const` at the top. Two profiles (FULL,
4//! COMPACT) define which sections compose each output format. Three wrappers
5//! (Dedicated, Shared, Bare) select the profile and wrapping style. One
6//! `render()` function assembles everything, including the compression-level
7//! output-style prompt (Lite / Standard / Max).
8//!
9//! ***Every*** template, injected rule file, AGENTS.md block, and MCP
10//! instructions field MUST derive its content from this module.
11
12use crate::core::config::CompressionLevel;
13
14/// Stable HTML-comment anchor that marks the start of any lean-ctx rule
15/// section.  Never changes — used for find/replace in shared files and for
16/// ownership detection in dedicated files.  The version number follows on the
17/// next line (see `render`).
18pub const START_MARK: &str = "<!-- lean-ctx-rules -->";
19
20/// Prefix shared by every lean-ctx rules marker including legacy versioned
21/// formats (`<!-- lean-ctx-rules-v9 -->`). Use for substring detection when
22/// the exact constant would miss older installs.
23pub const RULES_MARKER_PREFIX: &str = "<!-- lean-ctx-rules";
24
25/// Start marker for lightweight AGENTS.md/CODEBUDDY.md/CLAUDE.md pointer
26/// blocks. These are deliberately separate from `START_MARK` / `<!-- lean-ctx-rules -->`
27/// because the pointer-only vs full-rules distinction drives duplicate detection
28/// in `doctor overhead` — a pointer-only file (`is_pointer_only`) must not be
29/// counted as a second source for its client.
30pub const AGENTS_BLOCK_START: &str = "<!-- lean-ctx -->";
31
32/// End marker for AGENTS.md/CODEBUDDY.md/CLAUDE.md pointer blocks.
33pub const AGENTS_BLOCK_END: &str = "<!-- /lean-ctx -->";
34
35/// Closing marker that ends a lean-ctx rule section.
36pub const END_MARK: &str = "<!-- /lean-ctx-rules -->";
37
38/// Current rules version (monotonically increasing integer).  Embedded as
39/// `<!-- version: {RULES_VERSION} -->` right after `START_MARK` so the
40/// injection layer can parse it and decide whether a file is up-to-date.
41pub const RULES_VERSION: usize = 1;
42
43/// Banner placed at the top of dedicated rule files (non-shadow only).
44pub const CRITICAL: &str = "CRITICAL: ALWAYS use lean-ctx tools instead of native equivalents. \
45     This is NOT optional.";
46
47/// Compact bullet-list mapping (for both dedicated and shared contexts).
48pub const BULLETS: &str = "\
49MANDATORY MAPPING:\n\
50• Read/cat -> ctx_read(path, mode)\n\
51• Glob/find -> ctx_glob(pattern)\n\
52• Shell/bash -> ctx_shell(command)\n\
53• Grep -> ctx_search(pattern, path)\n\
54• ls/find -> ctx_tree(path, depth)";
55
56/// One-line admonition reinforcing the tool preference.  Always right after
57/// `BULLETS` in both profiles.
58pub const NEVER: &str =
59    "NEVER use native Read/Grep/Shell/Glob when ctx_* equivalents are available.";
60
61/// Intent-to-tool playbook — maps common agent questions to the right tool.
62pub const INTENT: &str = "\
63Tool selection by intent:\n\
64• Understand code / find answers / before editing -> ctx_compose (call FIRST)\n\
65• Read a file -> ctx_read(path, mode=signatures|map|full)\n\
66• Find a symbol by name (exact) -> ctx_symbol\n\
67• Search code by pattern (fuzzy) -> ctx_search\n\
68• Search by meaning (concepts) -> ctx_semantic_search\n\
69• Find files by pattern (glob) -> ctx_glob\n\
70• Project structure -> ctx_tree\n\
71• Who calls this / call graph -> ctx_callgraph\n\
72• Session state / memory -> ctx_session / ctx_knowledge";
73
74/// Anti-patterns that waste tokens and round-trips.
75pub const ANTI: &str = "\
76Anti-patterns — do NOT:\n\
77• Chain ctx_search -> ctx_read -> ctx_symbol — one ctx_compose replaces all three\n\
78• Grep for symbol definitions — ctx_symbol is faster + more precise\n\
79• Use ctx_read(mode=full) for orientation — use mode=signatures\n\
80• Use ctx_callgraph or ctx_graph for const/static/variable references — they track\n\
81  function call edges and file-level deps only. Use grep or ctx_compose instead";
82
83/// Encourages parallel tool calls to reduce round-trips.
84pub const PARALLEL: &str = "\
85PARALLEL tool calls: fire independent calls in the SAME turn — don't sequence them.\n\
86One turn with 5 parallel ctx_read calls completes faster than 5 sequential turns.\n\
87ctx_compose bundles multiple lookups into one call; for anything it doesn't\n\
88cover, batch independent reads/searches together.";
89
90/// One-line automation reminder.
91pub const AUTO: &str = "Auto: preload/dedup/compress run in background. \
92    ctx_session=memory, ctx_knowledge=facts, ctx_semantic_search=meaning search, \
93    ctx_shell raw=true=uncompressed. Details: LEAN-CTX.md";
94
95/// Context Engineering Protocol version reference.
96pub const CEP: &str = "CEP v1: 1.ACT FIRST 2.DELTA ONLY (Fn refs) 3.STRUCTURED (+/-/~) \
97     4.ONE LINE PER ACTION 5.QUALITY ANCHOR";
98
99/// Output style rule.
100pub const INTELLIGENCE: &str =
101    "OUTPUT: never echo tool output, no narration comments, show only changed code.";
102
103/// LITM end-of-instructions preference line.
104pub const LITM_END: &str = "TOOL PREFERENCE (END): ctx_compose>chain ctx_read>Read ctx_shell>Shell \
105     ctx_search>Grep ctx_glob>Glob ctx_tree>ls | Edit/Write/Delete=native";
106
107// ── Output-style compression prompts ───────────────────────────
108
109/// Lite compression — concise, bullet-point output.
110pub const LITE_PROMPT: &str = "\
111OUTPUT STYLE: concise
112- Bullet points over paragraphs
113- Skip filler words and hedging (\"I think\", \"probably\", \"it seems\")
114- 1-sentence explanations max, then code/action
115- No repeating what the user said";
116
117/// Standard compression — dense, atomic fact lines, abbreviations.
118pub const STANDARD_PROMPT: &str = "\
119OUTPUT STYLE: dense
120- Each statement = one atomic fact line
121- Use abbreviations: fn, cfg, impl, deps, req, res, ctx, err, ret
122- Diff lines only (+/-/~), never repeat unchanged code
123- Symbols: → (causes), + (adds), − (removes), ~ (modifies), ∴ (therefore)
124- No narration, no filler, no hedging
125- BUDGET: ≤200 tokens per response unless code block required";
126
127/// Max compression — expert-terse, telegraph format, symbolic vocabulary.
128pub const MAX_PROMPT: &str = "\
129OUTPUT STYLE: expert-terse
130- Telegraph format: subject-verb-object, drop articles/prepositions
131- Symbolic vocabulary: → cause, ∵ because, ∴ therefore, ⊕ add, ⊖ remove, Δ change, ≈ similar, ≠ different, ∈ in/member, ∅ empty/none, ✓ ok, ✗ fail
132- Code blocks: untouched (never compress code syntax)
133- Each line: max 80 chars
134- Zero narration, zero filler
135- BUDGET: ≤100 tokens per non-code response";
136
137/// Return the compression prompt text for a given level (empty string for Off).
138pub fn compression_text(level: CompressionLevel) -> &'static str {
139    match level {
140        CompressionLevel::Off => "",
141        CompressionLevel::Lite => LITE_PROMPT,
142        CompressionLevel::Standard => STANDARD_PROMPT,
143        CompressionLevel::Max => MAX_PROMPT,
144    }
145}
146
147const FULL_NON_SHADOW: &[&str] = &[
148    CRITICAL,
149    BULLETS,
150    NEVER,
151    INTENT,
152    ANTI,
153    PARALLEL,
154    AUTO,
155    CEP,
156    INTELLIGENCE,
157    LITM_END,
158];
159
160const FULL_SHADOW: &[&str] = &[INTENT, ANTI, PARALLEL, AUTO, CEP, INTELLIGENCE, LITM_END];
161
162const COMPACT_NON_SHADOW: &[&str] = &[CRITICAL, BULLETS, NEVER, INTENT, ANTI, PARALLEL];
163
164const COMPACT_SHADOW: &[&str] = &[INTENT, ANTI, PARALLEL];
165
166/// Selects the profile (FULL vs COMPACT) and the wrapping style (markers,
167/// headers, footers) for `render()`.
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum Wrapper {
170    /// **Dedicated rule file.**  FULL profile.  Wrapped with `START_MARK`,
171    /// `<!-- version: N -->`, and `END_MARK`.  Non-shadow includes the
172    /// `CRITICAL` banner before the body.  The whole file is lean-ctx–owned;
173    /// the injection layer detects staleness by parsing the version comment.
174    Dedicated,
175
176    /// **Shared file section** (appended to AGENTS.md, GEMINI.md, etc.).
177    /// COMPACT profile.  Same marker wrapping for find/replace within a
178    /// larger shared file.  Non-shadow includes `## Tool Mapping` header.
179    Shared,
180
181    /// **MCP session instructions.**  COMPACT profile.  No markers or
182    /// headers — bare content used inline in per-session MCP instructions.
183    Bare,
184}
185
186/// Render lean-ctx rules for a given wrapper, shadow mode, and compression level.
187///
188/// * `shadow` — when true, tool-mapping sections (BULLETS, NEVER,
189///   CRITICAL banner, "## Tool Mapping" header) are omitted.
190/// * `wrapper` — selects the profile (FULL / COMPACT) and wrapping style.
191/// * `level` — selects the output-style compression prompt (Lite / Standard /
192///   Max) which is appended to the body. `Off` omits it.
193pub fn render(shadow: bool, wrapper: Wrapper, level: CompressionLevel) -> String {
194    let profile = match (wrapper, shadow) {
195        (Wrapper::Dedicated, false) => FULL_NON_SHADOW,
196        (Wrapper::Dedicated, true) => FULL_SHADOW,
197        (_, false) => COMPACT_NON_SHADOW,
198        (_, true) => COMPACT_SHADOW,
199    };
200
201    let mut body = profile.join("\n\n");
202
203    // Append compression prompt for active levels
204    let compression = compression_text(level);
205    if !compression.is_empty() {
206        body.push('\n');
207        body.push_str(compression);
208    }
209
210    if matches!(wrapper, Wrapper::Bare) {
211        return body;
212    }
213
214    let version_line = format!("<!-- version: {RULES_VERSION} -->");
215
216    format!("{START_MARK}\n{version_line}\n\n{body}\n{END_MARK}")
217}
218// ============================================================
219// RULES FILE — centralized interface for reading rule files
220// ============================================================
221
222/// A parsed lean-ctx rules section from a file on disk.
223///
224/// Handles version detection, content boundary discovery, and prefix/suffix
225/// extraction.  This is the **only** place that parses `START_MARK` / version
226/// comments — every consumer (injection, drift detection, status reporting)
227/// goes through this struct.
228#[derive(Debug)]
229pub struct RulesFile<'a> {
230    content: &'a str,
231    /// Byte offset of `START_MARK` (or the first old-format marker found).
232    start: Option<usize>,
233    /// Byte offset of `END_MARK`.
234    end: Option<usize>,
235    /// Parsed version number (0 if no `<!-- version: N -->` comment found).
236    version: usize,
237}
238
239/// Parse the version number from the first `<!-- version: N -->` comment
240/// found at or after `search_start`.
241fn parse_version_number(s: &str) -> Option<usize> {
242    let prefix = "<!-- version: ";
243    let vs = s.find(prefix)?;
244    let num_start = vs + prefix.len();
245    let end = s[num_start..].find(" -->")?;
246    s[num_start..num_start + end].parse().ok()
247}
248
249impl<'a> RulesFile<'a> {
250    /// Parse `content`, scanning for `START_MARK` and version comment.
251    ///
252    /// * `START_MARK` not found → `has_content() = false`, version = 0.
253    /// * `START_MARK` found but no version → `has_content() = true`, version = 0
254    ///   (assume older than current → needs update).
255    pub fn parse(content: &'a str) -> Self {
256        let start = content.find(START_MARK);
257        let version = start
258            .and_then(|s| parse_version_number(&content[s + START_MARK.len()..]))
259            .unwrap_or(0);
260        let end = content.find(END_MARK);
261        RulesFile {
262            content,
263            start,
264            end,
265            version,
266        }
267    }
268
269    /// Whether the file carries any lean-ctx rules content.
270    pub fn has_content(&self) -> bool {
271        self.start.is_some()
272    }
273
274    /// The detected version (0 if no version marker — treat as older than
275    /// `RULES_VERSION`).
276    pub fn version(&self) -> usize {
277        self.version
278    }
279
280    /// Whether the file's version is at least `RULES_VERSION`.
281    pub fn is_current(&self) -> bool {
282        self.version >= RULES_VERSION
283    }
284
285    /// Content before the first `START_MARK` (user content / frontmatter).
286    /// Returns an empty string if no start marker was found.
287    pub fn prefix(&self) -> &'a str {
288        self.start.map_or("", |s| self.content[..s].trim())
289    }
290
291    /// Content after the last `END_MARK` (user content after the lean-ctx
292    /// block).  Returns an empty string if no end marker was found.
293    pub fn suffix(&self) -> &'a str {
294        self.end
295            .map_or("", |e| self.content[e + END_MARK.len()..].trim())
296    }
297
298    /// Merge freshly-rendered rules into this file.
299    ///
300    /// * If a lean-ctx section exists → replaces content between `START_MARK`
301    ///   and `END_MARK`, preserving user content before/after.
302    /// * If no section exists → appends fresh content at the end.
303    pub fn merged(&self, shadow: bool, wrapper: Wrapper, level: CompressionLevel) -> String {
304        let fresh = render(shadow, wrapper, level);
305        if self.start.is_some() {
306            let before = self.prefix();
307            let after = self.suffix();
308            let mut out = String::new();
309            if !before.is_empty() {
310                out.push_str(before);
311                out.push('\n');
312                out.push('\n');
313            }
314            out.push_str(&fresh);
315            if !after.is_empty() {
316                out.push('\n');
317                out.push('\n');
318                out.push_str(after);
319            }
320            if !out.ends_with('\n') {
321                out.push('\n');
322            }
323            out
324        } else {
325            // No existing section — append.
326            let trimmed = self.content.trim_end();
327            let mut out = trimmed.to_string();
328            if !out.is_empty() {
329                out.push('\n');
330                out.push('\n');
331            }
332            out.push_str(&fresh);
333            out
334        }
335    }
336
337    /// Create initial rules content (no existing section to merge with).
338    pub fn initial(shadow: bool, wrapper: Wrapper, level: CompressionLevel) -> String {
339        render(shadow, wrapper, level)
340    }
341
342    // ── Delete ─────────────────────────────────────────────────
343
344    /// Strip the lean-ctx section, keeping only user content before/after.
345    pub fn without_section(&self) -> String {
346        if let Some(start_pos) = self.start {
347            let before = self.content[..start_pos].trim();
348            let after = self.suffix();
349            let mut out = String::new();
350            if !before.is_empty() {
351                out.push_str(before);
352                out.push('\n');
353            }
354            if !after.is_empty() {
355                out.push('\n');
356                out.push_str(after);
357            }
358            out
359        } else {
360            self.content.to_string()
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368
369    // --- Constants ---
370
371    #[test]
372    fn bullets_uses_ctx_shell() {
373        assert!(BULLETS.contains("ctx_shell"));
374        assert!(!BULLETS.contains("lean-ctx -c"));
375        assert!(!BULLETS.contains("ctx_edit"));
376    }
377
378    #[test]
379    fn sections_not_empty() {
380        assert!(!BULLETS.is_empty());
381        assert!(!NEVER.is_empty());
382        assert!(!INTENT.is_empty());
383        assert!(!ANTI.is_empty());
384        assert!(!PARALLEL.is_empty());
385        assert!(!AUTO.is_empty());
386        assert!(!CEP.is_empty());
387        assert!(!INTELLIGENCE.is_empty());
388        assert!(!LITM_END.is_empty());
389        assert!(!CRITICAL.is_empty());
390    }
391
392    #[test]
393    fn intent_contains_ctx_compose() {
394        assert!(INTENT.contains("ctx_compose"));
395    }
396
397    #[test]
398    fn anti_contains_do_not() {
399        assert!(ANTI.contains("do NOT"));
400    }
401
402    #[test]
403    fn parallel_contains_parallel() {
404        assert!(PARALLEL.contains("PARALLEL"));
405    }
406
407    // --- render() — Dedicated ---
408
409    #[test]
410    fn dedicated_has_markers_and_version() {
411        let out = render(false, Wrapper::Dedicated, CompressionLevel::Off);
412        assert!(out.contains(START_MARK));
413        assert!(out.contains(&format!("<!-- version: {RULES_VERSION} -->")));
414        assert!(out.contains(END_MARK));
415        assert!(out.contains(BULLETS));
416        assert!(out.contains(NEVER));
417        assert!(out.contains("CRITICAL"));
418    }
419
420    #[test]
421    fn dedicated_shadow_omits_mapping() {
422        let out = render(true, Wrapper::Dedicated, CompressionLevel::Off);
423        assert!(out.contains(START_MARK));
424        assert!(
425            !out.contains("MANDATORY MAPPING"),
426            "shadow must not contain BULLETS"
427        );
428        assert!(!out.contains(NEVER), "shadow must not contain NEVER");
429        assert!(
430            !out.contains("CRITICAL"),
431            "shadow must not contain CRITICAL"
432        );
433        assert!(
434            out.contains(INTENT),
435            "shadow must keep non-mapping sections"
436        );
437    }
438
439    #[test]
440    fn dedicated_litm_structure() {
441        let out = render(false, Wrapper::Dedicated, CompressionLevel::Off);
442        let lines: Vec<&str> = out.lines().collect();
443        let first_5 = lines[..5.min(lines.len())].join("\n");
444        assert!(
445            first_5.contains("CRITICAL") || first_5.contains("MUST"),
446            "LITM: MUST/CRITICAL instruction near start"
447        );
448        // LITM_END or NEVER should appear in the final content lines (before END_MARK).
449        let tail = lines[lines.len().saturating_sub(8)..].join("\n");
450        assert!(
451            tail.contains("PREFERENCE") || tail.contains("NEVER"),
452            "LITM: reinforcement near end, tail={tail:?}"
453        );
454    }
455
456    // --- render() — Shared ---
457
458    #[test]
459    fn shared_has_markers_and_header() {
460        let out = render(false, Wrapper::Shared, CompressionLevel::Off);
461        assert!(out.contains(START_MARK));
462        assert!(out.contains(END_MARK));
463        assert!(out.contains("MANDATORY MAPPING"));
464        assert!(out.contains(BULLETS));
465    }
466
467    #[test]
468    fn shared_shadow_omits_mapping() {
469        let out = render(true, Wrapper::Shared, CompressionLevel::Off);
470        assert!(out.contains(START_MARK));
471        assert!(
472            !out.contains("MANDATORY MAPPING"),
473            "shadow must not have header"
474        );
475        assert!(
476            !out.contains("MANDATORY MAPPING"),
477            "shadow must not contain BULLETS"
478        );
479    }
480
481    // --- render() — Bare ---
482
483    #[test]
484    fn bare_has_no_markers() {
485        let out = render(false, Wrapper::Bare, CompressionLevel::Off);
486        assert!(!out.contains(START_MARK), "Bare must not have START_MARK");
487        assert!(!out.contains(END_MARK), "Bare must not have END_MARK");
488        assert!(!out.contains("<!-- version:"), "Bare must not have version");
489        assert!(out.contains(BULLETS));
490        assert!(out.contains(NEVER));
491    }
492
493    #[test]
494    fn bare_shadow_only_read_modes() {
495        let out = render(true, Wrapper::Bare, CompressionLevel::Off);
496        assert!(!out.contains(NEVER), "shadow Bare must not have NEVER");
497        assert!(
498            !out.contains("MANDATORY MAPPING"),
499            "shadow Bare must not have BULLETS"
500        );
501    }
502
503    // --- Compression level tests ---
504
505    #[test]
506    fn render_includes_lite_prompt() {
507        let out = render(false, Wrapper::Bare, CompressionLevel::Lite);
508        assert!(out.contains("OUTPUT STYLE: concise"));
509        assert!(out.contains("Bullet points"));
510    }
511
512    #[test]
513    fn render_includes_standard_prompt() {
514        let out = render(false, Wrapper::Bare, CompressionLevel::Standard);
515        assert!(out.contains("OUTPUT STYLE: dense"));
516        assert!(out.contains("atomic fact"));
517    }
518
519    #[test]
520    fn render_includes_max_prompt() {
521        let out = render(false, Wrapper::Bare, CompressionLevel::Max);
522        assert!(out.contains("OUTPUT STYLE: expert-terse"));
523        assert!(out.contains("Telegraph"));
524    }
525
526    #[test]
527    fn render_off_excludes_compression() {
528        let out = render(false, Wrapper::Bare, CompressionLevel::Off);
529        assert!(!out.contains("OUTPUT STYLE:"));
530    }
531
532    #[test]
533    fn compression_text_matches_level() {
534        assert!(compression_text(CompressionLevel::Off).is_empty());
535        assert!(compression_text(CompressionLevel::Lite).contains("Bullet"));
536        assert!(compression_text(CompressionLevel::Standard).contains("fn, cfg"));
537        assert!(compression_text(CompressionLevel::Max).contains("Telegraph"));
538    }
539
540    // --- Wrapper round-trip ---
541
542    #[test]
543    fn all_wrappers_produce_output() {
544        for shadow in [false, true] {
545            for wrapper in [Wrapper::Dedicated, Wrapper::Shared, Wrapper::Bare] {
546                let out = render(shadow, wrapper, CompressionLevel::Off);
547                assert!(!out.is_empty(), "{wrapper:?} shadow={shadow} is empty");
548            }
549        }
550    }
551
552    // --- RulesFile ---
553
554    #[test]
555    fn rules_file_parses_version() {
556        let content = format!(
557            "stuff before\n{START_MARK}\n<!-- version: {RULES_VERSION} -->\n\nbody\n{END_MARK}\nstuff after"
558        );
559        let f = RulesFile::parse(&content);
560        assert!(f.has_content());
561        assert_eq!(f.version(), RULES_VERSION);
562        assert!(f.is_current());
563        assert!(f.prefix().contains("stuff before"));
564        assert!(f.suffix().contains("stuff after"));
565    }
566
567    #[test]
568    fn rules_file_no_version_defaults_to_zero() {
569        let content = format!("{START_MARK}\nbody\n{END_MARK}");
570        let f = RulesFile::parse(&content);
571        assert!(f.has_content());
572        assert_eq!(f.version(), 0);
573        assert!(!f.is_current());
574    }
575
576    #[test]
577    fn rules_file_no_start_marker_no_content() {
578        let f = RulesFile::parse("just user stuff");
579        assert!(!f.has_content());
580        assert_eq!(f.version(), 0);
581    }
582
583    #[test]
584    fn rules_file_merged_replaces_section() {
585        let content =
586            format!("before\n{START_MARK}\n<!-- version: 1 -->\n\nold\n{END_MARK}\nafter");
587        let f = RulesFile::parse(&content);
588        let merged = f.merged(false, Wrapper::Shared, CompressionLevel::Off);
589        assert!(merged.contains("before"), "prefix preserved");
590        assert!(merged.contains("after"), "suffix preserved");
591        assert!(!merged.contains("old"), "old content replaced");
592        assert!(merged.contains(&format!("<!-- version: {RULES_VERSION} -->")));
593    }
594
595    #[test]
596    fn rules_file_merged_appends_when_no_section() {
597        let content = "user content";
598        let f = RulesFile::parse(content);
599        assert!(!f.has_content());
600        let merged = f.merged(false, Wrapper::Bare, CompressionLevel::Off);
601        assert!(merged.contains("user content"));
602        assert!(merged.contains(BULLETS));
603    }
604
605    #[test]
606    fn rules_file_without_section_strips_content() {
607        let content =
608            format!("header\n{START_MARK}\n<!-- version: 1 -->\n\nbody\n{END_MARK}\nfooter");
609        let f = RulesFile::parse(&content);
610        let stripped = f.without_section();
611        assert!(stripped.contains("header"));
612        assert!(stripped.contains("footer"));
613        assert!(!stripped.contains("body"));
614        assert!(!stripped.contains(START_MARK));
615    }
616
617    #[test]
618    fn rules_file_without_section_noop_when_no_content() {
619        let content = "just user text";
620        let f = RulesFile::parse(content);
621        assert_eq!(f.without_section(), content);
622    }
623}