Project Health
An open source project health checker for teams working with AI coding tools.
AI can speed up code production. Project Health asks whether the resulting project is still easy to read, change, test, and navigate—and whether its health is improving or deteriorating.
The CLI produces a one-page report with evidence, changes since a baseline, and up to three prioritized improvements. It runs local analysis without model calls and produces bounded evidence packets for AI tools.
Status: checkout version 0.13.2 is an installable Python, JavaScript/JSX, Go and C++ pilot with Graphify as the default structural graph provider. Local analysis, Git baseline comparison, console/Markdown reports, and JSON output are implemented. PyPI publication is separate from checkout installation. Maintainer validation remains pending.
Version 0.13.2 fixes analysis of lambdas without parameter lists and Graphify staging under symlinked temporary directories. Existing lambda symbols and independent complexity measurements are preserved.
Version 0.3.0 adds potential change-impact previews, compact agent briefs, declarative architecture boundaries, co-change investigation candidates, and accepted findings with review dates.
Version 0.4.0 adds default checks for god-module/function/class candidates and entangled file groups from Graphify evidence.
Version 0.5.0 adds supporting-metric regressions, conservative rename continuity, maintenance hotspots, changed-line/branch coverage, and local cognitive complexity.
Install and run
Requires Python 3.10+ and Git for revision/history analysis.
For a published release, install the CLI and its default Graphify provider together:
uv tool install 'project-health[graphify]' --python 3.12
project-health check .
Alternatively, use python -m pip install 'project-health[graphify]' in a Python environment. The base project-health package supports --graph-provider ast; the default Graphify provider requires the extra or a separately installed graphify executable. The tested Graphify dependency is pinned to 0.9.74.
For installation from this checkout:
uv tool install graphifyy==0.9.74 --python 3.12
uv tool install --editable . --python 3.12
project-health check .
project-health check /path/to/repository --baseline main --json-output /tmp/health.json
project-health check . --baseline HEAD~1 --format json > /tmp/health.json
project-health check . --baseline HEAD~1 --format markdown --output /tmp/health.md
The default prints a console page bounded to 45 lines at 96 columns, with up to three grouped actions. JSON includes all findings, source metrics, import edges, provenance, history and evidence gaps. Place output outside the scanned tree to avoid including it in a later run. Without --baseline, the report describes a snapshot and makes no deterioration claim.
An editable installation uses this checkout's code; keep the repository in place. For a regular installation, use uv tool install . --python 3.12. To uninstall, use uv tool uninstall project-health.
Using Project Health from an AI agent
The installed command includes workflow guidance: run project-health --help, project-health check --help, or project-health doctor --help. Start with installation diagnostics and a bounded evidence brief:
project-health doctor . --format json
project-health check . --format agent-json
For uncommitted changes, add --baseline HEAD. For committed branch changes, choose the intended base commit/ref and compare with --revision HEAD. Omitting a baseline produces a snapshot without improvement/deterioration claims. Use --format json or --json-output /tmp/project-health-full.json for complete findings and per-language capability statuses.
Read errors, comparison status, uncertainty, omissions, and quality gates before acting on recommendations. Exit 0 can still contain findings or evidence gaps; exit 1 means an enforced finding/gate failed; exit 2 means analysis/input failure or unknown evidence required by a gate. Unsupported or partial checks are not passes. Review evidence against the source, run the relevant tests separately, and rerun using the same baseline and options. See the agent workflow and JSON contracts for baseline selection, artifact handling, and output interpretation.
Maintenance evidence and CI gates (0.12.0)
Normal scans now check dependency hygiene, narrow error-handling patterns and contribution concentration. They preserve the one-page console report, full JSON, bounded agent packets and zero model calls. Existing Python, JavaScript and Go checks continue to run. Mutation results are optional supplied artifacts; tests and project commands are never executed.
project-health check . --baseline main --json-output /tmp/health.json
project-health check . --baseline main --format agent-json --max-context-bytes 12000
project-health check . --mutation-report /tmp/mutation.json --artifact-revision HEAD
Dependency hygiene reads selected snapshot manifests and reconciles literal imports with declared dependencies. Supported inputs are static PEP 621/Poetry declarations, requirements files, uv.lock, poetry.lock, npm package.json/package-lock.json, and Go go.mod. Nested projects use their nearest manifest; npm workspace declarations can use a parent lockfile. Findings include undeclared-import candidates, production imports declared only as JS development dependencies, unused-declaration candidates, missing lock entries and specification drift. Python exact pins are compared with locked version variants; general version-range solving is not performed. Go requested versions are not presented as installed/resolved versions. YAML/pnpm and yarn lock parsing is explicitly unsupported.
Python import names do not uniquely identify distributions. Explicit mappings establish that relationship; normalized names matching a declaration are labelled candidates. Other imports stay unknown. Optional, CLI, plugin and dynamic usage can keep an apparently unused dependency in use. Configure exclusions for known tools rather than automatically removing packages:
schema_version = 1
[dependency_hygiene]
ignore_dependencies = ["eslint", "pytest"]
[dependency_hygiene.python_import_map]
yaml = "PyYAML"
PIL = "Pillow"
Error handling adds reviewed-debt-compatible error-handling findings with stable path/symbol/kind IDs and site counts. Python checks empty handlers and broad handlers returning constants; JavaScript checks empty catches and retains comment intent. Go checks discarded error positions only for unshadowed selected package functions, a small literal standard-library catalog, and explicitly typed error parameters. Unknown calls and interface dispatch are not guessed. These are review candidates, not proven bugs, and participate in existing baseline comparisons, priorities, GitLab Code Quality and --fail-on-new.
Ownership concentration counts each touched selected file once per non-merge commit, using the configured history window (50 by default, at most 500 for this check). Git applies .mailmap; reports include bounded contributor names and hashed email identifiers, without raw emails. Candidates require at least three contributions, a leading share of 80%, and either fan-in of at least three or function complexity of at least five. Per-file insufficient history, shallow history, ignored bots, selected revision and omissions remain explicit. Concentration is not knowledge, bus factor or individual performance. Historical renames are not followed.
[ownership]
min_commits = 3
min_share = 0.8
min_fan_in = 3
min_complexity = 5
ignore_authors = ["*bot*", "dependabot*"]
Mutation evidence accepts Mutation Testing Elements JSON schema versions 1 and 2. File paths, embedded original sources, IDs, statuses and source bounds are validated against the selected snapshot. Reports retain killed, survived, uncovered, timeout, compile/runtime error, ignored and pending counts separately. The explicitly defined killed_percent is Killed / (Killed + Survived + NoCoverage) and excludes timeouts; it is not a reproduction of every runner's score. Verified surviving/uncovered candidates include churn and fan-in context. Unverified artifacts remain informational. Native runner formats need conversion to this schema; no native Python/Go mutation runner integration is implied.
project-health check . --baseline HEAD~1 --revision HEAD \
--mutation-report /tmp/current-mutation.json --artifact-revision HEAD \
--baseline-mutation-report /tmp/baseline-mutation.json \
--baseline-mutation-revision HEAD~1 --json-output /tmp/health.json
Mutation baselines compare per-file outcome counts, including conservative path renames; they do not claim equivalent mutant sets or repaired tests. Artifact revision association remains a user assertion, checked against the selected commit or clean working tree. Embedded source matching does not prove when tests were run.
Quality gates are opt-in conditions in .project-health.toml. They run independently of --fail-on-new, and inspect active findings after accepted-debt decisions:
[quality_gates]
max_new_findings = 0
max_worsened_findings = 0
max_complexity_growth = 2
min_changed_coverage = 80
max_architecture_violations = 0
on_unknown = "error"
Configure only the conditions you intend to enforce. New/worsened counts and complexity growth require a complete baseline comparison. Complexity growth measures existing uniquely matched functions, including matched file renames; new functions are covered by finding gates. Architecture violation limits require evaluated declared policy, not component membership alone. Changed coverage requires complete revision-matched executable-line evidence; no measured changed executable lines is unknown. Go block-start proxies cannot satisfy the exact changed-line gate. All gates emit measured values, thresholds, pass/fail/unknown and reasons. Exit 1 means a known policy failure; exit 2 means analysis failure or unknown required gate evidence. on_unknown = "warn" retains unknown results without blocking CI. No configured conditions means no additional gate.
JSON adds current.dependency_hygiene, current.error_handling, their baseline counterparts, dependency_changes, ownership, mutation and quality_gates. Console summaries show candidate counts and gate status; agent packets retain gate outcomes and include optional bounded maintenance summaries.
C++ support (0.13.0)
C++ source and headers are detected automatically, with no C++ toolchain, preprocessing, compilation, package download or project execution. Reinstall the checkout to obtain the pinned tree-sitter-cpp parser.
project-health check /path/to/cpp-repo --languages cpp --baseline main
project-health check /path/to/mixed-repo --json-output /tmp/health.json
Recognized extensions are .cpp, .cc, .cxx, .C, .hpp, .hh, .hxx, and .h. The uppercase .C extension is C++; lowercase .c remains unsupported. Ambiguous .h files are treated as C++ when C++ is selected, including automatic selection. Use --languages python,javascript,go or --exclude to omit headers that should not be checked as C++. This is a syntax policy, not C language or build-configuration detection.
The project-health-cpp-v1 adapter measures function/method complexity, cognitive complexity, exact duplicates and normalized near-clone candidates. Function symbols include namespaces, classes, parameter types and method qualifiers to distinguish overloads. Parameter names, defaults and ordinary signature layout do not define identity. Constructors, destructors, operators, templates and lambdas are parsed; lambda identities are location-based and excluded from unique function matching. Cyclomatic complexity starts at one and adds if/loop/catch/ternary/nondefault-case/logical-operator decisions. Cognitive complexity adds flow/switch nesting and logical operators. Nested lambdas are measured independently. Maintainability index remains null; measurements are local definitions rather than compiler or Sonar compatibility claims.
Graphify remains required for C++ scans. The C++ adapter supplements extracted graphs with literal quoted includes resolved against selected headers, first relative to the including file and then as repository-root candidates. Supplemental evidence carries origin: cpp-cst, provider and resolution provenance; repository-root fallbacks are inferred candidates and do not contribute to extracted dependency degrees; graph.cpp_resolution records unresolved includes and calls without adapter semantic resolution. Source include relationships feed shared boundaries, impact, churn, hotspots, ownership, baseline comparisons and quality gates. Cache hits recompute supplements; provider failures remain failures. Build/vendor directories remain excluded, and build scripts are never evaluated.
All 20 architecture checks remain visible through language_analysis.cpp.architecture_checks. Function interfaces, macro include visibility and bounded near clones have local syntax evidence. Class cohesion, inheritance depth, global writes and initialization effects are explicitly unsupported. Public API/ABI compatibility and external package identity checks are unsupported; C++ dependency hygiene and error handling retain partial status. There is no native C++ coverage importer in this milestone; unavailable coverage cannot satisfy a changed-coverage gate.
Macros, conditional branches and templates are analyzed as selected source syntax, without choosing a build configuration. cpp_checks lists macro/conditional paths. Include search flags, generated headers, aliases, type-based overload resolution and virtual dispatch remain unresolved. Findings and relationships are advisory syntax evidence, not proof of compilation, runtime behavior or ABI safety.
Go support (0.11.0)
Go projects are detected automatically. No Go toolchain, compilation, package download or project execution is required. Install this checkout to use 0.11.0; publishing it to PyPI is a separate step.
project-health check /path/to/go-repo --json-output /tmp/go-health.json
project-health check /path/to/go-repo --languages go --baseline main
project-health check /path/to/go-repo --coverage /tmp/coverage.out --artifact-revision HEAD
The Go adapter measures function/method complexity, cognitive complexity, duplicates, oversized typed interfaces, package-variable writes, initialization side effects and receiver-struct cohesion across files. Git churn defaults to the last 50 commits. Exported API comparisons operate on packages so moving a function between files in the same package does not report an API removal. Type/signature changes are review candidates, without type-checking or compatibility guarantees. All 20 architecture checks remain visible; Go inheritance depth is explicitly not applicable because Go uses embedding and interfaces.
Graphify remains the default graph provider. The installed extractor leaves some Go package imports and cross-file calls unresolved, so the Go CST adapter supplements it with literal go.mod package imports, unshadowed direct calls and receiver declarations. Supplemental edges carry origin: go-cst, their own evidence_provider and resolution method; graph.go_resolution records module hashes, added-edge counts and gaps. Shared checks use these edges for dependency impact, god-node and entanglement candidates. A Graphify failure remains a failure. Cache identities include selected go.mod contents and supplements are recomputed on cache reads and branch changes.
Existing Go text coverprofiles (mode: set, count or atomic) are accepted. Module paths map through the selected snapshot's go.mod; unrelated or ambiguous paths remain unmapped. Go coverage instruments basic blocks, so JSON preserves block spans, counts and statement weights. Generic line metrics use a conservative block-start proxy. Changes inside a block carry whole-block evidence and partial line status; branch outcomes and per-test contexts are not supplied. Generate coverage independently with your normal test workflow; the checker only reads artifacts. See Go coverage documentation.
Go analysis uses all selected source variants, without choosing GOOS/GOARCH/build tags. Build-constrained API comparisons, embedded-field cohesion and ambiguous declarations stay unknown. Interface/receiver dispatch, go.work, replacements, cgo, reflection and external implementations are not fully resolved. Vendor/build directories are excluded; use --exclude testdata when intentionally invalid Go fixture sources should be skipped. These limitations appear in JSON, including reports for mixed repositories.
Multiple languages (0.11.0)
Language detection is automatic. The CLI supports Python (.py), JavaScript (.js, .mjs, .cjs, .jsx), Go (.go) and C++ source/headers in standalone and mixed repositories. ES modules, CommonJS, JSX, arrow functions, methods and generators are parsed locally. The CLI itself remains a Python package; JavaScript analysis requires neither Node nor installing a project's packages. TypeScript, Vue/Svelte templates and nonstandard Babel/Flow syntax remain unsupported or explicit parse errors.
project-health check /path/to/javascript-repo --json-output /tmp/health.json
project-health check /path/to/mixed-repo --baseline main
project-health check . --languages javascript --baseline HEAD~1
project-health check . --languages python --graph-provider ast
project-health check . --baseline main --format agent-json --max-context-bytes 12000
--languages accepts auto (default) or a comma-separated python,javascript,go,cpp selection. The same selection applies to both snapshots and history. SQL schemas remain independently selected. JavaScript, Go and C++ require the default Graphify provider; an explicit Python AST graph with selected JS/Go/C++ files produces an actionable error. node_modules, vendor, build and dist directories remain excluded. No project scripts, source modules, ESLint configurations or package lifecycle hooks are executed by a check.
All 20 architecture checks have JavaScript implementations or shared extracted-graph/history implementations. Existing declared component/public-path/role/entry-point policies accept JS paths. Reports add current.language_analysis with file counts, primary-analysis status, providers and a 20-check capability/status inventory for each detected language. Syntax check evidence also appears under architecture_ast.by_language; mixed aggregate rows have independent output caps. Partial or unconfigured checks are not passing results. Shared graph checks describe repository-wide dependencies, including selected languages; language inventories reference that shared scope.
JavaScript quality uses project-health-javascript-v1: cyclomatic complexity starts at 1 and adds if/loop/catch/ternary/case/logical-operator decisions; cognitive complexity adds flow/switch nesting and logical operators. Nested functions are measured independently. Functions include arrow functions and methods. Exact duplicates use syntax tokens without comments/layout; near-clones normalize identifiers/literals while retaining properties. Function parameter counts, same-file inheritance depth, instance-property cohesion, captured mutable module writes, recognized import-time effects and dynamic dependencies are advisory architecture evidence. These are explicit local heuristics, not ESLint/Sonar compatibility claims. Radon maintainability index remains Python-only (null for JavaScript); language scores must not be treated as interchangeable.
API comparison reads conservative explicit ESM and CommonJS exports. Export removal, symbol-kind changes and async/generator changes are candidates. JS parameter-list changes are review signals, not proof of required-argument incompatibility. Star/conditional/computed/runtime exports, detached CommonJS exports aliases and unsupported bindings are unknown. Package subpaths collapse to import roots (including scoped packages); known Node builtins are excluded. Bundler aliases and installed package identities are not proven. Class fields/static initialization, IIFEs and custom mutators remain outside the recognized import-effect/global-state checks and their limitations are explicit. JS parse trees are bounded to 100,000 named nodes per file, with four-entry reuse; existing byte limits and clone/traversal caps also apply.
Existing JavaScript coverage
Supply an Istanbul file coverage map such as coverage-final.json using the existing --coverage option. Test and coverage commands must be run separately by the maintainer/CI; Project Health reads artifacts only.
project-health check . --revision HEAD --baseline HEAD~1 \
--coverage /path/to/coverage-final.json --artifact-revision HEAD \
--json-output /tmp/health.json
Istanbul statement and branch counters feed finding coverage and changed-code coverage. Paths and source-line bounds are checked against the selected snapshot. Generated/bundled paths are not remapped through source maps; unmatched paths stay unknown. Revision association remains a user assertion, not proof of artifact origin. A statement start line is covered if any statement starting on it executes; partially covered statement lines are recorded. Branch outcomes retain distinct IDs even when sharing source lines. Standard Istanbul JSON lacks per-test contexts, so it does not establish a safe test selection or populate observed test mappings.
The lazy adapter registry in languages.py, with python_adapter.py and javascript.py, defines extension discovery, source partitioning, primary analysis, function identity, syntax-check aggregation and capability reporting. javascript.py owns JS syntax and semantics; Python's existing analyzer retains its finding IDs. Future language adapters must supply stable symbols/ranges, measurement definitions and explicit unsupported/partial states, and must extend artifact/import/export behavior where semantics differ.
Workflow additions (0.8.0)
Diagnose the installation and policy before scanning a repository:
project-health doctor .
project-health doctor . --smoke-test --format json
doctor checks Python, required package versions, Graphify discovery/version and policy validity. A missing dependency can be diagnosed without loading the analyzers. The optional smoke test extracts a tiny isolated fixture, bypasses the cache and never executes repository code. Diagnostics include concrete fixes; errors exit 2, warnings alone exit 0. It does not diagnose network access to package indexes or validate a project's behavior.
Graphify now caches complete graphs by the scoped Python paths/content, provider version, extraction options and cache schema. Policy thresholds and history/coverage are recalculated on every check. Changing branches or source files selects a different cache key; returning to identical content can reuse its graph. The provider must still be available and its version is probed on every scan. Switching branches alone does not launch a check.
project-health check . # cache enabled by default
project-health check . --no-graph-cache # force fresh extraction; no cache reads/writes
project-health check . --graph-cache-dir /tmp/project-health-graphs
The default cache is $XDG_CACHE_HOME/project-health/graphify, or ~/.cache/project-health/graphify. Cache directories must be outside the scanned repository. Entries contain graph symbols/relationships and source hashes, not full source snapshots; treat them as local project evidence. Remove the directory to clear it. Writes are atomic, entries are size-bounded and integrity-checked, and corrupt/unavailable storage falls back to extraction with explicit warnings. Failed/partial extractions are never stored. Console and JSON show cache hit/miss/disabled status. No measured speed improvement is claimed yet.
For observed test mapping, export coverage with per-test dynamic contexts and coverage json --show-contexts. Ordinary coverage JSON still works, but cannot identify individual tests. See coverage contexts and JSON export.
# Map using contexts measured at the current revision:
project-health check . --revision HEAD --baseline main \
--coverage /tmp/coverage-contexts.json --artifact-revision HEAD
# Suggest previously observed tests before testing a change:
project-health check . --baseline main \
--baseline-coverage /tmp/main-contexts.json --baseline-artifact-revision main \
--json-output /tmp/health.json
test_impact compares function ASTs, including signatures/decorators, using unique qualified names and existing conservative file-rename mappings. It maps changed function ranges to observed contexts in the selected artifact snapshot; changes outside functions use changed nonblank/noncomment lines. Line movement alone does not change a function's AST. Baseline contexts use baseline line numbers, cannot map added functions, and can map deleted functions. Current contexts cannot map deleted functions. Artifact revision association remains a user assertion checked against the selected revision; mismatched/unverified artifacts supply no suggestions. Empty setup contexts are excluded. Context labels are user-controlled and may need translation to test-runner identifiers. Suggestions do not establish a complete test suite and never execute tests. Unknown mappings, source errors and caps (100 targets, 50 contexts per target, 100 aggregate suggestions) remain explicit. Agent briefs can include a bounded optional summary.
Export findings to GitLab Code Quality:
project-health check . --format gitlab-codequality \
--output gl-code-quality-report.json --json-output /tmp/health.json
The artifact is a JSON array with stable canonical finding fingerprints. It includes all active/reopened current findings, including persistent findings, so GitLab can compare branch reports. Accepted debt is omitted unless --codequality-include-accepted is set. Each finding uses one deterministic source anchor; related locations and complete provenance remain in the full health JSON. Architecture violations and conflicting referential actions are major; low-confidence god-node/entanglement and missing-PK/index candidates are info; other supported rules are minor. These are explicit review severities, not defect probabilities or priority weights. Analysis errors still exit 2 and are printed to stderr; always retain the full JSON and check pipeline status.
project-health:
image: python:3.12
script:
- pip install ".[graphify]"
- project-health check . --format gitlab-codequality --output gl-code-quality-report.json --json-output health.json
artifacts:
when: always
reports:
codequality: gl-code-quality-report.json
paths:
- health.json
Run the job on both merge-request and target-branch pipelines to supply GitLab's comparison reports. Use the existing explicit-baseline --fail-on-new workflow below when you also want Project Health's own regression gate.
Architecture, modularity and maintainability (0.9.0)
Every scan now includes an inventory of 20 architecture checks in current.architecture_checks, with evidence in architecture_graph, architecture_ast and architecture_evolution. The baseline contains the same sections when selected. The console summarizes available/partial checks, investigations and declared-policy findings within the existing one-page limit. Missing configuration or evidence is shown explicitly and is not a pass. There is no combined architecture score.
| Check | Evidence and scope |
|---|---|
| Indirect boundaries | Shortest extracted dependency paths crossing a configured forbidden boundary; opt in with transitive = true. |
| Package encapsulation | Cross-component dependencies targeting paths outside a declared public entry-point list. |
| Subsystem cycles | Cycles between configured components, or descriptive parent-directory groups when none are configured. |
| Coupling growth budgets | Unique outgoing file dependencies and same-path baseline growth, including candidates below god-node thresholds. |
| Subsystem cohesion | Internal/external outgoing file dependency counts and internal ratio; no dependencies gives an unknown ratio. |
| Class cohesion | Groups of direct instance methods sharing attributes; inherited/dynamic state, stateless classes and utility attributes limit interpretation. |
| Change locality | Mean/max components changed per observed production-Python commit; declared components required, tests and bulk commits excluded. |
| Transitive dependency burden | Other components reachable from each file, with lower bounds when traversal caps are reached. |
| Public API compatibility | Module export removals and undecorated function signature changes against a baseline; literal __all__ or explicitly labelled naming inference. |
| Import-time side effects | Recognized file/network/database/process calls in import execution contexts, including class bodies/defaults/decorators; no execution occurs. |
| Volatile dependency exposure | Frequently changed production modules with multiple extracted production dependents in the observed history window. |
| Port/adapter conformity | Declared core components directly reaching declared adapters; interfaces/ports are declared roles, not automatically inferred designs. |
| Near-duplicate implementations | Bounded normalized AST token-trigram similarity; exact duplicates and sparse bodies excluded; no semantic equivalence claim. |
| Shared mutable global state | Explicit global writes and recognized mutations of module-declared mutable values, with declaration evidence. |
| Inheritance depth | Unambiguous same-file class ancestry; imported/dynamic bases, cycles and traversal caps are explicit unknowns. |
| Oversized function interfaces | Explicit parameter counts, excluding conventional method receivers, with defaults/variadic parameters reported separately. |
| Dependency visibility gaps | Wildcard imports, recognized dynamic imports and eval/exec, distinguishing literal arguments without executing them. |
| Apparently orphaned modules | Production modules unreachable from declared entry points in extracted dependencies; review candidates, never proof of unused code. |
| Architecture policy coverage | Unassigned/ambiguous files and files with no applicable boundary/public-entry/role/enforced-budget policy. |
| External dependency spread | Nonlocal/nonstdlib import roots and their component spread; import roots are candidates, not proof of installed distributions. |
Use the normal command; history defaults to 50 commits:
project-health check . --json-output /tmp/health.json
project-health check . --baseline main --json-output /tmp/health-comparison.json
Without configured components, parent directories are descriptive groups for graph measurements. Define actual architecture in .project-health.toml when enforcing intent. Adapt the following paths to your repository and assign every analyzed Python file exactly once; add a tests/tools component or exclude those paths. Explicit policies that cannot be fully evaluated produce an analysis error rather than a successful CI verdict.
schema_version = 1
[architecture]
entry_points = ["src/app/cli.py"]
max_fan_out = 10
max_fan_out_growth = 3
max_transitive_components = 10
enforce_budgets = false
candidate_cap = 50
[[architecture.components]]
name = "app"
paths = ["src/app/**"]
[[architecture.components]]
name = "domain"
paths = ["src/domain/**"]
public = ["src/domain/__init__.py"]
role = "core"
[[architecture.components]]
name = "contracts"
paths = ["src/contracts/**"]
role = "port"
[[architecture.components]]
name = "storage"
paths = ["src/storage/**"]
public = ["src/storage/__init__.py"]
role = "adapter"
[[architecture.components]]
name = "tests"
paths = ["tests/**"]
[[boundaries]]
id = "domain-no-storage"
from = ["src/domain/**"]
deny = ["src/storage/**"]
reason = "Keep the domain independent from persistence."
relationships = ["imports", "imports_from", "calls", "inherits"]
transitive = true
Declared encapsulation, core/adapter and indirect-boundary violations produce architecture-policy findings. Setting enforce_budgets = true also turns exceeded dependency budgets into findings. These use existing baselines, accepted-debt reviews, priorities, --fail-on-new, GitLab Code Quality and source snippets. The remaining measurements and candidates are advisory and do not change CI outcomes. New architecture-policy identities are path-based and currently do not inherit accepted IDs across file renames.
Additional [architecture] thresholds are max_parameters = 8, max_inheritance_depth = 4, near_clone_similarity = 0.9, near_clone_min_lines = 10, min_volatile_commits = 5 (minimum observed changes for volatility candidates) and min_volatile_fan_in = 3. Candidates/metrics are capped at 50 per check by default; full explicit policy findings are retained. Near-clones screen at most 200 eligible functions and 10,000 pairs, excluding oversized AST bodies and sparse functions. Graph traversal is bounded per seed; omissions and lower bounds are recorded. Truncated evidence must not be interpreted as a healthy result.
API checks do not cover methods, constructors, decorators' runtime contracts or behavioral compatibility. Dynamic/conditional/star exports remain unknown. Import-time detection uses a conservative recognized-call catalog and suppresses recognized names when shadowed; unclassified calls are not assumed safe. Orphan candidates depend on the declared entry points and may still be used by plugins or dynamic imports. External import classification records the interpreter's stdlib version. Locality and dependency-spread deltas are available only with complete supporting evidence.
What the pilot measures
The pilot uses Radon 6.0.1 for function complexity and per-file Maintainability Index, exact Python AST function bodies for duplication, and Graphify for structural evidence. It also collects Git churn and co-change evidence. Centrality alone is not a defect. Radon's programmatic API supplies the quality metrics; no analyzer score is combined into an overall numerical health score.
Graphify runs automatically using extract --code-only --no-cluster --max-workers 1 on a temporary copy of each scoped Python snapshot. Model credentials and provider settings are not passed to the subprocess; automatic skill refresh is disabled. No graph files or hooks are written to your repository. The console identifies Graphify's version, node/edge counts, relationship counts and baseline changes. JSON preserves nodes, edges, calls, imports, inheritance, inferred/extracted tags, and hubs.
Only extracted local imports contribute to cycle findings. Extracted imports, calls and inheritance contribute to file dependency degree used in prioritization; inferred edges remain visible evidence without raising that priority. Static relationships require review in project context. Dynamic behavior, semantic duplication, stale documentation and behavioral-test loss are not detected. Selected exported SQL schemas have separate checks; other source languages are listed as unassessed. Clustering/community labels are not requested in this integration.
Graphify must be on PATH. A missing/failed extraction produces an explicit analysis error rather than silently switching providers. Use --graphify-timeout 180 to adjust the per-snapshot timeout, or explicitly select the earlier lightweight import graph with --graph-provider ast. The tested Graphify version is 0.9.74; its installed version is recorded in every graph report.
God nodes and graph entanglement
These checks run automatically with Graphify, using extracted local dependencies and Python workload metrics:
| Candidate | Default evidence required |
|---|---|
| God module | At least 5 incoming and 5 outgoing file dependencies, plus a function complexity of at least 15 or at least 300 module lines. |
| God function or method | At least 5 distinct local call targets, plus complexity of at least 15 or a span of at least 80 lines. |
| God class | At least 10 direct methods, plus summed method complexity of at least 40 or a span of at least 250 lines. |
| Entangled file group | At least 3 mutually reachable files with internal directed dependency density of at least 0.5. |
Entanglement uses imports, calls and inheritance together. Density is unique internal directed file dependencies divided by n * (n - 1). Containment, inferred links, unresolved endpoints and file self-edges do not inflate connectivity. Repeated calls to the same target count once. A heavily reused utility or a small orchestration hub alone does not create a god-node finding. These are review candidates with low confidence, not proof that a central component should be split; source spans include comments and docstrings.
project-health check . --baseline main --json-output /tmp/health.json
project-health check . --god-node-min-degree 8 --god-node-min-complexity 20 \
--entanglement-min-size 4 --entanglement-min-density 0.6
The same thresholds apply to both snapshots. god-node and graph-entanglement findings participate in comparisons, active priorities, accepted-findings review and --fail-on-new. The console shows candidate counts even when other findings lead the three priorities. JSON includes supporting graph-edge indices, workload/connectivity metrics and unflagged mutually dependent groups below the density threshold. With the AST-only provider these broader checks are explicitly unavailable.
Working scans include tracked and Git-unignored untracked files. Committed scans read Git blobs directly, without switching branches or executing project code. Standalone folders work too, with default directory exclusions. Repeat --exclude to add component/path globs:
project-health check . --exclude benchmarks --exclude 'generated/*'
project-health check . --baseline v1.0 --revision HEAD
project-health check . --complexity-threshold 15 --history-commits 50
Function complexity findings exceed the configured threshold (default 10); duplication requires identical AST bodies spanning at least 10 lines. Ranking favors new/worsened findings with explicit history, dependencies and verified line/branch coverage signals. Weights are heuristics, not calibrated defect probabilities. Ambiguous renames and body edits may appear as resolved/new findings.
Maintenance additions
Supporting metrics: comparisons inspect each numeric finding measurement, including size, complexity, connectivity and density. A god-function whose call count stays unchanged can still worsen when its complexity increases. JSON records metric_changes, absolute tolerances and metric_regressions; any regression makes the finding worsened, even if another measurement improves. Categories and booleans do not participate. These measurements are interpreted as higher-is-worse; the per-file Maintainability Index is not treated this way. Configure allowed absolute changes in .project-health.toml:
schema_version = 1
[metric_tolerances]
complexity = 2
lines = 10
Unspecified tolerances are zero; value controls the primary measurement. Tolerances affect baseline trend decisions, while acceptance uses explicit maximum allowances. Add accepted_metrics = { complexity = 18, lines = 100 } to an accepted-finding table to review those supporting measurements. Exceeding an allowance or losing its measurement reopens the decision even without a baseline. Existing acceptance entries continue to review only the primary value unless supporting allowances are supplied.
Rename continuity: with an explicit baseline, Git compares isolated trees of scoped Python sources at an 80% similarity threshold. Only unambiguous same qualified symbols or exactly mapped group memberships inherit baseline finding IDs. Current locations remain current; JSON records native and canonical IDs plus mapping evidence. Copies, renamed symbols, duplicate declarations and competing similar files are not silently matched. Each side is capped at 200 candidates and combined screening input at 20 MB. No repository index, checkout or objects are changed. This is inferred snapshot continuity, not proof of a recorded rename. Standalone scans retain native IDs; pre-rename churn is not aggregated.
Maintenance hotspots: a separate investigation list includes files changed in at least three observed commits with maximum function complexity at least five, even when no warning threshold is crossed. Order is commit count, then maximum complexity, then changed lines, with raw history, dependency and coverage context. Hotspots are not defect findings or CI failures. Shallow history is explicit. Adjust with --hotspot-min-commits, --hotspot-min-complexity and --hotspot-cap (default 10); the console shows the leading candidate.
Changed coverage: baseline comparisons use supplied revision-matched coverage.py JSON to report added/replaced executable lines and branch outcomes originating on changed lines. Deleted lines are recorded separately. Coverage exclusions follow the artifact; unknown files and unmeasured branches remain explicit. An unchanged matched rename introduces no added lines. Changes to a branch destination alone are not classified as changes to its source decision. Changed coverage is evidence for review, not a separate CI failure rule, and tests are never executed by the checker.
Cognitive complexity: all Python functions receive a local AST understandability score and contributing lines. Findings exceed --cognitive-threshold (default 15) and join normal comparison, review, priority and CI policies. This versioned heuristic is inspired by Sonar's definition, but does not reproduce SonarPython scores: nested definitions are independent, recursion is excluded, comprehensions count nested flow, match counts once with guards, and loop else counts as an alternative. JSON records the algorithm and deviations; a score is not proof of readability.
project-health check . --baseline main --json-output /tmp/health.json
project-health check . --baseline main --revision HEAD --coverage /tmp/coverage.json \
--artifact-revision HEAD --json-output /tmp/health.json
project-health check . --cognitive-threshold 20 --hotspot-min-commits 5
Code churn rate
Version 0.6.0 reports committed Python code churn automatically in console, full JSON and optional agent maintenance context. Churn is added plus deleted lines, not net growth. Two explicit rates are available:
lines_per_commit: changed lines divided by all observed repository commits in the selected history window.churn_percent: 100 times changed lines divided by analyzed Python snapshot lines. Repeated edits and creation commits can make this exceed 100%.
Every scan checks churn automatically using the last 50 commits by default. Use --history-commits to override the window. Files, findings and hotspots include per-file rates with the same repository-commit denominator. Aggregate churn includes scoped historical Python paths that were deleted; their individual size rate is unavailable. Sizes include comments and blank lines. Working edits affect snapshot size but are not counted as committed churn. Historical renames are not followed; moves may count as deletion plus addition. Shallow history is partial, and missing history or binary line evidence leaves rates unavailable.
Baseline scans calculate separate windows ending at each selected revision, with numeric rate deltas in JSON. These describe activity changes, not deterioration or a CI failure. Absolute commit windows are not calendar-time rates, and high churn alone does not prove poor maintainability.
project-health check . --json-output /tmp/health.json
Database schema practices
Version 0.7.0 inspects exported SQL schemas locally, without connecting to a database or executing SQL. It discovers schema.sql and *.schema.sql automatically. Select differently named or split exports with repeatable --db-schema repository-relative paths. Use --db-dialect postgres (default) or --db-dialect sqlite; arbitrary ORM models and migration histories are not inspected.
project-health check . --db-schema db/export.sql --db-dialect postgres \
--baseline main --json-output /tmp/health.json
project-health check . --db-schema db/schema.sql --db-schema db/indexes.sql \
--db-dialect sqlite
| Finding | Evidence and interpretation |
|---|---|
db-missing-primary-key |
No declared primary key on a parsed table. A low-confidence row-identity review candidate; staging/log tables can be intentional exceptions. |
db-unindexed-foreign-key |
No declared primary/unique key or observed full plain-column B-tree index covering the foreign-key columns as a leading group. A low-confidence performance review candidate; decide using workload and query plans. Composite key order within the leading equality group may differ. |
db-set-null-not-null |
ON DELETE/UPDATE SET NULL targets explicitly required columns, or PostgreSQL primary-key columns. Review conflicting referential actions. SQLite primary keys alone are not assumed universally non-null. |
Rules follow PostgreSQL constraint guidance and SQLite foreign-key/index guidance. Missing business constraints, nullable foreign keys, omitted deletion policies and naming conventions are not automatically defects. Findings include dialect, source line, table/column evidence and a supporting documentation URL.
Supported input is one final schema across selected files: CREATE TABLE, CREATE INDEX and ALTER TABLE ADD CONSTRAINT. Primary/unique constraints establish implicit index coverage. Partial, expression and non-B-tree indexes leave that table's index check unknown and explicitly skipped; they do not prove coverage or a defect. Unsupported mutations, duplicate table definitions or malformed input suppress schema findings and produce analysis errors (exit 2). Data statements are ignored and never executed. This is a bounded declaration review, not full database validation, migration replay or a query optimizer.
Schema findings participate in baseline comparisons, accepted-finding review, grouped priorities, JSON/agent briefs and --fail-on-new. SQL-only projects can produce a supported report. The console always identifies schema status/dialect and table/finding counts; no detected export means unavailable schema evidence, not a healthy database. SQL inputs respect Git snapshots, exclusions, symlink rejection, UTF-8 decoding and the existing 2 MB per-file bound. Graphify remains the Python graph provider.
Existing validation results
Supply existing coverage.py JSON and JUnit XML; the checker never runs project tests:
project-health check . --revision HEAD --coverage /tmp/coverage.json \
--junit /tmp/junit.xml --artifact-revision HEAD --json-output /tmp/health.json
--artifact-revision asserts which commit produced the artifacts. It cannot independently prove their origin. Artifacts only influence ranking when that commit matches the scanned revision, or a clean working tree at that commit. Missing, mismatched and invalid artifacts remain explicit. Coverage describes finding line ranges, not behavioral correctness.
Agents and CI
Use --format json for machine-readable stdout or --json-output alongside the console report. See the JSON contract. A successful scan exits 0 even when findings exist; --fail-on-new exits 1 for active new/worsened findings or reopened review decisions. Invalid revisions, unreadable/malformed inputs and incomplete source/schema analysis exit 2. No supported Python files or SQL tables yields an insufficient-evidence report rather than a healthy verdict.
For focused agent context, export up to three actions with bounded source snippets and evidence:
project-health check . --baseline main --format agent-json --max-context-bytes 12000
The limit covers serialized UTF-8 bytes, including JSON escaping; mandatory evidence that cannot fit produces an explicit input error. Committed snippets match selected Git blobs, and working snippets require fresh matching hashes. This does not execute verification steps or send source to a model.
Impact and co-change investigations
project-health check . --baseline main --impact --impact-depth 2 --impact-cap 100
project-health check . --impact-file src/validation.py --json-output /tmp/impact.json
Impact traverses extracted Graphify dependencies in reverse to identify potential callers/dependents. It includes evidence paths, snapshot provenance and truncation limits in JSON. Deleted-file seeds use the baseline graph. Without a baseline or explicit seed it reports unavailable evidence. Inferred links and containment do not contribute, and reachability is not proof of breakage.
Co-change investigations run when at least ten history commits are available. The initial thresholds are three shared commits and a Jaccard ratio of at least 0.6: shared commits divided by the number of commits touching either file. Candidates must have extracted endpoint coverage and lack a direct extracted import/call/inheritance dependency in either direction. Use --coupling-min-shared, --coupling-min-ratio and --coupling-min-history to adjust thresholds.
Candidates are investigation aids rather than defect findings or CI failures. JSON records counts, ratios, shallow-history warnings, skipped endpoints, bulk-commit omissions and pair truncation. Renames are currently treated as path changes.
Architecture rules and reviewed debt
Add .project-health.toml at the repository root, or select another file with --config. Relative overrides resolve from the repository root. The same selected policy applies to baseline and current source; its hash is recorded.
schema_version = 1
[[boundaries]]
id = "domain-does-not-import-api"
from = ["src/domain/**"]
deny = ["src/api/**"]
reason = "Domain logic must remain independent of the API layer."
[[accepted_findings]]
finding_id = "copy-the-exact-id-from-your-json-report"
reason = "Shared implementation retained until the compatibility release."
review_on = "2026-12-01"
accepted_value = 19
Boundary rules default to extracted local Graphify imports and imports_from, preserving existing import finding IDs. Optionally select exact relationship kinds:
[[boundaries]]
id = "domain-does-not-depend-on-api"
from = ["src/domain/**"]
deny = ["src/api/**"]
reason = "Domain logic must remain independent of the API layer."
relationships = ["imports", "imports_from", "calls", "inherits", "mixes_in"]
Inferred, unresolved and containment edges do not enforce policy. Imports share one finding per rule/file pair; each other selected relation kind has a separate stable finding identity, so a new call can be flagged even when an import violation already exists. Findings retain exact supporting edges and source locations. Globs are case-sensitive POSIX paths: *, ? and character classes match within one segment; a whole ** segment spans zero or more directories. Findings compare across revisions and participate in active priorities and CI decisions. Missing/partial graph evidence remains explicit.
Acceptance requires a current canonical finding ID, reason, review date and positive observed value, with optional accepted_metrics allowances. Accepted findings remain in full JSON but leave active priorities. They reopen on the review date or when a reviewed value exceeds its allowance, including without a source baseline. Rename continuity preserves accepted IDs only with an explicit baseline and unambiguous evidence; unmatched stale IDs remain visible. Acceptance does not hide analysis errors or imply that reviewed debt is healthy.
project-health check . --config .project-health.toml --as-of 2026-11-30 --fail-on-new
Use --as-of for reproducible review decisions; otherwise the checker uses today's local date. Review the example's ID, value and date before using it.
project-health:
image: python:3.12
variables:
GIT_DEPTH: "0"
script:
- pip install . graphifyy==0.9.74
- project-health check . --baseline "$CI_MERGE_REQUEST_DIFF_BASE_SHA" --format markdown --output health.md --json-output health.json --fail-on-new
artifacts:
when: always
paths: [health.md, health.json]
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
The CI example installs this project from its checkout. Installation may access package indexes. Analysis makes no model/network calls, consumes zero model tokens, and executes no project commands.
Run implementation checks with uv run python -m unittest discover -s tests -v after installing Graphify. The tests exercise live Graphify calls/inheritance, extraction isolation, inferred-edge handling, regression/repair, cycles, centrality controls, repeatability, artifact mismatches/errors, output formats and exit codes. Maintainer report usefulness remains unvalidated.
Proposal and research
- Project idea and MVP scope
- Example one-page report
- Similar projects and research
- Hands-on tool comparison and build decision
- Initial proposal for testing the reporting pilot
- Measured deterioration report example
Guiding principles
- Measure outcomes regardless of who or what wrote the code.
- Compare the project with its own history.
- Make every finding traceable to evidence.
- Keep analysis cheap, reproducible, and useful without an LLM.
- Show missing evidence instead of presenting false confidence.
Contributing
Feedback on useful health signals, misleading metrics, and real repositories for evaluation is welcome through GitLab issues and merge requests. Implementation proposals should explain their evidence, limitations, and analysis cost.
License
MIT.
Metadata
Release files for project-health 0.13.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| project_health-0.13.2.tar.gz | 234.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| project_health-0.13.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 400.3 kB
Release files / project_health-0.13.2.tar.gz
| Download URL | project_health-0.13.2.tar.gz |
|---|---|
| Size | 234.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a310a48685761627aaaa8637d90bbc72ded538f919d350f0b1652987c360d599
|
|
BLAKE2b-256 checksum How to use checksums |
b9c8b6e2b4fa689ea8fe7d4d402415fdcb569cc218664cd212200b4bbc925b3e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / project_health-0.13.2-py3-none-any.whl
| Download URL | project_health-0.13.2-py3-none-any.whl |
|---|---|
| Size | 166.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9a74fda6898af13b322c38a3c90bdce7ac054b68a75a4bd8a0f009643193fec3
|
|
BLAKE2b-256 checksum How to use checksums |
e475e189a96154467f9cd4c3d8606fd3557046a0835e317936b220a0130300ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.15 {"installer":{"name":"uv","version":"0.11.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|