Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,33 @@ across the whole thing, honestly*.

### Added

- **Generated-marker exclusion (opt-in)** (#114, plan 0002 Phase 2): with
`CHECKUP_EXCLUDE_GENERATED=1`, files whose head carries a **banner-shaped**
- **The honest banner — first-party coverage, made visible** (#117, plan 0002
Phase 3): `detection.json` now reports first-party coverage **by category**
(`coverage.tracked` / `firstParty` / `pctExcluded`, and `excluded.{generated,
authorDeclared,convention}`), surfaced on the console and the report headline, so
a shrunk scan can never read as "nothing here" — **"% excluded" is itself a
signal**. A **single-directory concentration** caveat catches the *markerless*
flat-vendored case no marker announces (the dotCMS class): a directory dominated
by a language **foreign** to the repo's primary is flagged as vendored-looking —
language-aware, so the primary source dir is never mistaken for vendored — and the
banner prints the exact one-line fix (`CHECKUP_EXCLUDE='<dir>/*'`). Threshold via
`CHECKUP_CONCENTRATION_PCT` (default 25); advisory only, never auto-excluded.
`detection.json` schema → **1.5**.
- **Generated-marker exclusion is now default-on** (#117): the banner-shaped marker
exclusion (#114, below) ships **on by default** — announced by a loud banner
naming the count plus the enumerated `raw/…​.generated` list — with the kill-switch
`CHECKUP_EXCLUDE_GENERATED=0` to restore the whole tree. The shrink is never silent.
- **Generated-marker exclusion** (#114, plan 0002 Phase 2): files whose head
carries a **banner-shaped**
generated marker (Go `// Code generated … DO NOT EDIT.`, a comment-leader
`@generated`, or C# `<auto-generated>`) are dropped from **both** the lizard
inventory and the scc keep-set — the lever that stops a codegen-heavy repo from
drowning the measurement (corvus: 39,208 → 3,569 source files) and pre-empts the
#105 lizard blow-ups. Markers are case-sensitive and anchored (not the loose
substrings a draft used), so first-party code that merely *quotes* a marker — or
owns a bare `DO NOT EDIT` (IaC, hand-written migrations) — is kept. Every drop is
**enumerated** to `raw/…​.generated` (loud, not a silent count). Default off until
field-tested; surfacing "% generated" as a headline signal is Phase 3.
**enumerated** to `raw/…​.generated` (loud, not a silent count). Default-on since
Phase 3 (#117).
- **First-party source — the scc-based engines honour the inventory** (#109,
plan 0002 Phase 1; closes #18): `codebase-stats`, stack-identity, the scc
complexity arm and tech-viability now filter + re-aggregate a single
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ finding shape and the rest of the substrate carries it through unchanged.
| `CHECKUP_MODE` | closing verdict (`checkup.sh` + renderer) | `tailored` | `tailored` (a repo you own & tune): verdict framed for your own codebase ("where to focus next"); a low score exits non-zero as a quality signal you may act on — not a deploy gate. `audit` (a repo you don't own / due diligence): informational only, framed as "where to invest", **always exits 0**. checkup never gates ([ADR-0009](docs/decisions/0009-deterministic-health-localiser.md)). |
| `CHECKUP_SRC_ROOTS` | complexity + git-axis sections | whole tree (VCS-tracked source) | NARROWS the complexity + git-forensics scan to specific space-separated roots (e.g. `app cmd`). By default checkup assesses **all** VCS-tracked source ([honest coverage](docs/architecture.md)); set this only to focus the scan or speed up a very large monorepo. |
| `CHECKUP_FORENSIC_SINCE` | git-axis sections | `6.months.ago` | `git log --since` window for hotspots / change-coupling / bug-fix-density. Widen (e.g. `2.years.ago`) for repos with sparse recent history; an empty window degrades to `skip`, never a false `pass`. |
| `CHECKUP_EXCLUDE` | lizard complexity + duplication scans | unset | Extra space-separated fnmatch globs excluded from the lizard scans, on top of the built-in generated/vendored defaults (node_modules, migrations, snapshots, `*.min.*`, …). |
| `CHECKUP_EXCLUDE` | source inventory (all scanners + scc/identity/stats) | unset | Extra space-separated fnmatch globs excluded from the whole inventory — complexity, duplication **and** the scc-based stats/identity (#109) — on top of the built-in generated/vendored defaults (node_modules, migrations, snapshots, `*.min.*`, …). Also settable as a top-level `exclude:` list in `.checkup.yml`. |
| `CHECKUP_EXCLUDE_GENERATED` | source inventory (generated-marker pass) | on (set `0` to disable) | Drop files carrying a banner-shaped generated marker (Go `// Code generated … DO NOT EDIT.`, comment-leader `@generated`, C# `<auto-generated>`). **Default-on** since v0.2.0; the dropped set is enumerated to `raw/…generated` and announced by a loud banner. `CHECKUP_EXCLUDE_GENERATED=0` keeps the whole tree. |
| `CHECKUP_CONCENTRATION_PCT` | coverage banner (single-dir concentration) | `25` | Threshold (% of all first-party code) at which one directory is flagged as a possible vendored tree — the banner names the directory and prints the exact `CHECKUP_EXCLUDE` snippet. Advisory only (never auto-excluded). |
| `CHECKUP_SHELL_DIRS` | `shellcheck` section | `scripts .husky .githooks .claude/hooks` | Space-separated dirs to search for shell scripts. Missing dirs are skipped silently. |
| `HADOLINT_DOCKERFILE` | `hadolint` section | auto-detect `Dockerfile*` at root | Override the Dockerfile filename when it is named non-conventionally. |
| `MUTATION_TEST` | `mutation` section | unset (skipped) | Set to `1` to enable Stryker; opt-in because mutation testing is slow (~2 min). |
Expand Down
2 changes: 2 additions & 0 deletions bin/checkup-report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ if [ -f "$OUT_DIR/detection.json" ]; then
COVERAGE_TXT=$(jq -r '
.coverage // empty
| "**Coverage:** \(.assessedFiles) source files assessed · scope: \(.scope) · excludes via \(.exclusionSource)"
+ (if (.excluded.total // 0) > 0 then " · \(.firstParty)/\(.tracked) first-party (\(.pctExcluded)% excluded: \(.excluded.generated) generated, \(.excluded.authorDeclared) author-declared, \(.excluded.convention) convention)" else "" end)
+ (if .concentration then " · ⚠️ \(.concentration.dir)/ is \(.concentration.pct)% of code (\(.concentration.files) files) \(.concentration.lang) tree vs \(.concentration.repoLang) codebase — exclude if vendored: CHECKUP_EXCLUDE=\u0027\(.concentration.dir)/*\u0027" else "" end)
+ (if .narrowed then " · ⚠️ scope NARROWED by CHECKUP_SRC_ROOTS" else "" end)
+ (if (.unmeasured // []) | length > 0 then " · ⚠️ not measured: " + ((.unmeasured) | join("; ")) else "" end)
' "$OUT_DIR/detection.json" 2>/dev/null)
Expand Down
50 changes: 48 additions & 2 deletions bin/checkup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,29 @@ COVERAGE_NARROWED=false; [ -n "${CHECKUP_SRC_ROOTS:-}" ] && COVERAGE_NARROWED=tr
# can't run, #79) — surfaced so the gap is loud, never a silent false-pass.
COVERAGE_UNMEASURED=$(printf '%s\n' "${CPLX_UNMEASURED[@]}" | jq -R . | jq -s 'map(select(length>0))')

# Coverage-by-category (#117 Phase 3): attribute every excluded file to a source
# (convention / author-declared / generated markers) over the ALL-extension keep-set
# — the same denominator scc/identity use — and surface "% excluded" as a first-class
# signal. Built by build_scc_keepset; absent on a non-VCS / no-scc target → 0s.
COVERAGE_TRACKED=${SCC_CANDIDATE_COUNT:-0}
COVERAGE_FIRSTPARTY=${SCC_KEEP_COUNT:-0}
COVERAGE_EXC_GENERATED=${GENERATED_EXCLUDED_COUNT:-0}
COVERAGE_EXC_AUTHOR=${GITATTR_EXCLUDED_COUNT:-0}
COVERAGE_EXC_CONVENTION=${CONVENTION_EXCLUDED_COUNT:-0}
COVERAGE_EXC_TOTAL=$(( COVERAGE_EXC_GENERATED + COVERAGE_EXC_AUTHOR + COVERAGE_EXC_CONVENTION ))
COVERAGE_PCT_EXCLUDED=0
[ "$COVERAGE_TRACKED" -gt 0 ] && COVERAGE_PCT_EXCLUDED=$(( COVERAGE_EXC_TOTAL * 100 / COVERAGE_TRACKED ))

# Single-directory concentration (#117 Phase 3, plan §6.5): the markerless flat-
# vendored detector. Re-uses the one scc --by-file walk (ensured during detection,
# above) filtered to the keep-set; null when scc is absent or no dir dominates.
COVERAGE_CONCENTRATION=null
if [ "${SCC_BYFILE_OK:-false}" = true ]; then
COVERAGE_CONCENTRATION=$(scc_concentration "$SCC_KEEP_JSON" "${CHECKUP_CONCENTRATION_PCT:-25}" < "$SCC_BYFILE" 2>/dev/null || echo null)
[ -z "$COVERAGE_CONCENTRATION" ] && COVERAGE_CONCENTRATION=null
echo "$COVERAGE_CONCENTRATION" > "$RAW_DIR/concentration.json"
fi

# Topology (#78): the scan root is a hypothesis. Tell a single package from a
# declared workspace (healthy) from an UNDECLARED fan-out (a thin orchestrator
# root over real packages one level down — a mild structural smell, and the case
Expand Down Expand Up @@ -501,17 +524,24 @@ jq -n \
--arg scope "${SOURCE_SCOPE:-unknown}" --arg excl "$COVERAGE_EXCL" \
--argjson byArea "${COVERAGE_BY_AREA:-{\}}" --argjson narrowed "$COVERAGE_NARROWED" \
--argjson unmeasured "${COVERAGE_UNMEASURED:-[]}" \
--argjson tracked "$COVERAGE_TRACKED" --argjson firstParty "$COVERAGE_FIRSTPARTY" \
--argjson excGen "$COVERAGE_EXC_GENERATED" --argjson excAuthor "$COVERAGE_EXC_AUTHOR" \
--argjson excConv "$COVERAGE_EXC_CONVENTION" --argjson excTotal "$COVERAGE_EXC_TOTAL" \
--argjson pctExcluded "$COVERAGE_PCT_EXCLUDED" --argjson concentration "$COVERAGE_CONCENTRATION" \
--arg toposhape "$TOPO_SHAPE" --argjson topowstool "$TOPO_WSTOOL_JSON" \
--arg topolock "$TOPO_ROOT_LOCK" --arg toporeal "$TOPO_ROOT_REAL" \
--argjson toporoots "$TOPO_ROOTS_JSON" --argjson topochildren "$TOPO_CHILD_COUNT" \
--argjson topocapped "$TOPO_CAPPED" '
{schemaVersion:"1.4",
{schemaVersion:"1.5",
primary: (if $primary=="" then null else $primary end),
primaryConfidence: $conf,
sccBreakdownAvailable: ($sccok=="true"),
stacks: $stacks, manifests: $manifests,
engines: {complexity:{engine:$ec, reason:$cr, slices:$slices}, duplication:{engine:$ed, reason:$dr}},
coverage: {assessedFiles:$assessed, scope:$scope, exclusionSource:$excl, narrowed:$narrowed, byArea:$byArea, unmeasured:$unmeasured},
coverage: {assessedFiles:$assessed, scope:$scope, exclusionSource:$excl, narrowed:$narrowed, byArea:$byArea, unmeasured:$unmeasured,
tracked:$tracked, firstParty:$firstParty, pctExcluded:$pctExcluded,
excluded:{generated:$excGen, authorDeclared:$excAuthor, convention:$excConv, total:$excTotal},
concentration:$concentration},
topology: {shape:$toposhape, workspaceTool:$topowstool, rootHasLockfile:($topolock=="true"), rootHasRealScripts:($toporeal=="true"), assessmentRoots:$toporoots, childCount:$topochildren, capped:$topocapped},
overridden: ($overridden=="true")}' > "$OUT_DIR/detection.json"

Expand All @@ -526,6 +556,22 @@ echo -e " Cross-stack checks always run (secrets, SAST, forensics, stats, docs
COVERAGE_NOTE=" 📐 Coverage: ${SOURCE_FILE_COUNT:-0} source files assessed (scope: ${SOURCE_SCOPE:-unknown}, excludes via ${COVERAGE_EXCL})"
[ "$COVERAGE_NARROWED" = true ] && COVERAGE_NOTE="$COVERAGE_NOTE — NARROWED by CHECKUP_SRC_ROOTS"
echo -e "$COVERAGE_NOTE"
# First-party split (#117 Phase 3): name what was excluded, by category, so the
# shrink is never silent. "% excluded" is itself a signal (a codegen-heavy repo).
if [ "$COVERAGE_EXC_TOTAL" -gt 0 ]; then
echo -e " 🧮 First-party: ${COVERAGE_FIRSTPARTY}/${COVERAGE_TRACKED} files (${COVERAGE_PCT_EXCLUDED}% excluded — ${COVERAGE_EXC_GENERATED} generated, ${COVERAGE_EXC_AUTHOR} author-declared, ${COVERAGE_EXC_CONVENTION} convention)"
fi
# Loud generated banner (#117 §10): default-on exclusion must announce itself + the
# enumerated list + the kill-switch, so a reader never mistakes the shrink for the truth.
if [ "${COVERAGE_EXC_GENERATED:-0}" -gt 0 ]; then
echo -e " ${YELLOW}🤖 Excluded ${COVERAGE_EXC_GENERATED} generated file(s)${NC} (banner-shaped markers) — enumerated in raw/scc-keep.lst.generated · keep them with CHECKUP_EXCLUDE_GENERATED=0"
fi
# Single-directory concentration caveat (#117 §6.5): a dir dominated by a language
# foreign to the repo's primary — looks vendored. Name it + the one-line fix.
if [ "$COVERAGE_CONCENTRATION" != "null" ]; then
read -r CONC_DIR CONC_PCT CONC_FILES CONC_LANG CONC_REPOLANG < <(echo "$COVERAGE_CONCENTRATION" | jq -r '"\(.dir) \(.pct) \(.files) \(.lang) \(.repoLang)"')
echo -e " ${YELLOW}📦 Concentration:${NC} ${CONC_DIR}/ is ${CONC_PCT}% of all code (${CONC_FILES} files, mostly ${CONC_LANG}; the codebase is mostly ${CONC_REPOLANG}) — looks vendored. To exclude: CHECKUP_EXCLUDE='${CONC_DIR}/*' (or add to .checkup.yml exclude:)"
fi
[ "${#CPLX_UNMEASURED[@]}" -gt 0 ] && echo -e " ${YELLOW}⚠️ Not measured:${NC} ${CPLX_UNMEASURED[*]}"
case "$TOPO_SHAPE" in
undeclared-fan-out) echo -e " ${YELLOW}🧩 Topology:${NC} undeclared fan-out — ${TOPO_CHILD_COUNT} sub-package(s) below an orchestrator root (${TOPO_ASSESSMENT_ROOTS[*]}); the root scan can't see them" ;;
Expand Down
15 changes: 13 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The plan is printed for a human and persisted to `detection.json` (in `OUT_DIR`,

```jsonc
{
"schemaVersion": "1.4",
"schemaVersion": "1.5",
"primary": "node", // largest stack, or null when ambiguous
"primaryConfidence": "high", // high (manifest + dominant) | medium | low
"sccBreakdownAvailable": true, // false → degraded to manifest/presence signal
Expand All @@ -152,10 +152,21 @@ The plan is printed for a human and persisted to `detection.json` (in `OUT_DIR`,
// dir; narrowed = true when CHECKUP_SRC_ROOTS restricted the scope; unmeasured
// names what a routed engine could NOT cover (e.g. JS/TS complexity when no
// resolvable ESLint config — #79), so the gap is explicit, not a false pass.
// tracked/firstParty/excluded/pctExcluded (#117, schema 1.5) attribute every
// excluded file to a category (generated markers | author-declared .gitattributes
// | convention globs) over the ALL-extension keep-set scc/identity use — "%
// excluded" is itself a signal. concentration (plan §6.5) names a single
// directory that dominates the first-party code (a possible markerless vendored
// tree) + the one-line fix; language-aware (a dir dominated by a language foreign
// to the repo's primary), so the source dir itself is never mistaken for vendored;
// null when scc is absent or no foreign dir dominates.
"coverage": {
"assessedFiles": 412, "scope": "git", "exclusionSource": ".gitignore",
"narrowed": false, "byArea": { "src": 280, "server": 110, "scripts": 22 },
"unmeasured": [] // e.g. ["JS/TS complexity (no resolvable root ESLint config)"]
"unmeasured": [], // e.g. ["JS/TS complexity (no resolvable root ESLint config)"]
"tracked": 4820, "firstParty": 470, "pctExcluded": 90,
"excluded": { "generated": 4100, "authorDeclared": 180, "convention": 70, "total": 4350 },
"concentration": null // or { "dir": "webapp/html/js", "code", "files", "totalCode", "pct": 25, "lang": "JavaScript", "repoLang": "Java" }
},
// Package topology (#78): the scan root is a hypothesis. shape distinguishes a
// single package from a declared workspace (healthy) from an UNDECLARED fan-out
Expand Down
65 changes: 65 additions & 0 deletions lib/scc-concentration.jq
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# scc-concentration.jq — single-directory concentration caveat (plan 0002 §6.5,
# #117 Phase 3). The detector for MARKERLESS flat-vendored code that no generated
# marker announces and no convention dir catches (the dotCMS class: a committed
# JS library tree sitting in the first-party keep-set, skewing identity). Too fuzzy
# to auto-exclude — surfaced as a BANNER that names the directory and prints the
# one-line fix, never a silent drop.
#
# LANGUAGE-AWARE, not raw share: the spike (§9) showed the tell is a dir dominated
# by a language FOREIGN to the repo's primary (dotCMS: webapp/html/js is JavaScript
# while the codebase is Java). Pure share alone flags the primary SOURCE directory
# on any normal repo (it is the largest dir) — a false positive that would fire
# everywhere. So a directory is a candidate only when its dominant language differs
# from the repo's dominant language: that is what "vendored-looking" means here.
#
# Input: scc --by-file --format json (array of language objects, each .Files[]).
# Args: --slurpfile keep <keepfile> (JSON array of TARGET-relative keep paths,
# ALL extensions — same set as scc-aggregate)
# --argjson pct <int> (threshold percent of total code, e.g. 25)
# Output: the most-SPECIFIC (depth-maximal) directory whose share of the kept code
# is ≥ pct AND whose dominant language ≠ the repo's, as
# { dir, code, files, totalCode, pct, lang, repoLang }, or null.
#
# Depth-maximal, not share-maximal: a vendored tree's own subdirs each fall below
# the threshold while the tree's own root is the deepest prefix still above it — so
# "deepest ≥ threshold" names the vendored ROOT (e.g. .../webapp/html/js), the most
# precise exclude path, rather than a shallow ancestor that also sweeps in siblings.
# Deterministic (#96): sort by [depth, share, dir], take the last.

( ($keep[0] // []) | map({ key: sub("^\\./"; ""), value: true }) | from_entries ) as $k
| [ .[].Files[]?
| { loc: (.Location | sub("^\\./"; "")), code: (.Code // 0), lang: (.Language // "") }
| select( $k[.loc] // false ) ] as $files
| ( $files | map(.code) | add // 0 ) as $total
| if $total <= 0 then null
else
# The repo's dominant language (by kept code) — the baseline a candidate dir
# must differ from to read as "vendored-looking".
( $files | group_by(.lang)
| map({ lang: .[0].lang, code: (map(.code) | add) })
| sort_by([ -.code, .lang ]) | .[0].lang ) as $repoLang
| ( $files
| map( . as $f
| ($f.loc | split("/")) as $parts
| ($parts[0:-1]) as $dirs # drop the filename
| [ range(1; ($dirs|length)+1) as $n
| { dir: ($dirs[0:$n] | join("/")), code: $f.code, lang: $f.lang } ] )
| add // []
| group_by(.dir)
| map( { dir: .[0].dir,
code: (map(.code) | add),
files: length,
share: ((map(.code) | add) / $total),
# this dir's own dominant language
lang: ( group_by(.lang)
| map({ lang: .[0].lang, code: (map(.code) | add) })
| sort_by([ -.code, .lang ]) | .[0].lang ) } )
| map( select( .share >= ($pct / 100) and .files >= 2 and .lang != $repoLang ) )
| sort_by( [ (.dir | split("/") | length), .share, .dir ] )
| last ) as $top
| if $top == null then null
else { dir: $top.dir, code: $top.code, files: $top.files,
totalCode: $total, pct: (($top.share * 100) | floor),
lang: $top.lang, repoLang: $repoLang }
end
end
Loading