Skip to content
Back to skills

Extract Architecture Deeply

ASecurity

Round-two deep architecture extraction — analyzes top-level package/module import graph, traces representative request lifecycles end-to-end, identifies bounded-context boundaries, locates cross-cutting concerns. Output is a structured architecture map with file:line citations. Used by /setup-project Phase 2.8 in REFINE mode to upgrade round-one architecture detection from "this is layered MVC" to a concrete layer diagram + 3-5 traced lifecycles.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentstypescriptpythonrustgojavarubybashnoderailsgit

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill extract-architecture-deeply --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Extract Architecture Deeply?

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

Security grade badge for Extract Architecture Deeply
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-extract-architecture-deeply/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-extract-architecture-deeply)

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-architecture-deeply
description: Round-two deep architecture extraction — analyzes top-level package/module import graph, traces representative request lifecycles end-to-end, identifies bounded-context boundaries, locates cross-cutting concerns. Output is a structured architecture map with file:line citations. Used by /setup-project Phase 2.8 in REFINE mode to upgrade round-one architecture detection from "this is layered MVC" to a concrete layer diagram + 3-5 traced lifecycles.
kind: skill
pack: learning
allowed-tools: [Read, Write, Edit, Grep, Glob, Bash]
---

# Skill: extract-architecture-deeply

## Purpose

Round-one Phase 2 detects the architectural shape from folder structure + framework conventions ("controllers/", "services/", "repositories/" → "this is layered MVC"). That gets you the floor (knowing which packs to apply).

Round-two needs the actual graph. A first-pass `ai/architecture.md` says "controllers depend on services depend on repositories." Round-two says "the request lifecycle for `POST /api/invoices` is: `app/controllers/invoices.py:Invoice.create:42` → `app/services/billing.py:BillingService.create_invoice:128` → `app/repositories/invoice.py:InvoiceRepository.persist:67` → `INSERT into invoices...` (transaction wraps lines 130-145, fans out to `LedgerService.append_entries:215` after persist). The `auth` middleware injects user at `app/middleware/auth.py:34` ahead of the controller; tracing is via `tracing.py:23` decorator." That second version is anchorable.

## Premise

- Real source is the truth. Read every handler in the traced lifecycle — entry, every middleware in the chain, every called function down to the I/O boundary — before writing the YAML.
- Every step, layer name, boundary, and cross-cutting concern cites `<path:line>` resolvable at the current commit.
- The import graph is computed from real `import`/`require` statements, not from folder naming conventions.
- Empty extraction is honest — a `not detected` row for a missing concern + the WEAK gate are valid outputs.
- Fabrication — inventing a layer from a folder name, a middleware that's defined-but-never-registered, or a lifecycle from framework defaults — is the failure mode the WEAK gate flags.

## Mechanical halt

- Hand-wave grep on architecture output — `etc.`, `...`, `the usual layers`, `roughly layered`, `appears MVC-ish`, a step listed without `<file:line>` — REFUSE to advance.
- Rewrite the offending block with concrete citations OR downgrade the section to `[REFINE-WEAK: architecture]`.
- If a cross-cutting concern is genuinely absent (no rate-limiter, no tracing), record `<NOT-DETECTED: <reason>>` (e.g. `<NOT-DETECTED: no middleware registered for rate-limiting>`).
- Never synthesize a "framework default" the codebase doesn't actually wire up — the rate-limit-or-tracing gap is the finding.

## When to use

- `/setup-project --refine` Phase 2.8 — once per project (not per domain).
- Manually when refreshing `ai/architecture.md` for a project that has substantial code but a generic round-one architecture file.

## Inputs

- `entry_surfaces` (optional) — list of surfaces to trace (default: auto-discover from `manage.py` / `main.ts` / `cmd/server/main.go` / `index.ts` / Procfile).
- `output_section` — section path inside `.claude/_refine-extract.md` (default: `## Architecture`).

## Procedure

### Step 1 — Build a top-level package/module graph

Detect the language + module-resolution rule:

- Python: every `__init__.py` directory is a package. Read each top-level package's `__init__.py` for re-exports.
- Node / TypeScript: top-level `src/<package>/` directories OR Nx / Turborepo / pnpm workspaces (`packages/*`).
- Go: top-level directories that aren't `vendor/` / `_examples/`. Each has its own `package <name>` declaration.
- Java: top-level Maven/Gradle modules; package roots inside `src/main/java/`.
- Rust: workspace members; or top-level `mod` declarations in `lib.rs`.
- Ruby: `app/` subdirectories in Rails; or gem `lib/<gem>/` roots.

For each top-level package P:

- List the packages it imports from (NOT external libs — only this codebase's own packages).
- Count import occurrences (a single line `import X` is one edge; `from X import (a, b, c)` is still one edge — fan-in/fan-out is per-edge, not per-symbol).

Output: an adjacency list. Compute fan-in (who imports me) and fan-out (who do I import).

### Step 2 — Identify layers from the graph

A layer is a *level* in the partial order. Detect by:

- Cycle detection: any cycle is a layer-violation candidate (or a sign that the project doesn't enforce layers).
- Levels: nodes with fan-in 0 are leaves (sinks) — typically infrastructure/data layer. Nodes with fan-out 0 are roots (entry points) — typically the API or CLI layer.
- Heuristic naming match: package names like `controllers`, `routers`, `endpoints`, `views`, `handlers` are presentation layer. `services`, `usecases`, `application` are application layer. `repositories`, `gateways`, `adapters` are infrastructure layer. `models`, `entities`, `domain`, `core` are domain layer.

Synthesize a layer diagram (text/ASCII):

```
┌────────────────────────────────────────────────────────┐
│ Presentation:  app/controllers/, app/middleware/       │
│                ↓                                        │
│ Application:   app/services/, app/usecases/            │
│                ↓                                        │
│ Domain:        app/models/, app/domain/                │
│                ↓                                        │
│ Infrastructure: app/repositories/, app/external/       │
└────────────────────────────────────────────────────────┘
```

If the codebase doesn't fit a clean layered shape (e.g. modular monolith / hexagonal / CQRS / actor-based), use the actual shape. **Never force a layered diagram onto a non-layered codebase.** Record what you find:

```
Hexagonal: ports = app/ports/, adapters = app/adapters/, domain = app/core/
Boundary discipline: domain NEVER imports adapters (verified by import graph)
```

### Step 3 — Identify bounded-context boundaries

A boundary is a *deliberate* lack of edge. Compute:

- For each pair of top-level packages (A, B), check if there's an import edge A→B AND B→A.
- If A and B BOTH have substantial fan-out elsewhere but no edge between them, it's a likely boundary.
- Cross-reference with the business-domain extraction (Phase 2.7). If `billing` and `healthcare` are detected as separate domains AND their packages don't import each other, that's a confirmed boundary.

Boundaries are mediated by something — find the mediator:

- Anti-corruption layer (translator class).
- Façade (a class in package A that's the only entry point used by B).
- Event bus / queue (A publishes; B consumes; no direct call).

Record: each boundary as `<A> ⇄ <B> via <mediator>`.

### Step 4 — Trace representative request lifecycles

Pick at LEAST 3 representative entry points per surface:

- HTTP: 1 GET (read-heavy), 1 POST (write-heavy), 1 DELETE (cascade-revealing).
- GraphQL: 1 query + 1 mutation.
- Queue consumer: 1 most-frequent message.
- Scheduled job: 1 longest-running.
- CLI: 1 critical command.

Pick by heuristics: high test coverage, mentioned in monitoring/Sentry config, high git churn, or appears in user-flow docs. If unclear, pick the first 3 that match a CRUD pattern.

For each picked entry:

1. Find the route/handler registration (e.g. `urls.py`, `app.use('/api/...')`, `@app.route`, `@Controller`).
2. Find the handler function.
3. Walk the call graph: for each function call inside the handler body, follow the import + read the called function. Stop at:
   - External lib calls (record but don't recurse).
   - DB ORM calls (record as `[DB-WRITE Invoice]` / `[DB-READ Invoice]`).
   - HTTP/queue calls (record as `[EXTERNAL <vendor>]` / `[QUEUE-PUBLISH <topic>]`).
   - The end of the handler.
4. Note middleware in the chain (auth, logging, rate-limit, tracing, error-handling).

Record per lifecycle:

```yaml
- name: POST /api/invoices
  entry: app/controllers/invoices.py:create:42
  middleware:
    - auth at app/middleware/auth.py:34
    - tracing at app/middleware/tracing.py:23
  steps:
    - app/services/billing.py:BillingService.create_invoice:128
    - app/repositories/invoice.py:InvoiceRepository.persist:67
    - "[DB-WRITE Invoice via INSERT]"
    - app/services/ledger.py:LedgerService.append_entries:215
    - "[DB-WRITE LedgerEntry x N via INSERT]"
    - app/events/outbox.py:OutboxRepository.append:45 (event: invoice.created)
  transaction_boundary: app/services/billing.py:128-145 (manual `with transaction.atomic()`)
  side_effects:
    - DB writes: Invoice (1), LedgerEntry (N), OutboxEvent (1)
    - External calls: none in transaction; outbox-relay polls separately
  error_paths:
    - ValidationError → 422 via app/exceptions/handlers.py:18
    - IntegrityError (FK violation) → 409 via app/exceptions/handlers.py:34
```

### Step 5 — Locate cross-cutting concerns

For each concern, find where it lives + how it's invoked:

- **Authentication** — middleware? decorator? In-handler check?
- **Authorization** — RBAC / ABAC? Where is the permission check?
- **Logging** — structured (JSON)? plain? Which library? Standard fields?
- **Tracing** — OpenTelemetry? framework-native? Which exporter?
- **Rate limiting** — middleware? per-user? per-IP? per-endpoint?
- **Error handling** — global handler? per-controller try/catch? framework default?
- **Caching** — decorator? service-layer? Where is the invalidation?
- **Feature flags** — library? per-request? per-tenant?

Each one: 1 line + `file:line`.

### Step 6 — Output

Write to `.claude/_refine-extract.md` under `## Architecture`:

```yaml
extraction_date: <YYYY-MM-DD>
shape: <layered|hexagonal|modular-monolith|microservices|cqrs|actor|mixed>
strong_signals: ["import-graph", "lifecycles", "boundaries", "cross-cutting"]

import_graph_summary:
  top_level_packages: <N>
  total_edges: <N>
  cycle_violations: <N>   # 0 means clean layered enforcement
  notable: |
    <2-3 sentences calling out anything unusual — heavy fan-in modules,
     packages that violate the layer ordering, etc.>

layer_diagram: |
  <ASCII diagram from Step 2>

bounded_contexts:
  - name: <context>
    packages: [<list>]
    boundary_discipline: <enforced|partial|none>
    mediator: <facade|event|anti-corruption-layer|none>

representative_lifecycles:
  - <YAML block per Step 4>

cross_cutting:
  authentication: { mechanism: <...>, location: <file:line> }
  authorization: { mechanism: <...>, location: <file:line> }
  logging: { library: <...>, shape: <...>, location: <file:line> }
  tracing: { library: <...>, exporter: <...>, location: <file:line> }
  rate_limiting: { mechanism: <...>, location: <file:line>, or "not detected" }
  error_handling: { strategy: <...>, location: <file:line> }
  caching: { strategy: <...>, invalidation: <...>, location: <file:line>, or "not detected" }
  feature_flags: { library: <...>, location: <file:line>, or "not detected" }
```

## Quality gate

- **STRONG**: import graph extracted, ≥ 3 lifecycles traced end-to-end, ≥ 1 boundary identified, ≥ 4 cross-cutting concerns located — **and each of those counts is evidenced the way the worked example above evidences them**: every lifecycle step carries `<file:function:line>`, every boundary names the two modules it separates plus the import that crosses it, every cross-cutting concern is `located` at a `<file:line>` in the chain rather than merely `detected` in the dependency list.
- A concern recorded as `not detected` counts toward the ≥ 4 **only when the greps that looked for it are named**. "Not detected" with no search behind it is silence, and silence should not clear a gate.
- **WEAK**: any of the above missed → flag `[REFINE-WEAK: architecture]`. Phase 4.7-DEEP will NOT rewrite `ai/architecture.md` from a WEAK extraction.
- The gate used to be four bare counts while the worked example above carried all the rigour — so a run could hit `3 lifecycles / 1 boundary / 4 concerns` with a layer diagram inferred from folder names and pass. The citation requirements are the example's standard, moved into the gate where it binds.

## Anti-patterns

- **Forcing a layered diagram** onto a non-layered codebase.
- **Skipping import-graph cycles** as inconvenient — cycles are real architectural information; report them.
- **Synthesizing lifecycles** from framework defaults instead of tracing actual handlers.
- **Citing middleware that's defined but never registered** — only middleware in the actual chain counts.
- **Generalizing across surfaces** — if HTTP and queue handlers behave differently (e.g. queue handlers skip tenant middleware), record both. Don't average.

## Verify (postconditions)

Contract: [`templates/snippets/postcondition-check.md`](../../../../snippets/postcondition-check.md). **Preconditions are not verification** — this skill guards its entry and must also check its exit. Every entry under `## Failure modes` that this skill's own write could cause becomes a check here, observed rather than assumed, reported when it passes as well as when it fails, and `NOT RUN` with a reason where no signal is available. A failure mode the skill documented and never checked is a risk it accepted on the caller's behalf without saying so.

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.