A Reference
Every rule,
with a specimen
from the field.
A reader's guide to the rules that ship by default in
phr-mcp init --packs llm,rust. Each entry
carries its predicate, its severity, a one-line distillation,
and a worked example drawn from real-world Rust code.
- Default packs
- 6
- Rules shipped
- 41±
- Blocks (pre)
- 13
- Warnings (pre/post)
- 11
- Audit-only
- 10
- Field examples
- 24
§I
1
Pack · LLM — behavior, not code shape.
6Rules
The LLM pack catches phrasing patterns that recur in long
LLM-assisted sessions: deflective disclaimers that shift
blame to pre-existing code, unverified completion claims
that report success without evidence, and workflow moments
where a nudge before commit or push is worth more than a thousand
words in a CLAUDE.md that has already faded from context.
✕
BLOCK
phase: pre
×3 variants
enforce-no-pre-existing-issue · -not-from-our-changes · -not-caused-by-our
Stops three closely-related disclaimer phrases that pre-emptively
blame the existing tree rather than naming the issue and deciding
to fix or defer.
All three fire on substring match against new content — no
file gates. They apply equally to code edits, commit messages,
and PR bodies. The shared shape: the model has noticed a
problem and is reaching for a phrase that retires it without
a decision.
trigger in a commit message
// Subject line, ~turn 80 of a long session
fix(scoring): switch priority ordering to descending
The test failure in play::turn_resolver_tests is
a pre‑existing issue not from our changes; ignored.
→
accepted form
// Subject line
fix(scoring): switch priority ordering to descending
Known broken: play::turn_resolver_tests::priority_order
fails on the new sort comparator. Filed as #421;
this PR does not address it. (See follow-up.)
Why The disclaimer pattern is a known
failure mode in long sessions: an issue gets surfaced,
attribution-shifted, and then forgotten. The accepted form
forces a binary — fix it as part of this change, or name it
and defer it explicitly.
✕
BLOCK
phase: pre
×2 variants
enforce-no-should-work-claim · enforce-no-should-be-fixed-claim
Stops "should work now" and "should be fixed" phrasing when
they appear without accompanying verification.
These phrases are the linguistic marker of the unfinished
handoff: a change made, a hypothesis stated, no test run.
The rule cannot tell whether you actually ran the test — but
it can force the language to match the evidence.
trigger
The duplicate-entry bug should work now. Closes #87.
→
verified report
Verified against the seed that triggered #87:
$ cargo test hand::duplicate_entry -- --exact
test ... ok (1 passed)
Closes #87.
!
WARN
phase: pre
on Bash content
nudge-verify-before-commit
Fires when a Bash command contains "git commit -m". A
reminder, not a refusal — to trace the call chain end-to-end
before claiming done.
Hook fires before the commit runs. The host prints the rule's
message to stderr at exit 1; the commit still proceeds.
The point is the moment of friction — a half-second to ask
did I actually verify this before the message is
permanent.
trigger
$ git commit -m "wire up new play ledger"
phronesis: WARNING — About to commit. Trace
the call chain end-to-end before reporting done.
Half-fixes where one layer is wired but another is
not are a recurring failure mode.
→
verified, then commit
$ cargo test --workspace -p core
test ... ok (124 passed)
$ cargo run -- --scenario play-smoke
... play smoke passed
$ git commit -m "wire up new play ledger"
§II
2
Pack · Rust — code shape, error handling, debt.
29Rules
The Rust pack mixes hard blocks (panic-shaped exits, sync/async
misuse, raw Result<_, String>), softer warns
(signature smells, dbg!() leftovers), and audit-only debt
indicators (newtype opportunities, god-files, manual error
propagation). The examples below all draw from a representative
Rust application — a game engine — chosen because the domain
tends to surface every category of Rust pain at once.
✕
BLOCK
phase: pre
audit: yes
×4 variants
enforce-no-{unwrap, panic, todo, unimplemented}-in-src
Four sibling rules. Each catches one panic-shaped exit
(.unwrap(), panic!(), todo!(),
unimplemented!()) added in any file under
src/. Test code is excluded via inline
#[cfg(test)] stripping.
All four share the same shape: substring match against new
content, gated by a file-path match on src. The
hook strips test blocks before evaluation, so usage inside
inline test modules — where panics are idiomatic — does not
trip the rule.
From the field src/example/hand.rs
pub fn current_card(&self) -> &Card {
self.members
.get(&self.turn_order[self.turn_index])
.unwrap()
}
→
refactored
pub fn current_card(&self) -> Result<&Card, HandError> {
let id = self.turn_order
.get(self.turn_index)
.ok_or(HandError::TurnIndexOutOfRange)?;
self.members
.get(id)
.ok_or_else(|| HandError::CardNotFound(id.clone()))
}
Why Panic-shaped exits in production paths
crash the host runtime. The cost of carrying a Result
through one more call is much lower than the cost of a
mid-session crash in a tool that holds long-lived state.
✕
BLOCK
phase: pre
predicate: AST
enforce-no-result-string-error
Fires when a function's signature returns
Result<_, String>. Strings are not error
types.
Detected via tree-sitter: the rule walks function signatures
and matches the precise return shape. Test functions are
excluded automatically. The remedy is a proper error enum,
usually with thiserror.
From the field src/core/event_pipeline.rs
pub fn enqueue(&mut self, event: Event) -> Result<(), String> {
if self.queue.len() >= self.cap {
return Err(format!("queue full at cap {}", self.cap));
}
self.queue.push(event);
Ok(())
}
→
refactored
// New: a domain error type
#[derive(Debug, thiserror::Error)]
pub enum PipelineError {
#[error("event queue full at cap {0}")]
QueueFull(usize),
}
pub fn enqueue(&mut self, event: Event) -> Result<(), PipelineError> {
if self.queue.len() >= self.cap {
return Err(PipelineError::QueueFull(self.cap));
}
self.queue.push(event);
Ok(())
}
Why A typed error makes the pipeline's
failure modes explicit to callers, lets ? compose
with From impls, and survives refactors. A
stringified error is a one-way trapdoor — every caller has
to parse the message to recover any structure.
!
WARN
phase: pre
audit: yes
warn-dbg-in-src
Catches dbg!() in any src/ file.
Diagnostic leftover; never an intentional shipping pattern.
From the field src/core/play/turn_resolver.rs
fn resolve_play(&mut self, play: Play) -> Resolution {
let roll = dbg!(self.compute_priority() + play.bonus);
match roll {
_ if roll >= play.target_threshold => Resolution::Valid,
_ => Resolution::Invalid,
}
}
→
refactored
use tracing::trace;
fn resolve_play(&mut self, play: Play) -> Resolution {
let roll = self.compute_priority() + play.bonus;
trace!(target: "play", ?play, roll, "resolving");
match roll {
_ if roll >= play.target_threshold => Resolution::Valid,
_ => Resolution::Invalid,
}
}
!
WARN
phase: post
predicate: AST
warn-rust-public-fn-takes-string-ref
Public function takes a &String parameter
where &str would accept a strictly larger
set of callers.
From the field src/example/hand.rs
pub fn find_card(&self, name: &String) -> Option<&Card> {
self.members.iter().find(|c| &c.name == name)
}
→
refactored
pub fn find_card(&self, name: &str) -> Option<&Card> {
self.members.iter().find(|c| c.name == name)
}
Why A &str accepts string
literals, slices into existing buffers, and the result of
.as_str() on a String. A
&String forces every caller to first own a
String — a needless allocation requirement at
an API boundary.
!
WARN
phase: post
predicate: AST
warn-rust-public-fn-takes-vec-ref
Public function takes a &Vec<T> where
&[T] would accept slices, arrays, and Vecs.
From the field src/world/state.rs
pub fn apply_events(&mut self, events: &Vec<Event>) {
for event in events {
self.apply(event);
}
}
→
refactored
pub fn apply_events(&mut self, events: &[Event]) {
for event in events {
self.apply(event);
}
}
!
WARN
phase: post
audit: yes
threshold: 5
warn-rust-function-param-count-high
Function takes five or more parameters (excluding self).
A god-signature smell.
From the field src/core/play/turn_resolver.rs
pub fn new(
id: String,
name: String,
score: u32,
max_score: u32,
rank: u32,
priority: i32,
actions: Vec<Action>,
members: Hand,
) -> Self { ... }
→
refactored
pub struct HandStats { pub score: u32, pub max_score: u32, pub rank: u32, pub priority: i32 }
pub fn new(
id: ActorId,
name: String,
values: HandStats,
strategy: Strategy, // holds actions + members
) -> Self { ... }
Why Long parameter lists correlate with
god-functions and make call sites stateal and brittle.
Grouping related params into named structs improves
readability and gives a place for invariants to live.
!
WARN
phase: pre
audit: yes
warn-deref-for-non-pointer-type
Catches impl Deref for outside of true smart-pointer
wrappers. Deref polymorphism is an anti-pattern.
From the field src/example/hand.rs
pub struct HandView { inner: Hand }
impl Deref for HandView {
type Target = Hand;
fn deref(&self) -> &Hand { &self.inner }
}
→
refactored
pub struct HandView { inner: Hand }
impl HandView {
pub fn hand(&self) -> &Hand { &self.inner }
}
// expose specific methods explicitly
pub fn name(&self) -> &str { self.inner.name() }
}
!
WARN
phase: pre
on Bash content
warn-cargo-build-without-workspace
Catches cargo build / test /
check / clippy invoked without
--workspace in a multi-crate workspace.
trigger
$ cargo test
phronesis: WARNING — Running `cargo test` without
`--workspace` only checks part of the workspace. Use
`cargo <subcommand> --workspace --tests --examples`
to catch sibling-crate breakage, or pass `-p <crate>`
if scope was intentional.
→
scoped, full
# Whole workspace, including sibling crates
$ cargo test --workspace --tests --examples
# Or scoped intentionally to one crate
$ cargo test -p core hand::
Why A bare cargo build in a
multi-crate workspace silently skips sibling crates whose
paths Cargo can't infer from the cwd. The result is the
classic "green locally, red on CI" failure when a small
refactor breaks a downstream crate.
!
WARN
phase: pre
predicate: AST
threshold: 3
warn-clone-heavy
A single function body contains three or more
.clone() calls — usually a sign that the borrow
checker has been argued with rather than appeased.
From the field src/example/hand.rs
fn snapshot_hand(hand: &Hand) -> HandSnapshot {
HandSnapshot {
name: hand.name.clone(),
members: hand.members.clone(),
state: hand.state.clone(),
log: hand.log.clone(),
}
}
→
refactored
#[derive(Clone)]
pub struct HandSnapshot { ... }
fn snapshot_hand(hand: &Hand) -> HandSnapshot {
hand.snapshot.clone() // one borrow into the canonical clone
}
// or, if read-only is enough:
fn snapshot_hand(hand: &Hand) -> &HandSnapshot { &hand.snapshot }
!
WARN
phase: post
predicate: AST
warn-empty-test
A #[test] function body with no assertion macro
and no ? operator. A placeholder that always
passes is a regression hidden in plain sight.
From the field tests/example/hand.rs
#[test]
fn hand_can_be_created() {
let _p = Hand::new("the winning hand");
}
→
refactored
#[test]
fn hand_starts_empty_and_named() {
let p = Hand::new("the winning hand");
assert_eq!(p.name(), "the winning hand");
assert!(p.members().is_empty());
}
✕
BLOCK
phase: pre
audit: yes
×2 variants
block-await-on-sync-execute-all-agenda-items · -fire-all-consequences
Catches stale .await calls on methods that were
made synchronous in a prior refactor — a recurring source of
stale call sites after API changes.
Project-specific in shape, broadly useful in concept: these
two methods on phronesis's own RETE network were converted
from async to sync in an earlier refactor, and old
.await call sites kept turning up in test code
and integration helpers afterwards. The rule pins the API
surface so that resurrected snippets get caught before they
reach a future is not awaitable compile error. The
same shape — naming two specific now-sync methods you wish
to police — generalizes cleanly to any project undergoing
a sync/async migration.
From the field tests/integration/round_smoke.rs
#[tokio::test]
async fn round_smoke() -> Result<(), TestError> {
let engine = make_engine();
let actions = engine.execute_all_agenda_items().await?;
assert_eq!(actions.len(), 3);
Ok(())
}
→
refactored
#[test]
fn round_smoke() -> Result<(), TestError> {
let engine = make_engine();
let actions = engine.execute_all_agenda_items()?;
assert_eq!(actions.len(), 3);
Ok(())
}
Why Without the rule, the compiler error is
confusing — "Result<…> is not a future" —
and easy to misdiagnose as a Tokio version mismatch. The
named rule turns a five-minute hunt into a one-line fix.
○
AUDIT
phase: audit
silent at hook
audit-manual-err-return
Catches the shape => return Err(…) in a match
arm — almost always a place where ? would
replace the whole construct.
Audit-only by design. Firing this at hook time would
interrupt every refactor mid-flight; firing it at sweep
time gives a debt count of "places where the
? operator wasn't used."
From the field src/core/event_pipeline.rs
let event = match parse_event(&payload) {
Ok(e) => e,
Err(e) => return Err(e),
};
→
refactored
let event = parse_event(&payload)?;
○
AUDIT
phase: audit
×2 variants
audit-newtype-id-string · audit-newtype-id-u64
Surfaces struct fields named *_id: String or
*_id: u64 — typical candidates for the newtype
pattern.
From the field src/topology/graph.rs
pub struct State {
pub id: String,
pub name: String,
pub exits: Vec<String>, // also ids, also strings
pub slot_ids: Vec<String>,
}
→
refactored
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct StateId(String);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SlotId(String);
impl StateId {
pub fn new(s: impl Into<String>) -> Self { Self(s.into()) }
pub fn as_str(&self) -> &str { &self.0 }
}
pub struct State {
pub id: StateId,
pub name: String,
pub exits: Vec<StateId>,
pub slot_ids: Vec<SlotId>,
}
Why A bare String id lets you
pass a SlotId where a StateId is
expected. The compiler will not save you. A newtype turns
the mistake into a type error at the call site.
○
AUDIT
phase: audit
×2 variants
audit-if-let-opportunity-none-empty · -err-empty
Catches None => {} and Err(_) => {}
match arms — either redundant if let opportunities
or silent error-swallows.
From the field src/core/play/turn_resolver.rs
match self.active_target.as_ref() {
Some(target) => self.narrate_play(target),
None => {}
}
match self.write_to_log(&entry) {
Ok(()) => {},
Err(_) => {} // silent swallow!
}
→
refactored
if let Some(target) = self.active_target.as_ref() {
self.narrate_play(target);
}
if let Err(e) = self.write_to_log(&entry) {
tracing::warn!(?e, "play log write failed");
}
○
AUDIT
phase: audit
threshold: 800
audit-file-loc-high
Flags .rs files exceeding 800 lines. A god-file
signal; not a violation, just a candidate for splitting.
audit output
$ phr-mcp audit --rule audit-file-loc-high
Rule Level Hits Files
audit-file-loc-high warn 3 3
src/core/event_pipeline.rs lines: 1
src/core/play/turn_resolver.rs lines: 1
src/example/round/mod.rs lines: 1
→
a plan, not a fix
// Split event_pipeline.rs (1418 LOC) into:
src/core/event_pipeline/
├── mod.rs // 80 LOC: the trait, re-exports
├── queue.rs // 220 LOC: the ring buffer
├── dispatch.rs // 410 LOC: the dispatch table
├── consequences.rs // 340 LOC: side-effect drainage
└── shape.rs // 180 LOC: payload types
// 1230 LOC total, 188 LOC saved as overlap
Why A file that crosses 800 lines is usually
doing several jobs. The rule is a sweep indicator, not a
mechanical fix — the value is in being told the pile exists
during a quarterly review, not on every edit.
✕
block-deny-warnings-attribute
Blocks #![deny(warnings)] in any .rs
file. A toolchain-fragility anti-pattern: every new rustc
release introduces new warnings and breaks the build.
avoid src/lib.rs
#![deny(warnings)]
// every cargo update is a coin flip
pub fn hello() { println!("hello"); }
→
put the policy in CI
// src/lib.rs — no global deny
pub fn hello() { println!("hello"); }
# .github/workflows/ci.yml
- run: cargo clippy --workspace -- -D warnings
env:
RUSTFLAGS: "-D warnings"
Why Library code with
#![deny(warnings)] stops compiling for downstream
consumers as soon as their toolchain introduces a new warning.
The policy belongs in CI, where it gates merges without
propagating fragility to anyone using the crate. Source:
rust-unofficial/patterns
§Anti-patterns (deny-warnings).
!
warn-public-fn-takes-box-ref
Detects parameter types of the form
&Box<T>. The borrow of an owned pointer
adds a useless layer of indirection;
&T conveys the same intent without the
wrapper.
avoid
pub fn describe(card: &Box<Card>) -> String {
format!("{}: VALUE {}", card.name, card.score)
}
→
prefer
pub fn describe(card: &Card) -> String {
format!("{}: VALUE {}", card.name, card.score)
}
Why A &Box<T> can
always be obtained from a &T via
auto-deref, but the reverse forces every caller to pre-wrap
their value in a Box regardless of where it
actually lives. Source:
rust-unofficial/patterns
§Idioms (borrowed-types-for-arguments).
!
WARN
phase: pre
audit: yes
warn-expect-with-empty-message
Flags .expect("") in src/. Strictly
worse than .unwrap() — the same panic, plus the
noise of a method named expect with nothing to
expect.
avoid
let config = load_config().expect("");
let port = config.port.expect("");
→
give the invariant a name
let config = load_config()
.expect("config must exist at $CARGO_MANIFEST_DIR/config.toml");
let port = config.port
.expect("validate_config() guarantees port is Some");
Why When the panic eventually fires in
production, the message is what tells the operator which
invariant was broken. An empty message turns a debuggable
assertion into an opaque crash; the existing
warn-unwrap-in-src rule already flags
.unwrap(), but .expect("") escapes
it by being technically a different call.
○
audit-rc-refcell-in-src
Flags occurrences of Rc<RefCell<T>>
inside src/. The textbook
"fighting-the-borrow-checker" shape — often a signal that the
ownership model could be redesigned.
audit output
$ phr-mcp audit --rule audit-rc-refcell-in-src
Rule Level Hits Files
audit-rc-refcell-in-src warn 4 2
src/world/region.rs lines: 18, 42
src/world/graph.rs lines: 7, 91
→
redesign options
// arena + indices replaces shared mutability
pub struct RegionArena {
regions: Vec<Region>,
}
pub struct RegionId(u32);
// or: precompute the graph, treat it as immutable
// or: move mutability into a single owner, pass &mut
Why Rc<RefCell<T>>
is sometimes the right answer — for graph-shaped data with
true aliasing, for callback registries, for single-threaded
observer patterns. But it is also the canonical shape that
appears when someone has lost a fight with the borrow checker
and reached for the escape hatch. Surface it during one-time
audits so design intent gets revisited.
○
audit-string-concat-with-plus
Flags the " + & shape — string-literal
concatenated with a borrowed string. A signal that
format! would be clearer.
avoid
let msg = "hand " + &name + " drew " + &card.to_string();
→
prefer
let msg = format!("hand {} drew {}", name, card);
Why Plus-chained string concatenation
produces a sequence of intermediate String
allocations and forces awkward .to_string()
calls on non-string arguments. format! reads as
a template, allocates once, and accepts anything that
implements Display. Source:
rust-unofficial/patterns
§Idioms (concat-format).
○
audit-env-set-var-in-src
Flags env::set_var( in src/. Edition
2024 marks this call unsafe because mutating the
process environment is unsound when other threads read it
concurrently. The audit gives you a chance to verify intent.
avoid in library code
pub fn configure_database(url: &str) {
unsafe { std::env::set_var("DATABASE_URL", url); }
}
→
pass configuration explicitly
pub struct DbConfig { pub url: String }
pub fn connect(cfg: &DbConfig) -> Result<Conn, DbError> {
Conn::open(&cfg.url)
}
Why The standard library was tightened in
edition 2024 for the same reason this rule exists: env-var
mutation is process-global and unsynchronized, and any read
from another thread during the write is undefined behavior.
Tests where you control thread count are usually fine (and
already opt in with unsafe); library code
almost never is. Prefer passing configuration explicitly
through arguments or a context struct.
○
AUDIT
phase: audit
predicate: substring
audit-allow-dead-code-in-src
Flags #[allow(dead_code)] attributes in
src/. The compiler is telling you something; the
attribute silences it without addressing the underlying
question of why the code exists.
avoid
#[allow(dead_code)]
pub fn compute_secondary_score(&self) { /* never called */ }
→
either delete it, or document why it stays
// option A: delete
// option B: document the reason
/// Held in reserve for the upcoming scoring-revision RFC (see #142).
/// Not yet wired into the dispatch table; cargo would otherwise warn.
#[allow(dead_code)]
pub fn compute_secondary_score(&self) { /* ... */ }
Why Many #[allow(dead_code)]
attributes get added during refactors and then forgotten.
Audit sweeps surface them so the team can decide, code by
code, whether each one is still earning its keep.
audit-rust-let-binding-count-high · audit-rust-let-mut-count-high
Two heuristic audits that surface candidates for John
Nunley's block pattern: functions whose top-level
let ladder runs eight bindings or longer, or
whose top-level let mut declarations reach
three or more. Both go silent on functions that have
already adopted the pattern.
audited
fn build() {
let a = step1();
let b = step2(a);
let c = step3(b);
let d = step4(c);
// … eight outer-scope
// lets total
use_result(h);
}
→
block pattern (silent)
fn build() {
let result = {
let a = step1();
let b = step2(a);
let c = step3(b);
step4(c)
};
use_result(result);
}
Why A block expression leads with intent
(let result = …), keeps intermediate names
from polluting the outer scope, and scopes mutability — a
let mut inside a block returns an immutable
binding to its caller.
The walker halts at child { … }, closures,
and nested fn bodies, so the rule does not
punish the very shape it surfaces. Conditional and loop
bodies still recurse (they continue the outer flow).
Source: John Nunley, “Rust’s Block Pattern” (Dec 2025).
§III
3
Pack · Rhai — script hygiene for embedded engines.
2Rules
Discipline for projects that embed the Rhai scripting language.
The pack assumes a host that loads .rhai files,
compiles them to ASTs, and registers host-side functions the
scripts can call. The two rules push back on the two most
common entropy sources: inline engine.eval in the
Rust source, and ad-hoc print() in the scripts
themselves. Messages are intentionally generic — wire any
project-specific guidance (which loader helper, which
response-proxy) into your own .phronesis/rules.json.
✕
BLOCK
phase: pre
predicate: AST
block-rhai-inline-eval-string
Blocks calls of the shape engine.eval(<string
literal>) in Rust source. Inline string-eval
bypasses any script registry and resists isolated testing;
a .rhai file plus compile_file /
eval_ast is the cached, reviewable alternative.
avoid src/scripting/util.rs
let result: i64 = engine.eval("let base = 10; base * 2 + 5")?;
→
precompiled AST
// scripts/calc_priority.rhai is loaded once,
// compiled to an AST, then eval'd many times.
let ast = engine.compile_file("scripts/calc_priority.rhai".into())?;
let result: i64 = engine.eval_ast(&ast)?;
Why The compiled-AST path lets the script
be values-checked at build time, exercised in isolation,
cached across calls, and discovered via the file system
rather than buried as a literal in unrelated code. Inline
string-eval gives up all of those affordances for a
character-count win.
✕
BLOCK
phase: pre
audit: yes
block-rhai-print-in-script
Blocks print( in any .rhai file.
Rhai's print is the equivalent of
dbg!() — debug output that bypasses whatever
response/logging channel the host has registered.
avoid scripts/save.rhai
fn execute(actor, slot) {
print(`Saving state for ${actor} to slot ${slot}`);
save_state(actor, slot);
}
→
use the host-registered channel
// Whatever your Engine exposes via register_fn:
// log / emit / notify / response_append / etc.
fn execute(actor, slot) {
log(`Saving state for ${actor} to slot ${slot}`);
save_state(actor, slot);
}
Why Output from a shipped script should
flow through the same path as the rest of the
application's output — to be captured, replayed, formatted,
or addressed to a specific surface (user chat, server
log, audit trail). print escapes to stdout,
which is usually nowhere useful.
§IV
4
Other packs — Python · TypeScript · Swift.
11Rules
Smaller language packs ship with minimal seeds. Compose them with
--packs llm,python or similar. The intent is a
starting point; downstream projects are expected to grow their
own rules over time.
Python · --packs python
warn · warn-print-in-src
block · enforce-no-bare-except
Catches print() in src/ (use
logging) and bare except: clauses
(which swallow KeymapInterrupt and
SystemExit together with everything else).
TypeScript · --packs typescript
warn · warn-any-in-src
warn · warn-console-log-in-src
Catches : any annotations (use unknown
and narrow with guards) and stray console.log
calls in committed source.
Swift · --packs swift
warn · warn-swift-force-unwrap
warn · warn-swift-try-bang
warn · warn-swift-force-cast
○ · audit-swift-fatal-error
○ · audit-swift-mutable-singleton
○ · audit-swift-legacy-constructor
○ · audit-swift-legacy-random
Three force-bang warnings cover the postfix
!, try!, and as!
escape hatches — prefer guard let /
if let, try? with
do/catch, and as? binding. Four
audit-only rules surface debt: fatalError( sites
(prefer throws, precondition, or
assertionFailure); static var shared
mutable singletons (the canonical pattern uses
static let shared); the pre-Swift-3 C-style
constructors CGRectMake, UIEdgeInsetsMake,
and their siblings (use the labeled Swift initializers); and
the pre-Swift-4.2 random APIs arc4random,
arc4random_uniform, drand48 (use
Int.random(in:) / randomElement()).
Sourced from
eleev/swift-design-patterns
and SwiftLint's default-enabled rule set.
All three packs are intentionally lean. Phronesis's value scales
with how well the rules fit the project; canned defaults beyond
these few would be presumptuous about your codebase. Start with
the seeds, then run the drift detectors to find gaps.
phr-mcp claude-md-drift flags CLAUDE.md imperatives
without a corresponding rule. phr-mcp memory-drift
flags auto-memory entries. phr-mcp wiki-drift flags
ADR-style decision pages under .phronesis/wiki/decisions/
— decisions with explicit enforces: frontmatter
resolve deterministically; the rest fall through to Jaccard
matching. Grow the pack from there.