Skip to content
Back to skills

Audit Quality

ASecurity

Scoped architectural quality audit of a module or changeset — classifies each file SOUND / SHORTCUT / HACK against THIS codebase's own architecture, with the architecturally correct alternative, fix effort, and blocking status, then hands back a phased refactor plan. Read-only: it never edits code. TRIGGER when: the user asks whether code is a hack or architecturally sound, wants a quality or architecture audit of a module/directory/changeset, says code "works but feels wrong", asks what the ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 5, 2026
ai-agentsgobashexpressrailsrefactoringcode-reviewgitdatabasesecurityperformance

Works with

  • cli

Security analysis

A100/100

Scanned September 24, 2026

npx -y skills add mik2win/foureyes --skill audit-quality --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Audit Quality?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Audit Quality
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mik2win-audit-quality/badge)](https://www.skillsdirectory.com/skills/mik2win-audit-quality)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: audit-quality
disable-model-invocation: true
description: >-
  Scoped architectural quality audit of a module or changeset — classifies each file
  SOUND / SHORTCUT / HACK against THIS codebase's own architecture, with the architecturally
  correct alternative, fix effort, and blocking status, then hands back a phased refactor plan.
  Read-only: it never edits code.
  TRIGGER when: the user asks whether code is a hack or architecturally sound, wants a quality
  or architecture audit of a module/directory/changeset, says code "works but feels wrong",
  asks what the shortcuts in a just-implemented feature cost, or wants a refactor plan for an
  area before extending it.
  DO NOT TRIGGER when: the target is a diff and the question is defects (correctness, security,
  performance) — that is /code-review; the target is the whole repository and the question is
  where the debt is concentrated — that is /arch-health; the user wants the cleanups applied
  rather than assessed — that is /refactor; the target is dead code — that is /clean-mvp.
allowed-tools: Read, Grep, Glob, Bash, Write
context: fork
effort: high
---

# Architectural Quality Audit: $ARGUMENTS

Target: a file, directory, changeset, or commit range. Empty `$ARGUMENTS` → audit the files
changed on this branch, and say that is what you scoped to.

**Read-only.** This skill classifies and recommends; it never edits code. The deliverable is a
written report plus a refactor plan, not a set of fixes.

## Principle

**Architecturally correct, even when longer, beats a quick hack that "just works".** Every piece
of code should be what you would design knowing the requirements will change. Where it is a
shortcut, say so plainly and show what SOUND would have looked like *here* — a verdict without
the correct alternative is a complaint.

"Correct" means correct **for this codebase**: the layer map, the canonical patterns, and the
sanctioned exceptions all come from the profile, never from a generic notion of good design.

## Phase 0 — Load the architecture you are auditing against

- [ ] `PROJECT.md` → **Architecture**: the layer/module map, the dependency direction, where
      logic belongs and where it must not go. This is the layer map for Check 2 — do not invent
      one.
- [ ] `PROJECT.md` → the **canonical exemplars** it names (the factory, the registry, the entry
      shape) — these are the "canonical example" column of Check 3.
- [ ] `.claude/rules/_generic/*.md` and every `.claude/rules/` file whose `paths` frontmatter
      matches the target. Project rules and `CONTEXT.md` carry the **sanctioned exceptions** —
      a documented, blessed deviation is not a finding.
- [ ] `CONTEXT.md` for domain vocabulary, so findings are named in the project's own terms.
- [ ] **No profile yet?** If `PROJECT.md` is missing or still `TEMPLATE`, do not stop: prefer any
      architecture the root `CLAUDE.md` carries, else infer the intended structure from the tree
      and the target's siblings. State the assumption at the top of the report and proceed — an
      audit against an inferred architecture is worth having, an audit that pretends to a profile
      it doesn't have is not.

## Phase 1 — Read before judging

Read every file in scope **fully**, plus enough surrounding code to judge *pattern conformance,
not style*: the callers, the module's siblings, the interface or factory it should conform to.

**The evidence gate — before any HACK or SHORTCUT verdict.** A pattern that looks wrong and has
a logged reason is intent, not a hack. Prove it, or the file stays SOUND:

1. `git log -p -5 -- <file>` — was this shape introduced deliberately, and does the message say
   why?
2. The callers (`grep` the imports/uses) — does the "wrong" shape exist because of a real
   constraint at the call site?
3. The **sanctioned exceptions** in the project rules / `CONTEXT.md` — a blessed seam is legal by
   definition.
4. Prior settled decisions (ADR / decision log / the plan that shipped it) — re-litigating a
   decision the project already made is out of scope here; route it to `finding-verifier`.

Only after those four does a finding get a HACK or SHORTCUT verdict.

## The checks

Run all eleven over the scope. A check that does not apply to this stack (9, for a project with
no database) is stated as not-applicable, not silently dropped.

### Check 1 — Abstraction quality

- Is there polymorphism expressed as type-switching (`isinstance` chains, `if kind == "…"`)
  where the language's interface/protocol mechanism belongs?
- Is construction direct where the codebase has a factory, and dispatch hardcoded where it has a
  registry?
- Are string literals doing the job of an enumerated type?
- Are there magic numbers that belong in named constants or config?

### Check 2 — Layer placement

Against the layer map from `PROJECT.md` → Architecture. The recurring red flags, expressed
layer-neutrally:

- A **domain/core** module doing I/O.
- A **service** module doing presentation/formatting.
- An **infrastructure** module carrying business rules.
- An **entry-point** module (CLI command, controller, handler) doing anything beyond building
  config and delegating — see `rules/_generic/service-layer.md`.
- A module importing a **sibling in the same outer layer** where the profile says bounded
  contexts must not know each other.

### Check 3 — Pattern conformance

Does new code follow the established pattern, or introduce a one-off? Build the table from the
profile's exemplars — one row per pattern the codebase actually has:

| Pattern | Canonical exemplar (from the profile) | Red flag |
|---------|---------------------------------------|----------|
| <how this codebase constructs the thing> | `<path>` | <the hand-rolled alternative> |
| <how it registers a new variant> | `<path>` | <bypassing the registry> |
| <how it emits output / formats> | `<path>` | <raw output in the wrong layer> |
| <how modules talk across a boundary> | `<path>` | <a direct import that skips the seam> |

N divergent shapes for one job in sibling files is itself a finding: which one does a newcomer
copy?

### Check 4 — Dependency direction

- Grep the imports in every target file and trace the chains: does any lower layer import a
  higher one?
- Circular-import risk, and annotation-only imports that should be guarded.
- Does this changeset **introduce** an upward dependency that did not exist before? That is the
  one worth blocking on — a pre-existing one is a finding for the plan, not a blocker for this
  change.

### Check 5 — Hack smell

For each hit, say *why* it is a hack and what the correct pattern is:

- Magic numbers in logic · string constants used as dispatch · `isinstance`/type-switch chains.
- Global mutable state; module-level state mutated at runtime.
- Runtime attribute injection / monkey-patching.
- **Defensive masking** — catch-all handlers, silent fallbacks that hide the bug instead of
  handling a real failure mode (`rules/_generic/exception-patterns.md`).
- Copy-paste: 3+ lines duplicated with minor variation where a helper belongs.
- A flag parameter (a literal at every caller AND a branch in the body; enums count) — two functions.
- Deep nesting past ~3 levels where guard clauses would flatten it.
- **Explanatory hack comments** — `# workaround for …` is the code telling you the finding.

### Check 6 — Composition over inheritance

- Inheritance for anything but a true IS-A: a supertype method that does not apply to the subtype, or
  a subtype instance invalid where the supertype is valid (Liskov substitution, the `/prepare` SOLID
  row) — name the condition, not the slogan. Tells: an override raising "not implemented"; callers
  branching on the concrete type; inputs narrowed or outputs widened against the parent.
- Callers normalising the result (cast, null-coalesce, type check) — the callee broke the contract.
- A second axis of variation in the hierarchy, or a class modelling both a type and its instances.
- `super()` chained through several levels — fragile; prefer explicit delegation.

### Check 7 — Extensibility

If the requirements shift slightly, how much has to change?

- Are the extension points **explicit** (interfaces, registries, factories, plugin hooks), or
  does extending mean editing existing code?
- Count it: **how many files change to add one more variant** of the thing this module is about?
  The healthy answer is usually two — the new file, and the registry that lists it.
- Would swapping one external dependency touch more than its adapter?
- Ask first whether a second variant is coming. A module with one provider in two years is not
  weakly extensible: an absent extension point is a finding only when the variation is real.
- A finding whose fix would remove, move or collapse a seam carries an `ignore-if` line — the
  condition under which the shape is deliberate, and the check that ruled it out.

### Check 8 — Error handling

- Handled at the right level — not swallowed at the boundary, not propagated past the layer that
  can act on it.
- Domain errors distinguished from infrastructure errors.
- Recovery paths explicit and tested, not hoped-for.
- Silent swallows, and error handling that masks a bug rather than handling an expected failure.
- Retries on non-idempotent operations (see Check 10).

### Check 9 — Data-access patterns *(where the project has a datastore)*

Per the stack rules for the actual engine. Engine-neutral shape of the check:

- Indexes present for the columns the queried code filters, joins and orders on.
- No unbounded column selection in production paths.
- No N+1 access pattern — a query inside a loop over rows.
- **The client's commit/transaction semantics are the ones the code assumes** — an async or
  autocommit-off client that the code treats as autocommit is a silent data-loss bug.
- Multi-step writes that must be all-or-nothing are inside one transaction.
- Connection-time settings applied where the engine requires them.

### Check 10 — Write-path idempotency **(mandatory, and it can block a SOUND verdict)**

The invariant is already a write-rule (`rules/_generic/resilience.md` → Idempotency); this is its
**read-side obligation**. The audit must *open* the write-path, not infer health from structure.

For every state-mutating path in scope (persist, accumulate, finalize, publish, notify):

- **Fires twice** — replay, restart, retry, a re-delivered event: same end state, or a doubled
  one? A blind `+=` on a counter or a balance with no processed-id guard is the canonical defect.
- **Fires a day later** — by the time the retry runs, the state it read has moved: a token expired, a
  price changed, the row was deleted. A decision recomputed from live state then does nothing, or the
  wrong thing, and "same end state" still passes — so the check above cannot see it. A decision that
  depends on a value at trigger time is *passed* that value. Partial idempotency is fine when named.
- **Two racing paths reach the same write** — a normal path and its timeout/cleanup twin, a
  reconciliation loop overlapping the live handler.
- **The guard is real, not conventional** — a predicate in the UPDATE, a unique constraint, an
  upsert on a stable key, a deterministic idempotency key, a processed-id set. It has to be
  *enforced at the write* — a duplicate key raised and caught — not a `SELECT`-then-`INSERT` check,
  which passes this audit today and still races. **"The caller only calls it once" is not a guard.**
- **The transaction boundary matches the invariant** — a multi-step write that commits per step
  is a partial apply on crash.

**Verdict consequence.** A module that writes state and whose write-path you did not open
**cannot be classified SOUND** — either open it, or name the gap in the verdict line itself
(`core.md` → *verdicts carry denominators*: "sound across the read-paths I opened; write-path
unread"). A structural audit that never opens the write-path can say "the structure is sound",
never "sound". A double-apply found here is a **correctness bug** → Phase 0 of the refactor plan,
not structural debt.

### Check 11 — Prose as a claim about the code

Docstrings, comments, help strings and plan sentences that were true when written and are false now. Checks 1–10 read the code; this one re-derives the sentences *about* it as claims. (Mined from a 287-entry agent-memory corpus: ~60 of 109 stack-neutral lessons are this shape, the single most repeated one 43×.)

- **Grep the rule as prose, not as the symbol** — the symbol finds the code, never its twin sentence. Search the old condition's wording, the cardinal ("four routers"), every enumerated member, the old text as a quoted literal, the flag token, header "limits" lists, the comment above a changed constant, help strings, operator docs.
- **A completed enumeration must not carry a quantifier** — "both", "all", "the only" over a list someone will extend: enumerate the members or drop the count. One added member falsifies every universally quantified header above it.
- **A constant changed by more than ~2× invalidates its numberless premise** — the sentence with no number in it (the cost, the reason, the headroom) is the one nobody re-derived.
- **Comparatives name both populations** — "stricter"/"looser" than what, over which inputs; a delta labelled as one job's cost names the other writers of the measured store.
- **An absolute guarantee is read against the producer's non-happy branches** (absent, default, inferred, empty-is-truthy). Where prose claims a guarantee and also lists a residual, the residual wins.
- **"Now redundant" / "0 live today" / a past-tense migration note is a licence to delete** that a later session will act on. Re-run the census, or the change ships the permission ahead of the fact.

A finding is `path:line` + the sentence + the single grep or input that falsifies it — not "this comment looks stale". Rarer shapes and worked cases: [`docs/claims-audit-patterns.md`](../../docs/claims-audit-patterns.md).

## Verdict rubric

Classify **each file** by the worst thing in it:

- **SOUND** — architecturally correct for THIS codebase: right layer, right seam, conforms to the
  pattern its siblings established, invariants respected, tested where the profile expects tests.
  Would survive a requirements change. No action.
- **SHORTCUT** — works, but takes a debt-generating shortcut: duplicated logic instead of reusing
  the existing seam, a bypassed abstraction, a missing test for behaviour the profile classes as
  critical, a hardcoded value that belongs in config, divergence from the sibling pattern.
  Bounded fix; schedule it before building dependent features.
- **HACK** — violates the architecture or plants a trap: logic in a forbidden layer, an
  upward/sibling dependency the profile forbids, a design decision leaked across modules,
  swallowed errors or fail-open paths, an unguarded state mutation two paths can reach
  (double-applies on replay or a race), fake conformance (implements the interface, breaks its
  contract). Must be fixed before building on top.

A "cleaner would be nicer" observation with no concrete harm scenario — a failure, a newcomer's
confusion, a future extension cost — is not a finding. Drop it.

## Completeness pass (multi-file targets)

Run this against the draft audit, on yourself, before writing the verdict — the scope being more
than a couple of files is what makes it mandatory:

- **Every file in scope got a row.** Rebuild the scope list from the target (glob or `git diff
  --name-only`), diff it against the table, and name any file that has no row.
- **Every SOUND verdict rests on cited evidence**, not on the file having been skimmed.
- **Every check ran or was declared not-applicable** — layer placement, data access, and
  **write-path idempotency** especially. A SOUND row on a state-writing file with no Check-10
  evidence is exactly what this pass exists to catch.

A gap found here is closed before reporting, not footnoted.

**Why this is a self-check and not the `completeness-critic` agent.** Neither context this rubric
runs in can spawn: in a fork the spawn tool stays listed but returns an error instead of launching,
and preloaded into `quality-auditor` it is a subagent, where `Agent` is stripped outright
(`rules/_generic/delegation.md` § Tools an agent will not get). Either way a fan-out here would
read on the way out as a failed phase rather than as a configuration mistake. The independent
critic is not lost — it moves one layer up: the run hands its report back, so the **caller** can
run `completeness-critic` over the written file (its input contract takes a path), which is a
stronger check anyway — a genuinely separate context, reading the artifact rather than the draft.
Recommend it in the return line when the scope was large.

## Report format

```markdown
## Quality audit: <target>

| File | Verdict | Key finding (path:line) | Correct alternative | Effort | Blocking? |
|------|---------|-------------------------|---------------------|--------|-----------|

### Overall — SOUND | NEEDS WORK | ARCHITECTURAL DEBT
<1–3 lines: is this safe to build on?>

### Coverage
write-paths opened: <paths> · read in full: <paths> · read by diff or window only: <paths, or
"none"> · unread: <paths, or "none in scope">

### Priority fixes
1. <most impactful — file, what, why>
2. …

Architectural debt estimate: N files need refactoring before this area can be safely extended.
```

Every finding is anchored to `path:line` **as the code exists now**, and only in files you
actually opened.

**The Coverage line is not optional**, and it names the **route**, not just the set — "read in
full" is checkable against your own tool log and is routinely false. Measured over a 146-run
subagent corpus (2026-07-26..28): of 10 reports asserting the whole changed set was read in full,
**6 were contradicted by their own calls** — three had windowed every read of a changed file, and
three had run only a diff. Splitting the line makes the honest answer sayable: "read by diff
only" is a legitimate coverage level; claiming it as a full read is not.

## Refactor plan (REQUIRED — findings alone are half the job)

An audit that stops at a list hands back unordered work. After the verdict, produce a
**prioritized, ready-to-work** plan:

1. **Separate bugs from refactoring.** A HACK that *misbehaves at runtime* (wrong result, silent
   no-op, corrupted state) is a correctness bug, not structural debt. Bug fixes go in **Phase 0** —
   an independent first phase that ships before any restructuring and does not depend on it.
2. **Phase the rest by risk and dependency.** Each phase states what it changes, why, and its
   **reversibility** (easy-undo vs one-way). Pure relocations and extractions with no behaviour
   change are grouped and labelled as such — that is the safe bulk.
3. **Name the invariants that must not break** across the refactor: the behavioural contract the
   module guarantees today. These are the regression guardrails.
4. **Say what NOT to do.** Explicitly decline work that is not worth it — a facade worth keeping,
   a file worth leaving alone. A plan that refactors everything is as wrong as one that refactors
   nothing.
5. **Untested surfaces.** Flag touched code with no test coverage: that is where the plan is
   riskiest, and a characterization test goes in before the change.

```markdown
| Phase | Type (bug/structure) | Changes | Reversibility | Invariants at risk |
|-------|----------------------|---------|---------------|--------------------|
| 0 | bug | <correctness fixes, independent, ship first> | — | <what must still hold> |
| 1 | structure | <relocations / extractions> | easy-undo | |

**Do NOT touch:** <what to deliberately leave, and why>
**Test-first:** <untested surfaces the plan hits — characterization test before changing>
```

## Write the report to a file (REQUIRED)

This skill runs in a **fork**, so a result left only in the conversation is lost. Write the full
report — per-file table, Overall, Coverage, Priority fixes, Refactor plan — to the plans/backlog
location from `PROJECT.md` (else offer a path and confirm):

`<plans>/<target-slug>-audit.md`

where the slug is the audited path. If that file exists, write `-audit-2.md` — **never overwrite
a prior audit**; the old one is the evidence that a finding is a repeat. Then return, in the
conversation, a **short summary plus the file path** — not the whole report. When the scope ran
past a handful of files, add one line recommending the main thread run `completeness-critic`
over that file: the fork cannot spawn it, the thread that receives the path can.

**Exception — preloaded into `quality-auditor`.** That agent carries this file as its rubric
(`skills:` frontmatter) and `/implement` spawns it *synchronously*, collecting its reply and
continuing it by `SendMessage`. No fork, no notification to lose: there the final message **is**
the deliverable (`agents/quality-auditor.md` → Output), the agent declares no `Write`, and no file
is written. The obligation above is for the `/audit-quality` run.

## Hard rules

- **Read-only on app code.** Report, never fix. The only file you write is the audit report.
- **Evidence before verdict.** The four-step gate in Phase 1 runs before any HACK/SHORTCUT.
- **Every architecture fact from the profile.** Layer maps, canonical patterns, and sanctioned
  exceptions are read, never assumed.
- **Check 10 gates SOUND.** A state-writing file with an unopened write-path gets a named gap,
  not a clean verdict.

## See also

- **`/code-review`** — defects in a diff (correctness, security, performance). This skill asks
  whether the design is right; that one asks whether the code is wrong.
- **`/arch-health`** — the whole repository, ranking where debt is concentrated. This skill is
  scoped to a module or changeset.
- **`/refactor`** — applies quality cleanups. This skill produces the plan those fixes follow.
- **`quality-auditor`** agent — the same rubric as a fan-out unit over a disjoint file list, for
  when an orchestrating skill needs per-file verdicts rather than a report.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.