Companion to the user-facing docs/CODE_HEALTH.md. This
document is for contributors: where every piece lives, how data flows from
parsed source to the dashboard, and the extension points for adding
biomarkers, languages, coverage formats, or alerts.
TL;DR. Health analysis is a deterministic, zero-LLM Python pipeline: tree-sitter walks every file once → biomarkers vote → scores aggregate per category → results land in four SQLAlchemy tables. The MCP server, CLI, and Next.js dashboard all read from those tables — no JSON cache, no intermediate files, no LLM in the loop.
Code Health is the fifth intelligence layer in Repowise, alongside Graph, Git, Docs, and Decisions. It reads from Graph and Git but never modifies them. Its only writes are to its own four tables.
┌─────────────┐ parsed_files ┌──────────────────┐
│ Ingestion │ ────────────────► │ │
│ (AST + git) │ git_meta_map │ HealthAnalyzer │ ──► HealthReport
│ │ ────────────────► │ (engine.py) │ │
│ │ community_labels │ │ │
└─────────────┘ └──────────────────┘ │
│ delete/upsert
▼
┌─────────────────────────────┐
│ SQLite via SQLAlchemy │
│ • health_findings │
│ • health_file_metrics │
│ • health_snapshots │
│ • coverage_files │
└──────────────┬──────────────┘
│
┌────────────────────────────────┼────────────────────────────────┐
▼ ▼ ▼
CLI (rich) MCP tools (FastMCP) Web dashboard
health, status, get_health, get_risk, /repos/[id]/health{,
health --trend get_context, get_overview /coverage,/refactoring-targets}
Three architectural rules govern the whole layer:
- Zero LLM. Every biomarker is AST, git, or coverage math.
- No JSON caches. SQLite is the single source of truth; everything else reads from it.
- No new runtime dependencies. Pure Python over tree-sitter (already in tree). No lizard, no jscpd, no Node.
analysis/health/
├── README.md # developer overview (this layer)
├── __init__.py # public API: HealthAnalyzer, HealthReport
├── engine.py # orchestrator: walker → biomarkers → scorer
├── scoring.py # weighted aggregation, category caps, KPIs
├── trends.py # snapshot diff, Declining/Predicted alerts
├── suggestions.py # deterministic refactoring text per biomarker
├── config.py # HealthConfig + .repowise/health-rules.json
├── models.py # HealthFindingData, HealthFileMetricData, HealthReport
│
├── complexity/ # tree-sitter AST walker
│ ├── README.md
│ ├── walker.py # CCN, nesting, cognitive, bumps, params, NLOC
│ └── languages.py # per-language control-flow node-type maps
│
├── coverage/ # coverage report ingestion
│ ├── README.md
│ ├── model.py # CoverageReport, FileCoverage
│ ├── detector.py # format auto-detect + test-file heuristic
│ ├── lcov.py # LCOV parser (stdlib only)
│ ├── cobertura.py # Cobertura XML parser
│ └── clover.py # Clover XML parser
│
├── duplication/ # native Rabin-Karp clone detection
│ ├── README.md
│ ├── tokenizer.py # tree-sitter token stream (ID/LIT normalized)
│ ├── rabin_karp.py # 64-bit rolling polynomial hash
│ └── detector.py # clone-pair build + co-change weighting
│
└── biomarkers/ # one detector per file
├── README.md
├── base.py # Biomarker Protocol + FileContext + BiomarkerResult
├── registry.py # detector list + detect_all()
├── brain_method.py
├── nested_complexity.py
├── bumpy_road.py
├── complex_method.py
├── large_method.py
├── primitive_obsession.py
├── dry_violation.py
├── untested_hotspot.py
├── coverage_gap.py
├── developer_congestion.py
└── knowledge_loss.py
core/persistence/
├── models.py # HealthFinding, HealthFileMetric, HealthSnapshot, CoverageFile
└── crud.py # save_/upsert_/get_ health functions
core/alembic/versions/
└── 000X_health_tables.py # migration that created the four tables
core/pipeline/
├── orchestrator.py # _run_health_analysis() — builds module_map, runs analyzer
└── persist.py # persist_pipeline_result() — writes findings/metrics/snapshot
cli/src/repowise/cli/commands/
├── health_cmd.py # repowise health [--trend|--coverage|--refactoring-targets|--module]
├── status_cmd.py # `Health: 7.4 (avg) · 6.2 (hotspots) · 2.1 (worst: ...)`
└── update_cmd.py # incremental path: HealthAnalyzer.analyze(changed_files=...)
server/src/repowise/server/
├── mcp_server/
│ ├── tool_health.py # @mcp.tool get_health(targets, include, repo, limit)
│ ├── tool_risk.py # enriched: health_score, top_biomarkers, coverage_pct
│ ├── tool_context.py # include=["health"]: score, top 2 biomarkers, suggestion
│ └── tool_overview.py # code_health block with KPIs
└── routers/
└── code_health.py # /api/repos/{id}/health/{overview,files,coverage,
# refactoring-targets,modules,findings}
packages/ui/src/health/ # shared React components (used by web + future hosted frontend)
├── kpi-cards.tsx
├── file-table.tsx
├── biomarker-list.tsx
├── coverage-bar.tsx
├── module-coverage-list.tsx
├── untested-hotspot-warning.tsx
├── refactoring-card.tsx
├── refactoring-target-list.tsx
├── health-badge.tsx
└── module-rollup-list.tsx
packages/web/src/app/repos/[id]/health/
├── page.tsx # KPIs + lowest-scoring files + per-module rollup
├── coverage/page.tsx # /health/coverage view
└── refactoring-targets/page.tsx # /health/refactoring-targets view
packages/web/src/components/health/
└── health-risks-panel.tsx # sidecar panel on Hotspots/Ownership/Graph pages
tests/unit/health/ # 99+ tests
├── test_complexity_walker.py # per-language CCN/nesting assertions
├── test_biomarkers.py
├── test_structural_biomarkers.py # bumpy_road, large_method, primitive_obsession
├── test_coverage_biomarkers.py # untested_hotspot, coverage_gap
├── test_organizational_biomarkers.py
├── test_dry_violation.py
├── test_duplication.py # tokenizer, hash, detector
├── test_coverage_parsers.py # LCOV/Cobertura/Clover
├── test_scoring.py # category caps, clamping
├── test_scoring_snapshot.py # stability snapshot — locks caps + deductions
├── test_health_config.py # .repowise/health-rules.json
├── test_trends.py # diff_snapshots, declining/predicted alerts
└── test_suggestions.py
tests/integration/
├── test_health_coverage_integration.py
└── test_health_perf_benchmark.py # 30 s budget on 3,000-file synthetic repo (slow)
repowise init runs run_pipeline() in core/pipeline/orchestrator.py.
Health analysis is a phase in that orchestrator, called between
_run_dead_code_analysis() and _run_decision_extraction():
# orchestrator.py — simplified
async def _run_health_analysis(graph_builder, git_meta_map, parsed_files, *, repo_path, progress):
# 1. Build a {file_path → community label} map from the graph.
# Used to populate HealthFileMetric.module so module rollups
# aren't NULL.
cd = graph_builder.community_detection()
ci = graph_builder.community_info()
module_map = {nid: ci[cid].label for nid, cid in cd.items() if ci.get(cid)}
# 2. Load per-file override rules from .repowise/health-rules.json
# (empty / no-op when the file doesn't exist).
cfg = HealthConfig.load(repo_path)
analyzer_config = cfg.to_analyzer_config([pf.file_info.path for pf in parsed_files])
# 3. Construct the analyzer with all inputs it needs.
analyzer = HealthAnalyzer(
graph_builder.graph(), # NetworkX DiGraph for dependents
git_meta_map=git_meta_map, # hotspot bit, owners, co-change, bus factor
parsed_files=parsed_files, # ParsedFile objects from the AST phase
module_map=module_map, # community labels
)
# 4. Pick sync vs parallel based on repo size. Tree-sitter releases the
# GIL during parsing, so asyncio.gather + asyncio.to_thread gives
# real wall-clock speedup on large repos.
if len(parsed_files) >= 500:
report = await analyzer.analyze_async(analyzer_config)
else:
report = await asyncio.to_thread(analyzer.analyze, analyzer_config)
return report # carried on PipelineResult.health_reportThen core/pipeline/persist.py writes everything in one session:
# persist.py — simplified
if result.health_report:
hr = result.health_report
await save_health_metrics(session, repo_id, hr.metrics)
if hr.findings:
await save_health_findings(session, repo_id, hr.findings)
# Snapshot for trend tracking — rolling 50-row window per repo.
await save_health_snapshot(
session,
repo_id,
hotspot_health=hr.kpis["hotspot_health"],
average_health=hr.kpis["average_health"],
worst_performer_path=hr.kpis["worst_performer_path"],
worst_performer_score=hr.kpis["worst_performer_score"],
per_file_scores={m.file_path: round(m.score, 2) for m in hr.metrics},
)Single pass over the parsed file list. For each file:
# engine.py — pseudocode
1. _walk(pf):
source = read_bytes(pf.file_info.abs_path)
walk_file_complexity(language, source) → list[FunctionComplexity]
# Each FunctionComplexity carries: name, line range, nloc, ccn,
# max_nesting, cognitive, bumps, param_count.
2. _populate_symbol_complexity(pf, fc_list):
# Side effect: write max(ccn) into Symbol.complexity_estimate so
# the ContextAssembler symbol ranker benefits even when callers
# don't query the health tables directly.
3. _evaluate_file(pf, fc_list, ...):
# Build a FileContext with:
# - nloc, has_test_file, module
# - function_metrics: dict[symbol_name → FunctionComplexity]
# - git_meta: per-file dict (hotspot, owners, bus factor, ...)
# - dependents_count: in_degree on the graph
# - line_coverage_pct, branch_coverage_pct, covered_lines (when ingested)
# - clones, duplication_pct (from cross-file detect_clones())
results = detect_all(ctx, disabled=file_disabled)
score, deductions = score_file(results)
findings = attach_impacts(results, deductions)
metric = HealthFileMetricData(...)After the loop:
hotspot_paths = {p for p, meta in git_meta_map.items() if meta.get("is_hotspot")}
kpis = compute_kpis(metrics, hotspot_paths)
return HealthReport(findings=..., metrics=..., kpis=kpis)Duplication runs once up-front (cross-file by nature). Each FileContext
gets a slice of the global clone report. The dry_violation biomarker
reads ctx.clones to rank pairs by co-change frequency from
git_meta_map[path]["co_change_partners_json"] — active clones rank
higher than dormant ones.
Each biomarker is a stateless class implementing the Biomarker Protocol
from biomarkers/base.py:
class Biomarker(Protocol):
name: str # "brain_method", "nested_complexity", ...
category: str # see scoring.CATEGORY_CAPS
def detect(self, ctx: FileContext) -> list[BiomarkerResult]: ...| Category | Cap | Biomarkers |
|---|---|---|
| Structural complexity | −3.5 | brain_method, nested_complexity, bumpy_road |
| Size & complexity | −2.0 | complex_method, large_method, primitive_obsession |
| Duplication | −1.5 | dry_violation |
| Test coverage | −2.0 | untested_hotspot, coverage_gap |
| Organizational | −1.0 | developer_congestion, knowledge_loss |
biomarkers/registry.py is an explicit list, not auto-discovery —
keeps the registration order deterministic and lets tests inject extras
via registered_biomarkers(extra=...).
Every file starts at 10.0. Each finding contributes a per-severity
deduction (low=0.3, medium=0.7, high=1.2, critical=2.0). Deductions are
capped per category — so even ten critical structural findings can
drive structural complexity down by at most 3.5 points, not 20.
# scoring.py — score_file()
1. Group findings by category.
2. Sum raw deductions per category.
3. If sum ≤ cap, accept; else scale every per-finding deduction
proportionally so the total equals the cap. This keeps the UI
"this finding cost you X points" honest after capping.
4. Clamp the final score to [1.0, 10.0].The per-finding scaled deduction lands on HealthFinding.health_impact
via attach_impacts() — that's what the dashboard's "−2.0" badge shows.
Snapshot tests in tests/unit/health/test_scoring_snapshot.py lock the
category caps, severity deductions, biomarker→category mapping, and two
known-fixture scores. A retune intentionally requires updating the
snapshot in the same PR.
Three repo-level numbers, computed in compute_kpis():
- Hotspot Health — NLOC-weighted average over files where
git_meta_map[path]["is_hotspot"]is true. - Average Health — NLOC-weighted average over all files.
- Worst Performer — lowest-scoring file + its score.
These flow into HealthSnapshot rows (rolling 50 per repo) and feed the
CLI status one-liner, the get_overview() MCP block, and the dashboard
KPI cards.
State-free — callers pass an oldest-first list of snapshot rows. Two alerts:
- Declining Health — current is ≥
DECLINE_THRESHOLD(default 0.5) below the snapshotDECLINE_LOOKBACK(5) positions back. Fires on the 6th+ snapshot. - Predicted Decline — the three most recent snapshots are each strictly below the one before. Magnitude is not required; direction is the signal.
recent_kpis(history, limit=10) returns a newest-first serialised view
for the CLI table and MCP get_health(include=["trend"]) response.
Full re-analysis would be wasteful on commit-sized diffs. HealthAnalyzer
accepts changed_files:
# engine.py
def analyze(self, config=None, *, changed_files=None):
# 1. Duplication still runs full-repo (a changed file's clone
# partner may be unchanged).
# 2. The per-file loop skips files not in changed_files.
# 3. KPIs are NOT recomputed on incremental runs — they'd be biased
# by the subset. The dashboard recomputes from the merged DB rows.update_cmd.py builds the changed-files set from
change_detector.get_changed_files(), runs the analyzer, and calls a
helper that uses the upsert variants (upsert_health_findings,
upsert_health_metrics) so unchanged files keep their existing rows:
async def _persist_partial_health(session, repo_id, report):
changed_paths = sorted({m.file_path for m in report.metrics})
await upsert_health_metrics(session, repo_id, report.metrics)
await upsert_health_findings(session, repo_id, report.findings, file_paths=changed_paths)The full-init writers (save_health_findings, save_health_metrics)
still use delete-then-insert — simpler, and the cost is amortised across
the whole repowise init.
Four tables, all in the repo's .repowise/wiki.db. Foreign-keyed to
repositories.id with ON DELETE CASCADE.
One row per biomarker hit. Lifecycle: open → acknowledged | resolved | false_positive (matches Dead Code). Bulk-deleted-and-rewritten on full
init; selectively upserted on repowise update.
| Column | Notes |
|---|---|
id |
UUID PK |
repository_id |
FK |
file_path |
indexed |
biomarker_type |
brain_method, nested_complexity, ... |
severity |
low / medium / high / critical |
function_name |
nullable for file-level findings |
line_start, line_end |
nullable |
details_json |
per-biomarker evidence (CCN values, clone span, etc.) |
health_impact |
per-finding scaled deduction |
reason |
one-line summary string |
status |
lifecycle |
created_at, updated_at |
datetime |
One row per file (unique on (repository_id, file_path)). Read directly
by the dashboard's file table.
| Column | Notes |
|---|---|
score |
1.0–10.0 final |
max_ccn, max_nesting, nloc |
aggregate function metrics |
duplication_pct |
percent of NLOC covered by clones; nullable |
has_test_file |
paired or heuristic |
line_coverage_pct, branch_coverage_pct |
nullable |
module |
community label from graph; falls back to top-level dir |
updated_at |
datetime |
KPI + per-file score history. Rolling delete on insert keeps the latest
50 per repo (HEALTH_SNAPSHOT_RETENTION in crud.py).
Per-file coverage, overwritten on every --coverage run. Carries the
explicit covered_lines_json array so the coverage_gap biomarker can
flag the exact uncovered surface, not just the percent.
packages/cli/src/repowise/cli/commands/health_cmd.py. Mirrors the
dead-code command's Click structure.
repowise health # KPIs + lowest-scoring files + findings
repowise health --file path/to/x.py # deep-dive one file
repowise health --module packages/server # restrict to a directory prefix
repowise health --refactoring-targets # ranked by impact / effort
repowise health --trend # last 10 snapshots + active alerts
repowise health --coverage coverage.lcov # ingest coverage; can repeat
repowise health --coverage-format cobertura
repowise health --format json | jq ...
repowise health --safe-only # confidence ≥ 0.8 only (placeholder)repowise status queries the same tables for a one-line summary:
Health: 7.4 (avg) · 6.2 (hotspots) · 2.1 (worst: packages/server/.../app.py)
repowise update is unchanged from the user's perspective — health is
silently re-scored for changed files only.
Defined in tool_health.py. Modes:
- Dashboard mode (
targets=None) — returns repo-level KPIs +worst_files(top N lowest-scoring) +top_findings+ a per-modulemodulesrollup. - Targeted mode (
targets=[...]) — returns fullmetrics+findingsfor the listed paths. Targets prefixedmodule:fooexpand to the file set in that module.
include flags layer richer data:
| Flag | Adds |
|---|---|
"biomarkers" |
full findings list (already present in target mode) |
"coverage" |
per-file coverage rows + summary |
"refactoring" |
deterministic suggestion text on every finding |
"trend" |
snapshot diff + alerts + last 10 KPI rows |
get_risk(targets)— each per-target row carrieshealth_score,top_biomarkers,coverage_pct,branch_coverage_pct.get_context(targets, include=["health"])— per-filescore,max_ccn,max_nesting,nloc,module,duplication_pct, top 2 biomarkers (each with asuggestionstring), and coverage block.get_overview()— adds acode_healthblock: avg, hotspot, worst performer, open finding count.
Every response carries the standard _meta envelope via build_meta().
packages/server/src/repowise/server/routers/code_health.py. All under
/api/repos/{repo_id}/health/:
| Route | Returns |
|---|---|
GET /overview |
summary + lowest-scoring files + top findings + module rollup |
GET /files |
per-file metrics |
GET /findings |
findings list (filterable by biomarker_type, severity, file_path) |
GET /coverage |
coverage summary + per-file rows |
POST /coverage |
ingest a coverage report (used by some CI integrations) |
GET /refactoring-targets |
ranked by total_impact / effort_bucket |
GET /modules |
NLOC-weighted module rollup table |
Auth is the standard verify_api_key dependency from
server/deps.py.
Three routes under /repos/[id]/health/:
| Route | What it shows |
|---|---|
/health |
KPI cards, lowest-scoring file table, top findings, per-module rollup (added in Phase 4) |
/health/coverage |
Coverage summary, untested-hotspot warnings, module-level bars, per-file drill-down |
/health/refactoring-targets |
Cards sorted by impact-per-effort, each with severity, biomarker, score, NLOC, effort bucket, deterministic suggestion |
Plus a sidecar HealthRisksPanel on the Hotspots, Ownership, and Graph
pages — surfaces the lowest-scoring files inline without touching the
shared table/graph components.
All visual primitives live in packages/ui/src/health/ so the hosted
frontend/ repo (separate git checkout) can reuse them — port is mostly
data fetching + auth.
The auto-generated CLAUDE.md includes a ## Code health section when
the health tables are populated. The block is intentionally short —
filter rules in core/generation/editor_files/data.py:
- Score ≤ 5.0 and file is a hotspot
- Any Brain Method in a file with > 10 dependents
- Any Untested Hotspot
- DRY violations > 70 % similarity
- Declining trend (> 1.0 drop in last 5 snapshots)
Everything else is filtered out so the CLAUDE.md doesn't drown a fresh
agent in noise. The Jinja stanza lives in
core/generation/templates/claude_md.j2.
User-authored (the only JSON file in the layer). Loaded by
HealthConfig.load(repo_path):
{
"disabled_biomarkers": ["primitive_obsession"],
"rules": [
{
"glob": "tests/**/*.py",
"disabled_biomarkers": ["large_method", "complex_method"]
},
{
"glob": "src/legacy/**",
"disabled_biomarkers": ["dry_violation"]
}
]
}to_analyzer_config(file_paths) resolves globs to per-file disabled
sets, which the engine honors in _evaluate_file().
Plan §4 P4.6 targets < 30 s on a 3,000-file synthetic repo. The
parallel path in HealthAnalyzer.analyze_async() parallelises tree-sitter
parsing across worker threads (asyncio.gather + asyncio.to_thread).
tree-sitter releases the GIL on parse, so this scales on single-process
CPython.
The orchestrator chooses the parallel path automatically when
len(parsed_files) >= 500. The benchmark lives at
tests/integration/test_health_perf_benchmark.py and is marked slow
(opt-in via pytest -m slow or make health-bench).
Other perf notes:
- Duplication is O(total_tokens). Bucket walk is near-linear on repos with low duplication.
- Walker re-parses files because
ParsedFiledoesn't retain a tree-sitterTreeacross the ingestion boundary. Acceptable (~1 ms per file); switching to a shared parse cache is a Phase 5 stretch. - No N² loops in scoring. Category aggregation is O(findings).
| Suite | What it locks |
|---|---|
tests/unit/health/test_complexity_walker.py |
Per-language CCN, nesting, cognitive assertions on handcrafted fixtures |
tests/unit/health/test_<biomarker>.py |
Each biomarker — positive in two languages + one negative |
tests/unit/health/test_duplication.py |
Tokenizer normalization, rolling-hash determinism, co-change weighting |
tests/unit/health/test_coverage_parsers.py |
LCOV / Cobertura / Clover happy paths + edge cases |
tests/unit/health/test_scoring.py |
Deduction caps, clamping, KPI math |
tests/unit/health/test_scoring_snapshot.py |
Stability guard — caps, severity table, biomarker→category mapping, two known fixture scores |
tests/unit/health/test_trends.py |
Declining + predicted alerts, ordering |
tests/unit/health/test_suggestions.py |
Suggestion strings keyed correctly |
tests/unit/health/test_health_config.py |
.repowise/health-rules.json parsing + glob matching |
tests/integration/test_health_coverage_integration.py |
End-to-end LCOV → analyzer → coverage_gap fires |
tests/integration/test_health_perf_benchmark.py |
30 s budget on 3,000 synthetic files (-m slow) |
99 unit tests + 2 integration tests at time of writing. Run with
make health-check.
- New file under
biomarkers/implementing theBiomarkerProtocol. - Append to
_DETECTOR_FACTORIESinbiomarkers/registry.py. - Add the biomarker→category mapping in
scoring._BIOMARKER_CATEGORY. - Add a suggestion template in
suggestions._TEMPLATES. - Add at least three test cases (two positive in different languages, one negative).
- Update
biomarkers/README.md's "Registered v1 detectors" list.
Add one LanguageNodeMap entry to complexity/languages.py mapping the
language's tree-sitter control-flow node-type names to abstract BRANCH
/ LOOP / TRY / BOOLEAN_OP categories. Add a fixture under
tests/fixtures/lang_samples/<lang>/. No .scm files needed — those
are owned by the ingestion parser.
Drop a parser under coverage/ returning a CoverageReport. Route to it
from coverage/detector.parse. Stdlib-only (no extra XML libraries).
Users — not contributors — author .repowise/health-rules.json. To add
a new override key (beyond disabled_biomarkers), extend
HealthConfig and thread it through to_analyzer_config() →
engine._evaluate_file().
A short list of things the v1 layer does not do, by design. Future phases may revisit; the constraints kept v1 shippable.
- No LLM-generated suggestions.
suggestions.pyis static templates. An optional LLM mode is Phase 5, gated behind an explicit flag. - No symbol-level scoring. Score lives at the file granularity to
match how engineers think about refactor units. Symbol-level CCN
still feeds the file score via
function_metrics. - No
complexity_estimatepropagation backfill. The walker writes the field as a side effect during the current run; old indexes don't get touched until a re-index. - No PR-mode delta in v1.
get_risk(changed_files=...)returns the current health score, not before/after. Phase 5. - No predictive ML.
Predicted Declineis a 3-snapshot direction check, not a model. Phase 5.
| I want to... | Edit... |
|---|---|
| Tweak a category cap | scoring.CATEGORY_CAPS (snapshot test will fail — update it) |
| Tweak a severity deduction | scoring._SEVERITY_DEDUCTION (ditto) |
| Add a new biomarker | biomarkers/*.py, registry.py, scoring.py, suggestions.py |
| Change the suggestion text for a biomarker | suggestions._TEMPLATES |
| Adjust the trend-alert threshold | trends.DECLINE_THRESHOLD / DECLINE_LOOKBACK |
| Change snapshot retention | crud.HEALTH_SNAPSHOT_RETENTION |
Add a new MCP include flag |
tool_health.py — append handling near the existing "coverage" / "refactoring" branches |
| Add a new REST route | routers/code_health.py — auth is wired at the router level |
| Add a new dashboard view | new file under packages/web/src/app/repos/[id]/health/, primitives under packages/ui/src/health/ |
| Add a CLI flag | packages/cli/src/repowise/cli/commands/health_cmd.py |
| Wire the analyzer into a new entry point | call HealthAnalyzer.analyze() directly; persist via the upsert variants if your caller is incremental |
docs/CODE_HEALTH.md— user-facing guide.packages/core/src/repowise/core/analysis/health/README.md— developer overview at the layer root.- Sub-package READMEs under
complexity/,coverage/,duplication/,biomarkers/. docs/architecture/graph-algorithms.md— the graph layer health depends on.