@nurix/etna 0.18.3 → 0.18.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,445 @@
1
+ # Maintainability — Extraction Plan
2
+
3
+ - **Sub-criterion:** 1.4 (AI-only)
4
+ - **Weight:** 12.5 pts (4 Code AI × 12.5 = 50 pts half)
5
+ - **Contenders:** gitleaks + lizard (**per-language CCN**: Py 10 / JS-TS 10 / Java 12 / Go 15) + knip/deptry + **dep-vuln scan** (`pip-audit` / `npm audit --json` / `govulncheck` / Trivy) + LLM folder/README read (layered)
6
+ - **Source quote:** _"Folder structure and clarity that makes it easy for another engineer to pick up and extend."_
7
+
8
+ Bands are **absolute** — per-signal thresholds in the _Banding_ section. Gates this recipe owns: **`SECRETS`** (trigger: `gitleaks_findings ≥ 1`, caps `maintainability ≤ 2`) and **`VULN`** (fires when dep-vuln scan reports ≥ 1 CVE at CRITICAL OR ≥ 3 at HIGH across any dep, direct or transitive; caps `maintainability ≤ 2`). Dep-vuln tool selection by ecosystem: Python → `pip-audit`, JS/TS → `npm audit --json` (reads package-lock.json), Go → `govulncheck`, built container → `trivy image`. Skipped if no recognized package manifest. Data fields: `dep_vuln_critical` (int), `dep_vuln_high` (int), `dep_vuln_tool` (string), `dep_vuln_by_ecosystem` (map). Raw dumps: `raw/maintainability-dep-vuln-pip-audit.json` / `raw/maintainability-dep-vuln-npm-audit.json` / `raw/maintainability-dep-vuln-govulncheck.json` / `raw/maintainability-dep-vuln-trivy.json` (one file per tool that ran). lizard `--ccn` threshold uses a per-language lookup (see Signal 2 below).
9
+
10
+ ---
11
+
12
+ ## Extraction recipe
13
+
14
+ ### Signal 1 — Committed secrets (SECRETS gate trigger)
15
+
16
+ ```bash
17
+ # Scan full git history. Single Go binary, JSON out.
18
+ gitleaks detect --source <repo> --report-format json \
19
+ --report-path /tmp/gitleaks.json \
20
+ --no-banner --exit-code 0
21
+ ```
22
+
23
+ Parse:
24
+
25
+ ```bash
26
+ jq '[.[] | {rule: .RuleID, file: .File, commit: .Commit, date: .Date}]' /tmp/gitleaks.json
27
+ ```
28
+
29
+ Capture:
30
+
31
+ - `gitleaks_findings` (count)
32
+ - `gitleaks_by_rule` (map rule → count)
33
+ - `gitleaks_any_still_in_head` — bool; true if any finding's `File` is present in current HEAD (not just history)
34
+
35
+ ### Signal 2 — Polyglot complexity (lizard, per-language CCN)
36
+
37
+ ```bash
38
+ # 16+ langs. lizard auto-detects languages from file extensions — do NOT pass
39
+ # `--languages`; it was removed in lizard 1.21+ and the wrapper errors with
40
+ # `unrecognized arguments` (or, depending on the version, silently no-ops).
41
+ lizard -o /tmp/lizard.csv <repo> \
42
+ -x '*/node_modules/*' -x '*/.venv/*' -x '*/dist/*' -x '*/build/*'
43
+ ```
44
+
45
+ **Per-language CCN thresholds** — a function is "god" when its CCN exceeds the threshold for its language. Python/JS/TS are idiom-dense and deserve the tightest cap; Go's for/if-heavy control flow tolerates higher CCN before it becomes hard to read.
46
+
47
+ | Language (lizard CSV column 7 / filename extension) | God-function CCN threshold |
48
+ | --- | --- |
49
+ | Python (`.py`) | > 10 |
50
+ | JavaScript / TypeScript (`.js`, `.jsx`, `.ts`, `.tsx`) | > 10 |
51
+ | Java / Kotlin (`.java`, `.kt`) | > 12 |
52
+ | Go (`.go`) | > 15 |
53
+ | Everything else | > 15 (fallback) |
54
+
55
+ Parse using the per-language lookup. The lizard CSV has columns: `NLOC,CCN,token,param,length,location,file,name`. Key on file extension:
56
+
57
+ ```bash
58
+ # God-functions counted per-language threshold
59
+ awk -F',' '
60
+ NR == 1 { next }
61
+ {
62
+ file = $7
63
+ ccn = $2
64
+ # last path segment → extension
65
+ n = split(file, parts, ".")
66
+ ext = (n > 1) ? parts[n] : ""
67
+ thresh = 15 # default
68
+ if (ext == "py") thresh = 10
69
+ else if (ext == "js" || ext == "jsx" || \
70
+ ext == "ts" || ext == "tsx") thresh = 10
71
+ else if (ext == "java" || ext == "kt") thresh = 12
72
+ else if (ext == "go") thresh = 15
73
+ if (ccn + 0 > thresh) n_god++
74
+ n_total++
75
+ sum_ccn += ccn + 0
76
+ }
77
+ END {
78
+ printf "total=%d god=%d mean_ccn=%.2f\n", n_total+0, n_god+0, (n_total?sum_ccn/n_total:0)
79
+ }' /tmp/lizard.csv
80
+ ```
81
+
82
+ Capture:
83
+
84
+ - `lizard_total_functions`
85
+ - `lizard_god_functions` (CCN > per-language threshold)
86
+ - `lizard_god_by_language` — map `{py: N, js: N, ts: N, go: N, java: N, other: N}`
87
+ - `lizard_god_ratio` = `lizard_god_functions ÷ lizard_total_functions`
88
+ - `lizard_god_per_kloc`
89
+ - `lizard_mean_ccn`
90
+ - `lizard_thresholds_applied` — `{py: 10, js: 10, ts: 10, java: 12, go: 15, default: 15}` (constant, for audit)
91
+
92
+ ### Signal 3 — Dependency hygiene
93
+
94
+ Run per ecosystem present in the repo. Detect via package manifest:
95
+
96
+ ```bash
97
+ # JS/TS — install project deps BEFORE running knip. knip walks imports and
98
+ # needs the repo's own node_modules to resolve them; a bare `npx knip` on a
99
+ # freshly-cloned repo with no deps installed silently produces `_NOT_RUN` /
100
+ # empty output (observed when a sub-app reported `Cannot find module 'vite'`
101
+ # and knip never ran).
102
+ #
103
+ # This branch is MANDATORY when package.json exists. The deps-install step is
104
+ # not optional — do not skip it for time/budget reasons. If the install
105
+ # genuinely fails (registry down, lockfile broken), knip still runs and
106
+ # records the failure in /tmp/knip.err; the resulting `knip_NOT_RUN: true` in
107
+ # the scorecard MUST be accompanied by a `knip_reason` quoting the install-step
108
+ # error from /tmp/jsdeps.log. A reason of "deps-install step not executed this
109
+ # pass" is non-conformant.
110
+ if [ -f <repo>/package.json ]; then
111
+ cd <repo>
112
+ # Detect lock file → pick the matching package manager. If no lock file
113
+ # exists, assume pnpm (preflight installs pnpm/yarn/npm up front).
114
+ if [ -f pnpm-lock.yaml ]; then install_cmd="pnpm install --frozen-lockfile --prefer-offline"
115
+ elif [ -f yarn.lock ]; then install_cmd="yarn install --frozen-lockfile --prefer-offline"
116
+ elif [ -f package-lock.json ]; then install_cmd="npm ci --prefer-offline"
117
+ elif [ -f bun.lockb ]; then install_cmd="bun install --frozen-lockfile"
118
+ else install_cmd="pnpm install --prefer-offline" # no lock → assume pnpm per convention
119
+ fi
120
+ # 300s cap — slow registries shouldn't hang the run. Install errors are
121
+ # non-fatal here; knip still runs and records the failure in its stderr.
122
+ timeout 300 bash -c "$install_cmd" > /tmp/jsdeps.log 2>&1 || true
123
+ # knip proper — preserve stderr for audit (a NOT_RUN outcome needs the
124
+ # install log + knip stderr to explain itself).
125
+ npx --yes knip --reporter json > /tmp/knip.json 2> /tmp/knip.err || true
126
+ fi
127
+
128
+ # Python — detect src-layout / Poetry packages declarations so deptry doesn't
129
+ # false-positive on first-party imports. One observed project reported 292
130
+ # bogus DEP001s because deptry treated `app.*` imports (declared via Poetry's
131
+ # `packages = [{ include = "app", from = "src" }]`) as third-party.
132
+ #
133
+ # Strategy: pull every `tool.poetry.packages[].include` and every
134
+ # `tool.setuptools.packages.find` package name out of pyproject.toml, plus
135
+ # any top-level package directory under src/ (setuptools-src layout
136
+ # convention). Pass them as a comma-separated list to --known-first-party.
137
+ # Falls back gracefully (empty list → flag omitted) if no pyproject.toml or
138
+ # no parseable declarations.
139
+ if [ -f <repo>/pyproject.toml ] || [ -f <repo>/requirements.txt ]; then
140
+ cd <repo>
141
+ kfp="" # known-first-party packages, comma-separated
142
+ if [ -f pyproject.toml ]; then
143
+ # Poetry packages = [{ include = "X", from = "..." }]
144
+ kfp_poetry=$(python3 -c "
145
+ import sys, tomllib
146
+ try:
147
+ d = tomllib.loads(open('pyproject.toml').read())
148
+ pkgs = d.get('tool', {}).get('poetry', {}).get('packages', [])
149
+ print(','.join(p.get('include', '') for p in pkgs if p.get('include')))
150
+ except Exception: pass
151
+ " 2>/dev/null)
152
+ [ -n "$kfp_poetry" ] && kfp="$kfp_poetry"
153
+ fi
154
+ # Top-level dirs under src/ are first-party by setuptools-src convention
155
+ if [ -d src ]; then
156
+ kfp_src=$(find src -maxdepth 1 -mindepth 1 -type d -exec basename {} \; | tr '\n' ',' | sed 's/,$//')
157
+ [ -n "$kfp_src" ] && kfp="${kfp:+$kfp,}$kfp_src"
158
+ fi
159
+ if [ -n "$kfp" ]; then
160
+ deptry . --known-first-party "$kfp" -o /tmp/deptry.json 2>/dev/null
161
+ else
162
+ deptry . -o /tmp/deptry.json 2>/dev/null
163
+ fi
164
+ fi
165
+
166
+ # Go
167
+ if [ -f <repo>/go.mod ]; then
168
+ cd <repo> && go mod tidy -diff > /tmp/gomod-diff.txt 2>/dev/null
169
+ fi
170
+ ```
171
+
172
+ Parse (examples):
173
+
174
+ ```bash
175
+ # knip: unused deps + dead files
176
+ jq '{unused_deps: (.dependencies // []) | length,
177
+ unlisted_deps: (.unlisted // []) | length,
178
+ unused_files: (.files // []) | length,
179
+ unused_exports: (.exports // []) | length}' /tmp/knip.json
180
+
181
+ # deptry: DEP001-004 counts
182
+ jq 'group_by(.error.code) | map({code: .[0].error.code, count: length})' /tmp/deptry.json
183
+ ```
184
+
185
+ Capture:
186
+
187
+ - `knip_unused_deps`, `knip_unlisted_deps`, `knip_unused_files`, `knip_unused_exports`
188
+ - `deptry_findings_by_code` (map DEP001-004 → count)
189
+ - `gomod_tidy_diff_lines` (number of diff lines; 0 = clean)
190
+ - `dep_hygiene_overall_clean` (bool; true if all above are 0 / empty / no issues)
191
+
192
+ ### Signal 4 — File-size & tech-debt density
193
+
194
+ ```bash
195
+ # God-file detection — file LOC distribution
196
+ find <repo> -type f \
197
+ \( -name '*.py' -o -name '*.ts' -o -name '*.tsx' \
198
+ -o -name '*.js' -o -name '*.jsx' -o -name '*.go' \
199
+ -o -name '*.java' -o -name '*.kt' -o -name '*.rs' \) \
200
+ -not -path '*/node_modules/*' -not -path '*/.venv/*' \
201
+ -not -path '*/dist/*' -not -path '*/build/*' \
202
+ -exec wc -l {} + 2>/dev/null | sort -rn | head -20
203
+
204
+ # Tech-debt marker density
205
+ grep -rnE '\b(TODO|FIXME|HACK|XXX)\b' <repo> \
206
+ --include='*.py' --include='*.ts' --include='*.tsx' \
207
+ --include='*.js' --include='*.jsx' --include='*.go' \
208
+ --include='*.java' --include='*.kt' --include='*.rs' \
209
+ --exclude-dir=node_modules --exclude-dir=.venv \
210
+ --exclude-dir=dist --exclude-dir=build 2>/dev/null \
211
+ | wc -l
212
+ ```
213
+
214
+ Capture:
215
+
216
+ - `files_over_500_loc` (count — god-file candidates)
217
+ - `largest_file_loc`
218
+ - `tech_debt_markers_total` (TODO/FIXME/HACK/XXX)
219
+ - `tech_debt_per_kloc`
220
+
221
+ ### Signal 5 — Folder structure + README (LLM interpretive)
222
+
223
+ Assemble context for LLM:
224
+
225
+ 1. `tree -L 3 <repo>` (top 3 levels, no `node_modules`/`.venv`)
226
+ 2. `README.md` (full contents)
227
+ 3. `CONTRIBUTING.md` if present
228
+ 4. Three sampled source files (largest + 2 random across distinct dirs)
229
+
230
+ Score on:
231
+
232
+ - Is the top-level folder structure self-explanatory to a new engineer?
233
+ - Does README explain what the project is and how modules relate?
234
+ - Are module boundaries clear, or is everything in one soup?
235
+ - Are naming conventions consistent across files?
236
+ - Does the commit history (`git log --pretty='%s' | head -100`) show disciplined messages?
237
+
238
+ Capture:
239
+
240
+ - `llm_structure_score` (0–5)
241
+ - `llm_readme_score` (0–5)
242
+ - `llm_commit_quality_score` (0–5)
243
+ - `llm_pickup_score` (0–5) — the integrated "can another engineer pick this up?" judgment
244
+ - `llm_notes` (one sentence)
245
+
246
+ ### Signal 6 — Dependency vulnerabilities (VULN gate trigger)
247
+
248
+ Scan committed dependency manifests for known CVEs. One tool per ecosystem present. **Skipped when no recognized manifest is present** — emit `dep_vuln_NOT_RUN: true, reason: "no recognized manifest"` in that case.
249
+
250
+ ```bash
251
+ # Python — pip-audit against requirements.txt / pyproject.toml (reads both).
252
+ # 120s cap: pip-audit fetches advisory DB on first run; subsequent calls are fast.
253
+ if [ -f <repo>/requirements.txt ] || [ -f <repo>/pyproject.toml ]; then
254
+ cd <repo>
255
+ timeout 120 pip-audit --format json --progress-spinner off \
256
+ > /tmp/pipaudit.json 2> /tmp/pipaudit.err || true
257
+ fi
258
+
259
+ # JS/TS — npm audit --json reads package-lock.json. Requires a lockfile; yarn/pnpm
260
+ # lockfiles need their native audit (`yarn npm audit --json`, `pnpm audit --json`).
261
+ # All three output the same vulnerability envelope.
262
+ if [ -f <repo>/package-lock.json ]; then
263
+ cd <repo>
264
+ timeout 120 npm audit --json > /tmp/npmaudit.json 2>/dev/null || true
265
+ elif [ -f <repo>/pnpm-lock.yaml ]; then
266
+ cd <repo>
267
+ timeout 120 pnpm audit --json > /tmp/npmaudit.json 2>/dev/null || true
268
+ elif [ -f <repo>/yarn.lock ]; then
269
+ cd <repo>
270
+ timeout 120 yarn npm audit --json > /tmp/npmaudit.json 2>/dev/null || true
271
+ fi
272
+
273
+ # Go — govulncheck walks the module graph. Skipped when go is not on PATH
274
+ # (preflight.sh documents this; sub-agent emits _NOT_RUN when appropriate).
275
+ if [ -f <repo>/go.mod ] && command -v govulncheck >/dev/null 2>&1; then
276
+ cd <repo>
277
+ timeout 180 govulncheck -json ./... > /tmp/govulncheck.json 2>/dev/null || true
278
+ fi
279
+
280
+ # Container — Trivy scans a built image OR the filesystem directly. FS scan is
281
+ # cheaper (no docker build) and covers the same OS+lang package surface.
282
+ timeout 180 trivy fs --format json --scanners vuln --quiet \
283
+ <repo> > /tmp/trivy.json 2>/dev/null || true
284
+ ```
285
+
286
+ Parse per tool — fields normalize to `{critical, high, medium, low}` counts:
287
+
288
+ ```bash
289
+ # pip-audit → count by aliased severity (pip-audit uses OSV severity strings)
290
+ jq '[.dependencies[]?.vulns[]? | .aliases[]?] as $_
291
+ | [.dependencies[]?.vulns[]?]
292
+ | group_by(.severity // "UNKNOWN")
293
+ | map({sev: .[0].severity, count: length})' /tmp/pipaudit.json
294
+
295
+ # npm audit → advisory severity bucket
296
+ jq '.metadata.vulnerabilities // {}' /tmp/npmaudit.json
297
+
298
+ # govulncheck → CVSS severity from OSV DB; count only reachable findings
299
+ jq '[.osv?, .finding?] | map(select(.)) | length' /tmp/govulncheck.json
300
+
301
+ # trivy → JSON envelope, Severity field on each Vulnerability
302
+ jq '[.Results[]?.Vulnerabilities[]? | .Severity]
303
+ | group_by(.) | map({sev: .[0], count: length})' /tmp/trivy.json
304
+ ```
305
+
306
+ Capture (aggregated across every tool that ran):
307
+
308
+ - `dep_vuln_critical` (int, sum across ecosystems) — `CRITICAL` severity
309
+ - `dep_vuln_high` (int) — `HIGH` severity
310
+ - `dep_vuln_medium` (int)
311
+ - `dep_vuln_low` (int)
312
+ - `dep_vuln_tool` — comma-joined list of tools that ran on this repo (e.g. `"pip-audit,trivy"`)
313
+ - `dep_vuln_by_ecosystem` — map `{python: {critical, high, ...}, npm: {...}, go: {...}, container: {...}}`
314
+ - `dep_vuln_NOT_RUN` — bool; true when no recognized manifest was present
315
+ - `dep_vuln_reason` — string; populated when `_NOT_RUN` or when a tool's stderr pointed at a genuine failure (lockfile corrupt, registry down)
316
+
317
+ **VULN gate trigger:** `dep_vuln_critical ≥ 1` OR `dep_vuln_high ≥ 3`. Caps `maintainability ≤ 2`. Skipped (gate does not fire) when `dep_vuln_NOT_RUN == true` — no manifest, no signal, no gate.
318
+
319
+ ---
320
+
321
+ ## Raw dumps (flat — files under `raw/` named `maintainability-*`)
322
+
323
+ Each signal writes its raw output to the flat `raw/` directory at the workspace root. Absent files = not extracted / not applicable. Aggregates go to `scorecard.yaml` under `data.`.
324
+
325
+ | File | Source signal | Shape / note |
326
+ | --- | --- | --- |
327
+ | `raw/maintainability-gitleaks.json` | Signal 1 | Raw `gitleaks detect --report-format json` output |
328
+ | `raw/maintainability-lizard.csv` | Signal 2 | Per-function: `{name, file, ccn, nloc, params}` for every function in scope. Shared with 1.1. |
329
+ | `raw/maintainability-knip.json` | Signal 3 | Raw `npx knip --reporter json` (absent when no `package.json`) |
330
+ | `raw/maintainability-deptry.json` | Signal 3 | Raw `deptry` output (absent when no Python manifest) |
331
+ | `raw/maintainability-gomod-diff.txt` | Signal 3 | `go mod tidy -diff` output (absent when no `go.mod`; empty content = clean) |
332
+ | `raw/maintainability-file-sizes.txt` | Signal 4 | `wc -l` for source files, sorted descending. Shared with 1.1. |
333
+ | `raw/maintainability-tech-debt.txt` | Signal 4 | `grep -rnE 'TODO\|FIXME\|HACK\|XXX'` output with `file:line` (absent when count = 0) |
334
+ | `raw/maintainability-llm-read.md` | Signal 5 | LLM's folder-tree + README + commit-quality read: `{structure_score, readme_score, commit_quality_score, pickup_score, one_sentence}` + short rationale |
335
+ | `raw/maintainability-dep-vuln-pip-audit.json` | Signal 6 | Raw `pip-audit --format json` output (absent when no Python manifest) |
336
+ | `raw/maintainability-dep-vuln-npm-audit.json` | Signal 6 | Raw `npm/pnpm/yarn audit --json` output (absent when no JS lockfile) |
337
+ | `raw/maintainability-dep-vuln-govulncheck.json` | Signal 6 | Raw `govulncheck -json ./...` output (absent when no `go.mod` or `go` not on PATH) |
338
+ | `raw/maintainability-dep-vuln-trivy.json` | Signal 6 | Raw `trivy fs --format json` output (always attempted — covers OS + lang packages) |
339
+
340
+ ---
341
+
342
+ ## Aggregation → 0–5 band
343
+
344
+ Scorer integrates the 5 signals. Rough directionality:
345
+
346
+ | Signal | Pulls score UP | Pulls score DOWN |
347
+ | --- | --- | --- |
348
+ | `gitleaks_findings` | 0 | ≥ 1 triggers `SECRETS` gate |
349
+ | `lizard_god_per_kloc` | < 2 | > 10 |
350
+ | `lizard_god_ratio` | < 0.05 | > 0.20 |
351
+ | `dep_hygiene_overall_clean` | true | false with many findings |
352
+ | `dep_vuln_critical` + `dep_vuln_high` | 0 critical, 0 high | ≥ 1 critical OR ≥ 3 high triggers `VULN` gate |
353
+ | `tech_debt_per_kloc` | < 1 | > 5 |
354
+ | `files_over_500_loc` | 0–1 | > 3 |
355
+ | `llm_pickup_score` | 4–5 | 0–2 |
356
+
357
+ **Absolute bands (per-signal thresholds):**
358
+
359
+ - **5** — `gitleaks_findings == 0`; `dep_vuln_critical == 0` AND `dep_vuln_high == 0`; lizard god-ratio < 0.05; dep hygiene clean; tech-debt < 1/kLoC; clear `src/` layout; LLM pickup ≥ 4
360
+ - **4** — Strong but one weaker signal (e.g. 1 high-severity CVE, or god-ratio 0.05–0.10, or tech-debt 1–3/kLoC)
361
+ - **3** — Reasonable structure; some dep drift or moderate god-functions (ratio 0.10–0.15); tech-debt 3–5/kLoC; README workable
362
+ - **2** — Flat dump with god-files; god-ratio > 0.15; messy deps; README absent or stale; **`SECRETS` or `VULN` gate fires here (cap at 2)**
363
+ - **1** — Actively hostile to reading: no README, files > 1000 LOC common, tech-debt > 10/kLoC
364
+ - **0** — Reserved for extreme cases (would be rare)
365
+
366
+ ---
367
+
368
+ ## Gates
369
+
370
+ | Gate | Trigger | Cap |
371
+ | --- | --- | --- |
372
+ | `SECRETS` | `gitleaks_findings ≥ 1` (any secret in git history — even if rotated) | `maintainability ≤ 2` |
373
+ | `VULN` | `dep_vuln_critical ≥ 1` OR `dep_vuln_high ≥ 3` across any ecosystem tool that ran (`pip-audit` / `npm audit` / `govulncheck` / `trivy fs`). Skipped when `dep_vuln_NOT_RUN == true` (no recognized manifest). | `maintainability ≤ 2` |
374
+
375
+ The `SECRETS` gate fires even if the secret has been rotated out of HEAD — leaked-and-revoked still blocks "pick up and extend" because the leak is permanent in git history. `gitleaks_any_still_in_head` lets the scorer comment on severity but doesn't change the gate.
376
+
377
+ The `VULN` gate fires on committed dependency manifests only; it does not attempt to resolve what version is actually running in production (runtime verification is out of scope — a separate maintainer follow-up). A repo that pins vulnerable versions but ships a build that avoids the vulnerable code path still takes the gate — the signal is "ships committed dependency hygiene", not "is exploitable at runtime".
378
+
379
+ ---
380
+
381
+ ## Fallback — signals missing
382
+
383
+ - Repo has only Go / Rust (no JS/Python) → drop knip/deptry; rely on `go mod tidy -diff` or `cargo tree` + other signals.
384
+ - lizard doesn't support a language in the repo → drop Signal 2 for that language; rely on file-size distribution + LLM.
385
+ - Zero-commit or zero-LoC repo → score 0, flag `NO_SIGNAL`, skip.
386
+ - README missing → LLM Signal 5 degrades; use `llm_confidence: low`.
387
+
388
+ Annotate evidence with `"Signals used: [...]"`.
389
+
390
+ ---
391
+
392
+ ## Output (what the scorer emits)
393
+
394
+ ```yaml
395
+ sub_criterion: maintainability
396
+ score: 3
397
+ evidence: "Clean src/ layout; god-per-kLoC 1.8; knip clean; tech-debt per kLoC 3.2; no secrets in history; README explains modules but not setup. LLM pickup: 3 — middleware layer is a god-file (src/app.ts = 680 LOC)."
398
+ data:
399
+ gitleaks_findings: 0
400
+ gitleaks_by_rule: {}
401
+ gitleaks_any_still_in_head: false
402
+ lizard_total_functions: 114
403
+ lizard_god_functions: 8
404
+ lizard_god_ratio: 0.07
405
+ lizard_god_per_kloc: 1.8
406
+ lizard_mean_ccn: 4.2
407
+ knip_unused_deps: 0
408
+ knip_unlisted_deps: 0
409
+ knip_unused_files: 0
410
+ knip_unused_exports: 2
411
+ deptry_findings_by_code: {}
412
+ gomod_tidy_diff_lines: 0
413
+ dep_hygiene_overall_clean: true
414
+ files_over_500_loc: 1
415
+ largest_file_loc: 680
416
+ tech_debt_markers_total: 14
417
+ tech_debt_per_kloc: 3.2
418
+ llm_structure_score: 3
419
+ llm_readme_score: 3
420
+ llm_commit_quality_score: 4
421
+ llm_pickup_score: 3
422
+ dep_vuln_critical: 0
423
+ dep_vuln_high: 1
424
+ dep_vuln_medium: 4
425
+ dep_vuln_low: 12
426
+ dep_vuln_tool: "pip-audit,trivy"
427
+ dep_vuln_by_ecosystem:
428
+ python: { critical: 0, high: 1, medium: 2, low: 5 }
429
+ container: { critical: 0, high: 0, medium: 2, low: 7 }
430
+ dep_vuln_NOT_RUN: false
431
+ signals_used: [gitleaks, lizard, knip, filesystem, llm, pip-audit, trivy]
432
+ gates_triggered: []
433
+ ```
434
+
435
+ ---
436
+
437
+ ## Caveats
438
+
439
+ - **lizard CCN thresholds are language-idiomatic.** Python's 10 ≠ Kotlin's 12 ≠ Go's 15. The per-language lookup in Signal 2 sets the threshold per file extension; absolute and fixed.
440
+ - **knip / deptry only cover their ecosystems.** Mixed repos run both and aggregate; there's no polyglot dep-hygiene tool.
441
+ - **LLM "pickup" judgment is brittle at sample size = 3 sampled files.** Evidence should name the files sampled.
442
+ - **`SECRETS` gate is conservative.** Fires even on rotated secrets — "pick up and extend" assumes trust in git history, and rotation doesn't erase the leak. Scorer should mention severity (`still_in_head` vs. history-only) in evidence.
443
+ - **`VULN` gate is committed-manifest only.** Does not attempt runtime resolution or version-pinning analysis. A repo that pins a vulnerable version but ships a build that avoids the vulnerable code path still takes the gate — by design, it's a cleanliness signal, not an exploitability signal.
444
+ - **`CONTRIBUTING.md` / ADR / RFC presence** is a weak signal — absence is common even in healthy repos. Don't penalize for missing; reward when present.
445
+ - **Tech-debt markers** may be legitimate scoping comments (`TODO: address in v2`) or abandoned work. LLM adjudicates when count is suspiciously high.