@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.
@@ -0,0 +1,283 @@
1
+ # UX Mechanics — Extraction Plan
2
+
3
+ - **Sub-criterion:** 2.2 (Product Thinking)
4
+ - **Weight:** 12.5 pts
5
+ - **Contenders:** UX heuristic grep bundle (web) + clig.dev greps (CLI) + **enterprise API quality signals** (API-only path) + LLM read (layered, path-dependent)
6
+ - **Source quote:** _"Clear user flows, thoughtful UX (even with minimal UI). Usability, not just features."_
7
+
8
+ This recipe scores usability and user-flow quality across three surfaces (web UI / CLI / API-only). Bands are **absolute**. The **API-only path is not capped** — enterprise backends can score up to 5. API-only signal bundle: (a) OpenAPI/Swagger spec presence + completeness (all routes documented, examples present, error shapes declared), (b) route naming consistency (kebab / snake / camel singular-vs-plural coherence), (c) response envelope + error-handling consistency (uniform `{data, error}` or RFC 7807), (d) versioning strategy (`/v1/` prefix, `Accept-Version` header, or clearly no-version-needed), (e) idempotency on mutations (Idempotency-Key header support on POST/PUT/PATCH), (f) rate-limiting presence (middleware + headers), (g) input validation at boundaries (schema validation on incoming requests), (h) API documentation quality (written docs beyond autogen). Three-path detection (web / CLI / API-only) writes `raw/ux-mechanics-path-detection.json`. Gates: none owned by this recipe.
9
+
10
+ ---
11
+
12
+ ## Path detection (first step)
13
+
14
+ ```bash
15
+ # Web-UI — presence of framework routes/components
16
+ find <local-repo-path> -maxdepth 4 -type d \
17
+ \( -name pages -o -name app -o -name routes -o -name screens -o -name views \
18
+ -o -name components \) \
19
+ -not -path '*/node_modules/*' 2>/dev/null
20
+
21
+ find <local-repo-path> -maxdepth 4 -type f \
22
+ \( -name '*.tsx' -o -name '*.jsx' -o -name '*.vue' -o -name '*.svelte' \) \
23
+ -not -path '*/node_modules/*' 2>/dev/null | head -5
24
+
25
+ # CLI — executable entrypoint indicators
26
+ grep -lE '"bin"\s*:' <local-repo-path>/package.json 2>/dev/null
27
+ grep -lE '\[project\.scripts\]|console_scripts' \
28
+ <local-repo-path>/pyproject.toml <local-repo-path>/setup.cfg 2>/dev/null
29
+ grep -rlE 'cobra\.Command|click\.(group|command)|typer\.Typer|commander|yargs' \
30
+ <local-repo-path>/cmd <local-repo-path>/src <local-repo-path>/main.go 2>/dev/null
31
+
32
+ # Backend-only — HTTP server without UI components
33
+ grep -rlE 'Flask|FastAPI|express\(\)|gin\.Default|fiber\.New|@nestjs/core' \
34
+ <local-repo-path>/src <local-repo-path>/app <local-repo-path>/internal 2>/dev/null
35
+ ```
36
+
37
+ Capture: `path` ∈ `web-ui` / `cli` / `backend-only` / `mixed` (run all relevant bundles).
38
+
39
+ ---
40
+
41
+ ## Path A — Web-UI
42
+
43
+ ### Signal A1 — UX heuristic grep bundle
44
+
45
+ Six UX signals (each +1 if present):
46
+
47
+ ```bash
48
+ # Loading states
49
+ grep -rlE 'isLoading|isFetching|<Skeleton|<Spinner|<Suspense[^>]*fallback' \
50
+ <local-repo-path>/src <local-repo-path>/app <local-repo-path>/pages 2>/dev/null | head -1
51
+
52
+ # Error states
53
+ grep -rlE 'isError|<ErrorBoundary|error\s*\?|<ErrorState|toast\.error|notify\.error' \
54
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head -1
55
+
56
+ # Empty states
57
+ grep -rlE 'length\s*===?\s*0|isEmpty|<EmptyState|"No results"|"Nothing here"' \
58
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head -1
59
+
60
+ # Form validation
61
+ grep -rlE 'zod|yup|react-hook-form|vee-validate|required=|pattern=' \
62
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head -1
63
+
64
+ # Destructive-action feedback
65
+ grep -rlE 'toast|<Dialog|<AlertDialog|confirm\(' \
66
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head -1
67
+
68
+ # Responsive
69
+ grep -rlE '\b(sm:|md:|lg:|xl:|@media)\b' \
70
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head -1
71
+ ```
72
+
73
+ Capture: `ux_heuristics_hit` (count 0-6).
74
+
75
+ ### Signal A2 — Route / navigation tree
76
+
77
+ ```bash
78
+ # Route tree
79
+ tree -L 3 <local-repo-path>/{pages,app,routes,src/routes} 2>/dev/null
80
+
81
+ # Route count (Next.js file-based, React Router, Vue Router)
82
+ grep -rcE '<Route\s+path|createBrowserRouter|router\.(addRoute|push)' \
83
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | awk -F: '{n+=$2} END {print n+0}'
84
+
85
+ # Orphan detection — route files with no inbound <Link> / router.push
86
+ # (LLM does this better; surface as context)
87
+ ```
88
+
89
+ Capture: `route_count`, `route_tree_depth`, `has_nav_primitive` (bool).
90
+
91
+ ---
92
+
93
+ ## Path B — CLI
94
+
95
+ ### Signal B1 — shellcheck (shell repos)
96
+
97
+ ```bash
98
+ if find <local-repo-path> -name '*.sh' -not -path '*/node_modules/*' | head -1 > /dev/null; then
99
+ shellcheck -f json <local-repo-path>/**/*.sh > /tmp/shellcheck.json 2>/dev/null
100
+ fi
101
+ ```
102
+
103
+ Capture: `shellcheck_findings`, `shellcheck_by_severity` (error/warning/info/style).
104
+
105
+ ### Signal B2 — clig.dev conformance greps
106
+
107
+ ````bash
108
+ # --help wiring
109
+ grep -rlE 'argparse|click\.(command|group|option)|typer\.Typer|commander|cobra|yargs|\.addHelp\(' \
110
+ <local-repo-path>/cmd <local-repo-path>/src <local-repo-path> 2>/dev/null
111
+
112
+ # stderr discipline — writes to stderr for errors
113
+ grep -rlE 'sys\.stderr|stderr\.write|console\.error|process\.stderr|log\.(Error|Fatal)|fmt\.Fprintln\(os\.Stderr' \
114
+ <local-repo-path>/src <local-repo-path> 2>/dev/null
115
+
116
+ # Non-zero exit paths
117
+ grep -rcE 'sys\.exit\([1-9]|process\.exit\([1-9]|os\.Exit\([1-9]|return [1-9]' \
118
+ <local-repo-path>/src <local-repo-path> 2>/dev/null
119
+
120
+ # Common flags: --verbose / --dry-run / --yes / --quiet
121
+ grep -rcE '"--?(verbose|quiet|dry-run|yes|version)"' \
122
+ <local-repo-path> 2>/dev/null
123
+
124
+ # Logging library
125
+ grep -rlE 'logging\.|logger\.|log\.|pino|winston|zerolog|zap' \
126
+ <local-repo-path>/src <local-repo-path> 2>/dev/null
127
+
128
+ # README usage examples — fenced code blocks with command invocations
129
+ grep -cE '^\s*\$ |^\s*```(sh|bash|console|shell)' <local-repo-path>/README.md 2>/dev/null
130
+ ````
131
+
132
+ Capture 6 booleans + counts:
133
+
134
+ - `cli_help_wired` (bool)
135
+ - `cli_stderr_discipline` (bool)
136
+ - `cli_exit_codes_used` (count)
137
+ - `cli_common_flags_count`
138
+ - `cli_logging_present` (bool)
139
+ - `cli_readme_examples` (count of fenced cmd blocks)
140
+ - `cli_clig_conformance_score` = count of the 6 booleans satisfied (0-6)
141
+
142
+ ---
143
+
144
+ ## Path C — Backend-only (API-UX surrogate)
145
+
146
+ ### Signal C1 — OpenAPI / docs presence
147
+
148
+ ```bash
149
+ # OpenAPI / Swagger spec
150
+ find <local-repo-path> -maxdepth 3 \
151
+ \( -name 'openapi.yaml' -o -name 'openapi.json' \
152
+ -o -name 'swagger.yaml' -o -name 'swagger.json' \
153
+ -o -name 'api.yaml' \) 2>/dev/null
154
+
155
+ # FastAPI / NestJS / Hono / etc. auto-doc imports (implicit OpenAPI)
156
+ grep -rlE 'from fastapi|@nestjs/swagger|from hono|\.openapi\(' \
157
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null
158
+ ```
159
+
160
+ ### Signal C2 — Structured error responses
161
+
162
+ ```bash
163
+ # Error response shape — looks for consistent {error, code, message} JSON patterns
164
+ grep -rlE '\{\s*"error"|\{\s*error:\s*|HTTPException|abort\(|c\.JSON\(\d{3}' \
165
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null
166
+
167
+ # HTTP status code usage
168
+ grep -rcE '\b(status|code|HTTP_)(\s*=\s*|_| = )?[1-5]\d\d\b' \
169
+ <local-repo-path>/src <local-repo-path>/app 2>/dev/null | head
170
+ ```
171
+
172
+ ### Signal C3 — README API examples
173
+
174
+ ````bash
175
+ # curl / httpie invocation blocks in README
176
+ grep -cE '^\s*(curl |http )|```(http|json)' <local-repo-path>/README.md 2>/dev/null
177
+ ````
178
+
179
+ Capture:
180
+
181
+ - `api_has_openapi` (bool)
182
+ - `api_structured_errors` (bool)
183
+ - `api_status_code_variety` (count of distinct status codes used)
184
+ - `api_readme_examples` (count)
185
+ - `api_surrogate_score` (0–5; integrated mechanically)
186
+
187
+ ---
188
+
189
+ ## Signal X (shared) — LLM read
190
+
191
+ **Web-UI path:** read top 2 route files + main layout + 1 form component → score "thoughtful UX" **CLI path:** read main command file + 1 error-handling path → score "thoughtful CLI" **Backend-only path:** read 2 route handlers + error-response module → score "thoughtful API"
192
+
193
+ Capture:
194
+
195
+ - `llm_ux_score` (0–5)
196
+ - `llm_files_sampled` (list)
197
+ - `llm_confidence` (high/medium/low)
198
+ - `llm_notes` (one sentence)
199
+
200
+ ---
201
+
202
+ ## Raw dumps (flat — files under `raw/` named `ux-mechanics-*`)
203
+
204
+ Each signal writes its raw output to the flat `raw/` directory at the workspace root. Absent files = not extracted or path not applicable. Aggregates go to `scorecard.yaml` under `data.`.
205
+
206
+ | File | Source | Shape / note |
207
+ | --- | --- | --- |
208
+ | `raw/ux-mechanics-path-detection.json` | Path detection | `{path: "web-ui"/"cli"/"backend-only"/"mixed", web_evidence: [...], cli_evidence: [...], backend_evidence: [...]}` |
209
+ | `raw/ux-mechanics-ux-heuristics.json` | Signal A1 | 6-signal hit map: `{loading, error, empty, validation, confirmation, responsive}` — bool each (Path A only) |
210
+ | `raw/ux-mechanics-routes.json` | Signal A2 | `{route_count, route_tree_depth, has_nav_primitive, route_tree_raw}` (Path A only) |
211
+ | `raw/ux-mechanics-cli-probes.json` | Signal B2 | `{help_wired, stderr_discipline, exit_codes_used, common_flags_count, logging_present, readme_examples, clig_conformance_score}` (Path B only) |
212
+ | `raw/ux-mechanics-shellcheck.json` | Signal B1 | Raw `shellcheck -f json` output (Path B with `.sh` files only) |
213
+ | `raw/ux-mechanics-api-surrogate.json` | Signals C1–C3 | `{has_openapi, structured_errors, status_code_variety, readme_curl_examples, surrogate_score}` (Path C only) |
214
+ | `raw/ux-mechanics-llm-read.md` | Signal X | LLM UX read: `{files_sampled, ux_score, confidence, one_sentence}` + short rationale |
215
+
216
+ ---
217
+
218
+ ## Aggregation → 0–5 band
219
+
220
+ Scorer integrates the path-specific signals + LLM read.
221
+
222
+ **Banding (absolute):**
223
+
224
+ ### Path A (web-UI)
225
+
226
+ - **5** — ≥ 5/6 UX heuristics present, route tree clean + no orphans, LLM ≥ 4
227
+ - **3** — 3–4 heuristics present, routes inferable but some orphans, LLM ~3
228
+ - **1** — 0–2 heuristics, routes unclear
229
+
230
+ ### Path B (CLI)
231
+
232
+ - **5** — full clig.dev conformance (all 6 signals), shellcheck-clean, README has invocation examples, LLM ≥ 4
233
+ - **3** — help + errors + exit codes, missing common flags or logging, README thin
234
+ - **1** — bare script, no `--help`, no stderr discipline
235
+
236
+ ### Path C (backend-only / enterprise API) — **uncapped**
237
+
238
+ - **5** — OpenAPI spec complete + consistent response envelope + versioning + idempotency on mutations + rate-limiting + input validation, LLM ≥ 4
239
+ - **3** — OpenAPI + structured errors + HTTP status consistency, but missing versioning / idempotency / rate-limiting
240
+ - **1** — undocumented endpoints, inconsistent response shapes
241
+
242
+ ---
243
+
244
+ ## Fallback — signals missing
245
+
246
+ - Mixed-path repo (has both UI and CLI) → run both bundles, report both; take the stronger as primary, cite secondary as evidence.
247
+ - README absent → LLM confidence drops; emit `llm_confidence: low`.
248
+ - Repo has zero UI and zero CLI and no HTTP server → score `null` with an `UNCATEGORIZED` flag in evidence and explain why no path applied.
249
+
250
+ Annotate evidence with `"Path: A|B|C"` and `"Signals used: [...]"`.
251
+
252
+ ---
253
+
254
+ ## Output (what the scorer emits)
255
+
256
+ ```yaml
257
+ sub_criterion: ux_mechanics
258
+ score: 4
259
+ path: A
260
+ evidence: "Next.js repo. 5/6 UX heuristics hit; 12 routes, no orphans; LLM rates 4 — forms use zod + RHF and all have error states. No confirmation dialogs for delete actions."
261
+ data:
262
+ path: A
263
+ ux_heuristics_hit: 5
264
+ route_count: 12
265
+ route_tree_depth: 3
266
+ has_nav_primitive: true
267
+ llm_ux_score: 4
268
+ llm_files_sampled:
269
+ [app/layout.tsx, app/(auth)/login/page.tsx, components/TaskForm.tsx]
270
+ llm_confidence: medium
271
+ signals_used: [ux_heuristics, route_probe, llm]
272
+ gates_triggered: []
273
+ ```
274
+
275
+ ---
276
+
277
+ ## Caveats
278
+
279
+ - **UX heuristic grep is noisy.** Presence of `isLoading` ≠ loading UI is actually shown. LLM adjudicates when score conflicts with heuristic count.
280
+ - **Route-orphan detection is stack-specific** — reliable in Next.js file-based routing, weaker in hand-rolled React Router.
281
+ - **Backend-only / API-only is uncapped.** Enterprise API design (OpenAPI quality + versioning + idempotency + rate-limiting + input validation) can score up to 5 on the strength of those signals, matching an equally polished web UI.
282
+ - **CLI assessment via clig.dev** assumes conformance signals quality — some CLIs ship deliberately minimal with well-chosen defaults; LLM should adjudicate.
283
+ - **Accessibility deliberately out of scope.** A11y lint and the `aria/role/htmlFor` heuristic are not part of this recipe because tool coverage is uneven and a11y is out of scope for this rubric. UX heuristics, clig.dev conformance, API-UX surrogates, and the LLM read remain.
@@ -0,0 +1,76 @@
1
+ # Value Articulation — Extraction Plan
2
+
3
+ - **Sub-criterion:** 2.3 (Product Thinking)
4
+ - **Weight:** 12.5 pts
5
+ - **Source quote:** _"Does the README make a case for impact? Are claims quantified (time saved, cost, quality)?"_
6
+
7
+ This recipe scores how well the README articulates value — whether it makes a clear case for impact with a named audience and quantified outcomes (time / cost / quality).
8
+
9
+ ---
10
+
11
+ ## Extraction recipe
12
+
13
+ README-centric. The evaluator parses the README for impact claims, audience, scope coverage, and quantified outcomes.
14
+
15
+ ### Signal 1 — README impact read (LLM interpretive)
16
+
17
+ Feed the LLM the full README (truncated to the first ~1000 lines) plus a list of shipped capabilities (extracted from the `mechanical_ambition` recipe's `shipped_*` fields in its `raw/mechanical-ambition-llm-read.md`, if that recipe has already run for this project):
18
+
19
+ ```bash
20
+ head -1000 "$REPO/README.md" > /tmp/val-art-readme.md
21
+ ```
22
+
23
+ Write the LLM read to `raw/value-articulation-llm-read.md`. The LLM emits:
24
+
25
+ ```yaml
26
+ scope_breadth: N # number of distinct capabilities the README claims
27
+ integration_depth: "shallow" | "medium" | "deep" # does it integrate with internal systems, external APIs, just local data?
28
+ completeness_ratio: 0.0-1.0 # 1 − (stubs/handlers ratio) from mechanical_ambition, or inferred 1.0 if no stubs
29
+ time_saved_claim_in_readme: null | "<verbatim quote>" # e.g. "saves 3 hours per incident triage"
30
+ audience_inferred: "internal engineers" | "support agents" | "end customers" | "unclear"
31
+ value_articulation_score: 0-5 # 5 = crisp audience + quantified impact + scope matches claim; 0 = no value claim
32
+ one_sentence: "<LLM's one-line verdict>"
33
+ ```
34
+
35
+ ### Signal 2 — Proxies JSON
36
+
37
+ Aggregate into `raw/value-articulation-proxies.json`:
38
+
39
+ ```json
40
+ {
41
+ "scope_breadth": N,
42
+ "integration_depth": "shallow|medium|deep",
43
+ "completeness_ratio": 0.0-1.0,
44
+ "time_saved_claim_in_readme": null|"...",
45
+ "audience_inferred": "..."
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Banding (absolute)
52
+
53
+ | Band | Criteria |
54
+ | --- | --- |
55
+ | **5** | Crisp value story: named audience + quantified impact (time / cost / quality numbers) + scope matches the claim (completeness_ratio ≥ 0.85). `value_articulation_score: 5`. |
56
+ | **4** | Strong value story with one gap (quantified OR audience-specific, not both; or scope slightly short of claim). `value_articulation_score: 4`, `completeness_ratio ≥ 0.7`. |
57
+ | **3** | Value articulated but fuzzy: claim present but unquantified, OR audience generic. `value_articulation_score: 3`. |
58
+ | **2** | README mentions value in passing but no coherent story. `value_articulation_score: 2`. |
59
+ | **1** | No value claim, or README is an unmodified starter/template README. `value_articulation_score ≤ 1`. |
60
+ | **0** | Empty README. |
61
+
62
+ **Over-promise penalty:** if `completeness_ratio < 0.5` AND the README claims shipped functionality (not "WIP"), cap the band at **2** — the README is writing checks the code doesn't cash.
63
+
64
+ ---
65
+
66
+ ## Raw dumps
67
+
68
+ - `raw/value-articulation-llm-read.md` — LLM's full README-impact read with axis scores + verbatim quantified-claim quotes.
69
+ - `raw/value-articulation-proxies.json` — aggregated JSON (also embedded in `scorecard.yaml.value_articulation.data`).
70
+
71
+ ---
72
+
73
+ ## Not in scope
74
+
75
+ - Judging whether the claimed impact is realistic needs real operational/runtime data — out of scope; this recipe scores how well the README *articulates* value, not whether the claimed magnitude holds up.
76
+ - Measuring actual runtime behavior against claims — that's a separate runtime-verification pass, out of scope here.
@@ -0,0 +1,74 @@
1
+ # Evaluator — Preflight
2
+
3
+ > **Run before scoring a project.** This skill depends on external tools that must exist on the host. If a tool is missing, **install it** — do not silently fall back to grep / LLM-only. A silent fallback poisons the gate checks (VULN / SECRETS / AUTH / OBSERVABILITY) and makes the scorecard dishonest.
4
+
5
+ The evaluator is allowed to install its own tools via `brew` / `pipx` / `npm -g`. Installing is cheap (~1 minute total) and idempotent. Ignoring a missing tool is not.
6
+
7
+ ---
8
+
9
+ ## Required tools
10
+
11
+ | Tool | Purpose | Recipes that use it | Install (macOS, primary) |
12
+ | --- | --- | --- | --- |
13
+ | `gh` | GitHub API (CR scrape, GHA runs, PR enumeration) | 1.1, 1.2, 2.4 | `brew install gh` |
14
+ | `jq` | JSON parsing in shell pipelines | all | `brew install jq` |
15
+ | `gitleaks` | committed-secret scan — fires `SECRETS` gate | 1.4 | `brew install gitleaks` |
16
+ | `lizard` | polyglot complexity / god-function detection (per-language CCN: Py 10 / JS-TS 10 / Java 12 / Go 15) | 1.1, 1.4 | `brew install lizard` |
17
+ | `scc` | LoC + cocomo estimate (replaces tokei/cloc) | 1.4, 2.4a | `brew install scc` |
18
+ | `semgrep` | polyglot static analysis (quality + resilience + multi-protocol auth) | 1.1, 1.2, 2.4a | `brew install semgrep` |
19
+ | `hadolint` | Dockerfile lint | 1.3 | `brew install hadolint` |
20
+ | `ruff` | Python lint + formatter | 1.1 (Python repos), 2.4a (tier-A) | `brew install ruff` |
21
+ | `shellcheck` | shell-script lint | 2.2a (CLI path) | `brew install shellcheck` |
22
+ | `tree` | folder-tree read for LLM context | 1.4, 2.2a, 2.4a | `brew install tree` |
23
+ | `golangci-lint` | Go aggregate linter | 1.1 (Go repos) | `brew install golangci-lint` |
24
+ | `biome` | TS/JS lint + format (preferred over eslint for speed) | 1.1 (TS/JS repos) | `npm i -g @biomejs/biome` |
25
+ | `knip` | TS/JS dead-code + unused-deps | 1.4 | `npm i -g knip` |
26
+ | `pipx` | isolated install for Python CLIs below | — | `brew install pipx && pipx ensurepath` |
27
+ | `deptry` | Python unused-deps | 1.4 | `pipx install deptry` |
28
+ | `pip-audit` | Python dep-vuln scan — feeds `VULN` gate | 1.4 (Python repos) | `pipx install pip-audit` |
29
+ | `govulncheck` | Go dep-vuln scan — feeds `VULN` gate | 1.4 (Go repos) | `go install golang.org/x/vuln/cmd/govulncheck@latest` (conditional — skipped if go not on PATH) |
30
+ | `trivy` | Container + JS dep-vuln scan — feeds `VULN` gate | 1.4 | `brew install trivy` |
31
+
32
+ Host prerequisites that must already exist:
33
+
34
+ - `brew` (macOS) or `apt` (Linux)
35
+ - `node` + `npm` (≥ 18)
36
+ - `python3` (≥ 3.10) — `pipx` layers on top
37
+
38
+ ---
39
+
40
+ ## Invocation
41
+
42
+ Run once per host: `bash .claude/skills/evaluator/preflight.sh`. Idempotent — re-running no-ops when every tool is already present. Canonical install commands live in `preflight.sh` (the executable contract); edit there, not here. Host prerequisites (`brew`, `node`/`npm`, `python3`) must already exist — the script halts loud if any are missing.
43
+
44
+ ---
45
+
46
+ ## Required behaviour for scoring agents
47
+
48
+ 1. **Run preflight first.** Before extracting any signal, execute the preflight script.
49
+ 2. **If preflight exits non-zero, STOP.** Do not proceed to scoring with missing tools. Return a clear failure message to the caller naming which tools failed to install.
50
+ 3. **No silent fallback.** A signal that requires a tool must either run the tool or emit `{signal}_NOT_RUN: true` with `reason: "preflight failed"` in the matching sub-criterion's `data` block in `scorecard.yaml`. This is a loud failure mode, not a graceful one.
51
+ 4. **Re-running preflight is free.** Every invocation should run it — the first run installs; subsequent runs no-op.
52
+
53
+ ---
54
+
55
+ ## When a tool does not apply
56
+
57
+ Some tools apply only when a repo contains the target language (e.g. `ruff` on Python, `golangci-lint` on Go, `knip` on TS/JS). In that case:
58
+
59
+ - The tool is **not** required to be installed for repos where the language is absent.
60
+ - The signal is emitted as `{signal}: null, reason: "language_not_present"` — NOT as `_NOT_RUN`.
61
+ - Preflight still installs all tools idempotently; language filtering happens at the recipe level, not preflight.
62
+
63
+ This separates "tool missing from host" (a failure) from "tool not applicable here" (expected).
64
+
65
+ ---
66
+
67
+ ## Rationale
68
+
69
+ Silent fallback to grep / LLM equivalents corrupts the scorecard in two ways:
70
+
71
+ 1. **Wrong bands.** The absolute banding tables in each recipe are calibrated against real tool output (e.g. `lizard` CCN, `semgrep` finding density). A grep-estimated substitute lands the project in the wrong band.
72
+ 2. **Under-reporting bias.** `gitleaks` catches entropic tokens `grep` cannot; `pip-audit` / `trivy` catch CVEs no heuristic will. Missing them turns a SECRETS- or VULN-gate hit into silence — a dishonest pass.
73
+
74
+ Installing tools is cheap and one-time. Skipping them silently produces a scorecard that looks complete but isn't.
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # Evaluator preflight — install required tools or fail.
3
+ # Canonical source. Rationale + tool table live in preflight.md (the prose
4
+ # contract); this script is the executable contract. Edit this file, not the .md.
5
+ # Idempotent. Re-runnable. No-op when everything is installed.
6
+ set -euo pipefail
7
+
8
+ missing=()
9
+
10
+ # Portable timeout wrapper. macOS has no `timeout` by default; `gtimeout` lives
11
+ # in coreutils. If neither is present we run without a cap but warn — the User
12
+ # should `brew install coreutils` to restore the 180s guard.
13
+ if command -v timeout >/dev/null 2>&1; then
14
+ _timeout() { timeout "$@"; }
15
+ elif command -v gtimeout >/dev/null 2>&1; then
16
+ _timeout() { gtimeout "$@"; }
17
+ else
18
+ echo "[preflight] warning: no timeout/gtimeout on host — install coreutils for hang-safe installs (brew install coreutils)" >&2
19
+ _timeout() { shift; "$@"; } # drop the seconds arg, exec the rest
20
+ fi
21
+
22
+ need() {
23
+ local tool="$1"
24
+ local install_cmd="$2"
25
+ if ! command -v "$tool" >/dev/null 2>&1; then
26
+ echo "[preflight] missing: $tool — installing with: $install_cmd" >&2
27
+ # 180s covers slow brew/npm fetches; a stuck package manager fails loud
28
+ # instead of hanging the run indefinitely (falls back to no-cap
29
+ # when timeout/gtimeout is unavailable — see wrapper above).
30
+ _timeout 180 bash -c "$install_cmd" || { echo "[preflight] INSTALL FAILED (or timed out): $tool" >&2; missing+=("$tool"); }
31
+ else
32
+ echo "[preflight] ok: $tool"
33
+ fi
34
+ }
35
+
36
+ # Host prerequisites — do not attempt to install; fail if missing.
37
+ for bin in brew jq node npm python3; do
38
+ command -v "$bin" >/dev/null 2>&1 || { echo "[preflight] HOST MISSING: $bin — user must install" >&2; exit 2; }
39
+ done
40
+
41
+ # Homebrew-installable tools
42
+ # NOTE: `lizard` is intentionally NOT brew-installed — brew's `lizard` is the
43
+ # LZ5 data-compression CLI, which shadows the Python CCN analyzer of the same
44
+ # name when on PATH first. We install the Python tool via pipx below and then
45
+ # sanity-check that the `lizard` resolved on PATH is the analyzer, not the
46
+ # compressor. If a user has `brew install lizard` already they'll need to
47
+ # either `brew uninstall lizard` or reorder PATH so `~/.local/bin` is earlier.
48
+ need gh "brew install gh"
49
+ need gitleaks "brew install gitleaks"
50
+ need scc "brew install scc"
51
+ need semgrep "brew install semgrep"
52
+ need hadolint "brew install hadolint"
53
+ need ruff "brew install ruff"
54
+ need shellcheck "brew install shellcheck"
55
+ need tree "brew install tree"
56
+ need golangci-lint "brew install golangci-lint"
57
+ need pnpm "brew install pnpm"
58
+ need yarn "brew install yarn"
59
+ need pipx "brew install pipx && pipx ensurepath"
60
+ need trivy "brew install trivy"
61
+
62
+ # npm-global tools
63
+ need biome "npm install -g @biomejs/biome"
64
+ need knip "npm install -g knip"
65
+
66
+ # pipx-installed Python CLIs
67
+ need deptry "pipx install deptry"
68
+ need lizard "pipx install lizard"
69
+ need pip-audit "pipx install pip-audit"
70
+
71
+ # Go-installed CLIs — only install govulncheck when go is already on PATH.
72
+ # Hosts without Go skip this gracefully; Go-repo vuln scans then
73
+ # surface as `_NOT_RUN: true` with reason "govulncheck unavailable (go not installed)".
74
+ if command -v go >/dev/null 2>&1; then
75
+ need govulncheck "go install golang.org/x/vuln/cmd/govulncheck@latest"
76
+ else
77
+ echo "[preflight] skip: govulncheck (go not on PATH — install go manually + rerun preflight if you need Go dep-vuln scans)"
78
+ fi
79
+
80
+ # Sanity-check: the `lizard` on PATH must be the Python cyclomatic-complexity
81
+ # analyzer, not brew's LZ5 compressor. The Python tool's --help output always
82
+ # contains "Cyclomatic Complexity Analyzer"; the compressor prints a short
83
+ # LZ5-compression usage blurb. Halt loudly on mismatch.
84
+ if command -v lizard >/dev/null 2>&1; then
85
+ if ! lizard --help 2>&1 | grep -qi 'Cyclomatic Complexity Analyzer'; then
86
+ echo "[preflight] FAILED — 'lizard' on PATH is not the Python CCN analyzer." >&2
87
+ echo "[preflight] Resolved: $(command -v lizard)" >&2
88
+ echo "[preflight] Likely cause: brew's LZ5 compressor is shadowing pipx's lizard." >&2
89
+ echo "[preflight] Fix: 'brew uninstall lizard' OR move \"\$HOME/.local/bin\" earlier in PATH than \$(brew --prefix)/bin." >&2
90
+ missing+=("lizard (wrong binary — see above)")
91
+ fi
92
+ fi
93
+
94
+ if [ "${#missing[@]}" -gt 0 ]; then
95
+ echo "[preflight] FAILED — these tools could not be installed: ${missing[*]}" >&2
96
+ echo "[preflight] scoring cannot proceed; fix manually and re-run" >&2
97
+ exit 1
98
+ fi
99
+
100
+ echo "[preflight] all tools ready"
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env python3
2
+ """Validate a single scorecard.yaml against the evaluator schema.
3
+
4
+ Usage: python3 validate_scorecard.py <path-to-scorecard.yaml>
5
+ Exit: 0 on valid; 1 on first failure with a specific error on stderr.
6
+
7
+ Run after emitting a scorecard. Catches malformed scorecards before a reader
8
+ (or a downstream caller) trips over a missing field with no attribution.
9
+
10
+ Schema: 8 sub-criterion blocks (4 Code + 4 Product). Totals: total / available
11
+ / normalized. Every sub-criterion carries score + evidence + data.
12
+ """
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ try:
17
+ import yaml
18
+ except ImportError:
19
+ print("[validate] FAIL: PyYAML not installed (pip install pyyaml)", file=sys.stderr)
20
+ sys.exit(1)
21
+
22
+ REQUIRED_TOP = {
23
+ "project", "commit_sha_pinned", "rubric_version", "scored_at",
24
+ "agent_model",
25
+ "gates_triggered", "short_circuited",
26
+ "scored_sub_criteria", "total_sub_criteria",
27
+ "total", "available", "normalized",
28
+ }
29
+
30
+ # 1.x Production-Grade Code — 12.5 pts each
31
+ CODE_SUBS = {
32
+ "code_quality",
33
+ "testing",
34
+ "deployment_readiness",
35
+ "maintainability",
36
+ }
37
+
38
+ # 2.x Product Thinking — 12.5 pts each
39
+ PRODUCT_SUBS = {
40
+ "problem_substantiveness", # 2.1
41
+ "ux_mechanics", # 2.2
42
+ "value_articulation", # 2.3
43
+ "mechanical_ambition", # 2.4
44
+ }
45
+
46
+ SUB_CRITERIA = CODE_SUBS | PRODUCT_SUBS
47
+
48
+
49
+ def fail(msg: str) -> None:
50
+ print(f"[validate] FAIL: {msg}", file=sys.stderr)
51
+ sys.exit(1)
52
+
53
+
54
+ def main(path: str) -> None:
55
+ p = Path(path)
56
+ if not p.exists():
57
+ fail(f"file not found: {path}")
58
+
59
+ try:
60
+ doc = yaml.safe_load(p.read_text())
61
+ except yaml.YAMLError as e:
62
+ fail(f"YAML parse error in {path}: {e}")
63
+
64
+ if not isinstance(doc, dict):
65
+ fail(f"top-level of {path} is not a mapping")
66
+
67
+ missing_top = REQUIRED_TOP - doc.keys()
68
+ if missing_top:
69
+ fail(f"missing top-level keys: {sorted(missing_top)}")
70
+
71
+ missing_sub = SUB_CRITERIA - doc.keys()
72
+ if missing_sub:
73
+ fail(f"missing sub-criterion blocks: {sorted(missing_sub)}")
74
+
75
+ for sub in SUB_CRITERIA:
76
+ block = doc[sub]
77
+ if not isinstance(block, dict):
78
+ fail(f"{sub}: not a mapping")
79
+ if "score" not in block:
80
+ fail(f"{sub}: missing `score`")
81
+ if "evidence" not in block:
82
+ fail(f"{sub}: missing `evidence`")
83
+ if "data" not in block:
84
+ fail(f"{sub}: missing `data` block")
85
+
86
+ # total_sub_criteria must be 8 under this schema.
87
+ if doc.get("total_sub_criteria") != 8:
88
+ fail(f"total_sub_criteria must be 8 (got {doc.get('total_sub_criteria')!r})")
89
+
90
+ print(f"[validate] OK: {path}")
91
+ sys.exit(0)
92
+
93
+
94
+ if __name__ == "__main__":
95
+ if len(sys.argv) != 2:
96
+ print("Usage: python3 validate_scorecard.py <path-to-scorecard.yaml>", file=sys.stderr)
97
+ sys.exit(2)
98
+ main(sys.argv[1])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/etna",
3
- "version": "0.18.4",
3
+ "version": "0.18.5",
4
4
  "description": "Bootstrap a project with a Nurix harness — copies the harness's CLAUDE.md, skills, agents, rules, and hooks directly into the project from a bundled payload (no marketplace fetch, no network).",
5
5
  "type": "module",
6
6
  "bin": {