@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,414 @@
1
+ # Deployment-Readiness — Extraction Plan
2
+
3
+ - **Sub-criterion:** 1.3 (AI-only)
4
+ - **Weight:** 12.5 pts (4 Code AI × 12.5 = 50 pts half)
5
+ - **Contenders:** Filesystem probes + Hadolint + **observability markers** (structured-logging library presence, metrics/tracing library presence, `/health` endpoint detection) + LLM README/Dockerfile read (layered)
6
+ - **Source quote:** _"Can this actually ship? Docker, config, health checks, or clear instructions to run it outside localhost."_
7
+
8
+ Bands are **absolute** — per-signal thresholds in the _Banding_ section. Gates this recipe owns: **`DEMO`** (`deployment_readiness ≤ 1` when all 4 run-section/deploy signals are false OR README claims a deploy artifact that doesn't exist in the tree) and **`OBSERVABILITY`** (caps `deployment_readiness ≤ 3` when structured-logging library is absent AND metrics/tracing library is absent). Structured-logging detection: presence of `structlog` / `winston` / `pino` / `log-slog` / `zap` in deps OR JSON log format detected in code. Metrics/tracing detection: OpenTelemetry SDK / Prometheus client / Datadog agent / statsd client / a custom `/metrics` endpoint. Gate fires only when **both** categories are absent — a repo with structured logging but no metrics still ships above 3. Raw dump: `raw/deployment-readiness-observability-markers.json` with fields `{logging_lib: "<name>"|null, metrics_lib: "<name>"|null, has_health_endpoint: bool, json_log_detected: bool}`.
9
+
10
+ > Five signals, integrated by the scorer. Serverless-native repos (no Dockerfile) can reach band 5 via PaaS/managed-platform path — see bands. No weighted-math internal split — the LLM emits a single 0–5 band and cites the most load-bearing signal.
11
+
12
+ ---
13
+
14
+ ## Extraction recipe
15
+
16
+ ### Signal 1 — Containerization (Docker OR devcontainer OR compose-with-image)
17
+
18
+ ```bash
19
+ # Presence
20
+ ls <repo>/Dockerfile \
21
+ <repo>/docker-compose.yml \
22
+ <repo>/docker-compose.yaml \
23
+ <repo>/compose.yaml \
24
+ <repo>/compose.override.yml \
25
+ <repo>/.dockerignore \
26
+ <repo>/.devcontainer/devcontainer.json \
27
+ <repo>/devcontainer.json 2>/dev/null
28
+
29
+ # Dockerfile features (case-insensitive — Docker accepts `FROM ... as builder` too)
30
+ grep -liE '^HEALTHCHECK' <repo>/Dockerfile 2>/dev/null # healthcheck
31
+ grep -liE '^FROM[^#]*\bAS\b' <repo>/Dockerfile 2>/dev/null # multi-stage
32
+ grep -liE '^USER[[:space:]]' <repo>/Dockerfile 2>/dev/null # non-root
33
+
34
+ # Compose with inline image (no build:) → legitimate containerization without Dockerfile
35
+ grep -lE '^\s*image:' <repo>/docker-compose.y*ml <repo>/compose.yaml 2>/dev/null
36
+
37
+ # Lint (single Go binary; JSON out) — only if Dockerfile exists
38
+ if [ -f <repo>/Dockerfile ]; then
39
+ hadolint --format json <repo>/Dockerfile > /tmp/hadolint.json
40
+ fi
41
+ ```
42
+
43
+ Capture:
44
+
45
+ - `has_dockerfile`, `has_compose`, `has_dockerignore`, `has_devcontainer`
46
+ - `compose_has_inline_image` (compose without Dockerfile is legitimate)
47
+ - `docker_has_healthcheck`, `docker_multi_stage`, `docker_non_root`
48
+ - `hadolint_findings` (total), `hadolint_by_severity` (`error`/`warning`/`info`/`style`)
49
+ - `has_container_artifact` = `has_dockerfile OR has_devcontainer OR compose_has_inline_image`
50
+
51
+ ### Signal 2 — Config hygiene
52
+
53
+ ```bash
54
+ # env-example presence
55
+ ls <repo>/{.env.example,.env.sample,.env.template} 2>/dev/null
56
+
57
+ # .env gitignored?
58
+ grep -E '^\.env(\.|$)' <repo>/.gitignore 2>/dev/null
59
+
60
+ # Any tracked .env file? (should be zero)
61
+ find <repo> -maxdepth 3 -name '.env' \
62
+ -not -path '*/node_modules/*' -not -path '*/.venv/*'
63
+ ```
64
+
65
+ Capture: `has_env_example`, `env_in_gitignore`, `tracked_env_files` (list; should be empty).
66
+
67
+ ### Signal 3 — Deploy artifacts (GHA / PaaS / IaC)
68
+
69
+ ```bash
70
+ # GHA deploy workflows
71
+ ls <repo>/.github/workflows/ 2>/dev/null \
72
+ | grep -iE 'deploy|release|publish|ship'
73
+
74
+ # PaaS configs — broad 2026 landscape
75
+ ls <repo>/{\
76
+ vercel.json,netlify.toml,\
77
+ fly.toml,\
78
+ railway.json,railway.toml,\
79
+ render.yaml,\
80
+ Procfile,app.json,app.yaml,apprunner.yaml,\
81
+ wrangler.toml,wrangler.jsonc,\
82
+ serverless.yml,template.yaml,cdk.json,sst.config.ts,Pulumi.yaml,\
83
+ modal.toml,\
84
+ .replit,replit.nix,\
85
+ nixpacks.toml,project.toml,\
86
+ supabase/config.toml,\
87
+ .do/app.yaml,\
88
+ koyeb.yaml,zeabur.json,\
89
+ dagger.json,porter.yaml\
90
+ } 2>/dev/null
91
+
92
+ # Cloudflare Pages markers (no single file; check for any)
93
+ ls <repo>/{_routes.json,_headers,_redirects} 2>/dev/null
94
+
95
+ # IaC directories
96
+ ls -d <repo>/{k8s,kubernetes,manifests,helm,terraform,pulumi,cdk} 2>/dev/null
97
+
98
+ # Helm chart detection
99
+ ls <repo>/Chart.yaml 2>/dev/null
100
+
101
+ # Kustomize
102
+ find <repo> -maxdepth 3 -name 'kustomization.yaml' 2>/dev/null
103
+ ```
104
+
105
+ Capture:
106
+
107
+ - `deploy_workflow_files` (list)
108
+ - `paas_configs` (list of present files; extend the list as new deploy targets appear)
109
+ - `iac_dirs` (list of present dirs)
110
+ - `has_any_deploy_artifact` = bool (OR across above + Signal 1 containerization)
111
+
112
+ ### Signal 4 — Health checks + localhost counter-signal
113
+
114
+ ```bash
115
+ # Health route definitions (broad regex, all stacks)
116
+ grep -rnE '["'\''/](health(z)?|readyz|livez|liveness|readiness|ping|status)["'\''/\s(]' \
117
+ <repo>/src <repo>/app <repo>/internal 2>/dev/null \
118
+ | head -20
119
+
120
+ # K8s liveness/readiness probes (if iac_dirs includes k8s/)
121
+ grep -rE 'livenessProbe|readinessProbe' <repo>/k8s <repo>/helm 2>/dev/null
122
+
123
+ # Localhost hardcoding in non-test source (counter-signal)
124
+ # Exclude lines that reference env vars (fine: `process.env.X || 'localhost:3000'` is a dev default, not hardcoding)
125
+ grep -rnE 'localhost:[0-9]+|127\.0\.0\.1:[0-9]+|http://localhost' \
126
+ <repo>/src <repo>/app \
127
+ --include='*.py' --include='*.ts' --include='*.tsx' \
128
+ --include='*.js' --include='*.jsx' --include='*.go' \
129
+ --exclude-dir=node_modules --exclude-dir=__tests__ \
130
+ --exclude-dir=.next --exclude-dir=dist --exclude-dir=build \
131
+ 2>/dev/null \
132
+ | grep -vE 'process\.env|os\.environ|ENV\[|getenv|import\.meta\.env|viper\.Get|os\.Getenv' \
133
+ | wc -l
134
+ ```
135
+
136
+ Capture:
137
+
138
+ - `health_route_hits` (count; 0 = none)
139
+ - `k8s_probes_present` (bool)
140
+ - `localhost_hardcoded_count` (non-test source, env-var-filtered)
141
+
142
+ ### Signal 5 — README read (mechanical + LLM two-signal)
143
+
144
+ **Mechanical — README section heading presence:**
145
+
146
+ ```bash
147
+ grep -iE '^##+\s*(Deploy|Deployment|Production|Hosting|Running in production|Getting Started|Installation|Quickstart|Usage|How to run|Setup)' \
148
+ <repo>/README.md <repo>/readme.md 2>/dev/null
149
+ ```
150
+
151
+ Capture: `readme_has_run_section` (bool — any matching heading present).
152
+
153
+ **LLM — quality read:** Read and score:
154
+
155
+ - **README.md** (or `readme.md` / `README.rst`)
156
+ - **Dockerfile** (if present)
157
+
158
+ Score each on:
159
+
160
+ - Does the README teach a stranger to run the app **outside localhost**? (deploy target named, deploy command given, env vars documented?)
161
+ - Is the Dockerfile production-shaped (multi-stage, slim base, pinned versions, non-root, HEALTHCHECK)?
162
+ - Anti-signals: only `npm run dev` / `python app.py`; no env docs; localhost-only language.
163
+
164
+ Capture:
165
+
166
+ - `llm_readme_score` (0–5)
167
+ - `llm_dockerfile_score` (0–5 or `null` if absent)
168
+ - `llm_has_run_section` (bool)
169
+ - `llm_confidence` (`high`/`medium`/`low`)
170
+ - `llm_notes` (one sentence)
171
+
172
+ ### Signal 6 — Observability markers (OBSERVABILITY gate trigger)
173
+
174
+ Ships-to-prod requires signal — structured logs and metrics/traces. This signal looks for evidence that either is wired into the app. **Both** must be absent for the gate to fire; one present means the repo has the basics.
175
+
176
+ ```bash
177
+ # Structured-logging library — check deps (manifest) + imports (code) + JSON-log
178
+ # format heuristic. Any one match lets the repo off the gate's logging arm.
179
+ LOG_LIBS="structlog|loguru|winston|pino|log-slog|uber-go/zap|go.uber.org/zap"
180
+
181
+ # Dependency declaration — one of:
182
+ if [ -f <repo>/pyproject.toml ]; then
183
+ grep -iE "$LOG_LIBS" <repo>/pyproject.toml 2>/dev/null
184
+ fi
185
+ if [ -f <repo>/requirements.txt ]; then
186
+ grep -iE "$LOG_LIBS" <repo>/requirements.txt 2>/dev/null
187
+ fi
188
+ if [ -f <repo>/package.json ]; then
189
+ jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' \
190
+ <repo>/package.json 2>/dev/null | grep -iE "$LOG_LIBS"
191
+ fi
192
+ if [ -f <repo>/go.mod ]; then
193
+ grep -iE "$LOG_LIBS" <repo>/go.mod 2>/dev/null
194
+ fi
195
+
196
+ # Import sighting in source — covers vendored or pinned-but-renamed deps.
197
+ grep -rnE "import.*\b($LOG_LIBS)\b|from\s+($LOG_LIBS)" \
198
+ <repo>/src <repo>/app <repo>/internal \
199
+ --include='*.py' --include='*.ts' --include='*.tsx' \
200
+ --include='*.js' --include='*.jsx' --include='*.go' \
201
+ --exclude-dir=node_modules --exclude-dir=.venv \
202
+ 2>/dev/null | head -20
203
+
204
+ # JSON log-format heuristic — bare `json.dumps(...)` inside what looks like a
205
+ # log call. Weak signal; participates only as a fallback. Evaluator should
206
+ # confirm with LLM when this is the only logging-arm match.
207
+ grep -rnE '(logger|log)\.(info|warn|error|debug)\([^)]*json\.(dumps|stringify)' \
208
+ <repo>/src <repo>/app \
209
+ --include='*.py' --include='*.ts' --include='*.js' --include='*.go' \
210
+ --exclude-dir=node_modules 2>/dev/null | head -5
211
+
212
+ # Metrics / tracing library — OpenTelemetry SDK, Prometheus client,
213
+ # Datadog agent, statsd client, or a custom /metrics endpoint.
214
+ METRICS_LIBS="opentelemetry|otel-|prom-client|prometheus_client|prometheus-client|ddtrace|datadog|statsd|hot-shots"
215
+
216
+ # Dependency + import sighting (same pattern as logging)
217
+ if [ -f <repo>/pyproject.toml ]; then
218
+ grep -iE "$METRICS_LIBS" <repo>/pyproject.toml 2>/dev/null
219
+ fi
220
+ if [ -f <repo>/package.json ]; then
221
+ jq -r '(.dependencies // {}) + (.devDependencies // {}) | keys[]' \
222
+ <repo>/package.json 2>/dev/null | grep -iE "$METRICS_LIBS"
223
+ fi
224
+ if [ -f <repo>/go.mod ]; then
225
+ grep -iE "$METRICS_LIBS" <repo>/go.mod 2>/dev/null
226
+ fi
227
+ grep -rnE "import.*\b($METRICS_LIBS)\b|from\s+($METRICS_LIBS)" \
228
+ <repo>/src <repo>/app <repo>/internal \
229
+ --include='*.py' --include='*.ts' --include='*.tsx' \
230
+ --include='*.js' --include='*.jsx' --include='*.go' \
231
+ --exclude-dir=node_modules --exclude-dir=.venv \
232
+ 2>/dev/null | head -20
233
+
234
+ # Custom /metrics endpoint — Prometheus convention. Matches route definitions.
235
+ grep -rnE '["/]metrics["/\s(]' <repo>/src <repo>/app \
236
+ <repo>/internal --include='*.py' --include='*.ts' \
237
+ --include='*.js' --include='*.go' \
238
+ --exclude-dir=node_modules 2>/dev/null | head -10
239
+ ```
240
+
241
+ Assemble observability markers JSON (the sub-agent writes the interpreted fields, not the raw grep):
242
+
243
+ ```json
244
+ {
245
+ "logging_lib": "structlog" | null,
246
+ "logging_detected_via": "manifest" | "import" | "json-format-heuristic" | null,
247
+ "metrics_lib": "opentelemetry" | null,
248
+ "metrics_detected_via": "manifest" | "import" | "metrics-endpoint" | null,
249
+ "has_metrics_endpoint": true | false,
250
+ "has_health_endpoint": true | false,
251
+ "json_log_detected": true | false
252
+ }
253
+ ```
254
+
255
+ Capture:
256
+
257
+ - `logging_lib` — name of the structured-logging library, or `null` if none found
258
+ - `metrics_lib` — name of the metrics/tracing library, or `null` if none found
259
+ - `has_metrics_endpoint` — bool (custom `/metrics` route found)
260
+ - `has_json_log_format` — bool (heuristic match; low-confidence tiebreaker only)
261
+ - `observability_logging_present` = `(logging_lib != null) OR has_json_log_format`
262
+ - `observability_metrics_present` = `(metrics_lib != null) OR has_metrics_endpoint`
263
+
264
+ **OBSERVABILITY gate trigger:** `observability_logging_present == false` AND `observability_metrics_present == false`. Both must be absent for the gate to fire — one present is enough to avoid the cap. Caps `deployment_readiness ≤ 3`.
265
+
266
+ ---
267
+
268
+ ## Raw dumps (flat — files under `raw/` named `deployment-readiness-*`)
269
+
270
+ 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.`.
271
+
272
+ | File | Source signal | Shape / note |
273
+ | --- | --- | --- |
274
+ | `raw/deployment-readiness-artifacts.json` | Signals 1 + 3 + 4 | Combined filesystem probe: `{container: {dockerfile, compose, devcontainer, healthcheck, multi_stage, non_root}, deploy: {gha_workflows, paas_configs, iac_dirs, helm, kustomize}, env: {env_example, env_in_gitignore, tracked_env_files}, health: {routes, k8s_probes}}` |
275
+ | `raw/deployment-readiness-hadolint.json` | Signal 1 | Raw `hadolint --format json` output (absent when no Dockerfile) |
276
+ | `raw/deployment-readiness-localhost-refs.txt` | Signal 4 | `grep -rnE 'localhost:...\|127.0.0.1:...'` output with `file:line`, after env-var exclusion (absent when count = 0) |
277
+ | `raw/deployment-readiness-llm-read.md` | Signal 5 | LLM's README + Dockerfile analysis: `{readme_score, dockerfile_score, has_run_section, confidence, one_sentence}` plus a short paragraph of rationale |
278
+ | `raw/deployment-readiness-observability-markers.json` | Signal 6 | Structured object: `{logging_lib, logging_detected_via, metrics_lib, metrics_detected_via, has_metrics_endpoint, has_health_endpoint, json_log_detected}` — feeds the OBSERVABILITY gate decision |
279
+
280
+ ---
281
+
282
+ ## Aggregation → 0–5 band
283
+
284
+ Scorer integrates the 5 signals. Rough directionality:
285
+
286
+ | Signal | Pulls score UP | Pulls score DOWN |
287
+ | --- | --- | --- |
288
+ | `has_container_artifact` + (Hadolint-clean if Dockerfile) | Present + 0 Hadolint errors | Absent or > 5 Hadolint errors |
289
+ | `docker_has_healthcheck` / `docker_non_root` / `docker_multi_stage` | All true | None true |
290
+ | `has_env_example` + `env_in_gitignore` + zero tracked `.env` | All clean | Any violation |
291
+ | `has_any_deploy_artifact` (container / GHA / PaaS / IaC) | ≥ 2 distinct artifacts | None |
292
+ | `health_route_hits` or `k8s_probes_present` or Docker `HEALTHCHECK` | ≥ 1 present | 0 |
293
+ | `localhost_hardcoded_count` | 0 in non-test source | > 3 |
294
+ | `llm_readme_score` | 4–5 | 0–2 |
295
+ | `llm_dockerfile_score` | 4–5 | 0–2 |
296
+
297
+ **Bands (absolute):**
298
+
299
+ | Band | Path A — container-first | Path B — serverless-native |
300
+ | --- | --- | --- |
301
+ | **5** | Dockerfile Hadolint-clean with multi-stage + USER + HEALTHCHECK + `.env.example` + health route + deploy workflow + README LLM ≥ 4 + zero localhost hardcoding | PaaS/managed-platform config + deploy workflow + `.env.example` + health route (HTTP or framework) + README LLM ≥ 4 + zero localhost hardcoding |
302
+ | **4** | Strong container path, one weaker signal | Strong serverless path, one weaker signal |
303
+ | **3** | Some container or platform artifact; env vars referenced; mixed other signals |
304
+ | **2** | Minimal — one ship artifact present but weak README / no health / no env docs |
305
+ | **1** | DEMO gate fires (see below); localhost-only repo |
306
+ | **0** | Suppressed (DEMO gate caps at 1) |
307
+
308
+ Serverless-native repos (Vercel/Fly/Cloudflare/Modal) reach band 5 via Path B — do not penalize absent Dockerfile if a PaaS config carries the deploy artifact.
309
+
310
+ ---
311
+
312
+ ## DEMO gate — two-signal agreement
313
+
314
+ Gate fires only when **all three** are absent AND the LLM + mechanical README read agree:
315
+
316
+ ```
317
+ has_container_artifact == false
318
+ AND has_any_deploy_artifact == false (i.e. no GHA deploy, no PaaS config, no IaC)
319
+ AND readme_has_run_section == false (mechanical heading grep)
320
+ AND llm_has_run_section == false (LLM read)
321
+ ```
322
+
323
+ **Why two-signal:** a single LLM judgment as a hard gate means one noisy call caps the score. Requiring heading grep + LLM agreement makes the gate robust to LLM noise. If `llm_confidence == low`, defer to mechanical-only (skip the LLM side of the AND).
324
+
325
+ When fired, emit `gates_triggered: [DEMO]` and cap `deployment_readiness ≤ 1`.
326
+
327
+ ---
328
+
329
+ ## OBSERVABILITY gate — both arms absent
330
+
331
+ Gate fires only when **both** observability arms are absent:
332
+
333
+ ```
334
+ observability_logging_present == false
335
+ AND
336
+ observability_metrics_present == false
337
+ ```
338
+
339
+ That is: the repo ships neither a structured-logging library (structlog / winston / pino / log-slog / zap / a detected JSON-log format) **nor** a metrics/tracing library (OpenTelemetry / Prometheus client / Datadog / statsd / a custom `/metrics` endpoint).
340
+
341
+ **Why both-arms:** enterprise observability is "logs AND metrics", but a single-binary CLI or a thin FaaS handler may legitimately ship with just structured logs and skip metrics (or vice versa). Demanding both is punitive on small, valid shapes. The gate catches the repo that's shipping without _any_ production-grade signal — `print()`-only logging and no metrics — which is the failure we care about.
342
+
343
+ When fired, emit `gates_triggered: [OBSERVABILITY]` and cap `deployment_readiness ≤ 3`. Absent arm names ship in the evidence string: `"no structured logger AND no metrics/tracing — cap at 3"`.
344
+
345
+ ---
346
+
347
+ ## Fallback — signals missing
348
+
349
+ - No Dockerfile → drop Hadolint lint; rely on Signals 2, 3, 4, 5 + `has_devcontainer` / `compose_has_inline_image` as container signal.
350
+ - Hadolint not installed → drop lint component; rely on LLM Dockerfile read.
351
+ - Fully serverless (Vercel/Fly only, no Dockerfile) → Path B; PaaS config in Signal 3 carries deploy artifact weight.
352
+ - LLM low-confidence → DEMO gate falls back to mechanical-only (README heading grep + artifact absence).
353
+
354
+ Annotate evidence with `"Signals used: [...]"` and `"Path: A | B"` so the audit trail is explicit.
355
+
356
+ ---
357
+
358
+ ## Output (what the scorer emits)
359
+
360
+ ```yaml
361
+ sub_criterion: deployment_readiness
362
+ score: 3
363
+ evidence: "Dockerfile present (multi-stage, non-root, no HEALTHCHECK; Hadolint 2 warnings); .env.example + .env gitignored; no deploy workflow; README has install steps but no outside-localhost deploy section; 4 hardcoded localhost refs. Path A."
364
+ data:
365
+ has_dockerfile: true
366
+ has_compose: false
367
+ has_dockerignore: true
368
+ has_devcontainer: false
369
+ compose_has_inline_image: false
370
+ has_container_artifact: true
371
+ docker_has_healthcheck: false
372
+ docker_multi_stage: true
373
+ docker_non_root: true
374
+ hadolint_findings: 2
375
+ hadolint_by_severity: { error: 0, warning: 2, info: 0, style: 0 }
376
+ has_env_example: true
377
+ env_in_gitignore: true
378
+ tracked_env_files: []
379
+ deploy_workflow_files: []
380
+ paas_configs: []
381
+ iac_dirs: []
382
+ has_any_deploy_artifact: true # Dockerfile counts as containerization
383
+ health_route_hits: 0
384
+ k8s_probes_present: false
385
+ localhost_hardcoded_count: 4
386
+ readme_has_run_section: true
387
+ llm_readme_score: 2
388
+ llm_dockerfile_score: 3
389
+ llm_has_run_section: true
390
+ llm_confidence: medium
391
+ logging_lib: structlog
392
+ logging_detected_via: manifest
393
+ metrics_lib: null
394
+ metrics_detected_via: null
395
+ has_metrics_endpoint: false
396
+ has_json_log_format: true
397
+ observability_logging_present: true
398
+ observability_metrics_present: false
399
+ path: A
400
+ signals_used: [filesystem, hadolint, llm, observability-markers]
401
+ gate_demo_fired: false
402
+ gate_observability_fired: false
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Caveats
408
+
409
+ - File presence ≠ working artifact. Only runtime verification (a separate manual pass) catches broken Dockerfiles / deploy workflows.
410
+ - Hadolint catches form, not intent. A "clean" Dockerfile that installs nothing still lints green.
411
+ - PaaS probe list needs periodic update — new deploy targets appear faster than the list evolves.
412
+ - LLM sample is 1 README. Repos with multi-file docs (`docs/`, `CONTRIBUTING.md`, wiki) may be under-scored on the interpretive signal. Consider glob-and-concat for docs-heavy repos.
413
+ - Localhost grep is a noisy counter-signal even with env-var exclusion — LLM notes should adjudicate when the count is > 0 but reasonable.
414
+ - Two-signal DEMO gate may still false-negative on repos whose README has a heading like `## Getting Started` that only says `npm run dev` (LLM catches this; mechanical does not). That's why LLM is in the AND, not replaced.