Skip to content
Back to skills

Effect Model Audit

ASecurity

Use when asked to audit this codebase's type modelling — whether the domain types are honest, whether they can represent illegal states, or for a periodic model-level type review. Triggers on "audit our type modelling", "are our types honest", "can our types represent illegal states", "model-level type review", "audit our domain types". The JUDGMENT sibling to effect-native-audit — a workflow-driven reasoning sweep over the port/domain type surface, not the anchor-driven regression net and no...

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 29, 2026
ai-agentsrustgonodeexpressgit

Works with

  • mcp

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 6, 2026

npx -y skills add CodeForBreakfast/commy --skill effect-model-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Effect Model Audit?

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

Security grade badge for Effect Model Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/codeforbreakfast-effect-model-audit/badge)](https://www.skillsdirectory.com/skills/codeforbreakfast-effect-model-audit)

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: effect-model-audit
description: Use when asked to audit this codebase's type modelling — whether the domain types are honest, whether they can represent illegal states, or for a periodic model-level type review. Triggers on "audit our type modelling", "are our types honest", "can our types represent illegal states", "model-level type review", "audit our domain types". The JUDGMENT sibling to effect-native-audit — a workflow-driven reasoning sweep over the port/domain type surface, not the anchor-driven regression net and not the @effect/language-service linter.
---

# Effect-model type audit

Sweep the **port/domain type surface** for **model-level dishonesty** — types
whose *shape* lets a caller construct, or forces a caller to fake, a value the
domain does not mean. This is not "did we hand-roll a helper" (that's the
sibling) and not "is this line idiomatic" (that's the LSP). It is: **does the
type's value space match the domain's, and do all its producers and consumers
mean the same thing by it?**

Four orthogonal **value-space lenses**, each run against each type-family:

- **L1 Illegal-representable** — can the type represent a value that is *invalid*
  in the domain? A combination of fields, an optional that should be required, a
  bare primitive admitting an out-of-domain string, a boolean pair whose illegal
  corner is representable. The general form of "flags-and-optionals for state"
  and "stringly-typed value", **unbound from any grep costume** — reason about
  the value space, don't match a token. *Invalid state must be unrepresentable.*
- **L2 Under-expressive** — the dual: is there a *legal* domain state the type
  **cannot** represent, forcing a sentinel, a "transparent placeholder", an
  impossible branch, or a comment apologising for a field that is "always absent
  here"? An under-expressive type pushes the missing case into convention or into
  a lie.
- **L3 Role coherence** — from the producer/consumer **census**: do all
  construction sites and all read sites mean the *same thing* by this type, or is
  it two roles in a trenchcoat — an **address** (what a caller supplies to target
  something) vs an **observation** (what the substrate hands back)? The tell is a
  producer that can legally build a value that is *illegal for some consumer's
  role*. **This is the reasoning that isolation skips** — and exactly what let the
  comms-ro14 `ChannelRef`/`ThreadRef`/`MessageRef` conflation through: every field
  was honest alone, the illegality was *relational* across the census.
- **L4 Boundary honesty** — at a substrate/trust boundary, does the type's *role*
  demand a parsed-once strong representation (a `Schema.Struct` with required
  fields, branded members, a decoded-once refinement) that it does not have — a
  loose interface the boundary must defend by convention? *Parse at boundaries,
  trust inside.*

## Why this is the complement the sibling can't reach

`effect-native-audit` is a **frequent, cheap, anchored regression net**: every one
of its ~41 finders is **anchor-driven** — it starts from a `grep:` pattern and
confirms a shape at the hit. That is exactly what makes it cheap to re-run and
deterministic to reason about, and it is also a **structural blind spot**: a
finder can only find a defect that has a *lexical anchor*. Role-conflation has
none. The comms-ro14 address/observation defect — a `ThreadRef` whose optional
narrow was honest for the *address* role but illegal for the *observation* role —
was **not** caught by any effect-native-audit sweep. It was noticed by hand while
building `resolve_thread`, because **you cannot grep for a concept**: every field
was honest in isolation; the illegality was relational.

This skill is the **periodic, expensive, judgment** sweep that reaches those
defects. It keeps the catalogue's discipline that matters — *coverage by
decomposition*, one bounded agent per enumerated cell, deterministic coverage, no
agent cherry-picking — but **changes the cell** from `pattern` to **`unit × lens`**.
The agent holds one **unit** (a type-family plus its full producer/consumer
census) and one **lens** (a value-space question) and **reasons**; it does not
scan for a token.

Three coverage layers, three jobs — **never re-report across them**:

| | anchor | cost | catches |
|---|---|---|---|
| `@effect/language-service` | per-line rule (~60), `tsc`-time | free, every `bun run check` | floating Effects, unnecessary `Effect.gen`, global `fetch`/`Date` |
| `effect-native-audit` | a `grep:` pattern per finder | cheap, frequent | hand-rolled helpers, imperative spines, present-but-anchorable representation smells |
| **`effect-model-audit`** (this) | a **type-family + census** per finder, reasoned | expensive, periodic | **relational / model-level dishonesty with no lexical anchor** |

L4 deliberately **borders** the sibling's `unvalidated-boundary` (a *missing*
decode at a trust edge) and `internal-bridge` (a `decodeUnknownSync` bridge that
*exists* at a seam). L4 is **model-level**: the type's *role* demands a strong
parsed representation, independent of whether any decode call exists. If a finding
is really "a decode call is missing here" or "there is a `*Sync` bridge here",
that is the sibling's territory — file it there, not as L4. The lens carries an
explicit boundary statement (see `domains.md`) so the axes don't double-count.

## How it works

A `Workflow` script (`effect-model-audit.workflow.js`) runs a four-phase pipeline.
The lenses are grounded in **modelling principles first** — the local
`strengthening-types` skill (`~/.claude/skills/strengthening-types`), DDD
make-illegal-states-unrepresentable, and Effect's `data-types/*` + `code-style/*`
design docs as the *vocabulary* for the honest representation (not an export
inventory to match against).

1. **Discover** — a cheap pre-pass enumerates the raw export surface
   (`grep '^export'` over `packages/core/ports.ts`) and groups every exported
   domain type into type-**families**, each bundled with **every** construction
   site (producers) and **every** read site (consumers) across `packages/`. The
   census is the heart of the pass — *reasoning about a type without its usage is
   the isolation that blinds the anchored audit*, so it is what makes cross-type
   role-conflation visible.
   - **HARD INVARIANT — a verified partition.** Every exported domain type lands
     in **exactly one** family. This is checked as a **coverage equation**
     (`assertPartition`) against the raw export list — missing (an export in no
     unit), duplicated (in >1), or unexpected (a member that isn't an export) all
     fail. A deterministic backstop sweeps any un-grouped export into an explicit
     `unassigned` unit so nothing escapes audit. Coverage is an equation, not a
     trusted list — this is how the no-cherry-picking bar is met at the unit level.
2. **Reason** — one bounded agent per **unit × lens** cell. The agent holds one
   family + its census and one lens, reads the actual type definitions and census
   sites, and **reasons** about the value space. Empty findings is a valid, honest
   result — a well-modelled family *should* return none.
3. **Verify** — per-finding, no barrier: each finding flows into one adversarial
   refuter the moment its finder returns. Verify is **default-surfaced and
   evidence-gated** — consistent with a modelling audit, a surfaced false positive
   is cut cheaply at the gate, but a refuted true smell is invisible. The refuter
   **refutes** any finding not grounded in **concrete evidence**: a named illegal
   value (L1), a specific missing legal state + the workaround it forces (L2), a
   specific disagreeing producer/consumer *pair* (L3), or the specific boundary
   (L4). "I'd model this differently" with no named illegal value is taste-noise,
   cut. **"Is the rewrite worth the blast radius" is never a refutation ground —
   it is a gate concern.** A verifier error keeps the finding (surfaced,
   unverified), never silently drops it.
4. **Synthesise** — a synthesis agent writes the report prose, deduping
   cross-lens sightings (L1 and L3 overlap **by design** — the same relational
   defect reached by the isolation path and the census path; they dedup by
   `file:line`). The confirmed / low-confidence **issue partition** is **not**
   trusted to that agent — the workflow derives it deterministically from each
   survivor's own confidence (`splitSurvivors`: dedup by `file:line`, highest
   confidence wins, then split).

Each surviving finding carries **concrete evidence** (the illegal value / the
disagreeing pair / the boundary), a **proposed remodelling**, and its
**blastRadius** — every producer + consumer a remodelling ripples to. The census
already computed the blast radius; here the ripple *is the whole point*.

## Running it

1. Get today's date (`date +%F`).
2. Launch the workflow, passing the date so the report file is named:

   ```
   Workflow({
     scriptPath: '.claude/skills/effect-model-audit/effect-model-audit.workflow.js',
     args: { date: '<YYYY-MM-DD>' },
   })
   ```

   It runs in the background (a discovery pass, then one agent per `unit × lens`
   cell — ~10 concurrent — then per-finding verify + synth); you are notified on
   completion. Watch live with `/workflows`. To run a single lens (cheaper re-run
   after editing its prompt, or to resume just one), pass
   `args: { date, lenses: ['L3'] }`.

3. The workflow writes the report to `docs/effect-model-audit-<date>.md` and
   returns `{ confirmed[], lowConfidence[], survivors[], refutedCount, coverage,
   partitionOk }`.

## After it returns — gate, then file

The workflow returns findings and does not file them. Model findings are
**architectural** — a sweep can surface a handful of type remodellings, and
filing them unreviewed clogs the backlog with work that hasn't been arbitrated.

The report is working scaffolding for the review, not a permanent record. File
what is worth keeping, then delete `docs/effect-model-audit-<date>.md`.

1. **Present** the `confirmed[]` list to the maintainer — `file:line`, unit,
   lens, the **evidence** (the named illegal value / disagreeing
   producer-consumer pair / boundary), the proposed remodelling, and the
   **blastRadius**. Also surface the `lowConfidence[]` items so any worth keeping
   can be promoted; un-promoted ones are dropped with the report.
2. **Wait for go-ahead — the gate leans harder on the maintainer here than the
   sibling's.** A model remodelling is a **taste-and-cost call**, not a
   mechanical helper swap: the finding says a *real* illegal state or
   role-conflation exists, but whether to split a type, brand a scalar, or absorb
   the blast radius is a judgment about the whole design. The maintainer may cut,
   downgrade, or reshape a confirmed item, or promote a low-confidence one,
   before anything is filed. **"Is the remodelling worth the ripple" is decided
   *here*, deliberately — the verify pass was forbidden from touching it.**
3. **File each approved finding** as its own issue, grouped under one tracking
   issue. Each description must be **self-contained** — it has to stand on its
   own once the report is gone. Two fields are non-negotiable:
   - the **evidence** — the concrete illegal value, missing state, or disagreeing
     producer/consumer pair (with both sites) that proves the smell is real, and
   - the **blastRadius** — every producer + consumer the remodelling ripples to. A
     type change is never local; that ripple is the real cost and the issue is
     unfileable without it.

   Do not point at the report; it won't survive.

Each filed item is its own remodelling; implement them separately with TDD, not
as a batch — a type change ripples through its whole census.

## Notes

- **Modelling principles ARE the ground, not an export inventory.** Unlike the
  sibling's substitution axis (which needs the complete `Array`/`Option`/… export
  surface in front of the finder), these lenses are grounded in *why a
  representation is honest*: the local `strengthening-types` skill, DDD
  make-illegal-states-unrepresentable, and Effect's `data-types/*` + `code-style/*`
  design docs as the vocabulary for the fix. See `domains.md`.
- **Local clones, GitHub fallback.** `strengthening-types` at
  `~/.claude/skills/strengthening-types/SKILL.md`; Effect docs at
  `~/Development/references/effect-website` (the URL `…/docs/<slug>/` maps to
  `…/content/src/content/docs/docs/<slug>.mdx`). Both are workstation-local; a
  cloud/CI agent falls back to `WebFetch` on effect.website, just slower.
- **Scope v1 is the port/domain surface.** `packages/core/ports.ts` (the branded
  schemas, the address/observation refs, the port interfaces, the tagged errors),
  with the producer/consumer census spanning every construction + read site across
  `packages/` (adapters *construct* refs; application/mcp *read* them). Not every
  exported type in every package — that's where model-honesty defects bite and it
  keeps v1 bounded. Widen later if it earns its keep. `*.test.ts` and
  `node_modules` are out of scope.
- **The authoritative lens catalogue is the `LENSES` array** in
  `effect-model-audit.workflow.js` (the workflow can't read files, so it drives the
  finders from that array directly). `domains.md` mirrors it for humans — keep them
  in sync.
- **No auto-fix.** Findings become tracked, arbitrated work, not an automated diff.

Files in this skill

  • SKILL.md14.2 KB
  • domains.md9.1 KB
  • effect-model-audit.workflow.js32.2 KB

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.