@nurix/etna 0.18.4 → 0.18.6
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 +3 -3
- package/data/harnesses/code/rules/typescript.md +10 -0
- 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,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.
|
|
3
|
+
"version": "0.18.6",
|
|
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": {
|