Skip to content

v2.0.0

Choose a tag to compare

@github-actions github-actions released this 29 Jun 17:18
· 719 commits to main since this release

The project's first major version bump since 1.0, and the first
stable release on the 2.x line. It collects every breaking change
staged across the 1.x cycle behind a single major boundary. The
detailed (breaking) entries are listed under the headings below;
the migration notes here summarise the surface and consolidate the
serialized key map and the metric-value re-baseline that the
contract promised the 2.0.0 entry would carry. (The 2.0.0-rc1
release candidate, tagged 2026-06-19, carried the same change set
ahead of this stable cut.)

Migration from 1.x

The breaking changes fall into a few groups, each detailed under its
own (breaking) entry below:

  • Library Stats accessors and metric naming — Halstead,
    NArgs, and MI accessors were renamed to a uniform wire
    vocabulary, the exit metric module was renamed to nexits, and
    the Metric::NArgs / Metric::Exit variants became
    Metric::Nargs / Metric::Nexits.
  • Serialized output shape — metric keys were normalised
    (#510, #511), integer-valued metrics now serialize as integers and
    their accessors return u64 (#530), non-finite floats serialize as
    a uniform null (#531), and several wire keys were renamed (see the
    key map below).
  • CLI grammar — flags renamed (--language-type--language,
    --num-jobs--jobs, --warning--warnings), exit codes
    restructured (argv errors exit 1; 2–5 reserved for metric gates),
    and several argument-parsing behaviours tightened.
  • REST schema — uniform {error, error_kind, id} error and
    {id, language} analysis envelopes, stricter unknown-field
    rejection, a nested per-file vcs object, and removal of the
    unprefixed route aliases.
  • Default grammars.js / .jsx now parse through upstream
    tree-sitter-javascript (the Mozilla fork is demoted to the opt-in
    mozjs, owning only .jsm), .cpp / .h through upstream
    tree-sitter-cpp (Mozilla fork demoted to opt-in mozcpp), .c
    through a new LANG::C, and .m through a new LANG::Objc.
  • Python bindings — the typed surface tightened and
    analyze_batch's skip_generated default flipped from False to
    True.

Metric-value re-baseline

2.0.0 is a one-time metric-value re-baseline boundary. Values
shifted across the 1.x cycle from metric-definition fixes
(divide-by-zero guards across the suite, the per-function
cyclomatic average) and, at 2.0, from the default-grammar flips:
.c files now parse through tree-sitter-c, .m through
tree-sitter-objc, and the Mozilla C++ overlay was swapped for
upstream tree-sitter-cpp — each moves the affected files' numbers.
The integration snapshots were re-baselined in lockstep. Consumers
comparing across the 1.x2.0 boundary should treat it as a
single re-baseline rather than reconciling the union of every
patch-level drift; pin an exact version and store it alongside your
results if you need bit-for-bit reproducibility.

Serialized key & accessor renames

Library Stats accessor renames — serialized output keys are
unchanged
(these are Rust method names only):

Metric Old accessor New accessor
Halstead u_operators unique_operators
Halstead operators total_operators
Halstead u_operands unique_operands
Halstead operands total_operands
NArgs fn_args (+ _sum/_average/_min/_max) function_args (+ _sum/_average/_min/_max)
NArgs nargs_total / nargs_average total / average
MI mi_original / mi_sei / mi_visual_studio original / sei / visual_studio
Nexits (was exit) exit (+ _sum/_average/_min/_max) nexits (+ _sum/_average/_min/_max)

Module / variant renames: the exit metric module became nexits
(crate::exitcrate::nexits), Metric::ExitMetric::Nexits,
Metric::NArgsMetric::Nargs. The retired "exit" metric parse
alias no longer resolves — only "nexits" parses.

Serialized wire-key renames (JSON / YAML / TOML / CBOR, and the
matching CSV columns):

Block Old key New key
npm classes / interfaces class_npm_sum / interface_npm_sum
npa classes / interfaces class_npa_sum / interface_npa_sum
wmc classes / interfaces class_wmc_sum / interface_wmc_sum
tokens tokens_average / tokens_min / tokens_max average / min / max

The bare-sum tokens leaf is kept, and the terminal dump's
tokens-sum label changed sumtokens. The truthful sibling keys
on npm / npa / wmc (class_methods, total, coa, cda, …)
are unchanged.

Type / shape: integer-valued metrics (every count, sum, and min/max,
plus Halstead length / vocabulary and all WMC values) now
serialize as integers and their Stats accessors return u64
instead of f64; ratios, averages, ABC magnitude, the derived
Halstead scores, and MI stay f64. No value changes — only the type.

Added

  • Python Node is now a closer py-tree-sitter drop-in: a type property
    aliases kind (the py-tree-sitter spelling — kind stays the canonical
    bca name), and text is now a property rather than a method, matching
    py-tree-sitter's node.text. Together these erase the two most-common
    mechanical edits when porting a py-tree-sitter walker. The text
    method→property shape change is part of the still-unreleased #728 surface,
    so it lands free before 2.0. Covered by the make py-stubtest gate (#975).

  • Public metric_catalog::MetricScope enum (File / Function /
    Container) with MetricScope::admits(SpaceKind), a
    metric_catalog::scope(id) lookup, a scope field on the
    #[non_exhaustive] MetricInfo, and SpaceKind::from_serialized
    the single source of truth for which space kind each threshold metric
    gates (#969), shared by the CLI gate and the Python to_sarif binding
    so they cannot drift.

  • Lazy Node traversal handle for Python (big_code_analysis.Node) over
    the tree retained by Ast, so a caller walks the AST py-tree-sitter-style
    kind (with the py-tree-sitter-compatible type alias), byte offsets,
    points, children, child_by_field_name, the text property, a lazy
    pre-order walk(), descendants_by_kind()without
    materialising the tree into dicts the way dump() does (#728). Reach one
    through the new Ast.root_node property or Ast.find(filters); the node
    keeps its Ast alive, so it stays valid after every other reference to
    the parse is dropped, and is safe to share across a ThreadPoolExecutor.
    Kinds are the raw grammar kinds (not the Alterator-curated kinds
    dump() emits — they intentionally disagree on altered nodes such as
    string literals), and each node exposes its location in every vocabulary:
    start_byte/end_byte, 0-based start_point/end_point (py-tree-sitter
    parity), and 1-based start_line/end_line plus a span dict matching
    dump(). Covered by the make py-stubtest gate.

  • Node::preorder() (a pre-order iterator) and
    Node::descendants_by_kind(kinds) on the Rust Node surface — the
    Rust counterparts the Python walk() / descendants_by_kind() mirror,
    so Rust callers gain the same ergonomic traversal helpers (#728).

  • Python Ast parse-once handle (big_code_analysis.Ast) binds the Rust
    Ast seam, so a Python caller parses a source once and draws both
    metrics and the AST from the same parse instead of parsing twice — once
    in py-tree-sitter, once in analyze() (#727). Ast.parse(code, language) and Ast.from_path(path) construct the handle; .metrics()
    (byte-for-byte analyze_source), .dump() (the bca dump / /ast node
    tree), .functions(), .ops(), .count(), .strip_comments(), and
    .suppressions() all reuse the one parse. from_path is no-magic: it
    reads through the same text reader as analyze (so metrics match) but
    does not skip generated files and never silently returns nothing. New
    AstNodeDict / SpanDict / FunctionSpanDict / OpsDict /
    SuppressionMarkerDict TypedDicts; all covered by the make py-stubtest
    gate.

  • Ast::from_path on the Rust surface (the file-backed counterpart to
    Ast::parse): reads + language-detects + parses one file, returning a
    new FromPathError for each distinct failure (I/O, non-UTF-8 path,
    empty/binary/non-text file, unknown language, disabled-language build)
    (#727).

  • language_grammar_version(language) (Python) and LANG::grammar_version
    (Rust) return the pinned tree-sitter grammar crate version backing a
    language (e.g. "0.25.1" for bash) — the exact upstream version for
    crates.io grammars, the fork crate version for the vendored forks
    (#727).

  • Byte offsets in the AST dump span: every dump node's span now carries
    start_byte / end_byte (0-based, half-open) alongside the existing
    1-based line/column pairs, across the library, CLI dump, web /ast,
    and the Python dump() (#727). Structural consumers can slice the
    original source for any node — including internal nodes whose value
    the dump omits — without re-deriving offsets from lines and columns.
    (See the (breaking) Span note under Changed for the Rust
    struct-shape impact.)

  • MetricSet::resolved() returns the set closed under
    Metric::dependencies (idempotent), the set-in/set-out counterpart of
    from_slice_with_deps (#743).

  • defang_formula is now public (re-exported from the crate root) so the
    CLI's VCS-report CSV writer can share the lib's CWE-1236 spreadsheet
    formula-injection mitigation rather than duplicating it (#794).

  • AuthorId::has_identity() reports whether a VCS author carries any
    usable name or email key (#817).

  • Per-space own value for the four subtree-aggregate metrics in the
    serialized wire shape: cyclomatic.value, cyclomatic.modified.value,
    cognitive.value, and abc.value (#958). Each space already carried
    its subtree aggregate (sum / magnitude); the new value field adds
    the per-space scalar — the value the CLI thresholds against, excluding
    nested function/closure spaces. SemVer-additive: it appears in every
    output format (JSON / YAML / TOML / CBOR) and in the Python
    CyclomaticDict / CyclomaticModifiedDict / CognitiveDict /
    AbcDict TypedDicts.

  • Opt-in keyed author-identity hashing for --emit-author-details
    (#956). A secret key — --author-hash-key <KEY> (or the
    BCA_AUTHOR_HASH_KEY environment variable, preferred so the secret
    stays off the process list), the REST author_hash_key field, and the
    Python vcs.Options(author_hash_key=…) — hardens the emitted author
    digests into an HMAC-SHA256(key, SHA-256(email)), defeating the
    email-enumeration and precomputed-table attacks a bare SHA-256
    pseudonym is vulnerable to (the Gravatar weakness; see #811). The key
    hardens only the emitted digests (it requires --emit-author-details)
    and is applied at finalization, so default output is unchanged and the
    persistent-cache replay invariant (#334) holds: the cache stores the
    unkeyed inner digest and a cached walk re-finalizes under any key
    without a re-walk. New library surface: vcs::AuthorHashKey, the
    additive Options::author_hash_key field, and AuthorId::emit_hashed.

  • LANG::Objc (slug objc) and the objc Cargo feature: dedicated
    Objective-C support backed by upstream tree-sitter-objc =3.0.2,
    owning the .m extension and the objc / objective-c emacs modes
    (all moved off LANG::Cpp). Objective-C is a strict superset of C, so
    .m files now parse correctly instead of ERROR-cascading every
    @interface / @implementation / message send through the C++
    grammar. Objective-C++ (.mm) deliberately stays on LANG::Cpp: the
    Objective-C grammar cannot parse the C++ half of a .mm file, and C++
    is the larger surface, so the C++ grammar degrades more gracefully
    there — the same asymmetric trade-off .h uses (a known limitation;
    metrics for the Objective-C portions of .mm files are approximate).
    Real impls ship for all metrics: cyclomatic, cognitive, exit,
    Halstead, LoC, nom, and nargs (#724), plus abc (message sends count
    as calls; @try / @catch as conditions) and the OO metrics npa
    (@property and @public instance variables), npm (@interface /
    @protocol declarations and @implementation definitions), and wmc
    (per-method cyclomatic rolled into the @implementation class) (#737).
    The retired internal fake::get_true Objective-C slug overlay (#540)
    is gone — .m reports "objc" and .mm reports "cpp" natively.
    all-languages now includes objc (#724, part of #718).

  • New book recipe, Feeding metrics to an agentic coding tool
    (recipes/agent-feedback.md): wires the existing bca check surface
    into an agent's after-edit feedback loop with copy-pasteable sections
    for Claude Code (PostToolUse hook with stderr/additionalContext
    injection) and opencode (a tool.execute.after plugin that throws to
    signal). Ships a verbatim anti-gaming guidance block, the exact
    in-source suppression syntax (canonical nexits, never exit), and
    the task-boundary-vs-per-edit and Goodhart caveats. Documentation
    only — no binary changes; contrasts itself with the proposed
    bca lsp (#384) (#733).

  • LANG::C (slug c) and the c Cargo feature: a dedicated C
    language backed by upstream tree-sitter-c =0.24.2, owning the
    .c extension and the c emacs mode (both moved off LANG::Cpp).
    .h deliberately stays on Cpp — a C++ header through the C grammar
    ERROR-cascades, while a C header through the C++ grammar only trips on
    C++-keyword identifiers. C code that uses C++ keywords (new,
    class, delete, template) as identifiers now parses cleanly
    instead of ERROR-cascading through the C++ grammar. C has no classes,
    so npm / npa / wmc are no-ops; the .c re-routing shifts metric
    values on C files (integration snapshots re-baselined). all-languages
    now includes c (#721, part of #718).

  • LANG::Mozcpp (slug mozcpp) and the opt-in mozcpp Cargo feature:
    the Mozilla/Gecko C++ dialect, backed by the vendored
    bca-tree-sitter-mozcpp fork (upstream tree-sitter-cpp plus the
    MOZ_* / QM_TRY_* / alone-macro overlay). It owns no file
    extensions — select it explicitly with --language mozcpp, a
    manifest, or the API, mirroring mozjs for .jsm since #507. The
    all-languages feature now includes mozcpp (#720, part of #718).
    Mozcpp is a first-class C++ dialect everywhere Cpp is: it shares
    the preprocessor macro-replacement pass, the comment-stripping
    redirect, and the bca-web GET /v1/languages listing (where it
    appears with an empty extensions array, for parity with the Python
    supported_languages() surface).

  • exclude_tests bca.toml manifest key: opt a project's bca check /
    bca metrics into Rust test-subtree pruning declaratively, mirroring
    the --exclude-tests flag (CLI wins; the presence-only flag means the
    key can only turn pruning on). Rust-only; purely additive — absent key
    preserves today's behaviour (#717).

  • metric_catalog::lower_is_worse(id): a #[must_use] helper answering
    whether a metric's unhealthy direction is downward (the mi.* family),
    single-sourcing the direction predicate the CLI threshold gate, the
    Code Climate severity inversion, and the Python SARIF binding all share
    (#698).

  • write_csv_aggregate (re-exported from the crate root): writes several
    metric trees into one CSV document under a single shared header row.
    Backs bca metrics/ops --output <FILE> --format csv (#669), which
    previously repeated the header before every file's rows.

  • A mypy stubtest gate (make py-stubtest) verifies the hand-written
    PyO3 type stub
    big-code-analysis-py/python/big_code_analysis/_native.pyi against the
    compiled extension — diffing names, signatures, and defaults — so a
    stub default can no longer silently drift from the
    #[pyo3(signature = …)] runtime the way #583 did (the usage-only
    make py-typecheck mypy/pyright passes cannot catch that class of
    drift). Wired into make pre-commit and make ci (chained after
    py-test, sharing its maturin develop build) and skipped with a clear
    "not found" message when the venv / maturin / stubtest are absent. A
    minimal, commented allowlist
    (big-code-analysis-py/stubtest-allowlist.txt) covers the deliberate
    facade differences (the vcs submodule, runtime __all__) (#673).

  • The HTML report's table of contents now nests each language's h3 hotspot
    subsections under its h2 entry in a collapsible <details> list (#685),
    reusing the per-language-unique ids the report already mints, so a reader
    can jump straight to one hotspot table instead of landing at the top of a
    ten-screen section. A global cross-language Actionable Summary roll-up is
    rendered near the TOC so a multi-language report gives one top-of-page
    signal (#678).

  • Legend entries and HTML column headers now link to the hosted metric
    reference (metrics.html#<anchor>) via one shared docs-base-URL constant
    and a per-metric anchor map (#675), so a one-line legend entry can hand
    the reader the full chapter. A test asserts every legend header maps to an
    anchor that actually exists in the book's metrics.md, so a renamed
    heading fails CI rather than shipping a dead link. The Markdown legend,
    HTML legend, HTML headers, and VCS legend all share the one constant.

  • Added a provenance footer to the Markdown and HTML AST reports (#680):
    Generated by bca <version> on <date> over <paths> — top <N> per table, suppression markers <honored|ignored>. The date honors SOURCE_DATE_EPOCH
    for reproducible builds; the suppression line is load-bearing, since
    hotspot-table membership depends on it.

  • The HTML report now carries a <meta name="viewport"> tag and wraps every
    table in an overflow-x:auto scroll container (#686), so it renders at
    device width on mobile and a wide table (the 21-column VCS table) scrolls
    instead of clipping its right-most columns.

  • (Python) big_code_analysis.language_for_extension(ext) — a
    filesystem-free extension → language lookup that accepts both "py" and
    ".py" (case-insensitive), returns None for an unknown extension, and
    never reads a file or raises (#682). Paired with a new
    language_for_file(path, *, read=False) option that resolves by
    extension alone — answering for paths that do not exist yet (archive
    listings, git trees, candidate filtering) — so the README/example dance
    of inverting the per-language extension table by hand collapses to one
    call.

  • (Python) big_code_analysis.analyze_paths(*paths, include=None, exclude=None, …) — a directory-walk entry point that reuses the CLI's
    gitignore-aware walker (include/exclude globs, generated-file filter,
    language inference) and returns the analyze_batch shape with the same
    never-raise semantics (per-file failures become AnalysisFailure
    elements) (#658). Each positional seed may be a file or a directory; it
    forwards the analyze kwargs (including vcs / vcs_per_function) so a
    data-science consumer can point it at a repository root instead of
    writing their own walker.

  • (Python) vcs / vcs_per_function boolean kwargs on analyze_batch,
    mirroring single-file analyze (#670). The batch builds one shared
    history index / blame engine per containing repository — keyed by the
    discovered work-tree root (vcs::workdir_root), so files in different
    subdirectories of one checkout (src/a.rs, tests/b.rs) share a single
    index rather than rebuilding it per directory — and reuses it across that
    repo's files (amortising the walk the comprehension form repeats per
    file); a VCS failure leaves the AST metrics intact and never becomes an
    AnalysisFailure. Keeps the "migrating
    [analyze(p) for p in paths] to analyze_batch(paths) is
    behaviour-preserving" claim true even when the comprehension used vcs=.

  • (Python) Typed TypedDicts for the change-history report shapes
    (#664): VcsReportDict (from vcs.rank), VcsTrendDict (from
    vcs.trend), JitCommitReportDict (from vcs.commit), and
    JitDiffReportDict (from vcs.score_diff) replace the former
    dict[str, Any] returns. The report / trend envelope structs are
    single-sourced in big_code_analysis::wire (the same drift-gated
    generator the analysis-result dicts use), so the Python types cannot
    diverge from the JSON the CLI emits.

  • big_code_analysis::vcs::workdir_root(path) — discover the canonicalised
    work-tree root of the repository enclosing a file or directory, or None
    when it is outside any repository (or the repository is bare). Lets a
    front end coalesce a batch of files onto the repository each belongs to;
    the Python analyze_batch(vcs=True) cache uses it so files in different
    subdirectories of one checkout share a single history index (#670).
    Additive, vcs-git-gated.

  • bca metrics --metrics <name,…> restricts computation to a subset of
    metrics via the public MetricsOptions::with_only (dependencies
    auto-resolved). Accepts comma-separated and/or repeated values using
    the same canonical ids as check --threshold / diff --metric
    (dotted and bare loc sub-metric spellings included); an unknown name
    errors (exit 1) with a did-you-mean. Default (flag absent) computes
    every metric (additive) (#691).

  • bca diff / bca diff-baseline gain an opt-in --exit-code flag:
    exit with the metric-gate code (2) when the diff, after the active
    --metric / --min-change (or --*-only section) filtering, is
    non-empty; exit 0 when empty. Default behavior is unchanged (always
    0 on success); a tool error still exits 1. git diff --exit-code-style boolean for grammar-bump CI (#692).

  • Metric legend in reports: Markdown gains a ### Legend footnote and
    HTML a visible collapsible legend, with per-column definitions hoisted
    onto the shared column specs so the HTML tooltips and both legends
    draw from one source; also fixes the bus-factor "Files" / "Bus
    factor" HTML tooltips (#611, refs #610).

  • HTML report navigation: slug-based heading anchors, a
    table-of-contents <nav>, and aria-sort initial-sort indication on
    each pre-ranked table column (#622).

  • --color auto|always|never global CLI flag with tty detection and
    NO_COLOR support; piped text dumps (metrics/ops default tree,
    dump, find, functions) no longer emit ANSI escapes. Library
    gains the additive ColorMode enum and dump_*_with_color /
    dump_function_spans_with_color variants (#605).

  • vcs::Error::is_client_input(): exhaustive client-input vs
    environment classification shared by the web 400/500 mapping and the
    Python exception taxonomy (additive) (#641).

  • GET /v1 route index endpoint generated from a single route table
    (the unprefixed / alias serves it with deprecation headers), and
    the REST book chapter now documents the full /vcs family (#643).

  • Deprecation/Sunset/Link: rel="successor-version" headers on the
    unprefixed legacy web route aliases; removal of the aliases remains
    scheduled for the 2.0 cut (#637, refs #517).

  • Python: typed VCS exception taxonomy — VcsError(ValueError) with
    NotARepositoryError, InvalidRevisionError, InvalidDiffError,
    and VcsEnvironmentError; existing except ValueError handlers
    keep working (#624).

  • Python: generated TypedDict stubs for the analysis result shapes
    (FuncSpaceDict and nested metric dicts), rendered from the Rust
    wire shapes with a byte-compare drift gate; analyze /
    analyze_source / batch returns are now statically typed (#623).

  • Python: VCS kwargs accept native types — cache_dir takes
    os.PathLike, as_of takes datetime, file_types takes a
    sequence of extensions — alongside the existing string forms (#619).

  • Change-history (VCS) metrics: a new, language-agnostic metric family
    derived from git history rather than the AST (#328). A single history
    walk produces per-file signals over two windows (default 12mo / 90d) —
    distinct commits, line churn, distinct authors, top-author ownership
    share, burst, bug-fix / security-fix / revert commit counts, file age
    and last-modified days — combined into an ordinal, formula-versioned
    composite risk_score (--risk-formula weighted|percentile), plus a
    hotspot_score (complexity × recent churn) when AST metrics are
    computed alongside. Built on gix
    behind the hierarchical vcs = ["vcs-git"] Cargo feature; the generic
    vcs module is backend-neutral so future backends (#335) reuse it.
    Surfaces:

    • Library: big_code_analysis::vcs::{build_history_index, Options, Stats, HistoryIndex, …}, wire::Vcs, and
      CodeMetrics::vcs: Option<vcs::Stats> (all behind vcs-git).
    • CLI: a new bca vcs subcommand. --format accepts a rendered
      report page (markdown / html, a self-contained sortable table
      styled like bca report html), the structured formats (json /
      yaml / toml / cbor / csv), or a default ranked table.
      bca vcs --output <file> writes a single whole-repo document (not
      the per-file directory metrics / ops emit). Plus bca metrics --vcs to attach a vcs block to each file's metrics, and bca report markdown|html --vcs to append a "Change-history risk"
      section to the aggregated quality report (#573). The Markdown and
      HTML renderers share one column spec so they cannot drift. The HTML
      report (both bca vcs --format html and the bca report html --vcs
      section) severity-heats the risk_score cell on a green→yellow→red
      gradient (#577); because risk_score is ordinal, the band is derived
      from each row's relative rank within the displayed set (five equal
      quantile bands), not from absolute thresholds, with WCAG-AA-contrast
      light and dark-mode palettes. Markdown output stays plain text.
      bca vcs errors clearly outside a git working tree; --include /
      --exclude / --paths are reused.
    • Web: a new POST /vcs endpoint taking a server-side repo_path.
    • Python: vcs_metrics(repo_path, …) and an opt-in vcs=True on
      analyze().

    Design note: the issue proposed a Metric::Vcs enum variant +
    --metrics vcs; VCS is file-level, has no per-function threshold, and
    is not suppressible, so it is exposed via a dedicated --vcs flag
    rather than overloading the per-function Metric bitfield.

  • Change-entropy and co-change graph-entropy VCS signals (#330). The
    single history walk now also emits four per-file fields:
    change_entropy_long / change_entropy_recent (Hassan 2009 History
    Complexity Metric — how scattered a file's changes are across commits;
    file-level Pearson 0.54 with defects on Apache projects) and
    cochange_entropy_long / cochange_entropy_recent (arXiv 2504.18511,
    2025 — how widely a file's changes ripple to co-changing partners,
    computed from a sparse co-change graph built during the walk). Both are
    Shannon entropies in bits; a 0.0 is computed (the file only ever
    changed alone), not "missing". Bulk-import commits wider than 1000 files
    are excluded from the co-change graph to bound its O(width²) growth.
    These fold into the composite score as a risk_score_version bump to
    2
    (the recent-window pair enters both the weighted and percentile
    formulas); the new formula is documented in src/vcs/score.rs and the
    mdBook VCS chapter. Because the serialized field set grew, the
    output-shape stamp vcs_schema_version also bumps to 2. Surfaced
    on every VCS front end (library Stats / wire::Vcs, bca vcs CSV /
    Markdown / HTML, POST /vcs, and the Python bindings). Additive
    field-set change — no existing field moved.

  • Per-function change-history metrics via git blame (#329). bca metrics --vcs-per-function (which implies --vcs) attaches a vcs
    block to every nested function / method / class space in addition to
    the file-level block, by blaming each file once and bucketing the
    surviving lines into the AST function spans. Each function's block
    reuses the same fields and ordinal risk_score as the file block,
    plus a per-function hotspot_score. The per-function numbers are a
    current-blame snapshot and deliberately differ in meaning from the
    file-level walk: churn counts surviving lines last touched in the
    window (not historical added+deleted), and ownership is by touching
    commit. Surfaces:

    • Library: big_code_analysis::vcs::{PerFunctionBlame, LineSpan} and a
      new vcs::Error::Blame variant (all behind vcs-git); nested
      CodeMetrics::vcs is now populated for function spaces, not only the
      file space.
    • CLI: bca metrics --vcs-per-function.
    • Enables the blame feature on the pinned gix dependency.

    Design note: the issue proposed a --metrics vcs:per-function
    sub-selector; consistent with the --vcs flag chosen for #328, this
    ships as a dedicated --vcs-per-function flag instead. Documented
    limitations cover renames, function splits, deletion+recreation, and a
    narrow gix-blame robustness bug on pathologically repetitive files
    (real source is unaffected; an unblameable file degrades gracefully to
    the file-level block only). Python analyze() / web parity for the
    per-function selector is tracked as a follow-up.

  • Just-in-time (commit-level) VCS risk scoring (#331). bca vcs jit <commit> scores a single commit for defect-induction risk at
    check-in — the unit a CI gate reviews — rather than ranking files at
    HEAD. It is a static, rule-based scorer (no trained model, so nothing
    drifts as a project ages), with feature groups and signs taken from the
    just-in-time defect-prediction literature (Kamei et al., IEEE TSE 2013;
    open replications Commit Guru, FSE 2015 and McIntosh & Kamei, IEEE TSE
    2018): size (lines added/deleted, files, hunks), diffusion
    (subsystems, directories, within-commit change entropy), history
    (the touched files' priors — prior changes, distinct authors, bug- and
    security-fix counts, and the #328 composite risk_score, measured from
    history before the commit), experience (the author's prior commit
    count, which lowers the score — the one protective Kamei signal), and
    purpose (fix / security-fix / revert classification). The output is
    a stable JSON document with per-group feature contributions and an
    ordinal, formula-versioned composite score; --fail-over <SCORE>
    exits 2 (the check metric-gate convention) for CI use. Merge commits
    are scored against their first parent and flagged; root commits and new
    files carry zero priors by construction. Surfaces:

    • Library: big_code_analysis::vcs::{score_commit, JitReport, JitFeatures, JitContributions, JitCommit, JIT_SCORE_VERSION, JIT_SCHEMA_VERSION, …} (behind vcs-git); reuses the #328 history
      walk for the file priors and a separate cheap author-only walk for
      experience.
    • CLI: bca vcs jit <commit> [-O json|yaml|toml|cbor] [--fail-over <SCORE>], reusing the parent bca vcs window / --ref / bot /
      merge / rename flags. The bare bca vcs ranking path is unchanged.

    Scope note: scoring an arbitrary --diff <file> (no commit, so no
    author / parent / file-history context — only size and diffusion would
    be computable) and web / Python parity are deferred to follow-ups;
    ML-based JIT and server-side hooks are out of scope per the issue.

  • Directory- and repo-level bus factor (truck factor) VCS aggregate
    (#332). When a front end opts in, the single history walk now also
    emits a top-level vcs_aggregate.bus_factor object alongside the
    per-file vcs data: the minimum number of developers whose departure
    would orphan more than a configurable fraction (default 0.5, per
    Avelino) of a directory's files. Authorship is scored with the Avelino
    Degree-of-Authorship heuristic (Avelino et al., ICPC 2016 — 3.293 + 1.098·FA + 0.164·DL − 0.321·ln(1+AC), normalised, with the paper's
    0.75 author threshold), and the truck factor is the greedy
    most-files-first removal. Reported for the whole repository (repo)
    and for each top-level directory and its immediate subdirectories
    (by_directory); under --emit-author-details each group also lists
    the SHA-256-hashed key developers in removal order. Files with no
    in-window authorship (and bot identities, already filtered) are
    excluded from the denominator. Surfaces:

    • Library: big_code_analysis::vcs::{BusFactor, GroupBusFactor, DirectoryBusFactor, VcsAggregate, BUS_FACTOR_SCHEMA_VERSION}, a new
      HistoryIndex::bus_factor() accessor + with_bus_factor builder,
      Options::{compute_bus_factor, bus_factor_threshold}, a
      vcs::options::validate_bus_factor_threshold helper, and a new
      vcs::Error::InvalidBusFactorThreshold variant (all behind
      vcs-git). The generic bus_factor module is backend-neutral.
    • CLI: bca vcs and bca report --vcs emit vcs_aggregate in every
      structured format and render it in the table / Markdown / HTML
      pages; --bus-factor-threshold <F> (in (0, 1)) tunes the coverage
      fraction.
    • Web: POST /vcs gains a bus_factor_threshold field and returns
      vcs_aggregate.
    • Python: vcs_metrics(…, bus_factor_threshold=…) returns
      vcs_aggregate in the result dict.

    Opt-in by design (compute_bus_factor, off by default): it retains
    per-file authorship beyond the per-file Stats, so the repeated
    JIT-prior and per-file-injection walks neither compute nor pay for it.
    Additive — no existing field moved, and vcs_schema_version is
    unchanged (the aggregate carries its own BUS_FACTOR_SCHEMA_VERSION).

  • Historical metric trend (#333). Samples the change-history metrics
    at several points in time so a consumer sees whether a file's risk is
    improving or degrading over the project's life, not only its risk now
    — the actionable question for technical-debt programs (the Kamei JIT
    survey notes single-snapshot models lose predictive power as a project
    ages; a trend is the more durable framing). points evenly-spaced
    samples (inclusive of both endpoints) cover a span, ending at as_of
    (or wall-clock now); each sample re-anchors at the mainline tip that
    existed at or before that moment
    (resolved from one first-parent walk)
    rather than windowing today's HEAD tree, so it is a faithful
    historical snapshot — a file not yet born at an older point is null
    there. The output is a versioned (trend_schema_version) time series:
    as_of_points (oldest-first) plus a per-file array aligned to it, and a
    most-improved / most-regressed deltas summary by risk_score. The
    point count is bounded (2–120) to cap the per-point walks on deep
    histories. Surfaces:

    • Library: big_code_analysis::vcs::{build_trend, Trend, TrendDelta, TrendDeltas, TREND_SCHEMA_VERSION}, the wire::{VcsTrend, VcsTrendPoint, VcsTrendDelta, VcsTrendDeltas} projection, and a new
      vcs::Error::InvalidTrend variant (all behind vcs-git). The generic
      trend module is backend-neutral.
    • CLI: bca vcs trend [--points N] [--span DURATION] [--top-deltas N] [-O json|yaml|cbor], reusing the parent bca vcs window / --ref /
      bot / merge / rename / --as-of / --top flags. (TOML is excluded —
      an absent point serializes as null, which TOML cannot represent.)
    • Web: a new POST /vcs/trend endpoint taking the /vcs fields plus
      points / span / top_deltas.
    • Python: vcs_trend(repo_path, points=…, span=…, …).

    Rename limitation: renames are followed within each sample's walk,
    but a file renamed between two samples appears as two separate path
    series (old name, then new); cross-sample rename stitching is deferred.

  • Persistent change-history cache keyed by HEAD SHA and repository
    identity (#334). Re-running a VCS analysis on an unchanged tree now
    replays a cached, pre-finalize event log instead of re-walking history;
    when HEAD has advanced it walks only the new commits and splices them
    onto the cached tail. The cache is a pure optimization — a hit is
    bit-identical to a fresh walk, and re-windowing tracks the current
    reference time rather than freezing at cache-write time. A force-push
    (the cached head is no longer an ancestor) falls back to a full walk;
    an entry is ignored when the cache format, vcs_schema_version,
    risk_score_version, or the walk-option fingerprint (windows, traversal
    mode, merge / rename / bot toggles, --as-of) differs, so a window
    change forces a fresh walk. Writes are atomic (temp file + rename), and
    a missing or corrupt entry is silently recomputed, never fatal. Author
    identities are stored only as their irreversible SHA-256 digests —
    never plaintext — so the cache is not a side channel for raw emails.
    Surfaces:

    • Library: big_code_analysis::vcs::{build_history_index_cached, CacheConfig, CACHE_SCHEMA_VERSION} and the vcs::cache module
      (behind vcs-git); AuthorId::from_digest reconstructs a hashed
      identity for replay. The generic event-log replay (vcs::replay) is
      now the single fold shared by the live walk and a cache hit, so the
      two cannot diverge.
    • CLI: bca vcs --no-cache / --clear-cache / --cache-dir <DIR>.
      The cache defaults to $XDG_CACHE_HOME/big-code-analysis/vcs (or the
      platform equivalent) and is enabled by default; bca metrics --vcs
      and bca report --vcs reuse it transparently.
    • Web: POST /vcs gains optional no_cache / cache_dir fields.
    • Python: vcs_metrics(…, no_cache=False, cache_dir=None).
  • File-type scoping for the change-history ranking (#576). bca vcs now
    ranks only files bca has metrics for by default instead of every
    tracked text file, so high-churn non-source files (CHANGELOG.md,
    Cargo.lock, CI config) no longer dominate the risk ranking and the
    standalone ranking agrees with the AST hotspot tables (bca report --vcs). A new --file-types <SCOPE> flag selects the scope: metrics
    (the default — resolved by the same extension predicate the metrics
    walk uses, so it stays in lockstep as languages are added/removed),
    all (the previous whole-tree behaviour), or a comma-separated
    extension allow-list (rs,py,toml; leading dots optional,
    case-insensitive). The filter is extension-only (no blob content is
    read) and ANDs with --paths / --include / --exclude. Because the
    whole VCS surface is still unreleased, the default flip is not a
    stability break. The scope is applied at file enumeration (an
    out-of-scope file is never seeded), so it does not affect the cached
    event log — a cache written under one scope replays correctly under
    another. Surfaces:

    • Library: big_code_analysis::vcs::{FileTypeScope, Options::file_types}
      and a new vcs::Error::InvalidFileTypeScope variant (behind
      vcs-git).
    • CLI: bca vcs --file-types <metrics|all|EXT,…> plus a bca.toml
      [vcs] file_types key (the CLI flag replaces the manifest value).
    • Web: POST /vcs gains an optional file_types field.
    • Python: vcs_metrics(…, file_types=None) and vcs_trend(…, file_types=None).
  • Ast::strip_comments(), Ast::functions(), Ast::dump(cfg),
    Ast::count(filters), Ast::find(filters), and Ast::suppressions()
    complete the parse-once Ast seam: comment removal, function-span
    detection, AST-node dumping, node counting/finding, and suppression
    scanning now have explicit-name, re-parse-free counterparts alongside
    Ast::metrics / Ast::ops. Ast is now the single entry point for
    every analysis operation. Output is identical to the existing
    parser-generic free fns and the action/Callback dispatch (which
    become redundant and are retired in the 2.0 surface reshape,
    #566/#570) (#567, #571).

  • ParseMetricError::input() and ParseLangError::input() accessors
    return the rejected input string (previously Display-only) (#536).

  • bca strip-comments gains --output/-o to write a single file's
    stripped source to a path (stdout when omitted); mutually exclusive
    with --in-place, and a multi-file input is rejected rather than
    clobbering one path (#539).

  • bca-web: GET /v1/version and GET /v1/languages introspection
    endpoints (with unprefixed /version and /languages aliases),
    mirroring the Python __version__ / supported_languages() /
    language_extensions() surface (#541).

  • bca-web --num-jobs now accepts <N|auto> and defaults to a
    cgroup-quota- / cpuset-aware auto, matching the bca CLI. The
    clap-agnostic NumJobs worker-count selector (FromStr + resolve())
    is now public library API, re-exported from big_code_analysis; its
    FromStr::Err is the named ParseNumJobsError (Zero /
    NotAPositiveInteger, each exposing the rejected input(),
    Display + Error), matching the ParseMetricError / ParseLangError
    convention (#560).

  • Derived PartialEq on the compute-side per-metric Stats types (abc,
    cognitive, cyclomatic, exit, halstead, loc, mi, nargs, nom, npa, npm,
    tokens, wmc) and on CodeMetrics / FuncSpace / Metrics, so callers
    can compare analyses without round-tripping through to_wire() (Eq
    omitted due to float fields); derived Hash on SpaceKind and
    metric_catalog::Direction; derived Hash + PartialOrd / Ord on
    Severity (ordered scale: Error > Warning, following declaration
    order) (#552).

  • ConcurrentErrors now implements Display + std::error::Error, so it
    composes with ? into Box<dyn Error> / anyhow and participates in a
    source() chain (#553).

  • Documented the workspace-wide bca exit-code convention (0 success,
    1 tool error, 2 check gate, 3-5 check --strict-exit-codes) in
    top-level bca --help and the book, pinned by exit-code tests (#561).

  • STABILITY.md now locks the output-format contracts: CSV_HEADER
    column order, the SARIF 2.1.0 schema version + canonical URI, the
    code-climate field set and fingerprint algorithm, the AST JSON shape
    (one-way Serialize-only), and the round-trip vs one-way format split;
    wire::CyclomaticModified is named in the serialized-shape enumeration
    (#559).

  • Library: big_code_analysis::VERSION constant exposing the crate
    version (#541).

  • Python: Lang and MetricName StrEnums (generated from the live
    LANG / Metric::NAMES tables, so Lang.CPP == "cpp" and values
    round-trip with the CLI/JSON slugs); analyze_batch gains
    exclude_tests / allow_lossy_path / skip_generated keyword
    arguments (#542).

  • The serialized metric output is now readable back: a new public
    big_code_analysis::wire module provides plain Serialize/Deserialize
    structs (wire::FuncSpace, wire::CodeMetrics, wire::Ops,
    wire::FunctionSpan, and one per metric) mirroring the exact JSON / YAML
    / TOML / CBOR shape. The compute types' Serialize impls now delegate to
    these structs (the single definition of the wire shape — output is
    byte-identical), and the public types gain to_wire()
    (FuncSpace/CodeMetrics/Ops/FunctionSpan). Read a tree back with, e.g.,
    serde_json::from_str::<wire::FuncSpace>(&json). Non-finite floats map
    nullNaN (the deserialize side of #531); integer-valued fields are u64
    (#530); wire::CodeMetrics elides unselected metrics and exposes
    selected() to rebuild the MetricSet from present keys. SpaceKind,
    SuppressionScope, and Metric now also derive Deserialize, and the
    crate enables serde_json's float_roundtrip feature so float values
    round-trip bit-exactly through JSON. Additive — no serialized output or
    existing accessor changes
    (#532, keystone of
    the #510/#530/#531 serialization-schema cluster, part of
    #505).

  • bca diff and bca diff-baseline now accept --output/-o <PATH> (writing
    to the file when given, stdout when omitted) and --strip-prefix <PREFIX>
    (trimming the prefix from displayed file paths in the TTY and Markdown
    per-file tables; a no-op for --format json), for parity with report and
    exemptions (#544).

  • Round-trip smoke-test coverage for the TOML, YAML, and CBOR per-file output
    formats in big-code-analysis-cli, parsing each format back and asserting
    structural keys and integer-valued numeric fidelity against JSON (#543).

  • Ast::ops() — the Source-based counterpart of get_ops. Returns the
    operator/operand Ops tree for a parsed Ast, carrying the
    Source::name (Option<String>) end-to-end instead of deriving the
    top-level Ops::name from a filesystem path via lossy UTF-8 conversion.
    This closes the last public seam that keyed function identity off a lossy
    path: a None source name now yields a None top-level Ops::name
    (which get_ops cannot express), and Ops::name_was_lossy is never set
    on this path. Mirrors Ast::metrics
    (#509,
    part of #505).

  • LANG now derives Hash and implements Display (its name()
    string) and FromStr (parsing that canonical name; case-sensitive, error
    type ParseLangError). After the #507 JavaScript-grammar split the only
    variants still sharing a display name are Tsx / Typescript (both
    typescript), which parse back to the first-declared variant (Tsx);
    every other name — including javascript and mozjs — round-trips
    exactly. Serialize / Ord are deferred to the 2.0 bump
    (#508).

  • The bca CLI is now pip-installable. pip install big-code-analysis-cli
    drops the compiled bca binary onto your PATH (no Rust toolchain
    required), the way pip install ruff installs the ruff command. The
    PyPI distribution name is big-code-analysis-cli while the installed
    command stays bca — distinct from the importable library bindings
    published as big-code-analysis. A new
    python-cli-wheels.yml
    workflow builds -b bin wheels for Linux (manylinux_2_28 x86_64 /
    aarch64), macOS (x86_64 / arm64), and Windows (x86_64),
    smoke-tests each, and publishes to PyPI via Trusted Publishing in
    lockstep with the workspace version. Each wheel carries the full
    all-languages grammar set and bundles the per-binary
    THIRD-PARTY-LICENSES-bca.md + LICENSE (in .dist-info/licenses/)
    and the bca man pages. (#408)

  • bca report markdown|html now honors in-source suppression markers
    (bca: suppress, bca: suppress-file, #lizard forgives) by
    default
    , omitting a function from a metric's hotspot table when that
    metric is suppressed for it — matching bca check and the SARIF
    emitter (the report previously listed raw values and re-surfaced every
    silenced function). Suppression is per-metric and folds the file's
    suppress-file scope into each function's own scope. bca report --no-suppress (or [report] no_suppress = true in bca.toml) opts
    into the raw audit view that lists every offender. The Actionable
    Summary roll-up is the sole figure that intentionally keeps counting
    raw measurements (a whole-codebase health indicator); every per-metric
    hotspot caption — including the cyclomatic Average/Max/CC>10 note —
    reflects the suppression-filtered set and is identical across the
    Markdown and HTML reports. SuppressionScope::merge is now pub
    (additive) so report consumers can fold scopes
    (#501).

  • bca check --report-suppressed: surface the debt the gate tolerates in
    the code-scan document instead of dropping it. Offenders silenced by an
    in-source bca: suppress marker or covered by the baseline stay out of
    the gate (exit code and human stream unchanged) but are emitted into the
    --output-format sarif document carrying a SARIF suppressions entry
    (kind: "inSource" for markers, "external" for the baseline). Only the
    SARIF format represents suppression; the flag is mutually exclusive with
    --no-suppress and --write-baseline. Note: GitHub code scanning does
    not honor the SARIF suppressions property natively — it ingests such
    results as open alerts — so this flag targets downstream tooling that
    reads suppressions (e.g. the advanced-security/dismiss-alerts action).
    The repo's own Pages workflow does not pass it; its Code Scanning upload
    carries active offenders only.

  • Library: new write_sarif_with_suppressed(active, in_source, baseline, writer) writer that emits SARIF suppressions entries for suppressed
    offenders. write_sarif is unchanged (the active-only special case),
    so existing output is byte-for-byte identical.

  • bca diff: compare two bca metrics -O json runs (single JSON files
    or directory trees), bucketing per-file metric deltas by metric in
    tty / markdown / json form, with --min-change and --metric
    filters. Replaces the external json-minimal-tests +
    split-minimal-tests.py grammar-bump diff chain (the latter is
    retired). Informational — always exits 0 on success
    (#487).
    A bca diff --since <ref> [<new>] mode analyzes the tree at a git ref
    (materialized into an auto-cleaning temp dir) for the "before" side
    and diffs it against the working tree or an explicit <new>,
    honouring the same --paths / --include / --exclude selection;
    it hard-errors (exit 1) on unresolvable refs or a non-git checkout
    (#492).

  • bca check baseline files now record tier/headroom provenance
    (format v5): a [provenance] table stamps the tier (and headroom for
    the scaled soft tier) the baseline was written at. bca check warns
    when the current run is stricter than the baseline was written
    against (the silent-desync the baseline-refresh discipline guards),
    staying silent for the safe hard-reads-soft and equal cases; v2–v4
    baselines read unchanged with provenance treated as absent
    (#486).

  • bca check --write-baseline now accepts an optional path. A bare
    --write-baseline (no value) writes to the baseline key from the
    auto-discovered bca.toml manifest — the same file bca check reads
    — so the baseline filename lives in exactly one place. Passing an
    explicit --write-baseline <path> still works; the bare form errors
    (exit 1) when no manifest baseline is set rather than guessing a
    filename. The repo's own make self-scan-write-baseline[-headroom]
    recipes drop their hard-coded path
    (#496).

  • Support for F5 iRules source files (.irule, .irules), a Tcl
    scripting dialect, via the
    tree-sitter-irules
    grammar. Adds the Irules LANG variant and the IrulesCode /
    IrulesParser re-exports, gated behind a new irules Cargo feature
    (enabled by all-languages). when EVENT { … } event handlers and
    proc definitions are treated as function spaces, so per-handler
    metrics are reported (the grammar's on / trap handler nodes are
    handled defensively but parse as ordinary commands in practice). Real
    implementations are provided for ABC,
    cognitive, cyclomatic (including the dedicated switch/switch_arm
    node and the and/or keyword operators), exit, Halstead, LoC, and
    NArgs; the remaining metrics use the shared defaults. Additive new
    language — minor bump per STABILITY.md.

  • Cyclomatic complexity's counting of Rust's ? operator (the
    try_expression grammar node) is now configurable
    (#409).
    MetricsOptions gains a count_cyclomatic_try field (default
    true) and a with_count_cyclomatic_try builder; the CLI gains a
    global --no-cyclomatic-try flag and a cyclomatic_count_try
    bca.toml key (the flag ORs on top — it can force opt-out but not
    force counting back on). Setting it false treats ? as linear error
    propagation rather than a branch, on both standard and modified
    cyclomatic. The repo's own make self-scan gate sets it via the
    bca.toml manifest. The default is unchanged: ? keeps
    counting +1, matching upstream rust-code-analysis, so every
    published metric value and existing snapshot is byte-identical (no
    value change on the default path). Rust-only — no other language
    emits the node, so the toggle is inert elsewhere. Additive per
    STABILITY.md: MetricsOptions is #[non_exhaustive], so the new
    field and builder do not break downstream callers; a global default
    flip is deferred to a deliberate 2.0 decision.

  • metric_catalog::MetricInfo gains a skip_at_unit: bool field
    recording whether a metric's serialized JSON headline at the
    file-level unit space is an aggregate over descendant spaces that
    does not match the CLI threshold accessor's per-space scalar (true
    for cognitive, cyclomatic, cyclomatic.modified, and abc).
    This is the single source of truth the CLI EXTRACTORS table and the
    Python to_sarif binding's METRIC_FIELDS table now both derive
    from, so a metric added to one front-end but not the other — or a
    skip_at_unit flag that disagrees — is a build/test failure instead
    of silent SARIF divergence
    (#442).
    Additive: MetricInfo is #[non_exhaustive], so the new field does
    not break downstream readers.

  • bca exemptions audits everything the bca check gate skips in one
    report (#386):
    in-source suppression markers (bca: suppress, #lizard forgives,
    …), [check.exclude] globs, and .bca-baseline.toml entries. Each
    marker is listed with its file, line, target (function/file), metric
    scope, dialect, and surrounding function, so reviewers can see every
    silencer in the tree — not just the offenders they happen to hide.
    --format tty|markdown|json selects the output style (json nests
    the three tiers under a single suppressions envelope; an omitted
    section is null, a requested-but-empty one is []); the combinable
    --only-markers / --only-excludes / --only-baseline flags narrow
    the report for PR-bot use. The walk honours [walker.exclude], and
    the baseline (bca.toml top-level baseline) and [check.exclude]
    inputs default to the same sources bca check reads. Read-only and
    informational: it
    always exits 0 on success (1 on a tool error such as a missing
    --baseline), never gating. The new big-code-analysis library
    re-exports SuppressionMarker, SuppressionTarget,
    SuppressionDialect, and the SuppressionScan callback that back it.

  • bca check --strict-exit-codes opts into tiered exit codes that
    split the violation case (previously a single exit 2) by severity
    (#385):
    2 new offenders only, 3 regressions only, 4 both, 5 a
    --tier=soft violation that also breaches the hard limit. CI can
    now branch on severity without parsing the [new] / [regr +N%]
    stderr tags. The default 0/1/2 contract is unchanged — the tiered
    mode is opt-in via the flag or [check] exit_codes = "tiered" in
    bca.toml (the flag ORs on top: a bare flag cannot represent
    "off"). Every fail-state stays non-zero, so existing
    exit != 0 → fail tooling is unaffected; only consumers that test
    $? -eq 2 explicitly need to widen to 2-5. --no-fail still
    forces exit 0. --print-effective-config reports the resolved
    exit_codes style.

  • bca diff-baseline old.toml new.toml emits a structured diff
    between two .bca-baseline.toml files — added, removed,
    worsened, improved — replacing the in-the-head TOML diff
    parsing the baselines recipe used to walk reviewers through
    (#382).
    Entries pair on their (path, qualified, metric) identity (line
    drift is tolerated, mirroring the on-disk matcher), so only genuine
    value changes surface. --format tty|markdown|json selects the
    output style (markdown fences each section for a sticky PR
    comment; json emits the full structured diff). The combinable
    --added-only / --removed-only / --worsened-only /
    --improved-only flags narrow the rendered sections for PR-bot use.
    Both files are read through the same loader bca check uses, so any
    supported legacy version (v2/v3) is migrated on read and an
    unsupported version is a clear error rather than a silent
    no-match. The command always exits 0 on success — the diff is
    informational, not a gate.

  • bca check gains a glob-level gate exemption via a [check] exclude list in bca.toml and the --check-exclude /
    --check-exclude-from flags
    (#378).
    Matching files are still walked, parsed, metric'd, and shown by
    bca report — only bca check drops their violations before
    emitting offenders and before --write-baseline records anything,
    so structural exemptions (test fixtures, generated code,
    macro-dispatch modules) stay out of .bca-baseline.toml.
    --check-exclude is repeatable; --check-exclude-from reads a
    .gitignore-style file (convention .bcacheckignore); the two
    union with each other, while an explicit --check-exclude replaces
    the manifest [check] exclude list (CLI-wins, like every other
    manifest key). Globs match the walked path exactly like --exclude.
    Precedence, most-specific first: in-source
    bca: suppress markers, then [check.exclude] globs, then the
    baseline. --print-effective-config reports the resolved
    check_exclude globs.

  • bca check gains a native two-tier threshold model via a
    [thresholds.soft] table and a --tier <hard|soft> flag
    (#375).
    The default hard tier compares against [thresholds] verbatim.
    --tier=soft is the early-warning tier: it merges
    [thresholds.soft] overrides on top of [thresholds] (per metric,
    either an absolute limit like cognitive = 18 or a
    scale-relative "0.9x" string that multiplies the hard limit);
    metrics absent from the soft table inherit their hard limit (no
    soft band). When no soft table is configured, --tier=soft falls
    back to scaling every limit by --headroom (default 0.95). Both
    the manifest bca.toml and --config files accept the soft
    sub-table, and both tiers ratchet through the same --baseline.
    --print-effective-config now reports the resolved tier. As a
    consequence, --headroom is now a soft-tier dial: it takes
    effect only under --tier=soft (ignored with a note at the hard
    tier), and an explicit [thresholds.soft] table takes precedence
    over --headroom (which is then ignored with a warning).

  • bca check baselines now match on the qualified symbol rather
    than the exact start line
    (#377).
    Each entry keys on (path, qualified_symbol, metric) — e.g.
    MyStruct::do_thing — so editing code above a named function no
    longer re-keys it as a [new] offender (the most common source of
    baseline churn). A configurable start_line tolerance
    (--baseline-line-tolerance <LINES>, default 50, or
    baseline_line_tolerance in bca.toml) disambiguates a symbol
    shared by several functions. A new --baseline-fuzzy-match flag
    (baseline_fuzzy_match in bca.toml) enables a rename-tolerant
    body-hash fallback: a function renamed but otherwise unchanged stays
    covered, because the normalised body digest (which elides the
    function's own name and ignores indentation/blank-line churn) still
    matches. The offender line and JSON function field now show the
    qualified symbol. The baseline schema bumps to v4 (function
    field renamed to qualified, optional body_hash added); v2/v3
    baselines are still read and degrade to bare-name + tolerance
    matching until refreshed with --write-baseline. See
    STABILITY.md for the migration. Additive, minor bump.

  • bca.toml manifest — auto-discovered at (or above) the working
    directory, consolidating the flags every local-gate recipe used to
    thread through each invocation
    (#374).
    Top-level keys paths, exclude_from, num_jobs, include,
    exclude, baseline, and headroom, plus an inline [thresholds]
    table, map to the corresponding flags. Explicit CLI flags always win
    over manifest keys; --config <file> merges on top of the manifest
    [thresholds] table (resolution order: manifest [thresholds]
    --config → tier resolution → --threshold overrides). Relative
    manifest paths resolve against the manifest's directory. A new global
    --no-config flag skips discovery for fully-explicit invocations
    (bca init also ignores any existing manifest, since it scaffolds
    config rather than consuming it).
    Unrecognized keys (forthcoming [check], exit_codes) are ignored
    with a one-line warning so projects can pre-adopt schema additions. bca check --print-effective-config gains
    a manifest provenance line. Additive, minor bump.

  • bca check --headroom <ratio> — scales every threshold from
    --config (or bca.toml's [thresholds]) by a ratio in (0, 1]
    before the offender comparison, implementing the soft-tier
    early-warning gate natively
    (#373).
    0.95 (the default knob in the local-gates recipe) fires on
    functions that have reached 95% of any limit; 1.0 is a no-op
    parity run with the hard gate; out-of-range values exit 1.
    --headroom is a soft-tier dial: it takes effect only under
    --tier=soft (see #375; ignored with a note at the default hard
    tier). Explicit --threshold name=value overrides are absolute and
    are applied after scaling (resolution order: config → tier
    resolution → --threshold). Stacks with --write-baseline (the
    baseline then captures offenders at the scaled limits) and is
    surfaced by --print-effective-config. Replaces the
    utils/bca-self-scan-headroom.py helper, which is removed;
    make self-scan-headroom and the local-gates book recipe now
    invoke --tier=soft --headroom directly. Additive, minor bump.

  • metric_catalog module — a single canonical registry of metric
    metadata (#397).
    Public items: metric_catalog::{MetricInfo, MetricFamily, MetricRow, Direction, METRICS, FAMILIES}. METRICS is the canonical list of
    offender sub-metric ids (halstead.volume, mi.original, …) with
    their long-form SARIF / Code Climate sentences and
    higher-/lower-is-worse Direction; FAMILIES is the view rendered
    by bca list-metrics. The library's offender formatters and the
    CLI's threshold engine now read this one source instead of three
    hand-maintained tables that had silently drifted (ten rule-
    description keys once matched no real offender id for two model
    versions). A cross-crate parity test pins the threshold extractor
    ids to METRICS, so a new metric can no longer ship with a half-
    updated catalog. SARIF, Code Climate, and bca list-metrics output
    are unchanged. Additive, minor bump.

  • Python bindings dev environment: make py-bootstrap, make py-sync
    (alias), make py-relock, and make py-clean Makefile targets.
    Bootstrap provisions big-code-analysis-py/.venv from the
    checked-in uv.lock via uv sync --locked --extra dev; relock
    regenerates uv.lock after a pyproject.toml edit; py-clean
    removes .venv, the editable-install compiled extension, per-tool
    caches (.pytest_cache, .mypy_cache, .ruff_cache), and
    __pycache__ trees. Requires uv to be installed locally; see
    CONTRIBUTING.md for the install one-liners.

  • make distclean target — chains py-clean and cargo clean for a
    full-wipe before a from-scratch bootstrap. make clean continues
    to do cargo clean only.

  • grammar-marker-sync static lint (check-grammar-marker-sync.py,
    baseline at .grammar-marker-baseline.toml) blocking the failure
    mode from
    #400:
    bumping the notification-only tree-sitter-javascript /
    tree-sitter-cpp marker in tree-sitter-{mozjs,mozcpp}/Cargo.toml
    without re-running the matching
    ./generate-grammars/generate-*.sh script ships a marker that
    lies about the bundled src/parser.c version. The gate compares
    the live marker against the baseline and fails on drift in either
    direction (marker bumped without regen, or regen without baseline
    refresh — --update after a verified regen). Wired into make lint, make pre-commit, make ci, the .pre-commit-config.yaml
    system hook, and a defensive explicit lint job step in
    .github/workflows/ci.yml. Verified against the
    tree-sitter-javascript 0.23.1 → 0.25.0 marker bump (#1207) that
    motivated #400: regen against the live 0.25.0 marker confirmed
    no source diff under
    tree-sitter-mozjs/src/{parser.c,scanner.c,grammar.json,node-types.json}
    with tree-sitter CLI 0.26.9.

  • enums-codegen-drift static lint
    (check-enums-codegen-drift.sh) blocking the failure mode from
    #405:
    running any recreate-grammars.sh invocation silently
    regenerated src/c_langs_macros/{c_macros,c_specials}.rs to a
    pre-optimization form (linear .contains() lookup + missing
    sorted-invariant tests), undoing months of hand-improved work.
    The enums/templates/c_macros.rs template now emits the
    binary_search-based lookup plus the *_is_sorted,
    *_lookup, and *_lookup_boundaries test modules; the gate
    runs the codegen into a tempdir and diffs against the
    checked-in files so any future divergence fails CI. Wired into
    make lint, make pre-commit, make ci, the
    .pre-commit-config.yaml system hook, and a defensive
    explicit lint-job step in .github/workflows/ci.yml.

  • check-manpage-assets static lint
    (check-manpage-assets.py) blocking the failure mode from
    #444:
    a bca subcommand man page that drops out of the
    hand-maintained deb/rpm asset lists ships a package without its
    page. The gate globs man/bca-*.1, partitions bca-web.1 to
    big-code-analysis-web and every other page to
    big-code-analysis-cli, and asserts each page appears in BOTH
    the [package.metadata.deb].assets and
    [package.metadata.generate-rpm].assets tables of its owning
    crate's Cargo.toml, failing loud with the offending
    filename(s). Wired into make lint, make pre-commit, make ci, the .pre-commit-config.yaml system hook, and a defensive
    explicit lint-job step in .github/workflows/ci.yml
    (#446).
    The guard is now bidirectional
    (#447):
    beyond asserting every page is listed in its owner, it also fails on
    a page listed in the wrong crate's asset tables (cross-contamination)
    and on a stale asset entry whose bca-*.1 source no longer exists
    under man/, both scoped to bca-*.1 basenames so binaries,
    completions, the top-level bca.1, and licences are not swept in.

  • bca check actionable failure output (umbrella
    #356
    now complete):

    • --since <ref> / --changed-only diff-aware mode: the
      summary footer surfaces "Files in this range:" (offenders in
      files touched between the diff base and HEAD) before the
      legacy offender list; --changed-only drops out-of-range
      rows entirely for terser PR-gate output. Auto-detects the
      diff base from BCA_DIFF_BASE, GITHUB_BASE_REF, or
      GITHUB_EVENT_BEFORE in that precedence. Pass
      -c core.quotePath=false to git so non-ASCII filenames
      survive the canonicalize roundtrip. Fixes
      #359.
    • --github-annotations (auto-enabled when
      $GITHUB_ACTIONS == "true") emits ::error file=…,line=…, title=…::msg workflow commands so the GHA UI renders
      inline annotations on the file-diff view. Capped at 10 per
      metric with an overflow rollup line so a 400-violation run
      cannot exhaust GitHub's 10-error-per-step UI quota. Fixes
      #360.
    • $GITHUB_STEP_SUMMARY markdown digest (or
      --summary-file <path>) — per-file rollup, per-metric
      breakdown, top-10 offenders by ratio. Bracketed by
      HTML-comment markers so a retried step replaces (not stacks)
      the previous block. Fixes
      #361.
    • Trailing --- next steps --- remediation block on stderr
      (and inside the step-summary digest) names the artifact,
      prints a copy-paste-safe --write-baseline refresh
      invocation that mirrors the gate's resolved path filters,
      and links to the Baselines recipe. Suppress with
      --no-remediation. Fixes
      #362.
  • bca check --output-format code-climate (new) emits GitLab Code
    Climate JSON directly into the MR Code Quality widget, replacing
    the previous third-party Checkstyle→Code-Climate converter recipe.
    Severity bands map metric-vs-threshold ratios onto GitLab's five
    levels (minor ≤1.5×, major ≤2×, critical ≤4×, blocker

    4×), inverted for the mi.* family where lower is worse.
    Fingerprints hash path \0 function \0 metric (deliberately
    excluding line and value) so cosmetic line-drift edits still
    collapse into the same widget entry. Fixes
    #354.

  • enums/tests/dispatch.rs (new) pins every Lang variant to its
    expected backing tree-sitter grammar crate via per-variant
    integration tests for get_language and get_language_name,
    catching the Cpp→mozcpp class of drift bug (fixed in
    #344)
    at cargo test time rather than first-dispatch panic. The new
    test suite runs under make enums-check (extended in this
    release) so pre-commit and CI gate on it. Fixes
    #350.

  • bca init: new subcommand scaffolds the canonical pre-#374
    adoption files in one shot — bca-thresholds.toml (with the
    full header comment), .bcaignore (with commented default
    patterns), and an initial .bca-baseline.toml derived from a
    write-baseline pass. Flags: --dir <DIR>, --force,
    --no-baseline. Interactive prompts and --emit make|just|pre-commit|github-actions skeletons are deferred to
    follow-up. Fixes
    #379.

  • bca check --print-effective-config[=FORMAT] serializes the
    resolved threshold / check configuration after merging
    --config TOML + --threshold CLI overrides, then exits 0
    without walking the codebase. Default format is TOML; =json
    selects JSON. Mutually exclusive with --write-baseline. The
    printed view is round-trippable through --config. Future
    layers (#373 headroom, #374 bca.toml, #375
    [thresholds.soft], #385 tiered exit codes) plug into the same
    printer without changing its CLI surface. Fixes
    #380.

  • bca check / config now suggests the closest known metric name
    when a --threshold flag or [thresholds] TOML key is
    misspelled. Uses Levenshtein with a min(2, max(len)/3) cutoff
    plus a shared-prefix rescue for truncations (covers
    cycliccyclomatic and halstead.efort
    halstead.effort). Up to three ties listed; unrelated input
    still falls back to the prior "unknown metric" error. Fixes
    #381.

  • Python analyze() gains a keyword-only vcs_per_function=True flag
    that mirrors the CLI's bca metrics --vcs-per-function: it blames the
    file once and attaches a per-function vcs block (byte-identical in
    shape to the CLI's) to every nested function/method/class space in the
    returned JSON tree. Independent of the file-level vcs= opt-in; degrades
    gracefully outside a git repository (#578).

  • bca vcs jit --diff <file> (and --diff - for stdin) scores an
    arbitrary git diff-style unified diff. A bare diff carries no author,
    parent, or file history, so only the size and diffusion feature groups
    are computable; the result is a distinct partial report (source: "diff", partial_score) whose history/experience/purpose groups are
    absent (not zero) and whose score is not comparable to a commit
    score. New library surface vcs::score_diff. Just-in-time scoring is
    now also exposed via the REST POST /vcs/jit endpoint and the Python
    vcs_jit(repo_path, commit=…, diff=…) binding, both reusing
    score_commit / score_diff. Commit-mode JIT output is unchanged
    (#580).

  • bca-web gains an opt-in --cors <ORIGINS> flag (off by default) so
    browser tooling can call the API cross-origin without a proxy. The
    argument is an explicit comma-separated allow-list; a listed origin is
    echoed back in Access-Control-Allow-Origin, an unlisted origin gets
    no header, and a literal * opts into a wide-open policy. Layered on
    the existing RFC 9110 OPTIONS→204 + Allow handling via a new
    CorsPolicy enum and from_fn middleware (the preflight sources its
    methods from the resource's own Allow header), wrapped under
    Condition so the default request path carries no extra layer.
    Access-Control-Allow-Credentials is never emitted (#694).

  • The REST API now honors the Accept header on every structured
    analysis endpoint (/v1/ast, /v1/comment JSON, /v1/function,
    /v1/metrics, /v1/vcs, /v1/vcs/trend, /v1/vcs/jit), reusing the
    same serde_yaml / ciborium serializers the CLI drives so a value is
    byte-identical over both surfaces. JSON stays the default (absent
    Accept, */*, application/*, application/json); application/yaml
    and application/cbor get that format with the matching Content-Type,
    q-weights are honored, and any other concrete type answers 406 Not Acceptable through the uniform {error, error_kind, id} envelope. TOML
    and CSV are excluded (#657).

  • Python VCS documentation: a new python/vcs.md book chapter covering
    the namespaced change-history surface (vcs.rank / vcs.trend /
    vcs.commit / vcs.score_diff and the shared vcs.Options), the
    widened option kwargs (#619), the as_of reproducible-snapshot
    semantics (#648), and the GIL-release ThreadPoolExecutor note (#620);
    python/errors.md gains the typed VCS exception taxonomy (#624) and the
    stale flat references in the CLI vcs.md are refreshed to the post-#612
    namespaced names (#649).

Changed

  • VCS JIT scoring diffs each touched blob once (computing added/deleted
    counts and hunk count from a single Diff::compute) instead of twice,
    with bit-identical results (#815).

  • The HTML and Markdown report's headline Average MI is now the
    SLOC-weighted mean of the unclamped Visual Studio MI and is
    relabelled Average MI (SLOC-weighted). Previously it averaged the
    clamped mi_visual_studio (floored at 0) over the file count, so a
    catastrophically unmaintainable file (true MI ≈ −400) and a marginally
    bad one (≈ −5) both contributed 0 and were indistinguishable, and a
    five-line file counted as much as a five-thousand-line one. The new
    headline mirrors the MI hotspot ranking (which already sorts on the
    unclamped value, #627): large files dominate and the figure can go
    negative for an unmaintainable codebase. The per-language overview's
    Avg MI column changes the same way and gains its own tooltip. The
    per-file MI hotspot column is unchanged (still the clamped Visual
    Studio value). Report output is not contract-locked, so this is not a
    SemVer break, but published headline numbers move (#725, follow-up to
    #627).

  • (breaking) Lib: the AST Span struct (big_code_analysis::Span)
    gains start_byte / end_byte fields (0-based, half-open byte offsets
    into the parsed source) and is now #[non_exhaustive]. Construct it via
    the new Span::new(...) constructor; struct-literal construction
    (Span { start_line, .. }) and exhaustive destructuring from outside the
    crate no longer compile. The serialized wire shape only adds the two
    byte fields (both #[serde(default)], so pre-existing line/col-only span
    JSON still deserializes), so /ast and dump consumers are unaffected
    beyond the additive fields; only Rust callers that built or destructured
    Span by literal are affected. Deferred to the 2.0 milestone (#727).

  • (breaking) LANG::Cpp (slug cpp) is now backed by the upstream
    community tree-sitter-cpp grammar instead of the Mozilla fork. The
    fork moved to the new opt-in LANG::Mozcpp (see Added). The cpp
    Cargo feature's dependency set changed accordingly
    (bca-tree-sitter-mozcpptree-sitter-cpp); a --no-default-features
    consumer that enabled cpp for the Mozilla dialect must now also enable
    mozcpp. Default (all-languages) builds analyze the same .c / .h /
    .cpp / … extensions as before. Generic C++ metric values shift
    slightly where the Gecko overlay diverged from upstream (≈0.6% of files
    in the measurement corpus, #719); the integration snapshots were
    re-baselined in the same change. Deferred to the 2.0 milestone
    (#720, part of #718).

  • (Python) analyze(..., vcs=True) and
    analyze(..., vcs_per_function=True) now release the GIL across their
    per-file history walk / blame-engine open via Python::detach, the same
    off-GIL treatment the vcs.rank/trend/commit entry points and the
    batch path already had — completing the GIL release across the VCS
    surface (#620). The walk touches no Python objects, so the cheap
    JSON-injection step stays under the re-acquired GIL; results and
    signatures are unchanged, so a ThreadPoolExecutor over several
    analyze(vcs=True) calls now parallelises instead of serialising on the
    walk.

  • Web/Lib: the id field on every JSON request payload (/v1/ast,
    /v1/comment, /v1/function, /v1/metrics, /v1/vcs,
    /v1/vcs/trend, /v1/vcs/jit) and the comment / span fields on
    /v1/ast are now optional (#[serde(default)]). Omitting id
    defaults to an empty string (the "no correlation id" sentinel echoed
    back unchanged); omitting comment / span defaults to false. This
    ends the JSON-vs-query-variant inconsistency where the query form
    already defaulted these fields while the JSON form returned a 400 missing field. Strictly request-side loosening: previously-valid
    payloads are unaffected and no new keys are accepted (#645).

  • (breaking) Lib: Halstead Stats accessors renamed to the wire
    vocabulary — u_operatorsunique_operators, operators
    total_operators, u_operandsunique_operands, operands
    total_operands. JSON/YAML/TOML/CBOR output keys are unchanged.
    Deferred to the 2.0.0 release (#588).

  • (breaking) Lib: the exit metric module is renamed to nexits
    (src/metrics/exit.rsnexits.rs; the crate-internal
    crate::exit path is now crate::nexits), its Stats accessors
    exit/exit_sum/exit_average/exit_min/exit_max become
    nexits/nexits_sum/nexits_average/nexits_min/nexits_max,
    and the retired "exit" parse alias for Metric::Nexits no longer
    resolves (only "nexits" parses). Output keys are unchanged.
    Deferred to the 2.0.0 release (#588).

  • (breaking) Lib: NArgs Stats accessors renamed —
    fn_args/fn_args_sum/fn_args_average/fn_args_min/fn_args_max
    function_args/function_args_sum/function_args_average/
    function_args_min/function_args_max, and nargs_total/
    nargs_averagetotal/average. Wire keys are unchanged.
    Deferred to the 2.0.0 release (#588).

  • (breaking) Lib: MI Stats accessors renamed —
    mi_original/mi_sei/mi_visual_studio
    original/sei/visual_studio. Output keys are unchanged.
    Deferred to the 2.0.0 release (#588).

  • (breaking) Lib: the Metric::NArgs enum variant is renamed to
    Metric::Nargs; its lowercase "nargs" serde/Display/FromStr
    spelling is unchanged. Deferred to the 2.0.0 release (#588).

  • (breaking) Output: the sum-carrying classes / interfaces
    wire keys on npm / npa / wmc are renamed to mirror their
    accessors — npm.{class_npm_sum,interface_npm_sum},
    npa.{class_npa_sum,interface_npa_sum},
    wmc.{class_wmc_sum,interface_wmc_sum} — across JSON/YAML/TOML/CBOR
    and the CSV columns. The truthful sibling keys (class_methods,
    total, coa, cda, …) are unchanged. Deferred to the 2.0.0
    release (#589).

  • (breaking) Output: the JSON tokens block's tokens_average /
    tokens_min / tokens_max leaves are renamed average / min /
    max, matching the CSV columns; the bare-sum tokens leaf is kept.
    The terminal dump's tokens sum label changes sumtokens to
    match. Deferred to the 2.0.0 release (#590).

  • (breaking) CLI: the default text metric dump is now driven from
    the serialized (wire::CodeMetrics) shape, so every metric block
    renders its full, uniform field set (e.g. loc now shows the
    averages and min/max, nexits renders as a sum/average/min/
    max aggregate) instead of a hand-picked per-metric subset. Float
    values render rounded to two decimals in the text view only; JSON
    keeps full precision. Deferred to the 2.0.0 release (#674).

  • (breaking) Output: the per-file change-history (VCS) block is now
    a nested vcs object under each ranked file (and each
    /vcs/trend point) for bca vcs, POST /vcs, vcs_metrics(), and
    vcs_trend(), replacing the former flattened-beside-path layout;
    CSV stays flat with dotted columns. Deferred to the 2.0.0 release
    (#684).

  • (breaking) Output: the per-row VCS block is always-slim — the
    four constant stamps vcs_schema_version, risk_score_version,
    long_window_days, and recent_window_days are carried exactly
    once on the enclosing /vcs and /vcs/trend envelope (and no
    longer duplicated per file row or per trend point). POST /vcs
    gains the two version stamps at the response top level. Deferred to
    the 2.0.0 release (#635).

  • (breaking) CLI: argv/usage/value-parse errors now exit 1 instead
    of clap's 2, reserving exit codes 2–5 for the check and
    vcs jit --fail-above-style metric gates; --help / --version
    still exit 0 (#594).

  • (breaking) CLI: renamed --language-type to --language
    (hidden alias kept one cycle). The flag accepts a language name
    (rust) or extension (rs); an unknown value is now a hard error
    listing valid languages instead of silently disabling analysis
    (#595).

  • (breaking) CLI: walk commands default --paths to . when no
    CLI/manifest seed is given; a nonexistent explicit path now fails
    with exit 1 instead of warning and exiting 0; a zero-file walk
    prints a stderr notice (#596).

  • (breaking) CLI: -I/--include and -X/--exclude take exactly
    one glob per occurrence and are repeatable; the greedy
    space-separated multi-value spelling no longer parses (#601).

  • (breaking) CLI: --top unified on usize with 0 meaning
    "all rows" across vcs, report, and vcs trend
    (report --top 0 was previously a usage error) (#602).

  • (breaking) CLI: renamed --num-jobs to --jobs and --warning
    to --warnings (hidden aliases kept one cycle), and the default
    tree output is now selectable explicitly as --format text on
    metrics/ops (#604).

  • (breaking) Manifest: the check-only keys baseline,
    baseline_line_tolerance, baseline_fuzzy_match, and headroom
    moved under [check] in bca.toml; the top-level spelling warns
    for one release cycle and goes away at 2.0 (#599).

  • (breaking) Wire: version stamps are now uniformly
    domain-prefixed — bus factor emits bus_factor_schema_version
    (schema 2, was bare schema_version) and JIT reports emit
    risk_score / partial_risk_score (schema 3, was bare
    score / partial_score) (#591).

  • (breaking) Web: /vcs/jit rejects a payload combining diff
    with any commit-mode field (400 naming the conflict) instead of
    silently ignoring repo_path/commit (#632).

  • (breaking) Web: an unsupported language now answers
    422 Unprocessable Entity with the machine token
    unsupported_language instead of 404; 404 is reserved for unknown
    routes (#634).

  • (breaking) Web: the /comment JSON response returns the
    stripped source as a string instead of an array of byte numbers
    (#629).

  • bca vcs jit / vcs trend accept the history-tuning flags
    (--long-window, --as-of, …) in the subcommand position;
    --ref combined with vcs jit is now a usage error instead of
    being silently ignored (#598).

  • Report headings and the Languages line show human-readable language
    names (C++, C#, TSX, …); slugs are unchanged in structured output
    and CSS classes (#613).

  • The report's three differently-filtered cyclomatic statistics are
    captioned (CC note excludes suppressed functions; the Actionable
    Summary names its raw basis and suppressed count; a fully-suppressed
    hotspot table leaves a "table omitted" note) (#616).

  • Halstead Effort and Functions-With-Many-Parameters hotspot tables
    gained the Line column in both report formats (#628).

  • Rendered VCS report polish: plain-English bus-factor wording,
    thousands separators on count cells, gap-free heading levels, and a
    provenance line with the ordinal-only Risk caveat (#618).

  • Python: language_for_file returns Lang | None (a StrEnum, so
    string comparisons keep working) and language_extensions accepts
    str | Lang (#625).

  • Python: pyproject metadata polish before first publish — Beta
    status, Typing :: Typed, Python 3.14 classifier, Documentation
    and Changelog URLs (#626).

  • The repository's own suppress-file markers migrated from the
    legacy exit spelling to the canonical nexits (the parser alias
    for exit is unchanged) (#593).

  • (breaking, deferred to 2.0) Retired the action / Callback
    dispatch and the path-positional analysis surface, leaving
    [Ast] (with analyze for the one-shot case) as the single public
    analysis seam (#566, #570). Removed: the Callback trait and its
    per-action tag/Cfg types (Dump/DumpCfg, CommentRm/CommentRmCfg,
    Function/FunctionCfg, Find/FindCfg, CountCfg,
    NodeTypeFilters, OpsCode/OpsCfg, Metrics/MetricsCfg,
    SuppressionScan, AstCallback); the action dispatcher; the
    parser-generic free functions metrics / metrics_with_options
    (in spaces) and operands_and_operators (in ops); and the
    path-positional shims get_function_spaces,
    get_function_spaces_with_options, metrics_from_tree, and
    get_ops. The internal parser machinery is demoted from pub to
    pub(crate) and dropped from the crate root and prelude:
    Parser, ParserTrait, Filter, LanguageInfo, Alterator,
    Getter, Checker, the per-metric compute traits
    (Cyclomatic/Cognitive/Halstead/Loc/Nom/Mi/NArgs/Exit/
    Wmc/Abc/Npm/Npa/Tokens), the per-language <Lang>Parser
    aliases and <Lang>Code tags, PreprocParser, and the
    rm_comments / function / count / find / suppression_markers
    walk cores. Callers migrate to Ast (parse, from_tree_sitter,
    metrics, ops, strip_comments, functions, dump, count,
    find, suppressions, root_node) or analyze. No metric values
    change — this is a pure removal/visibility change. The deletions land
    staged on main and take effect at the 2.0 major bump.

  • bca now analyzes each file through the explicit-name analyze /
    Ast::ops seams instead of the deprecated path-positional shims
    (get_function_spaces_with_options, get_ops). Behaviour is
    unchanged for UTF-8 paths; for a non-UTF-8 path the emitted top-level
    name is now empty rather than a lossy-mangled (U+FFFD) rendering of
    the path bytes. Part of the Ast-seam unification (#566/#568); the
    shims themselves are removed in the 2.0 surface reshape (#570).

  • (breaking, deferred to 2.0) Unified the two parallel metric enums:
    suppression now reuses the Metric enum and MetricKind is removed from
    the public API. Metric gains canonical-spelling serde (nargs /
    nexits, not n_args) and declaration-order Ord; the suppressed-scope
    serialization uses canonical names (nexits, not exit) and the
    nexits→exit alias bridge is gone; tokens is non-suppressible
    (rejected with a clear error). Suppression parsing now surfaces the
    offending token via ParseMetricError instead of Err = (), closing
    #554 (#555, #554).

  • (breaking, deferred to 2.0) Node's inner tree_sitter::Node is no
    longer a pub tuple field; reach it via the new
    Node::as_tree_sitter(&self) -> tree_sitter::Node<'a> accessor
    (value-not-stable, mirroring Ast::as_tree_sitter) (#556).

  • (breaking, deferred to 2.0) Marked the remaining open public enums
    #[non_exhaustive] (Severity, SpaceKind, SuppressionDialect);
    documented the deliberately-closed suppression enums (SuppressionPolicy,
    SuppressionScope, SuppressionTarget) (#551).

  • (breaking) Marked every per-metric compute-side Stats struct
    (abc, cognitive, cyclomatic, halstead, loc, mi, nargs,
    nexits, nom, npa, npm, wmc, tokens) #[non_exhaustive].
    Their fields were already private and read through accessors, so the
    marker is observationally invisible to existing callers; it makes the
    "no external struct-literal construction, no exhaustive match"
    guarantee explicit and keeps a future field addition additive within
    2.x rather than a shape break deferred to 3.0.

  • (breaking, deferred to 2.0) ConcurrentErrors is now
    #[non_exhaustive] and its Sender / Thread variants carry a boxed
    std::error::Error + Send + Sync source instead of a String (so
    source() chains); Producer / Receiver remain message-only (their
    cause is a thread-panic payload, not an Error) (#553).

  • (breaking, deferred to 2.0) The /comment endpoint now returns 200
    with a uniform empty payload across both content types for the "no
    comments" outcome — JSON returns {code: []} and octet-stream returns
    200 with an empty body, replacing the former octet-stream 204 No Content (#558).

  • CBOR output (bca metrics --format cbor) now serializes via
    ciborium instead of the unmaintained serde_cbor
    (RUSTSEC-2021-0127). Output remains valid CBOR; no public API or
    CLI change.

  • (breaking) Serialized AST node output (AstNode, REST /ast,
    AstCallback) now uses snake_case keys type / value / span /
    field_name / children (was Type / TextValue / Span /
    FieldName / Children); TextValue is renamed to value. Span
    changes from a bare (usize, usize, usize, usize) tuple to a named
    object {start_row, start_col, end_row, end_col} (still Option,
    null for root / span-disabled nodes); field order and 1-based
    row/column values are unchanged. Deferred to the next major bump
    (#535).

  • (breaking) Metric::Exit renamed to Metric::Nexits; its
    Display is now "nexits" and Metric::NAMES lists nexits,
    matching the nargs/nom/npa/npm "number-of" family. The CLI
    accepts nexits canonically with exit kept as a hidden parse alias
    for one cycle. The serialized field and JSON key were already nexits,
    so output is unchanged. Deferred to the next major bump (#536).

  • (breaking) Removed the never-produced MetricsError::NonUtf8Path
    and MetricsError::ParseHasErrors variants (the enum stays
    #[non_exhaustive], so a future strict mode can re-add them).
    EmptyRoot is retained — it is constructed at live forward-compat
    guards. Deferred to the next major bump (#536).

  • (breaking) FunctionSpan.name is now Option<String> and the
    error: bool field was removed; an unresolved name is None
    (serialized null), matching FuncSpace/Ops. The wire DTO and the
    REST /function JSON shape are updated accordingly. Deferred to the
    next major bump (#536).

  • (breaking) CountCfg and FindCfg no longer expose
    Arc<Mutex<Count>> / Arc<[String]> in their public fields.
    CountCfg.stats is now an opaque CountCollector
    (CountCollector::new(), into_count()); CountCfg.filters and
    FindCfg.filters are now an opaque NodeTypeFilters
    (NodeTypeFilters::new(&[String]) / From<Vec<String>>, borrowed
    as_slice()). Both newtypes are re-exported from the crate root.
    Deferred to the next major bump (#537).

  • (breaking) bca exemptions: section filters renamed to the
    --<section>-only idiom (--markers-only / --excludes-only /
    --baseline-only), matching diff-baseline. The old --only-*
    spellings remain as hidden aliases for one release cycle. Deferred to
    the next major bump (#538).

  • (breaking) CLI excludes now merge with the manifest. --exclude /
    --check-exclude (and their *-from files) UNION with the bca.toml
    exclude / [check] exclude lists instead of replacing them, so a
    command-line filter can no longer silently un-exclude a directory the
    project config skipped. Positive scope keys (paths, include) still
    replace on a CLI value; --no-config still bypasses the manifest.
    Deferred to the next major bump (#539).

  • (breaking) LANG::name/Display/FromStr now use one canonical
    lowercase slug per language; the pretty c/c++ / c# display forms
    are dropped and Tsx reports tsx. The serialized language value
    (CLI JSON, web /metrics, Python) changes accordingly and is now
    always a valid FromStr lookup token. Deferred to the next major bump
    (#540).

  • (breaking) bca-web: all error responses (including
    octet-stream/plain endpoints and the 415/405/404 fallbacks) now return
    a uniform JSON body {"error", "id"} with the correct status,
    replacing the former bare text/plain bodies. Deferred to the next
    major bump (#541).

  • (breaking) bca-web: /v1/function and /v1/comment responses
    now include id and the detected language (canonical slug),
    matching the /v1/metrics envelope. Deferred to the next major bump
    (#541).

  • (breaking) bca-web: the unit query flag on /v1/metrics now
    uses normal boolean semantics (true/false/1/0,
    case-insensitive); other values (including yes/on) return HTTP 400.
    Deferred to the next major bump (#541).

  • (breaking) Python: analyze_batch's skip_generated default
    flips to True, aligning with single-file analyze;
    supported_languages() now returns list[Lang] and METRIC_NAMES a
    tuple[MetricName, ...] (values remain string-compatible). Deferred to
    the next major bump (#542).

  • (breaking) Tidied internal-plumbing visibility. Cursor
    (src/node.rs) is narrowed from pub to pub(crate) and dropped from the
    lib.rs re-exports: every one of its methods was already pub(crate), so the
    re-exported type could be named but never used. Callback and LanguageInfo
    gain #[doc(hidden)] to match ParserTrait (Callback::call is bound on the
    hidden ParserTrait, and LanguageInfo is reachable from documented API only
    through the hidden Parser), so the bound and the trait now have coherent
    visibility; they remain pub for the action::<T> dispatcher and the
    in-crate / bca-web impl Callback blocks, so only their rustdoc presence
    changes. Node stays pub — the doc-hidden ParserTrait::root returns it,
    and it carries a genuine public method (has_error); Ast::as_tree_sitter is
    the preferred higher-level raw-tree seam. Removing Cursor from the public
    surface is SemVer-breaking; deferred to the 2.0.0 release (the
    release-prep commit moves this entry into the 2.0.0 section). The
    #[doc(hidden)] additions are not themselves SemVer-breaking.
    (#534, part of
    #505)

  • (breaking) The builder types Source, MetricsOptions, and
    MetricsCfg no longer expose pub fields — they are narrowed to
    pub(crate). These types are already documented as "construct via new +
    with_* setters" and carry #[non_exhaustive]; the pub fields only froze
    the internal representation (e.g. Source::code: &[u8], Source::name: String) as API for no benefit. Construction is unchanged
    (Source::new(...).with_*(...), MetricsOptions::default().with_*(...),
    MetricsCfg::new(...).with_options(...)); only direct field reads break, and
    the builders cover every supported use. No accessors were added — no consumer
    needs to read the config back. SemVer-breaking for code that read the fields
    directly; deferred to the 2.0.0 release (the release-prep commit moves
    this entry into the 2.0.0 section).
    (#533, part of
    #505)

  • (breaking) Non-finite float metric values (NaN/±Infinity) now
    serialize as a null uniformly across every structured format, enforced once
    at the serialize boundary via an internal NonFinite float wrapper rather
    than relying on each accessor staying finite. A non-finite value renders as a
    native null in JSON, YAML, and CBOR, and as an omitted key in TOML (which
    has no null literal). This replaces the previous per-format divergence — JSON
    silently emitted null, TOML nan, YAML .nan, and CBOR the raw IEEE-754
    bits — so YAML/TOML/CBOR consumers of a non-finite field see a changed shape;
    JSON is unchanged. The structured serializers also explicitly commit to
    full f64 precision, documented in STABILITY.md as not
    byte-stable across versions/platforms (the human-readable bca check warning
    path keeps its own six-decimal rounding, intentionally distinct from machine
    output). Finite values — every value the guarded metric accessors produce
    today (#428, #438, the Halstead/MI log/division guards) — serialize
    byte-identically to before, so this is a structural backstop with no
    observable change for current metrics. SemVer-breaking shape change to the
    serialized output, deferred to the 2.0.0 release (the release-prep
    commit moves this entry into the 2.0.0 section).
    (#531, part of
    #505)

  • (breaking) Integer-valued metrics now serialize as integers instead of
    floats, and their public Stats accessors return u64 instead of f64.
    Affected: every count, sum, and min/max (cyclomatic, cognitive, exit, nargs,
    nom, tokens, loc lines, ABC assignments/branches/conditions, npa/npm
    attribute/method counts), Halstead length/vocabulary and the four
    operator/operand counts, and all three WMC values. Ratios, averages, ABC
    magnitude, the derived Halstead scores (volume, difficulty, level,
    effort, time, bugs, purity_ratio, estimated_program_length), and the
    MI scores remain f64. JSON/TOML/YAML now emit "sloc": 5 rather than
    "sloc": 5.0, CBOR encodes these fields as compact integers rather than
    float64, and CSV output is unchanged (it already rendered integral values
    without a trailing .0). No metric value changes — only its type and
    representation. This is a SemVer-breaking shape change to the serialized
    output and the library accessor signatures; it is deferred to the 2.0.0
    release
    (the release-prep commit moves this entry into the 2.0.0 section).
    (#530, part of
    #505)

  • Internal refactor of the crate-private Checker classification trait
    (a pub(crate) extension point, not part of the public API or the
    STABILITY.md shape contract) so that adding a language
    no longer means copy-pasting -> false stubs. The ten predicates
    (is_comment, is_useful_comment, is_func_space, is_func,
    is_closure, is_call, is_non_arg, is_string, is_else_if,
    is_primitive) now carry -> false defaults, so a language implements
    only the categories its grammar expresses (~150 boilerplate lines removed
    across the 22 impls). is_primitive now takes &Node instead of a bare
    u16, matching every other predicate and removing the "two same-typed
    primitives" footgun, and Node::count_specific_ancestors is bound on
    Checker rather than the full ParserTrait. No public-API or
    metric-output change — this is internal plumbing only and the serialized
    metrics are byte-identical
    (#520,
    part of #505).

  • The bca line-range flags are now scoped to the dump and find
    subcommands
    instead of being global, and gain descriptive long
    names: --line-start / --line-end (canonical) with --ls / --le
    kept as hidden, deprecated aliases for one release cycle. Previously
    the flags were advertised on every subcommand's help even though only
    dump/find consumed them, and passing e.g. bca metrics --ls 5
    was silently ignored; that invocation — and the pre-existing
    flag-before-subcommand form bca --ls 5 dump — now errors. The new
    form puts the flag after the subcommand: bca dump --line-start 5 --line-end 10. The order change and the eventual removal of the
    --ls/--le aliases are (breaking) and deferred to the next
    major bump
    (#518,
    part of #505).

  • bca-web REST routes are now versioned under a /v1 prefix
    (/v1/ast, /v1/comment, /v1/metrics, /v1/function, /v1/ping).
    The original unprefixed paths remain available as deprecated
    aliases
    for one release cycle and resolve to the same handlers, so
    existing clients keep working; new clients should adopt the /v1
    paths. The known-endpoint set is no longer mirrored in a
    hand-maintained GUARDED_POST_PATHS constant — each resource carries
    its own default_service, so a request that reaches a known endpoint
    but matches no route is answered with a diagnostic 415/405 by the
    resource itself (a new endpoint can never silently regress to a
    bodyless 404), and a genuinely unknown URL falls through to the
    app-level 404. A side effect: POST /ping now returns 405 (was a
    bodyless 404). Additionally, errors are no longer signalled inside a
    200 body: the metrics endpoint's spaces field is now a
    non-optional FuncSpace (a successful response is byte-identical to
    before), and metric-computation / AST-construction failures now return
    500 Internal Server Error with an error body rather than 200 with
    spaces/root = null
    (#517,
    part of #505).

  • bca-web now logs server-side events via tracing instead of
    unstructured eprintln!: parse failures at error! and parse timeouts
    at warn!, each with a structured payload_id field taken from the
    request payload's id. It also wires tracing-actix-web's
    TracingLogger middleware for per-request spans (one access-log line
    per completed request, with its own request_id UUID, method, route,
    status, and latency). Log level and output are controlled by the
    RUST_LOG environment variable (default info). HTTP responses are
    byte-for-byte unchanged — this is server-side observability only
    (#516,
    part of #505).

  • Unified output-format selection across every bca subcommand
    (#513,
    part of #505).
    --format (short -O) is now the canonical spelling everywhere:

    • metrics / ops / check gain the long --format spelling;
      their previous --output-format is kept as a hidden, deprecated
      alias.
    • report gains a --format / -O flag and now defaults to
      markdown
      when no format is given (previously a missing
      positional was an error). The bare positional form
      (bca report markdown) is kept working as a hidden, deprecated
      alias; the --format flag wins when both are supplied.
    • diff / diff-baseline / exemptions gain the -O short for
      their existing --format flag.
    • These additions are backward-compatible. Removal of the deprecated
      --output-format alias and the bare report positional is
      (breaking) and deferred to the next major bump.
  • Unified the "average over a count" divisor convention and its
    divide-by-zero guard across the metric suite, and re-baselined the
    cyclomatic averages
    as part of the 2.0 re-baseline
    (#512,
    part of #505).

    • A single shared average(sum, count) helper now applies the .max(1)
      divisor guard (added for
      #428) for
      every metric average instead of repeating it per call site. This
      removes the former reliance on a counter that merely defaulted to 1
      for cyclomatic, nom, and the previously-unguarded per-space
      averages (loc, abc, tokens). Behaviour-preserving for every
      metric except cyclomatic (below): the guarded divisor is identical
      whenever the count is already non-zero.
    • Metric values change for cyclomatic.average and
      cyclomatic.modified.average only.
      They are now per function:
      the divisor is the number of function/closure spaces in the subtree
      — the per-function convention cognitive / exit / nargs use —
      rather than the previous per-space count (which also divided by
      classes, structs, and the file unit and so reported a smaller
      average). Files with classes/structs/units see a larger average.
      cyclomatic.sum / min / max and every other metric — including
      the Maintainability Index and WMC, which consume the cyclomatic
      sum — are unchanged. (The divisor counts the spaces that each carry
      a cyclomatic value, so it matches cognitive's function/closure count
      wherever every closure opens its own space; a closure form that opens
      no space, such as a Python lambda, is counted by cognitive but not
      as a separate cyclomatic divisor unit.)
    • The divisor is sourced from the space kind during finalization, not
      from the Nom metric, so a cyclomatic-only metric selection still
      divides per function without pulling a nom block into the output.
    • nom's own averages stay per space (it is the count metric;
      a per-function divisor would be circular).
  • get_ops, metrics_from_tree, and the doc-hidden
    operands_and_operators are now #[deprecated] in favour of the
    explicit-name Ast seams (Ast::ops, Ast::from_tree_sitter), which
    carry name: Option<String> from Source end-to-end. The shims keep
    their previous lossy-path behaviour (the lossy UTF-8 conversion now lives
    only in the deprecated path-positional shims; the shared walk core takes
    an explicit name), so existing callers see no behaviour or output change.
    This completes the Source/Ast migration begun for the metrics family
    in #254;
    removal is deferred to the 2.0.0 bump
    (#509,
    part of #505).

  • (breaking) Normalized the public language-dispatch surface
    (deferred to the 2.0.0 bump;
    #507):

    • Dropped the Java-style get_ prefix from every language getter, per
      the Rust C-GETTER guideline: LANG::get_namename,
      get_tree_sitter_languagetree_sitter_language, get_extensions
      extensions; LanguageInfo::get_lang / get_lang_namelang /
      lang_name; ParserTrait::get_language / get_root / get_code /
      get_filterslanguage / root / code / filters;
      Parser::get_ts_treets_tree.
    • The dispatchers action, get_function_spaces,
      get_function_spaces_with_options, metrics_from_tree, and get_ops
      now take lang: LANG by value instead of &LANG (LANG is a Copy
      1-byte enum, so the reference was pointless indirection). Call sites
      pass LANG::Rust, not &LANG::Rust.
    • Rename + signature only; no serialized output or metric values change.
  • (breaking) The default JavaScript grammar is now the upstream
    tree-sitter-javascript, not the vendored Mozilla tree-sitter-mozjs
    fork (the project is no longer Mozilla-driven;
    #507):

    • LANG::Javascript (upstream grammar) is the default for .js, .mjs,
      .cjs, and .jsx, and is declared first in the language list. .cjs
      (CommonJS) is newly recognized — it was previously unmapped.
    • LANG::Mozjs (the Mozilla/SpiderMonkey fork) is now opt-in: it owns
      only the .jsm (Firefox module) extension and its display name changed
      from javascript to mozjs, so .jsm files report
      "language": "mozjs". Select the fork explicitly via LANG::Mozjs.
    • The two grammars are metric-equivalent on real-world JavaScript (the
      fork only adds SpiderMonkey-specific node types absent from ordinary
      code), so no metric values change for .js / .jsx / .mjs
      files and no snapshots were re-baselined — verified against the full
      integration corpus (385 .js snapshots) plus an independent sample.
    • Builds that enable the mozjs feature but not javascript no
      longer analyze .js files (they resolve to the now-disabled
      Javascript variant and return LanguageDisabled); default
      all-languages builds are unaffected.
  • (breaking) Normalized the serialized metric output keys for a
    coherent 2.0 data contract (deferred to the 2.0.0 bump;
    #510,
    #511).
    Affects the JSON / YAML / TOML / CBOR / CSV output and the bca dump
    metric tree:

    • halstead: n1/N1/n2/N2unique_operators /
      total_operators / unique_operands / total_operands (the
      case-only-distinct keys collided for case-insensitive CSV/env
      consumers).
    • mi: leaves drop the redundant mi_ prefix — mi_original /
      mi_sei / mi_visual_studiooriginal / sei /
      visual_studio (now equal to the mi.* threshold ids).
    • nargs: total_functions / total_closuresfunction_args /
      closure_args; average_functions / average_closures
      function_args_average / closure_args_average; the
      functions_* / closures_* min/max keys gain the _args infix.
      Removes the total_functions sum-vs-count name collision and the
      adjective-order disagreement with nom.
    • npa / npm: the classes_average / interfaces_average /
      average keys carried CDA/COA accessibility ratios, not
      averages, and are renamed class_cda / interface_cda / cda
      (npa) and class_coa / interface_coa / coa (npm).
    • abc.magnitude is documented as a derived roll-up with no
      min/max/average projection (it is not accumulated per space).
    • Metric values are unaffected — this is a key-shape change only.
      (The separate per-function divisor re-baseline, #512, is deferred
      to its own change so it can be made self-contained rather than
      coupling cyclomatic to nom.)
  • guess_language now returns (Option<LANG>, &'static str) instead of
    (Option<LANG>, &'a str) with an unbound output lifetime, making the
    honest type explicit and removing a latent-unsoundness trap (every return
    path was already &'static). Source-compatible for normal callers
    (return-lifetime widening is covariant)
    (#506).

  • perf(node): has_sibling no longer heap-allocates a TreeCursor per
    call — it reuses the allocation-free sibling walk introduced in #217,
    eliminating the missed allocation on the JS/TS arrow-function
    closure-classification hot path
    (#521).

  • perf(spaces): the AST walker now computes a node's space kind lazily —
    only when the node is promoted to a function space or the Loc metric is
    selected — avoiding a wasted per-node source-text scan (notably Elixir's
    per-Call keyword scan) when the result would go unused. No metric
    values change
    (#522).

  • refactor(node): Node::children() drives termination off the cursor
    alone (struct iterator Children), eliminating latent duplicate-node
    padding if child_count() and the cursor sibling walk ever desync;
    ExactSizeIterator retained, no metric-value or public-API change
    (#523).

  • build(deps): exact-pin tree-sitter-kotlin-ng to =1.1.0 to match every
    sibling grammar, and guard the root vs enums/ external grammar-pin
    lockstep via check-versions.py so future drift fails fast in
    pre-commit / CI (resolved version unchanged)
    (#524).

  • build(deps): drop the num meta-crate from the library's direct
    dependencies (its sole use, num::FromPrimitive::from_u16, now goes
    through the already-present num-traits re-export) and hoist csv /
    tempfile into [workspace.dependencies]; no behavioral change
    (#525).

  • Python bindings: lang_to_name now delegates to LANG::get_name()
    for all but three lookup-token overrides (Cpp"cpp", Csharp
    "csharp", Tsx"tsx"), collapsing a 22-arm hand-maintained
    table that duplicated the upstream CLI display names. The Python-facing
    language identifiers are byte-identical for every variant; this only
    removes drift risk between the facade and the CLI display names
    (#500).

  • (breaking) FilesData and ConcurrentRunner are reshaped into a
    terminal file-set processor: FilesData drops its include /
    exclude GlobSet fields (now just FilesData { paths }),
    ConcurrentRunner::run returns Result<(), ConcurrentErrors> instead
    of Result<HashMap<String, Vec<PathBuf>>, ConcurrentErrors>, and the
    set_proc_dir_paths / set_proc_path builder methods are removed.
    The library previously re-walked and re-filtered the file list the
    CLI had already resolved and anchored (#489), causing a redundant
    per-file stat and dead, path-form-sensitive globsets. The library
    is now a pure concurrent processor of an already-resolved file list;
    the CLI's anchored, gitignore-aware expand_seed_paths is the single
    walk and filtering seam. This is a source-level break deferred to
    the next major (2.0)
    bump; the release-prep commit moves this
    entry into the 2.0.0 section
    (#495).

  • The project's own self-scan gate now reads all configuration (paths,
    exclude_from, baseline, thresholds, and the cyclomatic-? policy)
    from a single consolidated bca.toml manifest; the standalone
    bca-thresholds.toml and the redundant BCA_COUNT_CYCLOMATIC_TRY
    Makefile plumbing are retired, so bca check reproduces the gate
    with no flag threading
    (#483).

  • bca init now scaffolds a consolidated bca.toml manifest
    (auto-discovered zero-config) instead of the retired
    bca-thresholds.toml three-file split; .bcaignore and
    .bca-baseline.toml are still written
    (#484).

  • CI: removed the hand-translated .bcaignore mirror regex from the
    bca-self-scan / bca-self-scan-headroom pre-commit hooks;
    .bcaignore is now the single source for the self-scan deny-set
    (#485).

  • Cognitive complexity now applies the SonarSource §B2 jump-statement
    rule uniformly across languages: an unstructured jump (labeled
    break/continue, goto) adds +1 while a plain unlabeled
    break/continue adds +0. Previously this was inconsistent in both
    directions. The JS family (JavaScript/TypeScript/TSX/mozjs) now
    counts labeled break LABEL / continue LABEL (+1, gated on the
    statement_identifier label child); PHP now counts goto label;
    (+1). Conversely, Ruby no longer counts plain break/next (Ruby
    has no labeled loops, so these are always unlabeled → +0; redo and
    retry remain +1 as genuinely unstructured jumps), and Lua no longer
    counts plain break (Lua has no labeled break → +0; goto label
    remains +1). PHP's numeric break N; / continue N; stays +0 — it
    is a structured loop-level exit whose enclosing loops are already
    counted via nesting. This raises published cognitive (and the derived
    MI) values for JS/TS/PHP code using labeled jumps or goto, and
    lowers them for Ruby/Lua code using plain break/next, so cognitive
    scores are now comparable across languages.
    Fixes #435.

  • Cyclomatic complexity now counts the safe-navigation operator as a
    decision point for Kotlin (?., QMARKDOT) and PHP (?->,
    QMARKDASHGT), matching the existing JS/TS/C# treatment of ?.
    (#281).
    Each occurrence adds +1 to both standard and modified cyclomatic
    (a chain a?.b?.c adds +2). Matching the operator token — rather
    than the wrapper node — counts each operator exactly once across PHP's
    nullsafe_member_access_expression and
    nullsafe_member_call_expression forms, and across Kotlin's
    navigation_expression. This raises published cyclomatic (and the
    derived MI) values for Kotlin/PHP code that uses safe navigation, so
    metrics are now comparable across these languages.
    Fixes #436.

  • bca init now scaffolds bca-thresholds.toml with loc.sloc = 800
    (was 300). File-level SLOC counts inline #[cfg(test)] tests,
    comments, and blank lines, so the old limit sat below the median
    source file and flagged ordinary well-documented modules rather than
    genuinely oversized ones; 800 better reflects a healthy Rust file
    ceiling (inline tests inflate file SLOC 2-3x). The scaffold tracks the
    project's own gate, now pinned by a drift test so the two cannot
    silently diverge. init still refuses to overwrite an existing
    bca-thresholds.toml, so only newly-scaffolded files are affected.

  • Python's hidden block / lambda kind-id aliases are now normalized
    behind a single python_is_block helper, and is_closure accepts the
    currently-unseen Lambda2 alias, with drift-guard tests mirroring the
    Php::String3 / Java::MultilineStringLiteral guards. Defensive
    refactor; no metric output changes. Fixes
    #419.

  • Completed the #419 Python lambda-alias normalization in the cognitive
    metric: the three impl Cognitive for PythonCode lambda sites (the two
    boolean-operator ancestor-scope walks and the lambda-nesting dispatch
    arm) now recognize the Lambda2 (197) hidden alias, not just Lambda
    (196). Added a single cognitive::python_is_lambda chokepoint reused by
    those sites and by is_closure (mirroring python_is_block), so the
    closure and cognitive lambda detection can no longer desync. Defensive
    refactor; Lambda2 is unemitted by the current grammar pin, so there is
    no metric output change. Fixes
    #422.

  • The per-language Halstead string-interpolation operand skip (a literal
    is one operand unless it wraps interpolation, in which case the wrapper
    yields Unknown and the inner expressions are counted) is unified
    behind a Getter::string_operand_type default plus a Node::wraps_any
    primitive, retiring the two bespoke Tcl/PHP helpers and nine duplicated
    sites. No metric values change. Fixes
    #420.

  • The AST-dump renderer (bca dump) is refactored internally: the
    monolithic dump_tree_helper (cyclomatic 32, nexits 20, nargs 8)
    is split into a state struct plus single-purpose helpers
    (branch_glyphs, line_in_range, paint, write_node_line /
    _header / _location / _snippet, dump_children), each well
    under the per-function thresholds. Output is byte-for-byte
    identical
    — no public API, CLI, or dump-format change; a new
    byte-exact regression test (dump_output_matches_expected_tree)
    plus unit tests for the extracted predicates pin the behavior.
    Note: most of the original cyclomatic/nexits score was Rust's ?
    operator (each counts as a TryExpression decision point), not
    genuine branching — see
    #401.

  • tree-sitter-mozjs is regenerated against its declared
    tree-sitter-javascript 0.25.0 base grammar (with tree-sitter
    CLI 0.26.9), and its floating tree-sitter-cli ^0.25.3
    devDependency is pinned to 0.26.9. Investigation for
    #407
    found the bundled mozjs parser was stale at JS 0.23.1: the
    0.23.10.25.0 marker bump (#1207) shipped without the
    matching regen, and #400 then pinned the grammar-marker-sync
    baseline at 0.25.0 on the incorrect belief that the regen was a
    no-op. The real 0.25.0 regen is not a no-op — it adds the
    using / await using explicit-resource-management declaration
    (using_declaration), so the generated Mozjs node-kind enum in
    language_mozjs.rs gains Using and UsingDeclaration variants
    (the pre-existing switch_default node is renumbered, not added).
    The bump
    is **metric-neutral for the