v2.0.0
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
Statsaccessors and metric naming — Halstead,
NArgs, and MI accessors were renamed to a uniform wire
vocabulary, theexitmetric module was renamed tonexits, and
theMetric::NArgs/Metric::Exitvariants became
Metric::Nargs/Metric::Nexits. - Serialized output shape — metric keys were normalised
(#510, #511), integer-valued metrics now serialize as integers and
their accessors returnu64(#530), non-finite floats serialize as
a uniformnull(#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-filevcsobject, and removal of the
unprefixed route aliases. - Default grammars —
.js/.jsxnow parse through upstream
tree-sitter-javascript(the Mozilla fork is demoted to the opt-in
mozjs, owning only.jsm),.cpp/.hthrough upstream
tree-sitter-cpp(Mozilla fork demoted to opt-inmozcpp),.c
through a newLANG::C, and.mthrough a newLANG::Objc. - Python bindings — the typed surface tightened and
analyze_batch'sskip_generateddefault flipped fromFalseto
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.x → 2.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::exit → crate::nexits), Metric::Exit → Metric::Nexits,
Metric::NArgs → Metric::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 sum → tokens. 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
Nodeis now a closer py-tree-sitter drop-in: atypeproperty
aliaseskind(the py-tree-sitter spelling —kindstays the canonical
bca name), andtextis now a property rather than a method, matching
py-tree-sitter'snode.text. Together these erase the two most-common
mechanical edits when porting a py-tree-sitter walker. Thetext
method→property shape change is part of the still-unreleased #728 surface,
so it lands free before 2.0. Covered by themake py-stubtestgate (#975). -
Public
metric_catalog::MetricScopeenum (File/Function/
Container) withMetricScope::admits(SpaceKind), a
metric_catalog::scope(id)lookup, ascopefield on the
#[non_exhaustive]MetricInfo, andSpaceKind::from_serialized—
the single source of truth for which space kind each threshold metric
gates (#969), shared by the CLI gate and the Pythonto_sarifbinding
so they cannot drift. -
Lazy
Nodetraversal handle for Python (big_code_analysis.Node) over
the tree retained byAst, so a caller walks the AST py-tree-sitter-style
—kind(with the py-tree-sitter-compatibletypealias), byte offsets,
points,children,child_by_field_name, thetextproperty, a lazy
pre-orderwalk(),descendants_by_kind()— without
materialising the tree into dicts the waydump()does (#728). Reach one
through the newAst.root_nodeproperty orAst.find(filters); the node
keeps itsAstalive, so it stays valid after every other reference to
the parse is dropped, and is safe to share across aThreadPoolExecutor.
Kinds are the raw grammar kinds (not theAlterator-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-basedstart_point/end_point(py-tree-sitter
parity), and 1-basedstart_line/end_lineplus aspandict matching
dump(). Covered by themake py-stubtestgate. -
Node::preorder()(a pre-order iterator) and
Node::descendants_by_kind(kinds)on the RustNodesurface — the
Rust counterparts the Pythonwalk()/descendants_by_kind()mirror,
so Rust callers gain the same ergonomic traversal helpers (#728). -
Python
Astparse-once handle (big_code_analysis.Ast) binds the Rust
Astseam, 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 inanalyze()(#727).Ast.parse(code, language)andAst.from_path(path)construct the handle;.metrics()
(byte-for-byteanalyze_source),.dump()(thebca dump//astnode
tree),.functions(),.ops(),.count(),.strip_comments(), and
.suppressions()all reuse the one parse.from_pathis no-magic: it
reads through the same text reader asanalyze(so metrics match) but
does not skip generated files and never silently returns nothing. New
AstNodeDict/SpanDict/FunctionSpanDict/OpsDict/
SuppressionMarkerDictTypedDicts; all covered by themake py-stubtest
gate. -
Ast::from_pathon the Rust surface (the file-backed counterpart to
Ast::parse): reads + language-detects + parses one file, returning a
newFromPathErrorfor 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) andLANG::grammar_version
(Rust) return the pinned tree-sitter grammar crate version backing a
language (e.g."0.25.1"forbash) — 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
spannow carries
start_byte/end_byte(0-based, half-open) alongside the existing
1-based line/column pairs, across the library, CLIdump, web/ast,
and the Pythondump()(#727). Structural consumers can slice the
original source for any node — including internal nodes whosevalue
the dump omits — without re-deriving offsets from lines and columns.
(See the (breaking)Spannote 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_formulais 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, andabc.value(#958). Each space already carried
its subtree aggregate (sum/magnitude); the newvaluefield 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/
AbcDictTypedDicts. -
Opt-in keyed author-identity hashing for
--emit-author-details
(#956). A secret key —--author-hash-key <KEY>(or the
BCA_AUTHOR_HASH_KEYenvironment variable, preferred so the secret
stays off the process list), the RESTauthor_hash_keyfield, and the
Pythonvcs.Options(author_hash_key=…)— hardens the emitted author
digests into anHMAC-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
additiveOptions::author_hash_keyfield, andAuthorId::emit_hashed. -
LANG::Objc(slugobjc) and theobjcCargo feature: dedicated
Objective-C support backed by upstreamtree-sitter-objc=3.0.2,
owning the.mextension and theobjc/objective-cemacs modes
(all moved offLANG::Cpp). Objective-C is a strict superset of C, so
.mfiles now parse correctly instead of ERROR-cascading every
@interface/@implementation/ message send through the C++
grammar. Objective-C++ (.mm) deliberately stays onLANG::Cpp: the
Objective-C grammar cannot parse the C++ half of a.mmfile, and C++
is the larger surface, so the C++ grammar degrades more gracefully
there — the same asymmetric trade-off.huses (a known limitation;
metrics for the Objective-C portions of.mmfiles are approximate).
Real impls ship for all metrics: cyclomatic, cognitive, exit,
Halstead, LoC, nom, and nargs (#724), plusabc(message sends count
as calls;@try/@catchas conditions) and the OO metricsnpa
(@propertyand@publicinstance variables),npm(@interface/
@protocoldeclarations and@implementationdefinitions), andwmc
(per-method cyclomatic rolled into the@implementationclass) (#737).
The retired internalfake::get_trueObjective-C slug overlay (#540)
is gone —.mreports"objc"and.mmreports"cpp"natively.
all-languagesnow includesobjc(#724, part of #718). -
New book recipe, Feeding metrics to an agentic coding tool
(recipes/agent-feedback.md): wires the existingbca checksurface
into an agent's after-edit feedback loop with copy-pasteable sections
for Claude Code (PostToolUsehook with stderr/additionalContext
injection) and opencode (atool.execute.afterplugin that throws to
signal). Ships a verbatim anti-gaming guidance block, the exact
in-source suppression syntax (canonicalnexits, neverexit), 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(slugc) and thecCargo feature: a dedicated C
language backed by upstreamtree-sitter-c=0.24.2, owning the
.cextension and thecemacs mode (both moved offLANG::Cpp).
.hdeliberately stays onCpp— 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,
sonpm/npa/wmcare no-ops; the.cre-routing shifts metric
values on C files (integration snapshots re-baselined).all-languages
now includesc(#721, part of #718). -
LANG::Mozcpp(slugmozcpp) and the opt-inmozcppCargo feature:
the Mozilla/Gecko C++ dialect, backed by the vendored
bca-tree-sitter-mozcppfork (upstreamtree-sitter-cppplus the
MOZ_*/QM_TRY_*/ alone-macro overlay). It owns no file
extensions — select it explicitly with--language mozcpp, a
manifest, or the API, mirroringmozjsfor.jsmsince #507. The
all-languagesfeature now includesmozcpp(#720, part of #718).
Mozcppis a first-class C++ dialect everywhereCppis: it shares
the preprocessor macro-replacement pass, the comment-stripping
redirect, and thebca-webGET /v1/languageslisting (where it
appears with an emptyextensionsarray, for parity with the Python
supported_languages()surface). -
exclude_testsbca.tomlmanifest key: opt a project'sbca check/
bca metricsinto Rust test-subtree pruning declaratively, mirroring
the--exclude-testsflag (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 (themi.*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.
Backsbca metrics/ops --output <FILE> --format csv(#669), which
previously repeated the header before every file's rows. -
A
mypy stubtestgate (make py-stubtest) verifies the hand-written
PyO3 type stub
big-code-analysis-py/python/big_code_analysis/_native.pyiagainst 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-typecheckmypy/pyright passes cannot catch that class of
drift). Wired intomake pre-commitandmake ci(chained after
py-test, sharing itsmaturin developbuild) 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 (thevcssubmodule, 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'smetrics.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 honorsSOURCE_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 anoverflow-x:autoscroll 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), returnsNonefor 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 theanalyze_batchshape with the same
never-raise semantics (per-file failures becomeAnalysisFailure
elements) (#658). Each positional seed may be a file or a directory; it
forwards theanalyzekwargs (includingvcs/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_functionboolean kwargs onanalyze_batch,
mirroring single-fileanalyze(#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]toanalyze_batch(paths)is
behaviour-preserving" claim true even when the comprehension usedvcs=. -
(Python) Typed
TypedDicts for the change-history report shapes
(#664):VcsReportDict(fromvcs.rank),VcsTrendDict(from
vcs.trend),JitCommitReportDict(fromvcs.commit), and
JitDiffReportDict(fromvcs.score_diff) replace the former
dict[str, Any]returns. The report / trend envelope structs are
single-sourced inbig_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, orNone
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 Pythonanalyze_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 publicMetricsOptions::with_only(dependencies
auto-resolved). Accepts comma-separated and/or repeated values using
the same canonical ids ascheck --threshold/diff --metric
(dotted and barelocsub-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-baselinegain an opt-in--exit-codeflag:
exit with the metric-gate code (2) when the diff, after the active
--metric/--min-change(or--*-onlysection) filtering, is
non-empty; exit0when empty. Default behavior is unchanged (always
0on success); a tool error still exits1.git diff --exit-code-style boolean for grammar-bump CI (#692). -
Metric legend in reports: Markdown gains a
### Legendfootnote 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>, andaria-sortinitial-sort indication on
each pre-ranked table column (#622). -
--color auto|always|neverglobal CLI flag with tty detection and
NO_COLORsupport; piped text dumps (metrics/opsdefault tree,
dump,find,functions) no longer emit ANSI escapes. Library
gains the additiveColorModeenum anddump_*_with_color/
dump_function_spans_with_colorvariants (#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 /v1route 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/vcsfamily (#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,
andVcsEnvironmentError; existingexcept ValueErrorhandlers
keep working (#624). -
Python: generated
TypedDictstubs for the analysis result shapes
(FuncSpaceDictand 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_dirtakes
os.PathLike,as_oftakesdatetime,file_typestakes 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
compositerisk_score(--risk-formula weighted|percentile), plus a
hotspot_score(complexity × recent churn) when AST metrics are
computed alongside. Built ongix
behind the hierarchicalvcs = ["vcs-git"]Cargo feature; the generic
vcsmodule 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 behindvcs-git). - CLI: a new
bca vcssubcommand.--formataccepts a rendered
report page (markdown/html, a self-contained sortable table
styled likebca 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 directorymetrics/opsemit). Plusbca metrics --vcsto attach avcsblock to each file's metrics, andbca report markdown|html --vcsto 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 (bothbca vcs --format htmland thebca report html --vcs
section) severity-heats therisk_scorecell on a green→yellow→red
gradient (#577); becauserisk_scoreis 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 vcserrors clearly outside a git working tree;--include/
--exclude/--pathsare reused. - Web: a new
POST /vcsendpoint taking a server-siderepo_path. - Python:
vcs_metrics(repo_path, …)and an opt-invcs=Trueon
analyze().
Design note: the issue proposed a
Metric::Vcsenum variant +
--metrics vcs; VCS is file-level, has no per-function threshold, and
is not suppressible, so it is exposed via a dedicated--vcsflag
rather than overloading the per-functionMetricbitfield. - Library:
-
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; a0.0is 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 arisk_score_versionbump to
2(the recent-window pair enters both the weighted and percentile
formulas); the new formula is documented insrc/vcs/score.rsand the
mdBook VCS chapter. Because the serialized field set grew, the
output-shape stampvcs_schema_versionalso bumps to2. Surfaced
on every VCS front end (libraryStats/wire::Vcs,bca vcsCSV /
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 avcs
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 ordinalrisk_scoreas the file block,
plus a per-functionhotspot_score. The per-function numbers are a
current-blame snapshot and deliberately differ in meaning from the
file-level walk:churncounts 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
newvcs::Error::Blamevariant (all behindvcs-git); nested
CodeMetrics::vcsis now populated for function spaces, not only the
file space. - CLI:
bca metrics --vcs-per-function. - Enables the
blamefeature on the pinnedgixdependency.
Design note: the issue proposed a
--metrics vcs:per-function
sub-selector; consistent with the--vcsflag chosen for #328, this
ships as a dedicated--vcs-per-functionflag instead. Documented
limitations cover renames, function splits, deletion+recreation, and a
narrowgix-blamerobustness bug on pathologically repetitive files
(real source is unaffected; an unblameable file degrades gracefully to
the file-level block only). Pythonanalyze()/ web parity for the
per-function selector is tracked as a follow-up. - Library:
-
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 compositerisk_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 compositescore;--fail-over <SCORE>
exits2(thecheckmetric-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, …}(behindvcs-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 parentbca vcswindow /--ref/ bot /
merge / rename flags. The barebca vcsranking 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. - Library:
-
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-levelvcs_aggregate.bus_factorobject alongside the
per-filevcsdata: the minimum number of developers whose departure
would orphan more than a configurable fraction (default0.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.75author 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-detailseach 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_factorbuilder,
Options::{compute_bus_factor, bus_factor_threshold}, a
vcs::options::validate_bus_factor_thresholdhelper, and a new
vcs::Error::InvalidBusFactorThresholdvariant (all behind
vcs-git). The genericbus_factormodule is backend-neutral. - CLI:
bca vcsandbca report --vcsemitvcs_aggregatein 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 /vcsgains abus_factor_thresholdfield and returns
vcs_aggregate. - Python:
vcs_metrics(…, bus_factor_threshold=…)returns
vcs_aggregatein the result dict.
Opt-in by design (
compute_bus_factor, off by default): it retains
per-file authorship beyond the per-fileStats, so the repeated
JIT-prior and per-file-injection walks neither compute nor pay for it.
Additive — no existing field moved, andvcs_schema_versionis
unchanged (the aggregate carries its ownBUS_FACTOR_SCHEMA_VERSION). - Library:
-
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).pointsevenly-spaced
samples (inclusive of both endpoints) cover aspan, ending atas_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'sHEADtree, so it is a faithful
historical snapshot — a file not yet born at an older point isnull
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-regresseddeltassummary byrisk_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}, thewire::{VcsTrend, VcsTrendPoint, VcsTrendDelta, VcsTrendDeltas}projection, and a new
vcs::Error::InvalidTrendvariant (all behindvcs-git). The generic
trendmodule is backend-neutral. - CLI:
bca vcs trend [--points N] [--span DURATION] [--top-deltas N] [-O json|yaml|cbor], reusing the parentbca vcswindow /--ref/
bot / merge / rename /--as-of/--topflags. (TOML is excluded —
an absent point serializes asnull, which TOML cannot represent.) - Web: a new
POST /vcs/trendendpoint taking the/vcsfields 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. - Library:
-
Persistent change-history cache keyed by
HEADSHA 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;
whenHEADhas 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 thevcs::cachemodule
(behindvcs-git);AuthorId::from_digestreconstructs 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
andbca report --vcsreuse it transparently. - Web:
POST /vcsgains optionalno_cache/cache_dirfields. - Python:
vcs_metrics(…, no_cache=False, cache_dir=None).
- Library:
-
File-type scoping for the change-history ranking (#576).
bca vcsnow
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 newvcs::Error::InvalidFileTypeScopevariant (behind
vcs-git). - CLI:
bca vcs --file-types <metrics|all|EXT,…>plus abca.toml
[vcs] file_typeskey (the CLI flag replaces the manifest value). - Web:
POST /vcsgains an optionalfile_typesfield. - Python:
vcs_metrics(…, file_types=None)andvcs_trend(…, file_types=None).
- Library:
-
Ast::strip_comments(),Ast::functions(),Ast::dump(cfg),
Ast::count(filters),Ast::find(filters), andAst::suppressions()
complete the parse-onceAstseam: 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.Astis now the single entry point for
every analysis operation. Output is identical to the existing
parser-generic free fns and theaction/Callbackdispatch (which
become redundant and are retired in the 2.0 surface reshape,
#566/#570) (#567, #571). -
ParseMetricError::input()andParseLangError::input()accessors
return the rejected input string (previouslyDisplay-only) (#536). -
bca strip-commentsgains--output/-oto 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/versionandGET /v1/languagesintrospection
endpoints (with unprefixed/versionand/languagesaliases),
mirroring the Python__version__/supported_languages()/
language_extensions()surface (#541). -
bca-web --num-jobsnow accepts<N|auto>and defaults to a
cgroup-quota- / cpuset-awareauto, matching thebcaCLI. The
clap-agnosticNumJobsworker-count selector (FromStr+resolve())
is now public library API, re-exported frombig_code_analysis; its
FromStr::Erris the namedParseNumJobsError(Zero/
NotAPositiveInteger, each exposing the rejectedinput(),
Display + Error), matching theParseMetricError/ParseLangError
convention (#560). -
Derived
PartialEqon the compute-side per-metricStatstypes (abc,
cognitive, cyclomatic, exit, halstead, loc, mi, nargs, nom, npa, npm,
tokens, wmc) and onCodeMetrics/FuncSpace/Metrics, so callers
can compare analyses without round-tripping throughto_wire()(Eq
omitted due to float fields); derivedHashonSpaceKindand
metric_catalog::Direction; derivedHash+PartialOrd/Ordon
Severity(ordered scale:Error > Warning, following declaration
order) (#552). -
ConcurrentErrorsnow implementsDisplay+std::error::Error, so it
composes with?intoBox<dyn Error>/anyhowand participates in a
source()chain (#553). -
Documented the workspace-wide
bcaexit-code convention (0 success,
1 tool error, 2checkgate, 3-5check --strict-exit-codes) in
top-levelbca --helpand the book, pinned by exit-code tests (#561). -
STABILITY.mdnow 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-waySerialize-only), and the round-trip vs one-way format split;
wire::CyclomaticModifiedis named in the serialized-shape enumeration
(#559). -
Library:
big_code_analysis::VERSIONconstant exposing the crate
version (#541). -
Python:
LangandMetricNameStrEnums (generated from the live
LANG/Metric::NAMEStables, soLang.CPP == "cpp"and values
round-trip with the CLI/JSON slugs);analyze_batchgains
exclude_tests/allow_lossy_path/skip_generatedkeyword
arguments (#542). -
The serialized metric output is now readable back: a new public
big_code_analysis::wiremodule provides plainSerialize/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'Serializeimpls now delegate to
these structs (the single definition of the wire shape — output is
byte-identical), and the public types gainto_wire()
(FuncSpace/CodeMetrics/Ops/FunctionSpan). Read a tree back with, e.g.,
serde_json::from_str::<wire::FuncSpace>(&json). Non-finite floats map
null↔NaN(the deserialize side of #531); integer-valued fields areu64
(#530);wire::CodeMetricselides unselected metrics and exposes
selected()to rebuild theMetricSetfrom present keys.SpaceKind,
SuppressionScope, andMetricnow also deriveDeserialize, and the
crate enables serde_json'sfloat_roundtripfeature 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 diffandbca diff-baselinenow 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 withreportand
exemptions(#544). -
Round-trip smoke-test coverage for the TOML, YAML, and CBOR per-file output
formats inbig-code-analysis-cli, parsing each format back and asserting
structural keys and integer-valued numeric fidelity against JSON (#543). -
Ast::ops()— theSource-based counterpart ofget_ops. Returns the
operator/operandOpstree for a parsedAst, carrying the
Source::name(Option<String>) end-to-end instead of deriving the
top-levelOps::namefrom a filesystem path via lossy UTF-8 conversion.
This closes the last public seam that keyed function identity off a lossy
path: aNonesource name now yields aNonetop-levelOps::name
(whichget_opscannot express), andOps::name_was_lossyis never set
on this path. MirrorsAst::metrics
(#509,
part of #505). -
LANGnow derivesHashand implementsDisplay(itsname()
string) andFromStr(parsing that canonical name; case-sensitive, error
typeParseLangError). After the #507 JavaScript-grammar split the only
variants still sharing a display name areTsx/Typescript(both
typescript), which parse back to the first-declared variant (Tsx);
every other name — includingjavascriptandmozjs— round-trips
exactly.Serialize/Ordare deferred to the 2.0 bump
(#508). -
The
bcaCLI is now pip-installable.pip install big-code-analysis-cli
drops the compiledbcabinary onto yourPATH(no Rust toolchain
required), the waypip install ruffinstalls theruffcommand. The
PyPI distribution name isbig-code-analysis-cliwhile the installed
command staysbca— distinct from the importable library bindings
published asbig-code-analysis. A new
python-cli-wheels.yml
workflow builds-b binwheels for Linux (manylinux_2_28x86_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-languagesgrammar set and bundles the per-binary
THIRD-PARTY-LICENSES-bca.md+LICENSE(in.dist-info/licenses/)
and thebcaman pages. (#408) -
bca report markdown|htmlnow 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 — matchingbca checkand 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-filescope into each function's own scope.bca report --no-suppress(or[report] no_suppress = trueinbca.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::mergeis nowpub
(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-sourcebca: suppressmarker or covered by the baseline stay out of
the gate (exit code and human stream unchanged) but are emitted into the
--output-format sarifdocument carrying a SARIFsuppressionsentry
(kind: "inSource"for markers,"external"for the baseline). Only the
SARIF format represents suppression; the flag is mutually exclusive with
--no-suppressand--write-baseline. Note: GitHub code scanning does
not honor the SARIFsuppressionsproperty natively — it ingests such
results as open alerts — so this flag targets downstream tooling that
reads suppressions (e.g. theadvanced-security/dismiss-alertsaction).
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 SARIFsuppressionsentries for suppressed
offenders.write_sarifis unchanged (the active-only special case),
so existing output is byte-for-byte identical. -
bca diff: compare twobca metrics -O jsonruns (single JSON files
or directory trees), bucketing per-file metric deltas by metric in
tty/markdown/jsonform, with--min-changeand--metric
filters. Replaces the externaljson-minimal-tests+
split-minimal-tests.pygrammar-bump diff chain (the latter is
retired). Informational — always exits 0 on success
(#487).
Abca 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/--excludeselection;
it hard-errors (exit 1) on unresolvable refs or a non-git checkout
(#492). -
bca checkbaseline 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 checkwarns
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-baselinenow accepts an optional path. A bare
--write-baseline(no value) writes to thebaselinekey from the
auto-discoveredbca.tomlmanifest — the same filebca checkreads
— 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 manifestbaselineis set rather than guessing a
filename. The repo's ownmake 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 theIrulesLANGvariant and theIrulesCode/
IrulesParserre-exports, gated behind a newirulesCargo feature
(enabled byall-languages).when EVENT { … }event handlers and
procdefinitions are treated as function spaces, so per-handler
metrics are reported (the grammar'son/traphandler nodes are
handled defensively but parse as ordinary commands in practice). Real
implementations are provided for ABC,
cognitive, cyclomatic (including the dedicatedswitch/switch_arm
node and theand/orkeyword 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_expressiongrammar node) is now configurable
(#409).
MetricsOptionsgains acount_cyclomatic_tryfield (default
true) and awith_count_cyclomatic_trybuilder; the CLI gains a
global--no-cyclomatic-tryflag and acyclomatic_count_try
bca.tomlkey (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 ownmake self-scangate sets it via the
bca.tomlmanifest. 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:MetricsOptionsis#[non_exhaustive], so the new
field and builder do not break downstream callers; a global default
flip is deferred to a deliberate2.0decision. -
metric_catalog::MetricInfogains askip_at_unit: boolfield
recording whether a metric's serialized JSON headline at the
file-levelunitspace is an aggregate over descendant spaces that
does not match the CLI threshold accessor's per-space scalar (true
forcognitive,cyclomatic,cyclomatic.modified, andabc).
This is the single source of truth the CLIEXTRACTORStable and the
Pythonto_sarifbinding'sMETRIC_FIELDStable now both derive
from, so a metric added to one front-end but not the other — or a
skip_at_unitflag that disagrees — is a build/test failure instead
of silent SARIF divergence
(#442).
Additive:MetricInfois#[non_exhaustive], so the new field does
not break downstream readers. -
bca exemptionsaudits everything thebca checkgate skips in one
report (#386):
in-source suppression markers (bca: suppress,#lizard forgives,
…),[check.exclude]globs, and.bca-baseline.tomlentries. 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|jsonselects the output style (jsonnests
the three tiers under a singlesuppressionsenvelope; an omitted
section isnull, a requested-but-empty one is[]); the combinable
--only-markers/--only-excludes/--only-baselineflags narrow
the report for PR-bot use. The walk honours[walker.exclude], and
the baseline (bca.tomltop-levelbaseline) and[check.exclude]
inputs default to the same sourcesbca checkreads. Read-only and
informational: it
always exits 0 on success (1 on a tool error such as a missing
--baseline), never gating. The newbig-code-analysislibrary
re-exportsSuppressionMarker,SuppressionTarget,
SuppressionDialect, and theSuppressionScancallback that back it. -
bca check --strict-exit-codesopts into tiered exit codes that
split the violation case (previously a single exit2) by severity
(#385):
2new offenders only,3regressions only,4both,5a
--tier=softviolation 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 → failtooling is unaffected; only consumers that test
$? -eq 2explicitly need to widen to2-5.--no-failstill
forces exit0.--print-effective-configreports the resolved
exit_codesstyle. -
bca diff-baseline old.toml new.tomlemits a structured diff
between two.bca-baseline.tomlfiles —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|jsonselects the
output style (markdownfences each section for a sticky PR
comment;jsonemits the full structured diff). The combinable
--added-only/--removed-only/--worsened-only/
--improved-onlyflags narrow the rendered sections for PR-bot use.
Both files are read through the same loaderbca checkuses, 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 checkgains a glob-level gate exemption via a[check] excludelist inbca.tomland the--check-exclude/
--check-exclude-fromflags
(#378).
Matching files are still walked, parsed, metric'd, and shown by
bca report— onlybca checkdrops their violations before
emitting offenders and before--write-baselinerecords anything,
so structural exemptions (test fixtures, generated code,
macro-dispatch modules) stay out of.bca-baseline.toml.
--check-excludeis repeatable;--check-exclude-fromreads a
.gitignore-style file (convention.bcacheckignore); the two
union with each other, while an explicit--check-excludereplaces
the manifest[check] excludelist (CLI-wins, like every other
manifest key). Globs match the walked path exactly like--exclude.
Precedence, most-specific first: in-source
bca: suppressmarkers, then[check.exclude]globs, then the
baseline.--print-effective-configreports the resolved
check_excludeglobs. -
bca checkgains a native two-tier threshold model via a
[thresholds.soft]table and a--tier <hard|soft>flag
(#375).
The defaulthardtier compares against[thresholds]verbatim.
--tier=softis the early-warning tier: it merges
[thresholds.soft]overrides on top of[thresholds](per metric,
either an absolute limit likecognitive = 18or 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=softfalls
back to scaling every limit by--headroom(default0.95). Both
the manifestbca.tomland--configfiles accept the soft
sub-table, and both tiers ratchet through the same--baseline.
--print-effective-confignow reports the resolvedtier. As a
consequence,--headroomis 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 checkbaselines 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 configurablestart_linetolerance
(--baseline-line-tolerance <LINES>, default 50, or
baseline_line_toleranceinbca.toml) disambiguates a symbol
shared by several functions. A new--baseline-fuzzy-matchflag
(baseline_fuzzy_matchinbca.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 JSONfunctionfield now show the
qualified symbol. The baseline schema bumps to v4 (function
field renamed toqualified, optionalbody_hashadded); v2/v3
baselines are still read and degrade to bare-name + tolerance
matching until refreshed with--write-baseline. See
STABILITY.mdfor the migration. Additive, minor bump. -
bca.tomlmanifest — auto-discovered at (or above) the working
directory, consolidating the flags every local-gate recipe used to
thread through each invocation
(#374).
Top-level keyspaths,exclude_from,num_jobs,include,
exclude,baseline, andheadroom, 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 →--thresholdoverrides). Relative
manifest paths resolve against the manifest's directory. A new global
--no-configflag skips discovery for fully-explicit invocations
(bca initalso 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-configgains
amanifestprovenance line. Additive, minor bump. -
bca check --headroom <ratio>— scales every threshold from
--config(orbca.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.0is a no-op
parity run with the hard gate; out-of-range values exit 1.
--headroomis 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=valueoverrides 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.pyhelper, which is removed;
make self-scan-headroomand the local-gates book recipe now
invoke--tier=soft --headroomdirectly. Additive, minor bump. -
metric_catalogmodule — a single canonical registry of metric
metadata (#397).
Public items:metric_catalog::{MetricInfo, MetricFamily, MetricRow, Direction, METRICS, FAMILIES}.METRICSis the canonical list of
offender sub-metric ids (halstead.volume,mi.original, …) with
their long-form SARIF / Code Climate sentences and
higher-/lower-is-worseDirection;FAMILIESis the view rendered
bybca 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 toMETRICS, so a new metric can no longer ship with a half-
updated catalog. SARIF, Code Climate, andbca list-metricsoutput
are unchanged. Additive, minor bump. -
Python bindings dev environment:
make py-bootstrap,make py-sync
(alias),make py-relock, andmake py-cleanMakefile targets.
Bootstrap provisionsbig-code-analysis-py/.venvfrom the
checked-inuv.lockviauv sync --locked --extra dev; relock
regeneratesuv.lockafter apyproject.tomledit; py-clean
removes.venv, the editable-install compiled extension, per-tool
caches (.pytest_cache,.mypy_cache,.ruff_cache), and
__pycache__trees. Requiresuvto be installed locally; see
CONTRIBUTING.md for the install one-liners. -
make distcleantarget — chainspy-cleanandcargo cleanfor a
full-wipe before a from-scratch bootstrap.make cleancontinues
to docargo cleanonly. -
grammar-marker-syncstatic lint (check-grammar-marker-sync.py,
baseline at.grammar-marker-baseline.toml) blocking the failure
mode from
#400:
bumping the notification-onlytree-sitter-javascript/
tree-sitter-cppmarker intree-sitter-{mozjs,mozcpp}/Cargo.toml
without re-running the matching
./generate-grammars/generate-*.shscript ships a marker that
lies about the bundledsrc/parser.cversion. 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 —--updateafter a verified regen). Wired intomake lint,make pre-commit,make ci, the.pre-commit-config.yaml
system hook, and a defensive explicitlintjob step in
.github/workflows/ci.yml. Verified against the
tree-sitter-javascript0.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}
withtree-sitterCLI 0.26.9. -
enums-codegen-driftstatic lint
(check-enums-codegen-drift.sh) blocking the failure mode from
#405:
running anyrecreate-grammars.shinvocation silently
regeneratedsrc/c_langs_macros/{c_macros,c_specials}.rsto a
pre-optimization form (linear.contains()lookup + missing
sorted-invariant tests), undoing months of hand-improved work.
Theenums/templates/c_macros.rstemplate now emits the
binary_search-based lookup plus the*_is_sorted,
*_lookup, and*_lookup_boundariestest 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.yamlsystem hook, and a defensive
explicitlint-job step in.github/workflows/ci.yml. -
check-manpage-assetsstatic lint
(check-manpage-assets.py) blocking the failure mode from
#444:
abcasubcommand man page that drops out of the
hand-maintained deb/rpm asset lists ships a package without its
page. The gate globsman/bca-*.1, partitionsbca-web.1to
big-code-analysis-weband every other page to
big-code-analysis-cli, and asserts each page appears in BOTH
the[package.metadata.deb].assetsand
[package.metadata.generate-rpm].assetstables of its owning
crate'sCargo.toml, failing loud with the offending
filename(s). Wired intomake lint,make pre-commit,make ci, the.pre-commit-config.yamlsystem hook, and a defensive
explicitlint-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 whosebca-*.1source no longer exists
underman/, both scoped tobca-*.1basenames so binaries,
completions, the top-levelbca.1, and licences are not swept in. -
bca checkactionable failure output (umbrella
#356
now complete):--since <ref>/--changed-onlydiff-aware mode: the
summary footer surfaces "Files in this range:" (offenders in
files touched between the diff base andHEAD) before the
legacy offender list;--changed-onlydrops out-of-range
rows entirely for terser PR-gate output. Auto-detects the
diff base fromBCA_DIFF_BASE,GITHUB_BASE_REF, or
GITHUB_EVENT_BEFOREin that precedence. Pass
-c core.quotePath=falseto git so non-ASCII filenames
survive the canonicalize roundtrip. Fixes
#359.--github-annotations(auto-enabled when
$GITHUB_ACTIONS == "true") emits::error file=…,line=…, title=…::msgworkflow 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_SUMMARYmarkdown 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-baselinerefresh
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×,blocker4×), inverted for the
mi.*family where lower is worse.
Fingerprints hashpath \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 everyLangvariant to its
expected backing tree-sitter grammar crate via per-variant
integration tests forget_languageandget_language_name,
catching the Cpp→mozcpp class of drift bug (fixed in
#344)
atcargo testtime rather than first-dispatch panic. The new
test suite runs undermake 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.tomlderived from a
write-baseline pass. Flags:--dir <DIR>,--force,
--no-baseline. Interactive prompts and--emit make|just|pre-commit|github-actionsskeletons are deferred to
follow-up. Fixes
#379. -
bca check --print-effective-config[=FORMAT]serializes the
resolved threshold / check configuration after merging
--configTOML +--thresholdCLI 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, #374bca.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--thresholdflag or[thresholds]TOML key is
misspelled. Uses Levenshtein with amin(2, max(len)/3)cutoff
plus a shared-prefix rescue for truncations (covers
cyclic→cyclomaticandhalstead.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-onlyvcs_per_function=Trueflag
that mirrors the CLI'sbca metrics --vcs-per-function: it blames the
file once and attaches a per-functionvcsblock (byte-identical in
shape to the CLI's) to every nested function/method/class space in the
returned JSON tree. Independent of the file-levelvcs=opt-in; degrades
gracefully outside a git repository (#578). -
bca vcs jit --diff <file>(and--diff -for stdin) scores an
arbitrarygit 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 surfacevcs::score_diff. Just-in-time scoring is
now also exposed via the RESTPOST /vcs/jitendpoint and the Python
vcs_jit(repo_path, commit=…, diff=…)binding, both reusing
score_commit/score_diff. Commit-mode JIT output is unchanged
(#580). -
bca-webgains 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 inAccess-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 +Allowhandling via a new
CorsPolicyenum andfrom_fnmiddleware (the preflight sources its
methods from the resource's ownAllowheader), wrapped under
Conditionso the default request path carries no extra layer.
Access-Control-Allow-Credentialsis never emitted (#694). -
The REST API now honors the
Acceptheader on every structured
analysis endpoint (/v1/ast,/v1/commentJSON,/v1/function,
/v1/metrics,/v1/vcs,/v1/vcs/trend,/v1/vcs/jit), reusing the
sameserde_yaml/ciboriumserializers the CLI drives so a value is
byte-identical over both surfaces. JSON stays the default (absent
Accept,*/*,application/*,application/json);application/yaml
andapplication/cborget that format with the matchingContent-Type,
q-weights are honored, and any other concrete type answers406 Not Acceptablethrough the uniform{error, error_kind, id}envelope. TOML
and CSV are excluded (#657). -
Python VCS documentation: a new
python/vcs.mdbook chapter covering
the namespaced change-history surface (vcs.rank/vcs.trend/
vcs.commit/vcs.score_diffand the sharedvcs.Options), the
widened option kwargs (#619), theas_ofreproducible-snapshot
semantics (#648), and the GIL-releaseThreadPoolExecutornote (#620);
python/errors.mdgains the typed VCS exception taxonomy (#624) and the
stale flat references in the CLIvcs.mdare 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 singleDiff::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
relabelledAverage MI (SLOC-weighted). Previously it averaged the
clampedmi_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 MIcolumn changes the same way and gains its own tooltip. The
per-fileMIhotspot 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
Spanstruct (big_code_analysis::Span)
gainsstart_byte/end_bytefields (0-based, half-open byte offsets
into the parsed source) and is now#[non_exhaustive]. Construct it via
the newSpan::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/astand dump consumers are unaffected
beyond the additive fields; only Rust callers that built or destructured
Spanby literal are affected. Deferred to the 2.0 milestone (#727). -
(breaking)
LANG::Cpp(slugcpp) is now backed by the upstream
communitytree-sitter-cppgrammar instead of the Mozilla fork. The
fork moved to the new opt-inLANG::Mozcpp(see Added). Thecpp
Cargo feature's dependency set changed accordingly
(bca-tree-sitter-mozcpp→tree-sitter-cpp); a--no-default-features
consumer that enabledcppfor 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 viaPython::detach, the same
off-GIL treatment thevcs.rank/trend/commitentry 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 aThreadPoolExecutorover several
analyze(vcs=True)calls now parallelises instead of serialising on the
walk. -
Web/Lib: the
idfield on every JSON request payload (/v1/ast,
/v1/comment,/v1/function,/v1/metrics,/v1/vcs,
/v1/vcs/trend,/v1/vcs/jit) and thecomment/spanfields on
/v1/astare now optional (#[serde(default)]). Omittingid
defaults to an empty string (the "no correlation id" sentinel echoed
back unchanged); omittingcomment/spandefaults tofalse. This
ends the JSON-vs-query-variant inconsistency where the query form
already defaulted these fields while the JSON form returned a400 missing field. Strictly request-side loosening: previously-valid
payloads are unaffected and no new keys are accepted (#645). -
(breaking) Lib: Halstead
Statsaccessors renamed to the wire
vocabulary —u_operators→unique_operators,operators→
total_operators,u_operands→unique_operands,operands→
total_operands. JSON/YAML/TOML/CBOR output keys are unchanged.
Deferred to the 2.0.0 release (#588). -
(breaking) Lib: the
exitmetric module is renamed tonexits
(src/metrics/exit.rs→nexits.rs; the crate-internal
crate::exitpath is nowcrate::nexits), itsStatsaccessors
exit/exit_sum/exit_average/exit_min/exit_maxbecome
nexits/nexits_sum/nexits_average/nexits_min/nexits_max,
and the retired"exit"parse alias forMetric::Nexitsno longer
resolves (only"nexits"parses). Output keys are unchanged.
Deferred to the 2.0.0 release (#588). -
(breaking) Lib: NArgs
Statsaccessors 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, andnargs_total/
nargs_average→total/average. Wire keys are unchanged.
Deferred to the 2.0.0 release (#588). -
(breaking) Lib: MI
Statsaccessors 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::NArgsenum 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 onnpm/npa/wmcare 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
tokensblock'stokens_average/
tokens_min/tokens_maxleaves are renamedaverage/min/
max, matching the CSV columns; the bare-sumtokensleaf is kept.
The terminal dump's tokens sum label changessum→tokensto
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.locnow shows the
averages and min/max,nexitsrenders as asum/average/min/
maxaggregate) 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 nestedvcsobject under each ranked file (and each
/vcs/trendpoint) forbca vcs,POST /vcs,vcs_metrics(), and
vcs_trend(), replacing the former flattened-beside-pathlayout;
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 stampsvcs_schema_version,risk_score_version,
long_window_days, andrecent_window_daysare carried exactly
once on the enclosing/vcsand/vcs/trendenvelope (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 thecheckand
vcs jit --fail-above-style metric gates;--help/--version
still exit 0 (#594). -
(breaking) CLI: renamed
--language-typeto--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
--pathsto.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/--includeand-X/--excludetake exactly
one glob per occurrence and are repeatable; the greedy
space-separated multi-value spelling no longer parses (#601). -
(breaking) CLI:
--topunified onusizewith0meaning
"all rows" acrossvcs,report, andvcs trend
(report --top 0was previously a usage error) (#602). -
(breaking) CLI: renamed
--num-jobsto--jobsand--warning
to--warnings(hidden aliases kept one cycle), and the default
tree output is now selectable explicitly as--format texton
metrics/ops(#604). -
(breaking) Manifest: the check-only keys
baseline,
baseline_line_tolerance,baseline_fuzzy_match, andheadroom
moved under[check]inbca.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 emitsbus_factor_schema_version
(schema 2, was bareschema_version) and JIT reports emit
risk_score/partial_risk_score(schema 3, was bare
score/partial_score) (#591). -
(breaking) Web:
/vcs/jitrejects a payload combiningdiff
with any commit-mode field (400 naming the conflict) instead of
silently ignoringrepo_path/commit(#632). -
(breaking) Web: an unsupported language now answers
422 Unprocessable Entitywith the machine token
unsupported_languageinstead of 404; 404 is reserved for unknown
routes (#634). -
(breaking) Web: the
/commentJSON response returns the
stripped source as a string instead of an array of byte numbers
(#629). -
bca vcs jit/vcs trendaccept the history-tuning flags
(--long-window,--as-of, …) in the subcommand position;
--refcombined withvcs jitis 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_filereturnsLang | None(aStrEnum, so
string comparisons keep working) andlanguage_extensionsaccepts
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-filemarkers migrated from the
legacyexitspelling to the canonicalnexits(the parser alias
forexitis unchanged) (#593). -
(breaking, deferred to 2.0) Retired the
action/Callback
dispatch and the path-positional analysis surface, leaving
[Ast] (withanalyzefor the one-shot case) as the single public
analysis seam (#566, #570). Removed: theCallbacktrait and its
per-action tag/Cfgtypes (Dump/DumpCfg,CommentRm/CommentRmCfg,
Function/FunctionCfg,Find/FindCfg,CountCfg,
NodeTypeFilters,OpsCode/OpsCfg,Metrics/MetricsCfg,
SuppressionScan,AstCallback); theactiondispatcher; the
parser-generic free functionsmetrics/metrics_with_options
(inspaces) andoperands_and_operators(inops); and the
path-positional shimsget_function_spaces,
get_function_spaces_with_options,metrics_from_tree, and
get_ops. The internal parser machinery is demoted frompubto
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>Codetags,PreprocParser, and the
rm_comments/function/count/find/suppression_markers
walk cores. Callers migrate toAst(parse,from_tree_sitter,
metrics,ops,strip_comments,functions,dump,count,
find,suppressions,root_node) oranalyze. No metric values
change — this is a pure removal/visibility change. The deletions land
staged onmainand take effect at the2.0major bump. -
bcanow analyzes each file through the explicit-nameanalyze/
Ast::opsseams 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 theAst-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 theMetricenum andMetricKindis removed from
the public API.Metricgains canonical-spelling serde (nargs/
nexits, notn_args) and declaration-orderOrd; the suppressed-scope
serialization uses canonical names (nexits, notexit) and the
nexits→exitalias bridge is gone;tokensis non-suppressible
(rejected with a clear error). Suppression parsing now surfaces the
offending token viaParseMetricErrorinstead ofErr = (), closing
#554 (#555, #554). -
(breaking, deferred to 2.0)
Node's innertree_sitter::Nodeis no
longer apubtuple field; reach it via the new
Node::as_tree_sitter(&self) -> tree_sitter::Node<'a>accessor
(value-not-stable, mirroringAst::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
Statsstruct
(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.xrather than a shape break deferred to3.0. -
(breaking, deferred to 2.0)
ConcurrentErrorsis now
#[non_exhaustive]and itsSender/Threadvariants carry a boxed
std::error::Error + Send + Syncsource instead of aString(so
source()chains);Producer/Receiverremain message-only (their
cause is a thread-panic payload, not anError) (#553). -
(breaking, deferred to 2.0) The
/commentendpoint now returns200
with a uniform empty payload across both content types for the "no
comments" outcome — JSON returns{code: []}and octet-stream returns
200with an empty body, replacing the former octet-stream204 No Content(#558). -
CBOR output (
bca metrics --format cbor) now serializes via
ciboriuminstead of the unmaintainedserde_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 keystype/value/span/
field_name/children(wasType/TextValue/Span/
FieldName/Children);TextValueis renamed tovalue.Span
changes from a bare(usize, usize, usize, usize)tuple to a named
object{start_row, start_col, end_row, end_col}(stillOption,
nullfor root / span-disabled nodes); field order and 1-based
row/column values are unchanged. Deferred to the next major bump
(#535). -
(breaking)
Metric::Exitrenamed toMetric::Nexits; its
Displayis now"nexits"andMetric::NAMESlistsnexits,
matching thenargs/nom/npa/npm"number-of" family. The CLI
acceptsnexitscanonically withexitkept as a hidden parse alias
for one cycle. The serialized field and JSON key were alreadynexits,
so output is unchanged. Deferred to the next major bump (#536). -
(breaking) Removed the never-produced
MetricsError::NonUtf8Path
andMetricsError::ParseHasErrorsvariants (the enum stays
#[non_exhaustive], so a future strict mode can re-add them).
EmptyRootis retained — it is constructed at live forward-compat
guards. Deferred to the next major bump (#536). -
(breaking)
FunctionSpan.nameis nowOption<String>and the
error: boolfield was removed; an unresolved name isNone
(serializednull), matchingFuncSpace/Ops. The wire DTO and the
REST/functionJSON shape are updated accordingly. Deferred to the
next major bump (#536). -
(breaking)
CountCfgandFindCfgno longer expose
Arc<Mutex<Count>>/Arc<[String]>in their public fields.
CountCfg.statsis now an opaqueCountCollector
(CountCollector::new(),into_count());CountCfg.filtersand
FindCfg.filtersare now an opaqueNodeTypeFilters
(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>-onlyidiom (--markers-only/--excludes-only/
--baseline-only), matchingdiff-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*-fromfiles) UNION with thebca.toml
exclude/[check] excludelists 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-configstill bypasses the manifest.
Deferred to the next major bump (#539). -
(breaking)
LANG::name/Display/FromStrnow use one canonical
lowercase slug per language; the prettyc/c++/c#display forms
are dropped andTsxreportstsx. The serializedlanguagevalue
(CLI JSON, web/metrics, Python) changes accordingly and is now
always a validFromStrlookup 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 baretext/plainbodies. Deferred to the next
major bump (#541). -
(breaking)
bca-web:/v1/functionand/v1/commentresponses
now includeidand the detectedlanguage(canonical slug),
matching the/v1/metricsenvelope. Deferred to the next major bump
(#541). -
(breaking)
bca-web: theunitquery flag on/v1/metricsnow
uses normal boolean semantics (true/false/1/0,
case-insensitive); other values (includingyes/on) return HTTP 400.
Deferred to the next major bump (#541). -
(breaking) Python:
analyze_batch'sskip_generateddefault
flips toTrue, aligning with single-fileanalyze;
supported_languages()now returnslist[Lang]andMETRIC_NAMESa
tuple[MetricName, ...](values remain string-compatible). Deferred to
the next major bump (#542). -
(breaking) Tidied internal-plumbing visibility.
Cursor
(src/node.rs) is narrowed frompubtopub(crate)and dropped from the
lib.rsre-exports: every one of its methods was alreadypub(crate), so the
re-exported type could be named but never used.CallbackandLanguageInfo
gain#[doc(hidden)]to matchParserTrait(Callback::callis bound on the
hiddenParserTrait, andLanguageInfois reachable from documented API only
through the hiddenParser), so the bound and the trait now have coherent
visibility; they remainpubfor theaction::<T>dispatcher and the
in-crate /bca-webimpl Callbackblocks, so only their rustdoc presence
changes.Nodestayspub— the doc-hiddenParserTrait::rootreturns it,
and it carries a genuine public method (has_error);Ast::as_tree_sitteris
the preferred higher-level raw-tree seam. RemovingCursorfrom the public
surface is SemVer-breaking; deferred to the2.0.0release (the
release-prep commit moves this entry into the2.0.0section). The
#[doc(hidden)]additions are not themselves SemVer-breaking.
(#534, part of
#505) -
(breaking) The builder types
Source,MetricsOptions, and
MetricsCfgno longer exposepubfields — they are narrowed to
pub(crate). These types are already documented as "construct vianew+
with_*setters" and carry#[non_exhaustive]; thepubfields 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 the2.0.0release (the release-prep commit moves
this entry into the2.0.0section).
(#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 internalNonFinitefloat wrapper rather
than relying on each accessor staying finite. A non-finite value renders as a
nativenullin 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 emittednull, TOMLnan, 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
fullf64precision, documented in STABILITY.md as not
byte-stable across versions/platforms (the human-readablebca checkwarning
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/MIlog/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 the2.0.0release (the release-prep
commit moves this entry into the2.0.0section).
(#531, part of
#505) -
(breaking) Integer-valued metrics now serialize as integers instead of
floats, and their publicStatsaccessors returnu64instead off64.
Affected: every count, sum, and min/max (cyclomatic, cognitive, exit, nargs,
nom, tokens, loc lines, ABC assignments/branches/conditions, npa/npm
attribute/method counts), Halsteadlength/vocabularyand 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 remainf64. JSON/TOML/YAML now emit"sloc": 5rather 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 the2.0.0
release (the release-prep commit moves this entry into the2.0.0section).
(#530, part of
#505) -
Internal refactor of the crate-private
Checkerclassification trait
(apub(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-> falsestubs. 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-> falsedefaults, so a language implements
only the categories its grammar expresses (~150 boilerplate lines removed
across the 22 impls).is_primitivenow takes&Nodeinstead of a bare
u16, matching every other predicate and removing the "two same-typed
primitives" footgun, andNode::count_specific_ancestorsis bound on
Checkerrather than the fullParserTrait. No public-API or
metric-output change — this is internal plumbing only and the serialized
metrics are byte-identical
(#520,
part of #505). -
The
bcaline-range flags are now scoped to thedumpandfind
subcommands instead of beingglobal, 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/findconsumed them, and passing e.g.bca metrics --ls 5
was silently ignored; that invocation — and the pre-existing
flag-before-subcommand formbca --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/--lealiases are (breaking) and deferred to the next
major bump
(#518,
part of #505). -
bca-webREST routes are now versioned under a/v1prefix
(/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-maintainedGUARDED_POST_PATHSconstant — each resource carries
its owndefault_service, so a request that reaches a known endpoint
but matches no route is answered with a diagnostic415/405by the
resource itself (a new endpoint can never silently regress to a
bodyless404), and a genuinely unknown URL falls through to the
app-level404. A side effect:POST /pingnow returns405(was a
bodyless404). Additionally, errors are no longer signalled inside a
200body: the metrics endpoint'sspacesfield is now a
non-optionalFuncSpace(a successful response is byte-identical to
before), and metric-computation / AST-construction failures now return
500 Internal Server Errorwith an error body rather than200with
spaces/root=null
(#517,
part of #505). -
bca-webnow logs server-side events viatracinginstead of
unstructuredeprintln!: parse failures aterror!and parse timeouts
atwarn!, each with a structuredpayload_idfield taken from the
request payload'sid. It also wirestracing-actix-web's
TracingLoggermiddleware for per-request spans (one access-log line
per completed request, with its ownrequest_idUUID, method, route,
status, and latency). Log level and output are controlled by the
RUST_LOGenvironment variable (defaultinfo). HTTP responses are
byte-for-byte unchanged — this is server-side observability only
(#516,
part of #505). -
Unified output-format selection across every
bcasubcommand
(#513,
part of #505).
--format(short-O) is now the canonical spelling everywhere:metrics/ops/checkgain the long--formatspelling;
their previous--output-formatis kept as a hidden, deprecated
alias.reportgains a--format/-Oflag and now defaults to
markdownwhen 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--formatflag wins when both are supplied.diff/diff-baseline/exemptionsgain the-Oshort for
their existing--formatflag.- These additions are backward-compatible. Removal of the deprecated
--output-formatalias and the barereportpositional 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 the2.0re-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 to1
forcyclomatic,nom, and the previously-unguarded per-space
averages (loc,abc,tokens). Behaviour-preserving for every
metric exceptcyclomatic(below): the guarded divisor is identical
whenever the count is already non-zero. - Metric values change for
cyclomatic.averageand
cyclomatic.modified.averageonly. They are now per function:
the divisor is the number of function/closure spaces in the subtree
— the per-function conventioncognitive/exit/nargsuse —
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/maxand 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 matchescognitive's function/closure count
wherever every closure opens its own space; a closure form that opens
no space, such as a Pythonlambda, is counted bycognitivebut not
as a separate cyclomatic divisor unit.) - The divisor is sourced from the space kind during finalization, not
from theNommetric, so acyclomatic-only metric selection still
divides per function without pulling anomblock into the output. nom's own averages stay per space (it is the count metric;
a per-function divisor would be circular).
- A single shared
-
get_ops,metrics_from_tree, and the doc-hidden
operands_and_operatorsare now#[deprecated]in favour of the
explicit-nameAstseams (Ast::ops,Ast::from_tree_sitter), which
carryname: Option<String>fromSourceend-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 theSource/Astmigration begun for the metrics family
in #254;
removal is deferred to the2.0.0bump
(#509,
part of #505). -
(breaking) Normalized the public language-dispatch surface
(deferred to the2.0.0bump;
#507):- Dropped the Java-style
get_prefix from every language getter, per
the Rust C-GETTER guideline:LANG::get_name→name,
get_tree_sitter_language→tree_sitter_language,get_extensions
→extensions;LanguageInfo::get_lang/get_lang_name→lang/
lang_name;ParserTrait::get_language/get_root/get_code/
get_filters→language/root/code/filters;
Parser::get_ts_tree→ts_tree. - The dispatchers
action,get_function_spaces,
get_function_spaces_with_options,metrics_from_tree, andget_ops
now takelang: LANGby value instead of&LANG(LANGis aCopy
1-byte enum, so the reference was pointless indirection). Call sites
passLANG::Rust, not&LANG::Rust. - Rename + signature only; no serialized output or metric values change.
- Dropped the Java-style
-
(breaking) The default JavaScript grammar is now the upstream
tree-sitter-javascript, not the vendored Mozillatree-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
fromjavascripttomozjs, so.jsmfiles report
"language": "mozjs". Select the fork explicitly viaLANG::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.jssnapshots) plus an independent sample. - Builds that enable the
mozjsfeature but notjavascriptno
longer analyze.jsfiles (they resolve to the now-disabled
Javascriptvariant and returnLanguageDisabled); default
all-languagesbuilds are unaffected.
-
(breaking) Normalized the serialized metric output keys for a
coherent 2.0 data contract (deferred to the2.0.0bump;
#510,
#511).
Affects the JSON / YAML / TOML / CBOR / CSV output and thebca dump
metric tree:halstead:n1/N1/n2/N2→unique_operators/
total_operators/unique_operands/total_operands(the
case-only-distinct keys collided for case-insensitive CSV/env
consumers).mi: leaves drop the redundantmi_prefix —mi_original/
mi_sei/mi_visual_studio→original/sei/
visual_studio(now equal to themi.*threshold ids).nargs:total_functions/total_closures→function_args/
closure_args;average_functions/average_closures→
function_args_average/closure_args_average; the
functions_*/closures_*min/max keys gain the_argsinfix.
Removes thetotal_functionssum-vs-count name collision and the
adjective-order disagreement withnom.npa/npm: theclasses_average/interfaces_average/
averagekeys carried CDA/COA accessibility ratios, not
averages, and are renamedclass_cda/interface_cda/cda
(npa) andclass_coa/interface_coa/coa(npm).abc.magnitudeis 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
couplingcyclomatictonom.)
-
guess_languagenow 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_siblingno longer heap-allocates aTreeCursorper
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 theLocmetric is
selected — avoiding a wasted per-node source-text scan (notably Elixir's
per-Callkeyword scan) when the result would go unused. No metric
values change
(#522). -
refactor(node):
Node::children()drives termination off the cursor
alone (struct iteratorChildren), eliminating latent duplicate-node
padding ifchild_count()and the cursor sibling walk ever desync;
ExactSizeIteratorretained, no metric-value or public-API change
(#523). -
build(deps): exact-pin
tree-sitter-kotlin-ngto=1.1.0to match every
sibling grammar, and guard the root vsenums/external grammar-pin
lockstep viacheck-versions.pyso future drift fails fast in
pre-commit / CI (resolved version unchanged)
(#524). -
build(deps): drop the
nummeta-crate from the library's direct
dependencies (its sole use,num::FromPrimitive::from_u16, now goes
through the already-presentnum-traitsre-export) and hoistcsv/
tempfileinto[workspace.dependencies]; no behavioral change
(#525). -
Python bindings:
lang_to_namenow delegates toLANG::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
languageidentifiers are byte-identical for every variant; this only
removes drift risk between the facade and the CLI display names
(#500). -
(breaking)
FilesDataandConcurrentRunnerare reshaped into a
terminal file-set processor:FilesDatadrops itsinclude/
excludeGlobSetfields (now justFilesData { paths }),
ConcurrentRunner::runreturnsResult<(), ConcurrentErrors>instead
ofResult<HashMap<String, Vec<PathBuf>>, ConcurrentErrors>, and the
set_proc_dir_paths/set_proc_pathbuilder 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-filestatand dead, path-form-sensitive globsets. The library
is now a pure concurrent processor of an already-resolved file list;
the CLI's anchored, gitignore-awareexpand_seed_pathsis 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 the2.0.0section
(#495). -
The project's own self-scan gate now reads all configuration (paths,
exclude_from, baseline, thresholds, and the cyclomatic-?policy)
from a single consolidatedbca.tomlmanifest; the standalone
bca-thresholds.tomland the redundantBCA_COUNT_CYCLOMATIC_TRY
Makefile plumbing are retired, sobca checkreproduces the gate
with no flag threading
(#483). -
bca initnow scaffolds a consolidatedbca.tomlmanifest
(auto-discovered zero-config) instead of the retired
bca-thresholds.tomlthree-file split;.bcaignoreand
.bca-baseline.tomlare still written
(#484). -
CI: removed the hand-translated
.bcaignoremirror regex from the
bca-self-scan/bca-self-scan-headroompre-commit hooks;
.bcaignoreis 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/continueadds +0. Previously this was inconsistent in both
directions. The JS family (JavaScript/TypeScript/TSX/mozjs) now
counts labeledbreak LABEL/continue LABEL(+1, gated on the
statement_identifierlabel child); PHP now countsgoto label;
(+1). Conversely, Ruby no longer counts plainbreak/next(Ruby
has no labeled loops, so these are always unlabeled → +0;redoand
retryremain +1 as genuinely unstructured jumps), and Lua no longer
counts plainbreak(Lua has no labeled break → +0;goto label
remains +1). PHP's numericbreak 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 orgoto, and
lowers them for Ruby/Lua code using plainbreak/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 chaina?.b?.cadds +2). Matching the operator token — rather
than the wrapper node — counts each operator exactly once across PHP's
nullsafe_member_access_expressionand
nullsafe_member_call_expressionforms, 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 initnow scaffoldsbca-thresholds.tomlwithloc.sloc = 800
(was300). 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;800better 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.initstill refuses to overwrite an existing
bca-thresholds.toml, so only newly-scaffolded files are affected. -
Python's hidden
block/lambdakind-id aliases are now normalized
behind a singlepython_is_blockhelper, andis_closureaccepts the
currently-unseenLambda2alias, with drift-guard tests mirroring the
Php::String3/Java::MultilineStringLiteralguards. Defensive
refactor; no metric output changes. Fixes
#419. -
Completed the #419 Python lambda-alias normalization in the cognitive
metric: the threeimpl Cognitive for PythonCodelambda sites (the two
boolean-operator ancestor-scope walks and the lambda-nesting dispatch
arm) now recognize theLambda2(197) hidden alias, not justLambda
(196). Added a singlecognitive::python_is_lambdachokepoint reused by
those sites and byis_closure(mirroringpython_is_block), so the
closure and cognitive lambda detection can no longer desync. Defensive
refactor;Lambda2is 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
yieldsUnknownand the inner expressions are counted) is unified
behind aGetter::string_operand_typedefault plus aNode::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
monolithicdump_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 aTryExpressiondecision point), not
genuine branching — see
#401. -
tree-sitter-mozjsis regenerated against its declared
tree-sitter-javascript0.25.0base grammar (withtree-sitter
CLI0.26.9), and its floatingtree-sitter-cli^0.25.3
devDependency is pinned to0.26.9. Investigation for
#407
found the bundled mozjs parser was stale at JS0.23.1: the
0.23.1→0.25.0marker bump (#1207) shipped without the
matching regen, and #400 then pinned the grammar-marker-sync
baseline at0.25.0on the incorrect belief that the regen was a
no-op. The real0.25.0regen is not a no-op — it adds the
using/await usingexplicit-resource-management declaration
(using_declaration), so the generatedMozjsnode-kind enum in
language_mozjs.rsgainsUsingandUsingDeclarationvariants
(the pre-existingswitch_defaultnode is renumbered, not added).
The bump
is **metric-neutral for the