Skip to main content

Build a codebase health timeline in CI

A pull-request gate answers whether one change introduced a known problem. A codebase health timeline answers a different question: are complexity, coupling, duplication, hotspots, and architecture violations moving in a healthy direction across many changes?

This recipe captures one immutable metrics partition for every default-branch commit, keeps the history outside the product branch, and queries it with DuckDB through ctx sql.

:::note Worked-example provenance The 98-partition case study describes ctx's snapshot branch as measured with ctx 0.3.5 on 2026-07-14. It is an illustration, not a promised partition count or stable metric baseline. :::

Quickest version

ctx index
ctx snapshot
ctx sql --snapshots=.ctx/snapshots \
  "SELECT commit_sha, committed_at FROM snap.meta ORDER BY committed_at;"

One partition proves capture works; it does not establish a trend. Preserve immutable partitions across default-branch commits, normalize totals, and investigate the code behind sustained movement.

When to use this recipe

Use it when you want to:

  • distinguish repository growth from degrading design;
  • find metrics that worsen repeatedly across otherwise acceptable pull requests;
  • identify complex code that is also frequently changed;
  • compare releases or engineering periods;
  • give an agent evidence for investigating a trend.

Do not use a historical trend as an automatic verdict. Trends identify questions; engineers decide whether the underlying change is accidental debt, intentional design, or a measurement artifact.

Prerequisites

  • A git repository with enough history to compare.
  • A ctx build with the default duckdb feature. ctx snapshot and ctx sql are unavailable in builds without it.
  • An up-to-date index: ctx index.
  • Optional .ctx/rules.toml policy. Without active rules, a violation count of zero says nothing about architectural conformance.

1. Inspect the current state

Start with point-in-time evidence before building a history:

ctx index
ctx hotspots --since "90 days ago" --limit 20
ctx duplicates --threshold 0.95 --min-tokens 50

hotspots combines normalized churn and complexity. duplicates shows candidate pairs for human inspection. If .ctx/rules.toml exists, run ctx check --list to verify which architecture policy gives meaning to violation counts; if it does not exist, record that policy coverage is absent.

2. Capture one snapshot

ctx snapshot --json

ctx writes an atomic partition beneath .ctx/snapshots/sha=<commit>/ containing:

  • files.parquet — file complexity, churn, and violation counts;
  • symbols.parquet — symbol complexity, fan-in, and fan-out;
  • dup_pairs.parquet — structural near-duplicate pairs;
  • meta.parquet — commit and capture metadata.

Capturing the same commit again is a no-op unless you pass --force. A dirty-tree snapshot is labelled with HEAD but reflects working-tree content, so CI should capture from a clean checkout.

Bootstrap history, then verify coverage

Backfill can seed an existing repository before default-branch CI takes over:

ctx snapshot backfill --since <ref> --json > backfill.json

git rev-list --first-parent --reverse "<ref>^..HEAD"
ctx sql --snapshots=.ctx/snapshots \
  "SELECT commit_sha, committed_at
   FROM snap.meta
   ORDER BY committed_at;"

Compare the intended first-parent commits, adjusted for any --every sampling, with the rows in snap.meta. Do not treat a successful backfill exit or the length of backfill.json as proof of complete coverage. In ctx 0.3.5, a per-commit indexing or export failure is logged and skipped while the remaining walk continues; failed commits are omitted from the JSON report. Reports for existing partitions also contain zero row counts because ctx does not reopen those Parquet files. The persisted snap.meta table is the authority for which commits are actually queryable.

3. Append snapshots from default-branch CI

Keep generated metric history out of the product branch. The following shape uses an orphan ctx-snapshots branch and serializes writers so concurrent merges cannot overwrite each other:

name: snapshot

on:
  push:
    branches: [main]

permissions:
  contents: write

concurrency:
  group: ctx-snapshots
  cancel-in-progress: false

jobs:
  capture:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
        with:
          fetch-depth: 0
      - uses: dtolnay/rust-toolchain@fa04a1451ff1842e2626ccb99004d0195b455a88
        with:
          toolchain: "1.91"
      - run: cargo build --locked --release
      - run: |
          target/release/ctx index
          target/release/ctx snapshot --json
      - name: Append the partition
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"

          if git ls-remote --exit-code origin refs/heads/ctx-snapshots >/dev/null 2>&1; then
            git fetch origin +refs/heads/ctx-snapshots:refs/remotes/origin/ctx-snapshots
            git worktree add -B ctx-snapshots ../snapshots-branch origin/ctx-snapshots
          else
            git worktree add --orphan -b ctx-snapshots ../snapshots-branch
          fi

          mkdir -p ../snapshots-branch/snapshots
          cp -R .ctx/snapshots/. ../snapshots-branch/snapshots/
          cd ../snapshots-branch
          git add -A
          git diff --cached --quiet && exit 0
          git commit -m "snapshot: ${GITHUB_SHA}"
          git push origin ctx-snapshots

Keep third-party actions pinned to reviewed commit SHAs and restrict the workflow's write permissions according to your repository policy. ctx uses this pattern in its own snapshot.yml.

4. Query the history

Check out the data branch beside the repository:

git fetch origin ctx-snapshots
git worktree add ../ctx-snapshots ctx-snapshots

First confirm the coverage and writer versions:

ctx sql --snapshots=../ctx-snapshots/snapshots "
SELECT count(*) AS snapshots,
       min(committed_at) AS first_commit,
       max(committed_at) AS latest_commit,
       min(ctx_version) AS oldest_ctx,
       max(ctx_version) AS newest_ctx
FROM snap.meta;"

Then build a normalized series. Absolute complexity and duplicate counts naturally grow when a repository gains code, so include denominators and averages:

ctx sql --snapshots=../ctx-snapshots/snapshots "
WITH files AS (
  SELECT commit_sha,
         count(*) AS files,
         sum(total_complexity) AS total_complexity,
         sum(violation_count) AS violations
  FROM snap.files
  GROUP BY commit_sha
), symbols AS (
  SELECT commit_sha,
         count(*) AS symbols,
         avg(complexity) AS avg_complexity,
         avg(fan_out) AS avg_fan_out
  FROM snap.symbols
  GROUP BY commit_sha
), duplicates AS (
  SELECT commit_sha, count(*) AS duplicate_pairs
  FROM snap.dup_pairs
  GROUP BY commit_sha
)
SELECT m.committed_at,
       left(m.commit_sha, 7) AS commit,
       f.files,
       s.symbols,
       f.total_complexity,
       round(s.avg_complexity, 2) AS avg_complexity,
       round(s.avg_fan_out, 2) AS avg_fan_out,
       coalesce(d.duplicate_pairs, 0) AS duplicate_pairs,
       round(1000.0 * coalesce(d.duplicate_pairs, 0) / s.symbols, 1)
         AS duplicate_pairs_per_1000_symbols,
       f.violations
FROM snap.meta m
JOIN files f USING (commit_sha)
JOIN symbols s USING (commit_sha)
LEFT JOIN duplicates d USING (commit_sha)
ORDER BY m.committed_at;"

5. Read the evidence

Use several signals together:

ObservationInterpretation to investigate
Total complexity rises while average complexity is stableRepository growth, not necessarily degradation
Duplicate count rises but pairs per 1,000 symbols fallsDuplication grew more slowly than the codebase
Complexity and churn rise in the same filePossible chronic hotspot
Fan-out rises repeatedly around one symbolGrowing orchestration role or spreading coupling
Violation count stays at zeroConfirm that active rules cover the architecture before celebrating
One metric jumps at one commit and then stabilizesInspect that change before calling it a trend
Expected commits are absent from snap.meta after backfillTreat the history as incomplete and inspect the per-commit stderr diagnostics

Treat a trend as stronger evidence when it persists, appears in related metrics, and concentrates in code that changes frequently.

6. Investigate rather than auto-refactor

After locating a suspicious commit or file:

git show <commit> --stat
ctx map --focus <path> --budget 3000
ctx query find <symbol>
ctx query callers <symbol>
ctx query deps <symbol>
ctx query impact <symbol>
ctx source <symbol>

Classify the result before recommending work:

  • Intentional complexity — the function clearly owns an inherently complex operation.
  • Accidental complexity — responsibilities or dependencies accumulated without a coherent role.
  • Intentional similarity — implementations are parallel but should evolve independently.
  • Missed reuse — duplicated behavior should share an established implementation.
  • Insufficient evidence — the metric cannot support a safe recommendation yet.

Do not split a parser, state machine, transaction boundary, or orchestration function merely to lower a score. Do not abstract platform adapters or tests merely because their token shapes match.

What worked, and what did not in ctx itself

ctx's ctx-snapshots branch contained 98 partitions when this recipe was written, spanning 2025-11-25 through 2026-07-13 and writer versions 0.3.1 through 0.3.5.

Comparing release snapshots with the latest recorded 0.3.5 state produced:

StateSymbolsTotal complexityAvg. complexityAvg. fan-outDuplicate pairsPairs / 1,000 symbols
v0.3.11,64822,94413.926.2613783.1
v0.3.21,92226,72013.906.2413670.8
v0.3.32,02627,89713.776.1813767.6
v0.3.42,04828,12713.736.1613867.4
latest 0.3.5 snapshot (62ba248)2,16129,73313.766.1613964.3

The simplistic conclusion would be “complexity and duplication increased.” The normalized evidence says something different: the indexed symbol count grew by about 31%, average complexity and fan-out slightly declined, and duplicate pairs grew by only two. Duplicate pairs per 1,000 symbols fell by about 23%.

The current hotspot view still identified src/db/schema.rs as the strongest pressure point because it combined high complexity with 11 commits in the 90-day window. That is a useful investigation target, not an instruction to split the file blindly.

The duplicate list also contained parallel enum as_str methods, analogous test helpers, and similar read-only database query methods. Some may be reusable; others are intentionally explicit. Each pair needs source and ownership context before action.

A five-commit local backfill trial also demonstrated that coverage must be checked from persisted metadata. Four commits failed when the environment initially prevented temporary worktree creation, but the command still exited successfully and its JSON report mentioned only the already-existing partition. After worktree access was restored, the four missing partitions were captured and all five intended SHAs appeared in snap.meta.

Finally, every snapshot reported zero architecture violations because the repository had no committed active architecture rules; the current local starter file is also empty. That is a coverage gap, not proof of perfect architecture. This is why a health report must record the policy and tooling configuration that gave each metric meaning.

Give the workflow to an agent

Ask an agent to report evidence and uncertainty, not just recommendations:

Analyze the ctx snapshot history between v0.3.1 and the latest commit. Normalize totals by
repository size, identify persistent changes rather than one-commit spikes, and inspect the files
or symbols behind the strongest signal. Classify each finding as intentional, likely accidental,
or insufficient evidence. Do not recommend a refactor solely because a metric is high.

The published ctx agent skill encodes the same safeguards so compatible agents can route trend questions to ctx sql --snapshots and investigate the responsible code before proposing changes.

Next steps

  • Add a scheduled job or release workflow that turns the series into a health report.
  • Introduce architecture rules gradually, then annotate when policy coverage changes.
  • Compare release-to-release trends rather than gating every absolute metric.
  • Follow up with ctx score for point-in-time change evaluation and ctx snapshot for the complete snapshot schema and backfill behavior.