Skip to content
Back to skills

Extract Codebase Overview

ASecurity

Master orchestrator for deep codebase analysis in /setup-project Phase 2. Walks the project's architecture, modules, base classes, data model, API surface, naming conventions, and dependency graph — outputs `.claude/_extracted-codebase.md`, the substrate every Phase 4 generator reads to author project-specific (not generic) content. Invokes sub-skills (extract-base-class-idiom, extract-business-context) and consolidates results.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
developmentjavascripttypescriptpythonrustgojavashellbashsqlvue

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill extract-codebase-overview --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Extract Codebase Overview?

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

Security grade badge for Extract Codebase Overview
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-extract-codebase-overview/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-extract-codebase-overview)

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: extract-codebase-overview
description: Master orchestrator for deep codebase analysis in /setup-project Phase 2. Walks the project's architecture, modules, base classes, data model, API surface, naming conventions, and dependency graph — outputs `.claude/_extracted-codebase.md`, the substrate every Phase 4 generator reads to author project-specific (not generic) content. Invokes sub-skills (extract-base-class-idiom, extract-business-context) and consolidates results.
kind: skill
pack: learning
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash]
---

# Skill: extract-codebase-overview

## Purpose

`/setup-project` historically detected stack (manifest scan) + signals (grep) + walked one module. That's enough to copy generic packs but NOT enough to AUTHOR project-specific content. This skill produces the **full picture** that Phase 4.2-AUTHOR reads to generate output that matches your codebase as if a senior engineer who lives here wrote it.

The output (`.claude/_extracted-codebase.md`) is the **single source of truth** for every downstream generator: pattern files, agent system prompts, rules, conventions doc, ADRs.

## Premise

- Real source is the truth. Walk manifests + source files + migrations + tests + git log before populating a single section.
- Every cited path, base class, entity, controller, signal, and convention claim resolves to a real file at the current commit.
- Every confidence assertion (e.g. "multi-tenant: confirmed") is backed by ≥2 corroborating cites per Step 15.
- **Every claim declares its provenance class** (per `phase-2-profile.md § Provenance discipline`): a resolving `<path:line>` citation = found; no citation → the claim MUST carry `[inferred: <basis>]` or `[unconfirmed]`. Inference stated as fact is the Trusted Summary anti-pattern applied to the oracle itself.
- Empty extraction is honest — the explicit "no ORM-like data layer detected" / "no modules detected" / `[EXTRACTION-WEAK: <reason>]` flag is a valid section outcome.
- **Partial extraction is honest too — but only when it says so.** Several steps below cap or sample *by design* (Steps 3, 4, 5, 6, 8, 12). A section built from a sample carries `[SAMPLED: <seen>/<present> <unit>]` on its heading, and every run reports `## Coverage` unconditionally — including at 100%, which is the strongest claim this skill can make about its own output. A citation proves the file it points at; it never proves the population that file was drawn from.
- Fabrication — inventing a base class from a folder name, a convention from one occurrence, a signal from a dep that isn't actually used — corrupts every Phase 4 generator that reads this file.

## Mechanical halt

- Hand-wave content in any section — `etc.`, `...`, `roughly layered MVC`, `appears multi-tenant`, a row without `<path>` citation, a signal claim without ≥2 corroborating files — REFUSE to write.
- An uncited claim with no `[inferred: <basis>]` / `[unconfirmed]` marker is the same violation — regenerate with a citation or downgrade the claim to its honest provenance class (per `phase-2-profile.md § Provenance discipline`).
- A **generalizing** claim ("all entities carry a tenant column", "file naming is always kebab-case", "errors always route through the shared handler") drawn from a `[SAMPLED]` section may NOT be written as `[found:]`. The citation resolves; the generalization does not. Downgrade to `[inferred: <basis>; sampled <seen>/<present> <unit>]` per `phase-2-profile.md § Provenance discipline`. This is the one provenance error a resolving citation actively hides — Step 15's path check passes it, which is exactly why check 7 exists.
- Regenerate the section with concrete cites OR downgrade it to `[EXTRACTION-WEAK: <section>]` per Step 15.
- When a section genuinely has nothing (empty repo, no ORM, no controllers, no tests), record `<NOT-DETECTED: <section>: <reason>>` instead of synthesizing a placeholder row.
- Quality flags propagate to Phase 4.2-AUTHOR which falls through to COPY mode for affected topics — silent fabrication breaks that fallback.

## When to use

- `/setup-project` Phase 2 — runs once at the top of every ENHANCE / REFRESH session.
- On-demand via `/refresh-knowledge` after major refactors / new modules / dependency upgrades.
- Skipped in CREATE mode (no codebase to read yet — placeholder created, refresh runs once code exists).

## Inputs

- `project_root` — absolute cwd (the project being analyzed).
- `output_path` — defaults to `<project_root>/.claude/_extracted-codebase.md`.
- `parallelism` — default 6 concurrent extractor subagents (cap to avoid runaway tokens on huge repos). Recorded in `## Coverage` as part of `walk_scope`: it bounds what the run *could* visit, so it belongs with the census, not in a comment.
- `repo_shape` — **required**, one of `single` / `monorepo` / `workspace`. Decided by Phase 1 (`phase-1-detect-mode.md § Decide shape`) and handed in verbatim per `phase-2-profile.md § 2.0.a`. Step 2 consumes it; it is never re-derived here.
- `shape_signal` — **required**, the Phase 1 table row that fired, as a string. Recorded verbatim in `## Repository shape` so a reader can audit the shape decision without re-running it.
- `members` — **required**, never empty; `single` carries exactly one entry, root `.`. One entry per member:
  ```
  members:
    - name: <member-a>
      root: <path/to/member-a>    # `.` for repo_shape: single
      manifest: <path/to/member-a/<manifest-file>>   # or `(none)`
  ```
  This list is the split key for Step 2, Step 2.5 and every capped step. **Absent or empty `members` is a halt, not a default** — see Step 2.

## Procedure

### Step 1 — Stack + manifest detection (fast, deterministic)

Run Appendix A of setup-project.md detection commands. Capture:
- Languages (TypeScript, Python, Go, etc. — from file extensions).
- Frameworks (NestJS, Django, Rails, etc. — from manifests).
- Databases + ORMs (Postgres, MySQL, TypeORM, Prisma, etc.).
- Build tools (esbuild, vite, webpack, bun, turbo, etc.).
- Test frameworks (Jest, Vitest, Pytest, RSpec, etc.).
- Package manager (`bun`, `pnpm`, `npm`, `pip`, `cargo`, etc.).

**Persist as `## Stack` section.**

### Step 2 — Repository shape (handed in, not re-derived)

`repo_shape`, `shape_signal` and `members` arrive as inputs. This step **consumes** that decision; it does not re-answer it.

**Do not scan for a workspace manifest to decide the shape here.** `pnpm-workspace.yaml`, `lerna.json`, `nx.json`, `turbo.json`, Cargo `[workspace]`, `go.work` cover only the two shapes that declare their own members. The two that declare nothing — sub-manifest dirs inside ONE git repo (**monorepo**), and separate checkouts or a plain `server/`-beside-`client/` pair under one parent (**workspace**) — are precisely the shapes a manifest scan answers `single` for, and a plain `server/` + `client/` pair matches none of `apps/` / `packages/` / `libs/` / `services/` either. That silent `single` is not a loud failure: it writes zero member entries, Step 2.5 collapses to one `(root/other)` bucket, and every per-member contract below becomes unreachable without anything refusing. Phase 1 already fired on signals a manifest scan does not reproduce; re-deriving here discards that answer.

**Missing input is a halt, not a default.** `members` absent or empty → do NOT proceed to Step 2.5, and do NOT enumerate members yourself. Write `[SHAPE-INPUT-MISSING: members not handed in by Phase 1]` and stop; re-invoke with the Phase 1 output (`phase-2-profile.md § 2.0.a`). Inventing the list here reintroduces the exact bug the input exists to remove.

**The entry set IS `members`** — same order, one `## Repository shape` entry each, no additions and no drops. Per entry record:

| Field | Source | Rule |
|---|---|---|
| `name`, `root` | the input, verbatim | Never rewritten, never normalized away. |
| `manifest` | the input, verbatim | `(none)` is a legal value. A member directory with no manifest is still a member — that is the sibling-directory shape, not a non-member. |
| manifest type, one-line purpose | the member's OWN manifest description, or the top of the member's README | **Enrichment only.** `[unconfirmed]` when neither exists. Enrichment never adds, removes, renames or merges a member. |

Record `repo_shape` and `shape_signal` verbatim at the top of the section, so the shape decision is auditable from the artifact without re-running detection.

`repo_shape: single` carries exactly one member, root `.` — the same code path, not a special case.

**Halt**: `members` holds ≥ 2 entries while the section you are about to write holds 1. That is a dropped member; a member with no entry gets no census bucket, so it gets no walk, no conventions row, and no rules. Fix the entry set before continuing — `phase-5-verify.md § 5.1` errors on exactly this state, and nothing downstream can recover a split the substrate never made.

**Persist as `## Repository shape` section.**

### Step 2.5 — Source census (deterministic — runs before any prose is written)

Six of the steps below cap or sample. Until the population they sampled *from* is written down, "sample 10 files per category" is not a disclosure — it is a number with no denominator. This step computes the denominator, in shell, before the model writes a word of prose. That ordering is the whole point: Step 15 check 7 is then arithmetic against numbers the model did not author.

**Denominator (`present`) — tracked files, not `find`:**

```bash
git ls-files -z | tr '\0' '\n'        # census_method: git-ls-files
git rev-parse --short HEAD             # the commit the census is pinned to
```

`git ls-files` for three reasons: it inherits `.gitignore` for free, so vendored / installed / built trees never enter the denominator; it is deterministic at a commit, which is the same premise every citation in this file rests on (§ Premise); and it is uniform across every stack. **Repo without git** (already a declared failure mode) → fall back to `find` with an explicit exclude list and record `census_method: find`, so the number is only ever compared against another `find` number.

From that list, keep files whose extension is in the source-extension set derived from `## Stack` (Step 1), then subtract — and **name every subtraction in the output**:

- test-glob matches (Step 10 owns those; they are counted separately, not silently dropped),
- lockfiles and manifests,
- generated / vendored paths not already excluded by `.gitignore`,
- non-source extensions (docs, fixtures, assets, data).

An exclusion that is not named in the output makes the denominator unfalsifiable — the same violation § Premise forbids of every other claim in this file. If "is a `.sql` migration a source file?" is a judgement call, the answer does not matter as long as the call is printed.

Bucket the survivors **one row per `## Repository shape` entry** (Step 2) — which is one row per handed-in `member`, since Step 2's entry set IS `members` — plus a `(root/other)` row for tracked files under no member root. Without the per-package split, one well-covered app hides nine untouched ones behind a healthy aggregate. **Halt**: fewer buckets than `members` means a member was dropped upstream; return to Step 2 rather than writing an aggregate census that cannot show the gap.

**Numerator (`seen`) is NOT computed here.** It is counted in Step 15 as `files_cited` — the distinct paths appearing in `[found:]` citations actually written. Never label it `files_read`. It undercounts (a file can be read and cite nothing) and that is the correct trade: `files_cited` is the only number a third party can audit from the artifact alone, and Step 15 already walks every citation to confirm the path resolves. An unauditable coverage number would be a worse failure than today's silence.

Also record, for the header block: `census_method`, `walk_scope` (which directories the walk was allowed to enter, the depth cap in force, and `parallelism`), and the per-package `present`.

**Per-section denominators.** Each capped step declares what its population is, so its `[SAMPLED: <seen>/<present> <unit>]` marker is an auditable ratio rather than an adjective:

| Section | Population (`present`) | `seen` | Cap forcing the sample |
|---|---|---|---|
| `## Architecture` (Step 3) | source files under the layer dirs walked | files cited | 5-10 sampled per layer |
| `## Modules` (Step 4) | dirs matching the module test | rows emitted | cap 80 |
| `## Base classes` (Step 5) | bases with ≥3 extenders | bases with an idiom extraction | `parallelism` |
| `## Data model` (Step 6) | entity files matched by the Step-6 globs | rows emitted | cap 60 |
| `## API surface` (Step 7) | controller / router files found | rows emitted | **none declared → must be 100%** |
| `## Conventions` (Step 8) | files per category | files sampled per category (10) | sample 10 per category — **plus** `[CONTESTED: <A> n/N, <B> m/N]` per non-unanimous category (Step 8) |
| `## Cross-cutting concerns` (Step 9) | files matched by that signal's detection greps | files cited as corroboration | none declared — **a signal verdict is a ratio, never a bare word** (Step 9) |
| `## Anti-patterns observed` (Step 11) | source files in the member's bucket (the same `present` the census computed) | files the grep actually matched | none declared — a count with no population is not a finding (Step 11) |
| `## Recent activity` (Step 12) | commits in the window | 50 | `head -50` |

A `[CONTESTED]` row is auditable off this table for the same reason a `[SAMPLED]` heading is: both denominators are printed. `n + m ≤ sampled` and `sampled ≤ present` are checkable arithmetic, so a contest cannot be asserted without a population to assert it over.

**Every population in that table is per member.** When `members` holds ≥ 2 entries, each capped or sampled step below runs once per member, over that member's own bucket, and every row it emits names the member it came from. A step that walks the union and reports one winner produces a blended value that is wrong for every member and carries a citation that resolves — which is what lets it survive review (`phase-2-profile.md § 2.0.a` obligation 3, `§ 17`). Cross-member aggregates may be reported *in addition*, never *instead*.

**Persist as `## Coverage` section** — written last (it needs Step 15's numerator) but placed **FIRST** in the body, before `## Stack`. First, not last: a reader who reaches `## Conventions` should already know what it rests on.

### Step 3 — Architecture + layering

For **each member** in `## Repository shape` (Step 2), walk that member's top-level source dirs under its `root` and infer the architectural style — one verdict per member, never one blended verdict over the union:
- **Layered MVC**: `controllers/` + `services/` + `models/`.
- **Hexagonal / Clean**: `core/` + `application/` + `infrastructure/` + `adapters/`.
- **Module-per-feature**: each subdir under `src/<feature>/` has its own controller + service + repo.
- **DDD**: presence of `aggregates/`, `value-objects/`, `domain-events/`.
- **Functional / no-classes**: predominantly modules of pure functions.

Detect dependency direction via import graph (sample 5-10 files per layer; check what they import). Flag if direction violates declared style.

**Persist as `## Architecture` section** — heading carries the Step 2.5 `[SAMPLED: <seen>/<present> files]` marker whenever the per-layer sample did not exhaust the layer dirs walked.

### Step 4 — Module enumeration

Walk **each member's own source dir** — `<member.root>/src/`, `<member.root>/app/`, or whatever that member actually uses. Do NOT walk a fixed `apps/*/src/` / `libs/*/src/` glob as the member source: it misses every sibling-directory member (a plain `server/` + `client/` pair matches neither), which is the same manifest-shaped blindness Step 2 removes. Each subdir at depth 1-2 that contains a controller + service + repo (or analogue) = one module.

For each module, record one row:
- **member**, name, path (relative to the repo root, member root included: `apps/api/src/modules/orders/`, never `modules/orders/` — `ai/modules.md` and the boundary hook read it that way), layer (core/feature/infra/lib), purpose (one-line from README or top comment), primary entity, file count.

Cap at 80 modules in the output (sample more if larger; emit `(+N more)` line).

**Persist as `## Modules` section table** — when the 80-cap fires, the `(+N more)` line stays AND the heading carries `[SAMPLED: <rows>/<dirs matching the module test> modules]`. The `(+N more)` line discloses the truncation; the marker is what downstream consumers parse.

### Step 5 — Base class detection (then delegate)

For every TypeScript/Python/Java/etc. class with subclasses, count extenders:
```bash
# TS example — through the same census Step 2.5 took (git ls-files), never a raw tree walk:
# `grep -r … .` walks node_modules, dist and test trees and counts vendored extenders.
git ls-files -z -- '*.ts' ':!:**/node_modules/**' ':!:**/dist/**' ':!:*.spec.ts' ':!:*.test.ts' \
  | xargs -0 grep -lw -- "extends <BaseName>" 2>/dev/null | wc -l
```

Capture every base class with **≥3 extenders**. For each:
- Path of the base class file.
- Extender count.
- Sample 3-5 representative extenders (smallest, most-customized, edge-case).

For each base class meeting the threshold: **invoke `extract-base-class-idiom` skill** with `base_class_path` + `output_path = .claude/_extracted-idioms.md` (append section per base). Run up to `parallelism` extractions concurrently via Explore subagents.

If a base class has <3 extenders: skip extraction; record name + path + extender count only ("not yet load-bearing — re-evaluate on next refresh").

**Persist as `## Base classes` section + cross-link to `_extracted-idioms.md`** — heading carries `[SAMPLED: <bases with an idiom extraction>/<bases with ≥3 extenders> bases]` when `parallelism` or a run-length cap left qualifying bases unextracted.

### Step 6 — Data model

Find the entity/model layer:
- TypeORM: `*.entity.ts` files.
- Prisma: `schema.prisma`.
- SQLAlchemy: `Base.metadata` consumers.
- Django: `models.py`.
- ActiveRecord: `app/models/*.rb`.
- Sequelize: `*.model.ts`.

For each entity, record: name, path, table name (if explicit), columns (just names + types — not values), relations (`@OneToMany`, `belongs_to`, etc.), softdelete? (look for `deleted_at` column or `@SoftDelete()` decorator), tenantId? (look for `tenantId` / `tenant_id` column).

Cap at 60 entities; emit `(+N more)` line.

Detect cross-cutting concerns visible at the data layer:
- All entities have `tenant_id` → **multi-tenant** signal confirmed.
- All entities extend `SoftDeleteEntity` / `BaseEntity` → soft-delete is universal.
- Translation tables (`*_translations`) exist → i18n at data layer.
- Audit columns (`created_by_id`, `updated_by_id`) → audit subscriber pattern.

**Persist as `## Data model` section** — when the 60-cap fires, keep `(+N more)` AND add `[SAMPLED: <rows>/<entity files matched> entities]` to the heading. The cross-cutting inferences directly above ("ALL entities have a tenant column") are exactly the generalizing claims § Mechanical halt forbids writing as `[found:]` off a sample.

### Step 7 — API surface

Find controllers / routers / route files:
- NestJS: `*.controller.ts` with `@Controller()`.
- Django: `urls.py` + view classes.
- FastAPI: files with `@app.get` / `@router.get`.
- Express: `app.use(...)` / `router.get(...)`.
- Rails: `routes.rb` + `*_controller.rb`.

For each controller, record: name, path, route prefix, # endpoints, auth scheme used (decorator/middleware).

Also detect: how does the project structure response shapes? (e.g., `createApiResponse(dto, message)` wrapper detected → standard response envelope is in use). What's the error mapper? (HTTP exception filter, problem-details middleware, etc.).

**Persist as `## API surface` section** — no cap is declared for this step, so the Step 2.5 denominator row requires `seen == present` here, and **check 7 now enforces it** (see its `none declared → must be 100%` bullet). Until that bullet existed, a short walk marked `[SAMPLED: <seen>/<present> files]` passed check 7 while breaching this step's floor: the check only rejected an *undisclosed* sample, and a disclosed one looked identical to a complete walk. Disclosure is still required; it is no longer sufficient. If a run cannot reach every controller / router file found, that is a real coverage loss and the heading must say so rather than the cap being invented after the fact.

### Step 8 — Convention auto-detection

Sample 10 files per category and extract patterns:
- **File naming**: kebab-case / snake_case / camelCase / PascalCase. Suffix patterns (`.service.ts`, `.controller.ts`, `_service.py`, etc.).
- **Class naming**: PascalCase confirmed. Suffix matrix (Service / Controller / Repository / Mapper / Entity).
- **Property naming**: camelCase / snake_case.
- **DB column naming**: from migrations or entity column decorators.
- **Test naming + colocation**: `*.spec.ts` / `test_*.py`. Colocated next to source vs separate `test/` dir.
- **Constants**: UPPER_SNAKE_CASE / PascalCase / etc.
- **Imports**: relative-only / alias-based (`@app/`, `@/`, `~/`). Read tsconfig/pyproject for aliases.
- **DI tokens**: string constants / Symbols / class refs.

**Per category, the sample is not always unanimous — record the split, never the winner.** `[SAMPLED: 10/412 files]` is emitted identically whether 10 of 10 sampled files agreed or 6 of 10 did. Disagreement *inside* the sample is a different fact from disagreement between the sample and the population, and until this step records it, nothing downstream can: the honest output of a legacy codebase with three competing conventions was an average true of none of them, stated as this project's convention, carried into `ai/conventions.md` and into the anchor at the top of every adapted rule, where the next agent uses it to "fix" the code that follows the other two.

When a category's sample is **not unanimous**, the row carries:

```
[CONTESTED: <option-A> <n>/<sampled>, <option-B> <m>/<sampled>]
```

and the claim is written `[inferred: <basis>; contested <n>/<m>; sampled <sampled>/<N> files]` — never `[found:]`, and never as "the project's convention". Both options get a `<path:line>`. Report the split with its counts; do NOT pick a winner, do NOT round a 6/4 to "mostly A", and do NOT drop the minority as noise. If one option is clearly the newer one (concentrated in recently-touched files per Step 12) say so as an observation with its evidence, and still record both.

This is the same shape two later profile fields already use — `[CONCURRENCY-DRIFT: <primitive-A> at <n> sites, <primitive-B> at <m> sites]` and `[MIGRATION-WEAK]` (`phase-2-profile.md § Profile content` fields 15-16). Those two were written as one-off exceptions for two topics that happened to be noticed; `[CONTESTED]` is that shape generalized to the section where it does the most damage. A category that IS unanimous across its sample carries no `[CONTESTED]` marker — the marker's absence has to mean "checked and agreed", which is why it is emitted per category rather than per file, and why a category whose disagreement was never checked may not go unmarked.

**Persist as `## Conventions` section** with a complete suffix matrix table — heading carries `[SAMPLED: 10/<files in category> files]` per category sampled, and each contested row carries its own `[CONTESTED: …]` marker inline. A section can be both: `[SAMPLED]` reports how much was read, `[CONTESTED]` reports what was found in it. They answer different questions and neither substitutes for the other.

This is the highest-blast-radius section in the file: it drives `ai/conventions.md`, the `## Project-specific` block at the top of *every* adapted rule, and the suffix matrix. Generalized from 10 files, every row still earns a resolving citation, so the provenance sweep passes it — a convention observed in 10 of 400 files becomes law with proof attached. That is precisely the failure § Mechanical halt's generalizing-claim rule and Step 15 check 7 exist to catch: each row that generalizes beyond the sample is written `[inferred: <basis>; sampled 10/<N> files]`, not `[found:]`.

### Step 9 — Cross-cutting concerns + signals

Re-run signal detection from Appendix A but with deeper grep + corroboration:
- multi-tenant: tenant_id columns + AsyncLocalStorage usage + tenant resolution middleware.
- payment: SDK in deps + actual integration code.
- AI: SDK in deps + prompt construction code.
- multi-currency: currency columns + conversion service.
- search: Elastic/Meili/Typesense + indexing code.
- background-jobs: queue lib + worker definitions.
- (full list per Appendix A)

For each detected signal, note: confidence (number of corroborating signals), entry-point files, anti-patterns observed (e.g., manual tenant filter outside the auto-applied path).

**The verdict is a ratio, and only three words are legal.** This section is the most confidently-worded
in the file and it was, until this row existed, the least constrained: `multi-tenant: **confirmed**`
could be written off two corroborating files out of four hundred, and no denominator anywhere
contradicted it. Write instead:

```
<signal>: <confirmed|partial|not-detected> [<matched>/<present> files; entry: <path:line>, <path:line>]
```

- **`confirmed`** — the signal's greps match, AND the pattern holds **repo-wide** for the property
  claimed. For a *pervasive* property (multi-tenancy, i18n, auth) that means `matched / present`
  covers the population the property is asserted over — a `tenant_id` column on 6 of 412 entity
  files is `partial`, not `confirmed`, however many corroborating files you cite. For a *presence*
  property (payment SDK integrated, search engine wired) two corroborating files genuinely are
  enough, because the claim is "this exists here", not "this holds everywhere".
- **`partial`** — the signal is real and does not cover its population. Name what it covers:
  `multi-tenant: partial [6/412 entity files carry tenant_id; scoped to billing/, orders/]`. This
  is the most useful verdict this step produces and the one a bare `confirmed` destroys.
- **`not-detected`** — greps ran, nothing matched. Say which greps, per § Mechanical halt.

> **Hard rule:** `confirmed` on a pervasive property without `<matched>/<present>` printed beside it
> is the same violation as an uncited factual claim, and Step 15 check 7 fails it. "Cites ≥2
> corroborating files" is a floor on evidence, never a licence to generalise from it — that
> inference is exactly what § Mechanical halt's generalizing-claim rule and Step 6's own note
> ("the cross-cutting inferences directly above … are exactly the generalizing claims § Mechanical
> halt forbids") already forbid one section earlier. This step restates it because it is the step
> that emits the strongest word in the file.

**Persist as `## Cross-cutting concerns` section**, with `[SAMPLED: <seen>/<present> files]` on the
heading whenever the corroboration walk did not cover the population — same rule as every other
section, now that the row exists in the Step 2.5 table. Marking the heading `[SAMPLED]` does NOT
put this section's verdict lines in breach of check 7's forbidden-quantifier bullet: that bullet
carves out a Step 9 verdict that prints its own `<matched>/<present>` ratio, precisely so this
section can obey both rules at once. Write the marker; do not omit it to dodge the word list.

### Step 10 — Tests + coverage shape

- Test file count.
- Test framework + colocation pattern.
- Existence of e2e tests + their location.
- Mock strategy (jest.fn, sinon, pytest fixtures, factories).
- CI config: which tests run on PR? (parse `.github/workflows/*` if present).

**Persist as `## Tests` section.**

### Step 11 — Anti-patterns observed (acknowledge, don't fix)

Count + sample paths:
- `console.log` / `print(` outside loggers.
- `any` / `Any` type usage.
- Empty catch blocks (`catch {}`, `except: pass`).
- TODO / FIXME / XXX / HACK comments.
- `// @ts-ignore` / `# type: ignore`.
- Commits with messages like "wip", "fix later", "revert".

Each line is `<pattern>: <matched>/<present> files [<path:line>, <path:line>, …]` — the count is
meaningless without the population it was counted over. "47 `console.log` calls" reads as an
emergency in a 60-file project and as background noise in a 4,000-file one, and a generated rule
that mistakes the second for the first bans a practice the team has already contained.

**Persist as `## Anti-patterns observed` section**, `[SAMPLED]`-marked when the grep did not reach
every file in the bucket. This is intel for generated rules to know what to NOT introduce — not a
fix list.

### Step 12 — Recent activity (last 30 days)

```bash
git log --since="30 days ago" --pretty=format:"%h %s" | head -50
git log --since="30 days ago" --diff-filter=A --name-only | sort -u | head -50  # new files
```

Summarize: which areas saw the most activity? Any large refactors? Any new modules added? This gives the brain a "what's in flight" hint.

**Persist as `## Recent activity` section** — the `head -50` truncation is a cap like any other: heading carries `[SAMPLED: 50/<commits in the 30-day window> commits]` when the window held more than 50.

### Step 13 — Delegate to extract-business-context

Invoke the `extract-business-context` skill with:
- `output_path = .claude/_extracted-business.md`
- `extracted_codebase_path = .claude/_extracted-codebase.md` — **mandatory**, and the reason this step sits at 13 rather than at 1.

Steps 6-9 above have already extracted the evidence that answers half the business facets: entity clusters and `tenant_id` columns (Step 6), per-controller auth schemes and role/permission tables (Steps 6-7), corroborated payment / AI / search / background-jobs signals with their entry-point files (Step 9), plus contributor and cadence data (Step 12). Handing over only `output_path` meant that evidence was read and then thrown away before the question was composed — the business skill would fall back to README + manifests + git metadata and put five of eight facets to the user, several of which the walk just completed had already answered. Pass the path; the sub-skill's Step 0.5 mines it before it reads a single document.

Write the section ordering constraint down where it can be violated: `_extracted-codebase.md` need not be flushed to disk before this call, but Steps 1-12's content MUST be complete, because the sub-skill reads it. If a run reorders Step 13 earlier, it silently reverts to document-only business extraction and nothing in the pipeline notices.

Cross-link in the codebase overview: `## Business context: see _extracted-business.md`.

### Step 14 — Write the output

Write `.claude/_extracted-codebase.md` with all **13** sections above (skipping Step 13, which writes its own file): the 12 Step sections plus `## Coverage` from Step 2.5. `## Coverage` is written LAST — it needs Step 15's numerator — but placed FIRST in the body, before `## Stack`. Top of file:

```markdown
# Codebase Extracted Overview
Generated: <ISO timestamp> by `/setup-project` Phase 2
Source: `extract-codebase-overview` skill
Consumed by: Phase 4.2-AUTHOR generators (every output that mentions paths / base classes / conventions reads this)
approved_by:          <!-- empty at generation — human reviewer stamps <name>@<iso> after reading; see phase-2-profile.md § Oracle approval -->
approved_hash:        <!-- body hash at approval — /setup-project-health check 9 prints the paste-ready stamp command -->
Provenance: every claim is [found: <path:line>] (citation = marker), [inferred: <basis>], or [unconfirmed] — per phase-2-profile.md § Provenance discipline
Coverage: <S>/<P> source files cited (<PP>%) — census <git-ls-files|find>@<short-sha>; per-package + per-section seen/present in ## Coverage

> ⚠ THIS FILE IS REGENERATED — never hand-edit. Hand-edits in companion files:
> - `ai/conventions.md` (the human-readable summary of `## Conventions` here)
> - `ai/business-domain.md` (the human-readable summary of `_extracted-business.md`)
```

`Coverage:` sits beside `Provenance:` deliberately — they are the same kind of fact, and both sit next to the `approved_by:` stamp, because coverage is part of what a reviewer is being asked to approve. It is a plain `Coverage:` key, **not** an `approved_`-prefixed one: the approval recipe is `grep -v '^approved_' … | shasum`, so `## Coverage` and this line are *inside* the hashed body. Coverage changing therefore invalidates the approval stamp and forces a re-read. That is correct and costs nothing.

### Step 15 — Quality verification

Before returning success:
- Every cited file path exists (no hallucinations from grep misreads).
- `## Modules` section has ≥1 row OR an explicit "no modules detected — empty/script project" note.
- `## Base classes` section accounts for every `extends X` with ≥3 hits found in Step 5.
- `## Data model` section has ≥1 entity OR an explicit "no ORM-like data layer detected" note.
- Every confidence claim in `## Cross-cutting concerns` follows Step 9's three-word grammar and prints `<matched>/<present>`. Two corroborating files is the floor for `confirmed` on a *presence* property and is never sufficient for a *pervasive* one — check 7 below is what enforces the difference.
- **Provenance sweep**: no factual claim is both uncited AND unmarked — each is `[found:]` (via citation), `[inferred: <basis>]`, or `[unconfirmed]`. Count per class; the counts go to stdout and feed `/setup-project-health` check 9.
- **Coverage sweep (check 7)**: count `files_cited` = the distinct paths appearing in `[found:]` citations (the walk of every citation is already happening for check 1 — this is a `sort -u | wc -l` over it, not a second pass). Then, for every row of the Step 2.5 per-section table:
  - `seen` and `present` are both integers present in `## Coverage` — a missing or non-numeric denominator fails, it does not default to "assume complete";
  - `seen < present` and the heading carries **no** `[SAMPLED: <seen>/<present> <unit>]` → **FAIL** (undisclosed sample);
  - `seen == present` and the heading **does** carry `[SAMPLED]` → **FAIL** (a false `SAMPLED` makes every downstream consumer degrade for nothing, which is how a coverage signal gets switched off);
  - the row's `Cap` cell reads **`none declared`** and `seen < present` → **FAIL**, disclosed or not. A section with no declared cap has no licence to sample; `[SAMPLED: 12/40 files]` on `## API surface` is an honest report of a floor being breached, and honesty about a breach is not compliance. Remedy: finish the walk, or declare a cap in the Step 2.5 table so the sample is a decision rather than an accident;
  - every generalizing claim inside a `[SAMPLED]` section is marked `[inferred: <basis>; sampled <seen>/<present> <unit>]`, not `[found:]` (§ Mechanical halt). **This bullet is the one that was not arithmetic, so give it a closed trigger list rather than a judgement call**: inside a `[SAMPLED]` section, a line carrying `[found:]` may not also carry any of `all `, `every `, `always`, `never`, `throughout`, `repo-wide`, `project-wide`, `consistently`, `the codebase `, `confirmed` — those quantify beyond the sample by construction. A line that needs one of those words is `[inferred:]`. Greppable, so it fails the same way for everyone.

    **ONE carve-out, and it is not a loophole: a Step 9 verdict line that prints its own
    `<matched>/<present>` ratio.** Without it these three instructions are mutually
    unsatisfiable and the skill fails by construction: Step 9's grammar REQUIRES the word
    `confirmed` as one of its three verdicts, the Step 2.5 table REQUIRES `[SAMPLED]` on
    `## Cross-cutting concerns` whenever the corroboration walk did not cover the population,
    and this bullet FORBIDS `confirmed` in a `[found:]` line inside a `[SAMPLED]` section.
    Obey all three and check 7 is red no matter what you write. Measured on a live run: 11
    hits, every one of them a Step-9-mandated `**confirmed (presence)**` verdict, and they
    passed only because the author left the heading unmarked — i.e. the only way through was
    to break a DIFFERENT rule, silently. The carve-out is principled rather than convenient:
    the forbidden words are banned because they generalize BEYOND the sample, and a verdict
    that prints `<matched>/<present>` beside itself has declared its own scope — the ratio IS
    the scope. So: `confirmed` is permitted in a `[found:]` line inside a `[SAMPLED]` section
    **if and only if** the same line prints `<matched>/<present>`, and the next bullet then
    independently enforces `matched == present` for it. No ratio, no exemption. The other nine
    words have no carve-out, because none of them carries a denominator;
  - every `<signal>: confirmed` line in `## Cross-cutting concerns` that asserts a **pervasive** property carries `<matched>/<present>` and `matched == present` (Step 9). `confirmed` with a printed ratio below 100% is a `partial` mislabelled, and it is the single highest-authority claim the file makes;
  - every `[CONTESTED: <A> n/N, <B> m/N]` row has `n + m ≤ N`, `N ≤ sampled`, and a `<path:line>` for BOTH options — a contest with one citation is a preference with a footnote.

  Check 7 is different in kind from checks 1-6 and that difference is the reason it is worth adding. Its numerator and denominator were produced by shell in Step 2.5, *before* prose existed; the check is arithmetic against numbers the model did not author. Checks 2-5 can be satisfied by writing more confident prose. This one cannot.

If any check fails: regenerate the offending section (one retry). If still failing: write the section with `[EXTRACTION-WEAK: <reason>]` flag — Phase 4.2-AUTHOR will fall back to COPY mode for affected topics.

**Check 7's failure path is NOT the `[EXTRACTION-WEAK]` path.** Checks 1-6 fail because a section has no trustworthy content, so degrading it to `[EXTRACTION-WEAK]` (→ COPY mode) is the right remedy. Check 7 fails because a section has content whose *coverage is undeclared* — the remedy is to declare it. On failure: recompute the ratio, write the missing `[SAMPLED: <seen>/<present> <unit>]` marker (or remove a false one), and re-run the sweep once. Only if the census itself is unobtainable — no `git ls-files`, no usable `find` — does `## Coverage` become `[EXTRACTION-WEAK: coverage census unavailable]`, and even then the other twelve sections keep their content.

Routing a merely-sampled section to COPY mode would collapse `SAMPLED` and `EXTRACTION-WEAK` into one behaviour, at which point one of the two markers is redundant and should be deleted rather than kept. They stay distinct: **no signal → different generator; partial signal → same generator, qualified claims.**

**A coverage failure NEVER halts the run.** Same degrade-never-stop discipline as the rest of this file: a large repo that produces no oracle at all would push every track to COPY mode, which is strictly worse than an oracle that honestly reports it read 8% of the source. Check 7 flags; it does not stop.

## Output format

Single file: `.claude/_extracted-codebase.md`. Markdown with H2 sections in the order above. Every code path cited as `<relative-path>:<line>` for verifiability.

Print to stdout:
```
Extracted codebase overview → .claude/_extracted-codebase.md
  Stack: <key items>
  Modules: <N>
  Base classes (≥3 ext): <N>  → .claude/_extracted-idioms.md (M sections)
  Entities: <N>
  Controllers: <N>
  Conventions: <suffix matrix items>
  Signals confirmed: <list>
  Anti-patterns flagged: <count by type>
  Business context: → .claude/_extracted-business.md
  Provenance: <N> found / <N> inferred / <N> unconfirmed
  Coverage:   <S>/<P> source files cited (<PP>%)  [census: <git-ls-files|find>@<short-sha>; <X> excluded: <named reasons>]
    <pkg-a>   <s>/<p> (<pp>%)
    <pkg-b>   <s>/<p> (<pp>%)  [SAMPLED: walk depth 4]
  Sampled sections: <none | Conventions 10/412 files, Architecture 8/1204 files>
  Quality flags: <none | list of [EXTRACTION-WEAK]>
```

`Quality flags:` stays weakness-only. `Coverage:` is a fact, not a flag, and is printed on every run including at 100% — an oracle that reports only its weaknesses and never its strengths under-sells correct work, and `Coverage: 412/412 (100%)` is the strongest thing this skill can say. Note that stdout is ephemeral: the same numbers are persisted in the header block and `## Coverage`, because the reader who most needs them is the one opening `_extracted-codebase.md` cold, months later, deciding whether to stamp it.

## Failure modes

- **Empty project (no source files)** → write skeleton overview with `## Stack` only + note "extraction deferred until code exists." Phase 4 falls through to CREATE-mode behavior.
- **Massive monorepo (>10k source files)** → do NOT truncate by walk depth. Rank the census with `~/.claude/scripts/rank-source-files.py <repo> --limit <budget> --format list` and read that selection, highest first.

  Depth is a topological accident: it measures how deeply someone nested a folder, not how much the file matters. On a `packages/<pkg>/src/modules/<m>/service.ts` workspace every service sits at depth 5 and the whole layer disappears, while a top-level `tools/` folder survives intact — the fixture in `scripts/test-rank-source-files.sh` is exactly this shape, and a depth-4 cap there keeps 1 file of 6, the one worth nothing, and drops all five that carry the architecture. The ranker orders by how many distinct DIRECTORIES import a file (a file pulled in by 40 files from one folder is a local helper; one pulled in by 12 files from 9 folders is load-bearing), and reserves a quarter of the budget for entry points — routes, commands, job handlers — which nothing imports and which in-degree alone would bury, though they describe the system's surface better than any util.

  It resolves TypeScript / JavaScript, Vue and Svelte single-file components (the `<script>` block only), Python, and Dart (`package:<self>/` resolved through pubspec's `name`), and drops a specifier it cannot resolve rather than guessing, so a file reached solely through DI or a string-keyed dynamic require ranks low. Under-counting is the intended direction: it demotes a file, it never invents importance. Where the stack is none of those the ranker returns everything at rank 0 — fall back to a breadth-first walk and say so in `walk_scope`, rather than pretending the order means something.
 This threshold is a **walk-strategy** trigger, not a disclosure trigger — the two used to be one sentence and conflating them was most of the bug. Disclosure is unconditional (Step 2.5 + check 7) at every repo size, because damage is proportional to `present − seen`, not to `present`: a 900-file repo whose `## Conventions` rests on 10 files per category is nowhere near 10k and is exactly where the caps bite hardest. Making the marker conditional would also make its *absence* ambiguous ("we read everything" vs "we sampled but you were under the threshold") — a signal whose absence carries no information is not a signal, and check 7 could not be mechanically enforced against one. Under this branch, record the selection method in `walk_scope` — `walk_scope: centrality-ranked, top <N> of <P> (hub share 0.75)` — so the sampled rows say *why* these files and not others. `[SAMPLED: <seen>/<present> <unit>]` is unchanged and still mandatory: a better sample is still a sample.
- **Repo without git** → skip Step 12 (recent activity). All other steps work. Step 2.5's census falls back to `find` + an explicit exclude list and records `census_method: find`; a `find` denominator is only ever comparable to another `find` denominator, never to a `git-ls-files` one.
- **Unrecognized stack** → still emit `## Stack` with raw findings ("manifests detected: X, Y") + ask user one consolidated question to confirm framework.
- **Extraction conflicts with prior `_extracted-codebase.md` (REFRESH)** → write the new one; emit `## Diff vs prior` section listing what changed (added/removed/changed). Phase 6 learning loop reads this diff.

## Quality bar

The output should let any downstream generator (pattern author, agent author, rule author) write project-specific content **without asking another question + without re-grepping the codebase**. If the generator still needs to look something up, this skill missed it.

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.