Skip to main content

ctx snapshot

Capture per-commit Parquet metric snapshots for longitudinal quality analysis.

Synopsis

ctx snapshot [--force] [--churn-window <SPEC>] [--json]
ctx snapshot backfill --since <REF> [--every <N>] [--churn-window <SPEC>] [--json]

Description

Point-in-time commands like ctx score answer "did this change make the code better or worse?". ctx snapshot answers the longitudinal version: is the codebase trending better or worse over weeks and months? Each run exports the current commit's per-file and per-symbol metrics, near-duplicate pairs, and capture metadata as one Parquet partition:

.ctx/snapshots/sha=<sha>/symbols.parquet     per-symbol metrics
.ctx/snapshots/sha=<sha>/files.parquet       per-file metrics + churn + violations
.ctx/snapshots/sha=<sha>/dup_pairs.parquet   near-duplicate function pairs
.ctx/snapshots/sha=<sha>/meta.parquet        capture metadata (1 row)

Accumulate partitions over time (one per commit), then query them with ctx sql --snapshots to plot duplication, violation, and hotspot trends across the history.

A bare ctx snapshot captures HEAD: the index is refreshed incrementally first (same as ctx score), then the four Parquet files are written to a staging directory and moved into place with an atomic rename — readers never observe a half-written partition. If a partition for HEAD's sha already exists the command is a no-op (exit 0); --force rewrites it. When the working tree is dirty, a stderr warning notes that the snapshot is labeled with HEAD's sha but reflects the working tree.

Snapshot capture requires the duckdb feature (on by default); builds without it exit 2.

Options

OptionDescriptionDefault
--forceOverwrite an existing partition for HEADfalse
--churn-window <SPEC>How far back to count per-file churn (a git log --since date spec)"90 days ago"
--jsonMachine-readable output (global flag)false

backfill adds:

OptionDescriptionDefault
--since <REF>Starting commit/ref — the walk covers REF..HEAD first-parent, plus REF itself when it names a commitrequired
--every <N>Sample every Nth commit; sampling counts backwards from HEAD so the newest commit is always included1

Partition contents

Every row of every table is denormalized with the partition stamp, so partitions union cleanly across commits with a single read_parquet('.ctx/snapshots/*/*.parquet') glob per table:

ColumnTypeDescription
commit_shaVARCHARFull sha of the snapshotted commit
committed_atTIMESTAMPCommitter date of that commit, normalized to UTC

symbols.parquet — one row per symbol

Stamp columns plus id, name, qualified_name, kind, file, line_start, line_end, is_public, complexity, fan_in, fan_out — the same columns (and types) as the v1.symbols SQL view, minus doc.

files.parquet — one row per file

Stamp columns plus:

ColumnTypeDescription
pathVARCHARFile path
languageVARCHARDetected language
symbol_countBIGINTSymbols defined in the file
total_complexityDOUBLESum of symbol complexity for the file
max_complexityBIGINTHighest single-symbol complexity in the file
churn_commitsINTEGERCommits touching the file within the churn window
violation_countINTEGERArchitecture-rule violations in the file (0 when .ctx/rules.toml is absent)

dup_pairs.parquet — one row per near-duplicate pair

Stamp columns plus:

ColumnTypeDescription
file_a / file_bVARCHARFiles of the two symbols
symbol_a / symbol_bVARCHARNames of the two symbols
similarityDOUBLEVerified token similarity (0–1)
token_count_a / token_count_bBIGINTNormalized token counts

Pairs use the same detector and thresholds as ctx score's new_duplication (Jaccard >= 0.85, >= 50 tokens).

meta.parquet — one row per partition

Stamp columns plus:

ColumnTypeDescription
captured_atVARCHARRFC 3339 time the snapshot was captured
ctx_versionVARCHARctx version that wrote the partition
snapshot_schema_versionINTEGERSnapshot Parquet schema version (currently 1)
capture_modeVARCHARlive (bare ctx snapshot) or backfill

Backfilling history

ctx snapshot backfill --since <REF> captures partitions for historical commits so trend queries have a past to look at. It walks the first-parent range REF..HEAD oldest-first (including REF itself when it resolves to a commit), checks each missing commit out into a temporary git worktree, snapshots it into this repository's .ctx/snapshots/, and removes the worktree again — your working tree is never touched.

ctx snapshot backfill --since v0.1.0             # every first-parent commit since v0.1.0
ctx snapshot backfill --since main~200 --every 5 # sample every 5th commit

Caveats:

  • First-parent only. Commits that live on merged side branches are not walked; a squash/merge-based history is covered completely, a fast-forward-heavy one is not.
  • The churn window's lower bound is relative to now. --churn-window is a git log --since date spec, and git resolves relative specs like "90 days ago" against the wall clock — even in backfill mode, where only the upper bound is anchored to each commit's date. For old commits, a relative window therefore spans more than 90 days of their history; pass an absolute date if that matters to your analysis.
  • Per-commit failures are skipped, not fatal. A commit that fails to index or export is logged to stderr and the walk continues; the final report covers the captured and already-existing partitions only. Existing partitions are always skipped (no --force in backfill mode).

JSON output

ctx snapshot --json emits the standard envelope with command snapshot.capture:

{
  "command": "snapshot.capture",
  "ctx_version": "0.3.0",
  "data": {
    "commit_sha": "86258796aa7f19c06d310f6abce6c5f56465e316",
    "committed_at": "2026-07-10T21:25:27+02:00",
    "dup_pairs": 0,
    "files": 2,
    "partition_dir": ".ctx/snapshots/sha=86258796aa7f19c06d310f6abce6c5f56465e316",
    "skipped_existing": false,
    "symbols": 3,
    "violations": 0
  },
  "generated_at": "2026-07-10T19:25:28.09532Z"
}

ctx snapshot backfill --json emits snapshot.backfill with per-partition reports:

{
  "command": "snapshot.backfill",
  "ctx_version": "0.3.0",
  "data": {
    "captured": 1,
    "since": "3019df548fc417c7b6b06bef7defb74a0c01ba78",
    "skipped_existing": 1,
    "snapshots": [
      {
        "commit_sha": "3019df548fc417c7b6b06bef7defb74a0c01ba78",
        "committed_at": "2026-07-10T21:25:27+02:00",
        "dup_pairs": 0,
        "files": 1,
        "partition_dir": ".ctx/snapshots/sha=3019df548fc417c7b6b06bef7defb74a0c01ba78",
        "skipped_existing": false,
        "symbols": 2,
        "violations": 0
      },
      {
        "commit_sha": "86258796aa7f19c06d310f6abce6c5f56465e316",
        "committed_at": "2026-07-10T21:25:27+02:00",
        "dup_pairs": 0,
        "files": 0,
        "partition_dir": ".ctx/snapshots/sha=86258796aa7f19c06d310f6abce6c5f56465e316",
        "skipped_existing": true,
        "symbols": 0,
        "violations": 0
      }
    ]
  },
  "generated_at": "2026-07-10T19:25:28.670878Z"
}

When skipped_existing is true the partition was left untouched and its row counts are reported as zero — they are not re-read from the existing Parquet files. See JSON Output.

Querying snapshots with ctx sql

ctx sql --snapshots[=DIR] (default DIR is .ctx/snapshots) loads the partitions as snap.files, snap.symbols, snap.dup_pairs, and snap.meta tables alongside the usual v1 views. For example, the violation trend across all snapshotted commits:

ctx sql --snapshots "SELECT commit_sha, min(committed_at) AS committed_at,
       sum(violation_count) AS violations
FROM snap.files
GROUP BY commit_sha
ORDER BY committed_at;"

The snap.* column reference and more canned trend queries (duplication trend, hotspot mass) live in the SQL schema reference — run ctx sql --schema, or see the SQL Schema (v1) reference.

Snapshots in CI (data branch)

To build the trend history automatically, capture one partition per merge to your default branch and append it to an orphan data branch, keeping metric history out of your main history. ctx's own repository does this in .github/workflows/snapshot.yml: on every push to main it runs ctx index && ctx snapshot --json, checks the ctx-snapshots orphan branch out into a linked worktree, copies the new partition in, and pushes (runs are serialized via a concurrency group, and re-runs on the same sha are no-ops). Analyze the accumulated history from any machine:

git fetch origin ctx-snapshots
git worktree add ../snapshots ctx-snapshots
ctx sql --snapshots=../snapshots/snapshots "SELECT ..."

Exit Codes

CodeMeaning
0Snapshot written, or the partition already existed
2Operational error (not a git repo, build without the duckdb feature, IO failure)

Examples

ctx snapshot                                 # snapshot HEAD (skip if it exists)
ctx snapshot --force                         # rewrite the HEAD partition
ctx snapshot --json                          # machine-readable report
ctx snapshot --churn-window "180 days ago"   # wider churn window
ctx snapshot backfill --since v0.1.0         # snapshot v0.1.0..HEAD (first-parent)
ctx snapshot backfill --since main~20 --every 5
ctx sql --snapshots "SELECT commit_sha, count(*) FROM snap.dup_pairs GROUP BY commit_sha"

See Also

  • ctx score — the point-in-time quality delta the snapshots accumulate over history
  • ctx duplicates — the near-duplicate detector behind dup_pairs.parquet
  • ctx check — the architecture rules behind violation_count
  • JSON Output