@nurix/etna 0.18.4 → 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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/hr/CLAUDE.md +7 -0
- package/data/harnesses/hr/harness.json +2 -2
- package/data/harnesses/hr/rules/code-eval-batcher.md +71 -0
- package/data/harnesses/hr/skills/evaluator/SKILL.md +208 -0
- package/data/harnesses/hr/skills/evaluator/criteria/code-quality.md +350 -0
- package/data/harnesses/hr/skills/evaluator/criteria/deployment-readiness.md +414 -0
- package/data/harnesses/hr/skills/evaluator/criteria/maintainability.md +445 -0
- package/data/harnesses/hr/skills/evaluator/criteria/mechanical-ambition.md +300 -0
- package/data/harnesses/hr/skills/evaluator/criteria/problem-substantiveness.md +88 -0
- package/data/harnesses/hr/skills/evaluator/criteria/testing.md +399 -0
- package/data/harnesses/hr/skills/evaluator/criteria/ux-mechanics.md +283 -0
- package/data/harnesses/hr/skills/evaluator/criteria/value-articulation.md +76 -0
- package/data/harnesses/hr/skills/evaluator/preflight.md +74 -0
- package/data/harnesses/hr/skills/evaluator/preflight.sh +100 -0
- package/data/harnesses/hr/skills/evaluator/validate_scorecard.py +98 -0
- package/package.json +1 -1
|
@@ -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.
|