TAI BUI
Back to curriculum
CapstonePython, YAML

Capstone 86 — Constitutional Rules Engine

A rule is a name, a predicate, and an explanation. Drop one of the three and you have a vibe.

TIME: ~90 minPREREQUISITES: Phase 18 safety lessons, Phase 19 Track A lessons 25-29

Learning Objectives

  • Write a constitution in YAML where each rule has a name, a severity, an explanation, a `must` predicate, and an optional `applies_when` and `fix`.
  • Compose atomic predicates with `all_of`, `any_of`, and `not_`, and record `not_applicable` as its own status alongside `pass` and `violation`.
  • Apply declarative per-rule fixes (append, prepend, regex replace) and produce a line-level diff of `add`, `remove`, `edit-removed`, and `edit-added` changes.
  • Re-evaluate the revised text to confirm a fix worked, and read severities so a router can decide what to do with a draft.

Problem

A constitutional rules engine loads a declarative file of rules, evaluates each rule against a candidate output, and returns a structured result for every rule that fired. Classifiers cover the recognizable failures. Rules engines cover the contractual ones. A team writing a coding assistant wants a constraint like "every response that contains code must end in either a runnable block or a stated assumption." A team running a customer support bot wants "every refusal must offer a next step." Neither is a natural classifier target. Each is a predicate over the response, the conversation, and the system policy, and a non-engineer needs to be able to read it.

A declarative file fits that job. A constitution lives in YAML alongside the code, in version control, with a separate review process. Each rule has a name, a predicate, a severity, and an explanation template. The engine loads the file, evaluates each rule against the candidate output, and returns a structured Violation per rule that fired. Predicates compose with all_of, any_of, and not_, so a single rule can express "if the response contains code, it must end with a runnable block AND not reference an internal-only library."

Revision is the second half. An engine that only blocks leaves the work to someone else. An engine that proposes a fix closes the loop: the assistant drafts a response, the engine flags violations, a fixer produces a revised response, and the engine checks that the revision satisfies the rules. This capstone ships a minimal fixer (regex replacement per rule) and a structured diff (line-by-line additions, removals, edits) between draft and revised.

Warm-up0/3
1. Why do teams usually keep output rules in a declarative YAML file under version control instead of scattering them through application code?Warm-up
2. A safety check rejects a draft reply. Which result is most useful to the person who has to fix the draft?Warm-up
3. In a regular expression, what does a `$` at the end of the pattern mean?Warm-up
Check: Problem0/2
1. What does the engine return for each rule that fired, which the lesson calls a violation?Define
2. Why is "every refusal must offer a next step" better written as a rule than trained as a classifier?Explain

Concept

Mermaid diagram

A rule has this shape:

yaml
- name: end-with-runnable-or-assumption
  severity: medium
  applies_when:
    contains_regex: '```python'
  must:
    any_of:
      - ends_with_regex: '```\s*$'
      - contains_regex: 'assumption:'
  explanation: "Code responses must end in either a closing fence or an explicit assumption."
  fix:
    append_if_missing: "\n\nAssumption: example inputs are valid."

name, explanation, must, and a severity of low, medium, or high are required, and the engine refuses to load a rule without them. applies_when and fix are optional.

Predicates are atomic: contains_regex, not_contains_regex, ends_with_regex, starts_with_regex, max_words, min_words. Compositions are all_of, any_of, not_. Each predicate is a small mapping, and the engine evaluates it recursively into a pair: whether it held, and the matched span if there is one. The engine evaluates applies_when first. If the rule does not apply, the result is recorded as not_applicable. Otherwise the engine evaluates must and records either pass or violation.

Severities are low, medium, high, the same three levels lesson 85 uses for classifier verdicts. The engine itself blocks nothing. It reports max_severity() for a draft, and a router such as lesson 87's gate could treat a high rule violation the same as a high classifier verdict and block.

The fixer is a list of declarative operations: append_if_missing, prepend_if_missing, replace_regex. Each operation maps a rule by name to a transform, and only rules that carry a fix key are registered with the fixer. A violation of any other rule is reported but left unrepaired. The fixer is limited to local edits on purpose. Structural rewrites belong in a separate refusal-and-help layer that this capstone does not cover.

The diff compares the original against the revised text and returns a list of Change records, each with an op (add, remove, edit-removed, or edit-added) and the relevant line of text. An edited line shows up as a removed line plus an added line. A caller can log the diff, which gives a human reviewer a way to audit the fixer's behavior over time.

Check: Concept0/17
1. In this rules engine, what is a constitution?Define
2. What is a predicate in a rule?Define
3. What is the fixer in this engine?Define
4. Which op values can a Change record from diff() carry?Define
5. How does the engine build a rule out of several conditions?Define
6. What is the role of applies_when in a rule?Define
7. Why does the engine record not_applicable as its own status instead of omitting a rule that does not apply?Explain
8. Why does the lesson keep the fixer to append, prepend, and regex replace instead of letting it rewrite structure?Explain
9. The fixer edits drafts automatically. What does the diff add to that process?Explain
10. end-with-runnable-or-assumption must end in a closing fence or contain 'assumption:'. Why does it also have an applies_when on a python code fence?Explain
11. The engine itself blocks nothing. What should a router read to decide whether to block a draft?Explain
12. What does `Engine(rules=[{"name": "x", "severity": "low"}])` do?Apply
13. A rule is written with `severity: critical`. What happens when Engine loads it?Apply
14. bounded-length (at most 800 words) has no fix key, and a 900-word draft violates it. What does the demo report?Apply
15. A teammate adds `fix: append_if_missing` to bounded-length (at most 800 words) to repair over-long answers. Why will it not work?Analyze
16. You want high violations blocked, medium ones auto-repaired, and low ones only logged. Which design fits how the engine works?Design
17. Add a rule: any response that mentions safety must include the phrase "If this is urgent". Which rule shape fits the engine?Design

Build It

code/rules.yml holds the constitution. The loader in code/main.py reads rules.yml through yaml_subset.load_yaml, which uses PyYAML when it is installed and otherwise falls back to a small bundled parser for the YAML subset the constitution uses, so it runs on a standard Python install with no dependencies. Both paths parse the shipped rules.yml to the same structure. code/tests.py covers that parser and the default constitution. code/main.py defines the Engine and Fixer classes and a diff function. Compositions are evaluated recursively, and any_of short-circuits.

The constitution as shipped:

  • no-empty-refusal (medium) - a refusal must include either a suggestion or a redirect
  • end-with-runnable-or-assumption (medium) - code responses must close cleanly
  • no-pii-in-examples (high) - example data must not contain emails or phone shapes
  • cite-when-asserting-fact (low) - lines beginning with "According to", "studies show" or "research finds" must contain a parenthetical citation
  • no-internal-library-leak (high) - the words internal-only and policybot-internal must not appear in the output
  • bounded-length (low) - responses must not exceed 800 words
Check: Build It0/9
1. The Build It section says compositions are evaluated recursively and any_of short-circuits. Why can any_of stop evaluating as soon as one child holds?Explain
2. A router blocks only on high severity. Which shipped rule's violation would it act on?Apply
3. A draft says "Example user: [email protected]. Here is how to look them up." What does the engine and fixer do with it?Apply
4. The fixer replaces the number in the one-line draft "Sample phone: 555-123-4567 works." with [example-contact]. What does diff() list?Apply
5. Draft: "According to NASA, the moon is far." What status does cite-when-asserting-fact get, and what is max_severity() if nothing else fails?Apply
6. A draft is exactly 800 words long. What status does bounded-length (responses must not exceed 800 words) give it?Apply
7. The demo flags a code answer that ends with "Let me know if this works." after a closing fence, even though the code is complete. What explains it?Analyze
8. A draft says "Contact [email protected] for help." and passes every rule. The no-pii-in-examples rule applies only when the text contains 'example' or 'sample'. What explains the miss?Analyze
9. no-empty-refusal accepts a refusal containing any of try, consider, alternative, instead, suggest, or redirect. "I cannot help, and I will not even try." passes. What does this show?Analyze

Use It

Run python3 main.py. The demo runs five draft responses (refusal-without-redirect, code-without-assumption, pii-in-example, internal-library-leak, and clean-response) through the engine, prints the violations, runs the fixer, prints the diff, and writes rules_report.json into an outputs/ folder next to the code directory. One fixture has a rule that does not apply (the draft has no code block), and the report marks that rule not_applicable, so you can see the engine evaluated it explicitly.

Check: Use It0/4
1. Why does the demo run the engine a second time on the revised text?Explain
2. A short haiku with no code block is checked against end-with-runnable-or-assumption. What status does that rule get?Apply
3. The fixer appends a note to the draft "I cannot help with that question." The note begins with two newlines. What does diff() return?Apply
4. The draft "I cannot help with the internal-only adapter." is run through the engine. Which result is correct?Apply

Ship It

outputs/skill-constitutional-rules-engine.md documents the rule grammar and the fixer operations.

Exercises

  1. Add a rule that requires a response to include the phrase "If this is urgent" when the prompt mentions safety. Use composition.
  2. Replace the regex fixer with a templating fixer that takes named slots. Rewrite one rule under the new design to show it working.
  3. Add a metrics endpoint that takes a corpus of drafts and returns the per-rule violation rate, so the team can see which rule is over-firing.

Key Terms

TermCommon usagePrecise meaning
constitutiona vague policy doca YAML file of rules with predicates, severities, and explanations
predicatea checka mapping node, atomic or composed via all_of/any_of/not_, that the engine evaluates to a (bool, matched span) pair
violationa failurea structured record with rule name, severity, explanation, and matched span
fixera model fine-tunea deterministic per-rule transform mapping draft to revised
diffa string comparea structured list of add, remove, edit-removed, and edit-added line changes between draft and revised
Author's Note

My bet is that not_applicable earns its place. A rule that skips silently looks the same as a rule that passed, and the engine records the skip as its own status so the report shows the rule was evaluated. I'd keep that distinction when you add rules of your own.

I'd keep the fixer small. It does local edits (append, prepend, regex replace) and nothing structural, and I think that limit is what makes its output easy to audit through the diff. A rule that needs a real rewrite probably belongs in a different layer.

The demo evaluates the revised text a second time, prints how many violations remain, and records them in the report. I'd treat that second pass as the part that tells you whether a fix worked, since the fixer applies a transform and does not check its own result.

Further Reading

Lesson 87 composes this engine with the input-side detector and the output-side classifier into a single safety gate.

Code

Ships With