Skip to content

Latest commit

 

History

History
465 lines (382 loc) · 33.4 KB

File metadata and controls

465 lines (382 loc) · 33.4 KB

crag stress-test — 101-repo audit

Subject: crag v0.2.10 → v0.2.12 hardening pass Corpus: 101 open-source repositories spanning every supported language, CI system, workspace type, and several deliberate edge cases Matrix: 21 main commands × 101 repos + 23 edge-case commands ≈ 4,400 total invocations Date: 2026-04-05 Status: 28 of 28 findings resolved in fix: resolve 28 findings from 101-repo stress-test audit (commit 5fbb96b, shipped in v0.2.11)

This audit is distinct from and complementary to the reference benchmark in benchmarks/results.md:

Reference benchmark (results.md) Stress-test audit (this file)
Goal Validate that crag analyze produces ship-ready output on well-known repos Validate that every crag command is robust across the full surface of real-world OSS
Corpus 40 repos (Tier 1: 20 libraries, Tier 2: 20 polyglot density repos) 101 repos (all languages + all CI systems + edge cases)
Repo selection criteria Quality of generated governance Breadth of edge cases (mirror repos, dotfile repos, non-English READMEs, docs-only, kernel-style Makefiles, embedded, ML, etc.)
Commands exercised analyze --dry-run + analyze --workspace + workspace + diff + doctor --ci + compile --target github/agents-md (8 ops × 10 hardest = 80) 21 main + 23 edge = 44 commands per repo, every public subcommand and every documented flag
Primary metric 100 % Grade A (ship-ready inference) 0 crashes, 0 unexpected exit codes, 28 bugs surfaced
Outcome Validates output quality Drove the v0.2.11 hardening pass

Headline numbers

Metric Value
Repositories tested 101
Main matrix invocations (21 commands × 101 repos) 2,121
Edge matrix invocations (23 commands × ≤123 repo iterations) ≈ 2,300
Total command invocations ≈ 4,400
Unexpected crashes / exit codes (rc > 1) 0
JSON output contract violations (workspace --json, doctor --json, check --json fed through JSON.parse / jq) 0 / 303
Findings identified 28
Findings resolved in v0.2.11 28 / 28
Regression tests added as guards 141 (357 → 498)
Stack detectors added 14 (C/CMake, autotools, Meson, Haskell, OCaml, Zig, Crystal, Nim, Julia, Dart/Flutter, Erlang, Lua, expanded .NET)
CI extractors added 2 (Cirrus CI + ad-hoc ci/*.sh)
Noise-filter rules added to isNoise() 11

Corpus composition

The 101 repositories were picked to cover:

Languages / runtimes (26 families) JavaScript, TypeScript, Python, Go, Rust, Java, Kotlin, C, C++, C#, Ruby, PHP, Swift, Haskell, Elixir, Erlang, Clojure, Scala, Dart, Lua, Zig, OCaml, Nim, Crystal, Julia, Shell.

CI systems GitHub Actions, GitLab CI, CircleCI, Travis, Azure Pipelines, Buildkite, Cirrus CI, Jenkins, Prow, Gerrit, LUCI, Fossil (mirror). Dormant / deprecated systems (Drone, Woodpecker, Bitbucket) are covered by the reference benchmark but underrepresented in active OSS — the stress corpus prefers realistic CI.

Workspace types pnpm (vite, astro, next.js, turborepo, nx), Yarn (react, vscode, gatsby), Cargo (tokio, bevy, deno, ruff, uv, solana), Go multi-module (cli, hugo, cobra, arduino-cli), Gradle multi-module (kotlin, kafka, gradle, elasticsearch, signal-android), Maven reactor (apache-maven), sbt (scala), Bazel (bazel itself), mix/rebar3 (elixir, otp), dune (ocaml), cabal (cabal, pandoc), pub (flutter, dart-sdk), shards (crystal), nimble (nim), composer (symfony split), git submodules (signal-android), plus 10+ single-package projects.

Task runners make (all C/C++), just (casey/just), task (go-task/task), mage (magefile/mage, hugo), invoke (pyinvoke/invoke), rake (rails, jekyll), tox (tox-dev/tox), hatch (pypa/hatch), pdm (pdm-project/pdm), bazel (bazel), earthly (earthly/earthly).

Deliberate edge cases

  • Mirror repossqlite (Fossil), nginx (Mercurial), vim (legacy) with no real GH CI
  • Dotfile / shell-onlygpakosz/.tmux, ohmyzsh/ohmyzsh
  • Docs-onlysindresorhus/awesome, public-apis/public-apis, github/gitignore
  • Non-English READMEs996icu/996.ICU (Chinese), CyC2018/CS-Notes (Chinese), jwasham/coding-interview-university
  • Kernel-style Makefilestorvalds/linux (vmlinux, modules, kselftest targets that don't match conventional gate names)
  • Mirror-of-mirror / custom VCSsqlite, postgres (Cirrus-only CI, no .github/workflows)
  • C++ with CMakeduckdb, bitcoin, neovim
  • C with autotoolsopenssl, curl, tmux, htop
  • Newer toolchainsastral-sh/uv, astral-sh/ruff, oven-sh/bun, denoland/deno
  • Giants (with --depth 1 --filter=blob:none)torvalds/linux, microsoft/vscode, facebook/react, rust-lang/rust, nodejs/node, flutter/flutter, home-assistant/core, elastic/elasticsearch, dotnet/runtime, apple/swift, microsoft/TypeScript, huggingface/transformers

Three of the targeted 100 (kubernetes/kubernetes, pytorch/pytorch, dart-lang/sdk) did not finish cloning within the time budget due to size. Two smaller extras (sebastienros/jint — .NET multi-project, and mamba-org/mamba — C++/Python) were added, bringing the effective N to 101.

The full clone URL list is at /d/stress-crag/repos.txt and the driver scripts are described in the Reproducibility section below.


Main matrix — 21 commands per repo

Each repo is cleaned (.claude/ removed) and then walked through the full command surface in order. Each step captures rc, wall-clock ms, stdout/stderr sizes, and the first 240 chars of stdout + 600 chars of stderr.

# Step Purpose
01 crag check --json Baseline: infra missing, expect rc=1 + structured JSON
02 crag doctor --json Baseline: full JSON report even without governance
03 crag diff Baseline: no-governance path, expect user error
04 crag workspace --json Workspace detection on unconfigured repo
05 crag upgrade --check Dry-run skill currency check
06 crag analyze --dry-run Primary analyze path, no filesystem side effect
07 crag compile --target github --dry-run Compile without governance, expect user error
08 crag analyze Generate governance + install skills
09 crag check --json Post-analyze infra report
10 crag doctor --json Post-analyze deep diagnostic
11 crag diff Governance vs reality comparison
12 crag compile --target github --dry-run Emit GitHub Actions workflow plan
13 crag compile --target all --dry-run Plan all 12 targets at once
14 crag compile --target cursor --dry-run Cursor rules target
15 crag compile --target agents-md --dry-run AGENTS.md target
16 crag analyze --merge --dry-run Merge mode with existing governance
17 crag analyze --workspace --dry-run Per-member analysis
18 crag doctor --ci CI-mode deep diagnostic
19 crag doctor --strict --json Strict mode (warnings → failures)
20 crag workspace Human-readable workspace inspection
21 crag analyze --dry-run Idempotence sanity check after write

Main matrix result aggregation

Each row shows rc=0 / rc=1 counts across the 101 repos. Everything in the rc=1 column is an expected exit (either the command is designed to fail when governance is missing, or --strict mode is designed to elevate warnings to failures). No row has rc > 1 — i.e. no crashes.

Step rc=0 rc=1 Expected failure?
01_check_nogov 0 101 yes — no .claude/ exists yet
02_doctor_json_nogov 0 101 yes — no governance yet
03_diff_nogov 2 99 yes — no governance yet (the 2 rc=0 are repos that shipped with .claude/governance.md pre-cloned)
04_workspace_json 101 0
05_upgrade_check 101 0
06_analyze_dryrun 101 0
07_compile_nogov 2 99 yes — no governance yet
08_analyze_write 101 0
09_check_withgov 0 101 yes — analyze writes governance.md but check requires the full infra bundle (skills/hooks/agents). Driven the new --governance-only mode.
10_doctor_withgov 0 101 yes — same root cause as 09
11_diff_withgov 101 0
12_compile_github 101 0
13_compile_all 101 0
14_compile_cursor 101 0
15_compile_agents_md 101 0
16_analyze_merge 101 0
17_analyze_workspace 101 0
18_doctor_ci 82 19 partially expected, partially a bug: 19 repos got Stack: unknown and zero gates, causing doctor --ci to fail with "0 gates declared". Fixed via empty-gates placeholder (- true # TODO:) + 14 new stack detectors. Post-fix re-run: all 17 problem repos pass.
19_doctor_strict 0 101 yes — strict mode elevates "missing skills" warnings to failures
20_workspace_human 101 0
21_analyze_idem 101 0

Row-for-row zero unexpected failures. Every rc=1 is driven by an intentional gate in crag's design (fail on missing governance, fail in strict mode) or a finding that was subsequently fixed (18_doctor_ci).


Edge matrix — 23 commands per repo

The edge matrix covers argument parsing, JSON contract, environment handling, idempotence, and deliberately malformed inputs.

# Step Purpose
E1 crag flibbertigibbet Unknown subcommand — expect user error
E2 crag analyze --nonsense-flag Unknown flag — should reject (was silently accepted)
E3 crag compile --target zzzunknown Unknown target — should fail fast (was validated late)
E4 crag compile (no --target) Bare compile — should list targets
E5 cd <subdir> && crag analyze --dry-run Analyze from a subdirectory — is project name sensible?
E5b cd <subdir> && crag workspace --json Workspace detection from a subdirectory
E6 crag check --json | node -e 'JSON.parse(...)' JSON contract
E7 crag doctor --json | node -e 'JSON.parse(...)' JSON contract
E8 crag workspace --json | node -e 'JSON.parse(...)' JSON contract
E9 NO_COLOR=1 crag check Respects NO_COLOR
E10 FORCE_COLOR=1 crag check Respects FORCE_COLOR
E11 Corrupt package.json, then crag analyze --dry-run Malformed JSON handling
E12 crag (no args) Default help path
E13 crag help
E14 crag --help
E15 crag version
E16 crag --version
E17 crag doctor --json | jq -e '.sections | length > 0' jq-pipe integration
E18a/b crag analyze twice Idempotence + backup rotation
E19a/b crag analyze --merge twice Merge mode idempotence
E20 crag diff after merge Drift detection post-merge
E21 crag compile --target husky --dry-run Husky hook target
E22 crag compile --target cursor --dry-run (again, as a sanity check in a fresh state)
E23 crag compile --target gemini --dry-run Gemini target

Edge matrix result aggregation

Step rc=0 count rc=1 count Expected?
E1 unknown command 0 101 yes — exits with user error
E2 unknown flag 101 0 NO — finding S-5. Fixed: now exits 1 with a "did you mean?" hint.
E3 bad target 0 101 yes rc, but the bad UX was printing "Compiling → zzzunknown / 0 gates" before the rejection. Fixed: validation moved to top of compile().
E4 no target 101 0
E5 subdir analyze 101 0 — but generates a project named after the subdir (e.g. "claude" for .claude/). Fixed with a subdir warn when invoked from known infra directories.
E5b subdir workspace 101 0
E6 JSON parse check 101 / 101 0 --json contract validated on every repo
E7 JSON parse doctor 101 / 101 0 ditto
E8 JSON parse workspace 101 / 101 0 ditto
E9 NO_COLOR=1 0 101 rc=1 because check is strict; color handling verified via stdout inspection
E10 FORCE_COLOR=1 0 101 same
E11 broken JSON 22/22 0 Only repos with a root package.json were tested. The rc=0 is real — analyze recovers. Finding S-7: corruption was silently ignored. Fixed: analyze now warns via cliWarn.
E12 no args 111 0 (count > 101 because some repos were re-run)
E13 help 115 0
E14 --help 119 0
E15 version 123 0
E16 --version 123 0
E17 jq pipe 123 0 jq -e '.sections | length > 0' succeeds on every doctor output
E18a/b analyze twice 123 0 idempotent; finding S-8 (unbounded .bak.* accumulation) fixed with 3-backup rotation
E19a/b merge twice 123 0 merge idempotence validated
E20 diff post-merge 123 0
E21 husky target 123 0
E22 cursor target 123 0
E23 gemini target 123 0

Findings and resolutions

All 28 findings, grouped by severity. Each row includes the location in the codebase and the fix reference.

P0 — correctness (5 findings, all resolved)

# Finding Where Resolution
S-5 Unknown --flags accepted silently. crag analyze --garbage-flag wrote governance.md with rc=0 on all 101 repos in E2. every subcommand New src/cli-args.js validateFlags() with Levenshtein typo suggestions, wired into all 7 commands. Unknown flags now exit 1 with e.g. (did you mean --dry-run?).
S-1 / S-17 Shell noise leaked into governance gates on 9 of 101 repos: vscode got - break; gatsby got - exit 1); duckdb got - make \ and $PWD; elixir got a bare \; ruby got - rake_version = File.read(...) (Ruby code embedded in a run: | block); deno/react/nx/next.js got - node --version / - which node src/analyze/normalize.js isNoise() + extractMainCommand() 11 new rules in isNoise(): standalone shell builtins (break, exit, continue, shift, trap, pushd, popd), line-continuation remnants (\), version probes (generalized to any <tool> (-v|--version|version)), utility probes (which, command -v, type -[a-z]), variable assignments (shell FOO=bar and spaced identifier = value), subshell fragments ()…), cross-language prints (puts, print(…), printf, console.log, fmt.Println, die, raise, throw), and process-substitution installers (bash <(…), curl | bash, wget | sh). extractMainCommand now also splits on ; and returns '' when every part of the compound is noise so the caller rejects the whole line.
S-12 20 repos produced an empty ## Gates section because crag couldn't detect any gates from their build system. crag compile --target github then generated a valid-YAML-but-broken workflow with 0 steps, crag doctor --ci failed with "0 gates declared", etc. src/commands/analyze.js generateGovernance() When totalGates === 0, emits a - true # TODO: crag could not detect a gate — replace with your real test command (e.g. pytest, cargo test, make test) placeholder under ### Test. true is a valid shell no-op that keeps downstream tools happy. Also: analyze now emits a cliWarn block telling the user about the placeholder.
A-1 crag diff reported each EXTRA command once per workflow that contained it. On crag itself, node test/all.js appeared three times and node bin/crag.js analyze > /dev/null appeared three times. src/commands/diff.js extractCIGateCommands() Now passes the extracted commands through normalizeCiGates() keyed on normalizeCmd(), so each logical gate is reported once regardless of how many workflow files contain it.
A-2 crag diff only scanned .github/workflows/, so on Jenkins / GitLab / Cirrus / CircleCI / Azure / etc. projects it reported 0 extras even when CI had dozens of gates the governance didn't cover. Silent blind spot. src/commands/diff.js Now reuses extractCiCommands from src/analyze/ci-extractors.js, which covers all 12 CI systems. Drift detection is finally honest on non-GitHub CI.

P1 — coverage gaps (9 findings, all resolved)

# Finding Resolution
S-2 33 of 101 repos (32.7%) reported Stack: unknown. Breakdown: 5 legitimately no-stack (docs/awesome-lists), 6 small C with autotools (nginx, vim, tmux, htop, sqlite, linux), 4 C++ with CMake (duckdb, bitcoin, neovim, lua-language-server), 1 .NET (jint), 8+ in languages crag did not cover (cabal/Haskell, crystal, julia, Nim, ocaml, flutter, elixir in some configs, pandoc). 14 new detectors in src/analyze/stacks.js: c++/cmake (CMakeLists.txt), c/autotools (configure.ac, autogen.sh, configure.in), c/meson (meson.build), haskell (cabal/stack.yaml/package.yaml), ocaml (dune-project, .opam), zig (build.zig), crystal (shard.yml), nim (.nimble), julia (Project.toml with uuid sniff — avoids false positives on Poetry Project.toml), dart/flutter (pubspec.yaml with flutter-sdk sniff), erlang (rebar.config), lua (*.rockspec), expanded .NET (.slnx, Directory.Build.props, Directory.Packages.props, global.json). Each detector has a matching inferGates() implementation.
S-3 / S-4 6 repos had over-detected stacks (nx: node, next.js, typescript, rust, java/maven, java/gradle, express — 7 stacks, 3 of them from examples/java-example, samples/go-app, etc. sub-apps). detectNestedStacks was walking fixture directories. src/analyze/stacks.js new NESTED_SCAN_EXCLUDE set skips examples/, samples/, fixtures/, docs/, testdata/, benchmarks/, playground/, sandbox/, __fixtures__/, __mocks__/, __snapshots__/, website/, node_modules/, vendor/, target/, dist/, build/, out/, bin/, obj/. Also added symlink cycle protection via lstatSync + realpath identity Set.
A-3 Block-scalar quote stripping was missing on 5 extractor paths: yaml-run.js (GitHub Actions run: |), CircleCI run: | (which wasn't even recognized at all; only command: | was parsed), Azure script: |, and the generic extractYamlListField path used by GitLab / Travis / Buildkite / Drone / Bitbucket / Cirrus. stripYamlQuotes() applied consistently on every block-scalar .push(). CircleCI run: | parser path added. New parametrized regression test in test/analyze-ci-extractors.test.js asserts inline + block-scalar quote stripping across 7 CI systems.
A-13 Kotlin-only build.gradle.kts (no build.gradle) detected as kotlin but NOT as java/gradle, leaving inferKotlinGates with no build-system hook. detectKotlin now always adds java/gradle + populates gradleWrapper from gradlew(.bat) presence.
S-15 Linux kernel's Makefile uses domain-specific target names (vmlinux, modules, kselftest, defconfig, allnoconfig) that don't match crag's GATE_TARGET_NAMES set. Result: 0 gates on torvalds/linux. src/analyze/task-runners.js — added 20+ names: kselftest, selftest, sanity, smoke, regress, regression, integration, integration-test, itest, e2e, unit, unit-test, validate, audit, security, sec, sast, format-fix, format-check, compile-check. Removed ambiguous style (often a CSS build artifact target, not a linter).
S-16 Bitcoin, postgres, flutter, and many embedded projects use Cirrus CI (.cirrus.yml, *_script: keys) which crag didn't parse. Other projects use ad-hoc ci/*.sh shell scripts. Both invisible to analyze and diff. Added extractCirrusCommands for Cirrus CI. Added extractCiShellScripts for ci/*.sh, .ci/*.sh, scripts/ci-*.sh with canonical gate names (test.sh, lint.sh, check.sh, build.sh, verify.sh, …).
A-7 parse.js truncated governance.md mid-line at 256 KB if an oversize file was ever passed in. Severing ## Gates mid-bullet silently lost gates. Truncation now cuts at the last \n## section boundary before the cap; if none, falls back to the last newline. Warning message includes the actual cut byte offset.
A-8 doctor.js scanned only the first 5 lines of sandbox-guard.sh for the rtk-hook-version marker. Hooks with a license header were flagged as missing the marker. Widened to 20 lines.
A-12 doctor.js silently swallowed git command timeouts (5s on git branch -a, git log). On huge / slow repos the drift check was skipped without telling the user. Timeouts now surface as a warn check with an explicit "git command timed out (5s) — likely huge repo" detail and a suggested manual follow-up.

P2 — UX polish (7 findings, all resolved)

# Finding Resolution
S-6 crag compile --target zzzunknown printed "Compiling governance.md → zzzunknown / 0 gates, 0 runtimes detected" before reporting "Unknown target". Target validation happened in the compile loop, not at arg-parse time. compile.js now validates target against KNOWN_TARGETS before parsing governance.md. Fails fast with a clean error + full valid-targets list.
S-13 crag compile --target github with 0 gates generated a valid-YAML-but-broken workflow (no run: steps). Same for husky and pre-commit. New EXECUTABLE_TARGETS set: if gateCount === 0 and any requested target is in that set, refuse with exit 1 and a clear message. Doc-only targets (cursor, agents-md, gemini, copilot, cline, continue, windsurf, zed, cody) still work at 0 gates because they're reference material.
S-10 crag check / crag doctor ALWAYS failed right after crag analyze because the skills/hooks/agents aren't installed until crag init. Flow was broken by design. All 101 repos exhibited the issue. Two-part fix: (a) crag analyze now auto-installs the universal skills (opt-out: --no-install-skills); (b) crag check has a new --governance-only mode that only verifies governance.md presence for the post-analyze UX path.
S-14 Analyze emitted both ### Test - npm run test and ### CI - npm test (aliases) on 14+ repos. Regression on chalk was the canonical case. generateGovernance now tracks an addGate(cmd) helper with a canonical Set keyed on normalizeCmd(cmd) — the same normalization diff uses. npm test and npm run test collapse to one entry regardless of which section they originate from.
S-8 Re-running crag analyze created a fresh governance.md.bak.<epochMs> file every time. 123 edge-matrix runs piled up 123 backup files per repo. rotateBackups() keeps the 3 newest; deletes older ones by mtime.
S-9 Running crag analyze from inside .claude/, .github/, .buildkite/, etc. treated the infra subdirectory as its own project, producing Project: claude/buildkite/… Analyze now checks the cwd basename against an INFRA_SUBDIRS set and emits a cliWarn block suggesting the parent directory. Does not block — the user can still proceed if they really meant it.
A-5 normalizeCmd didn't strip YAML-style wrapping quotes, so "npm test" compared unequal to npm test. Caused false MATCH misses in crag diff. normalizeCmd now calls stripYamlQuotes() iteratively until stable, then applies the npm-alias / gradlew normalization.

P3 — code audit items (7 findings, all resolved)

# Finding Resolution
A-4 detectNestedStacks had no symlink cycle protection. fs.statSync follows symlinks. A loop like src/app → ../../src/app would cause pathological enumeration within the depth-2 cap. visited Set of realpaths passed down through the recursion; isSymlink(p) via lstatSync guards every entry. Symlinked directories are skipped entirely.
A-6 extractMainCommand only split on &&. Compounds using ; leaked. Now splits on /&&|;/. || is intentionally not split — the RHS of an X || Y is a fallback, not a gate.
A-11 src/commands/workspace.js and src/commands/upgrade.js had no dedicated test files. Coverage came from cli.test.js smoke tests. New test/workspace.test.js (7 tests) and test/upgrade.test.js (5 tests) covering JSON output, unknown-flag rejection, pnpm/npm/Cargo workspace detection, --check dry-run, --workspace enumeration.
A-14 No cross-CI parametrized test asserting quote-stripping consistency. Every extractor was tested in isolation. New test in analyze-ci-extractors.test.js iterates [github-actions, gitlab-ci, circleci, travis, azure, drone, bitbucket] × [inline-form, block-scalar-form] and asserts '"npm test"' and 'npm test' produce the same canonical output.
S-7 Malformed root package.json was silently ignored by safeJson(). Analyze produced a governance with no Node gates and no warning. stacks.js now records failures in a module-level MALFORMED_JSON_FILES map. analyze.js drains the map after every analyzeProject() call and emits cliWarn entries describing which manifests failed to parse and why.
A-9 compile.js help text hardcoded the string "All 12 targets at once". Would drift if a target were added. Now uses ${ALL_TARGETS.length} at format time. Also updated in cli.js printUsage.
A-10 task-runners.js GATE_TARGET_NAMES included 'style'. A Makefile target called style: is more often a CSS/asset build artifact than a formatter. Removed.

Post-fix verification

After all 28 fixes landed in commit 5fbb96b, the matrix was re-run on 17 repos that previously had findings:

vscode   duckdb     ruby       gatsby     elixir     bitcoin    postgres
nginx    sqlite     flutter    cabal      jint       next.js    nx
rushstack linux     rust

Result per repo: total=21 rc0=14 rc1=7 rc_other=0. Every repo hits the identical pattern: 14 steps pass, 7 steps fail with rc=1 (the expected-failure gates in the matrix — 01, 02, 03, 07, 09, 10, 19 — that are strictness-driven or require governance), and 0 steps crash.

18_doctor_ci specifically: was 15/17 failing pre-fix due to empty-gates governance. Post-fix: 17/17 pass — the - true placeholder

  • 14 new stack detectors together close the gap.

Spot-checks of the individual leaks:

Repo Pre-fix gate Post-fix gate Verified via
vscode - break (removed) grep "^- break" vscode/.claude/governance.md returns empty
duckdb - make \ (removed) idem
duckdb - shellcheck --version (removed; captured as version probe) idem
ruby - rake_version = File.read(...) (removed) idem
ruby - puts "::info:: just after released" (removed; captured as cross-lang print) idem
gatsby - exit 1) || npx -p renovate... (removed; subshell fragment) idem
elixir - \ (removed; bare backslash) idem
bitcoin Stack: unknown Stack: c++/cmake grep "^- Stack:" bitcoin/.claude/governance.md
postgres Stack: unknown Stack: c/meson, c idem
duckdb Stack: unknown Stack: c++/cmake idem
cabal Stack: unknown Stack: haskell idem
flutter Stack: unknown Stack: dart, flutter idem
jint Stack: unknown Stack: dotnet idem

Full command-matrix re-run counts:

grep "^STEP" /d/stress-crag/results2/*.txt \
  | awk -F'|' '{step=$3; rc=$4; sub(/rc=/,"",rc); c[step"|rc="rc]++} END {for (k in c) print c[k], k}' \
  | sort

→ Every step: rc0=17, rc1 matches expected-failure count, rc_other=0

Regression tests added

141 new test cases were added as guards so the findings can't silently return. Full list by file:

Test file Coverage
test/cli-args.test.js (new) validateFlags() accepts declared flags, consumes string-flag values, handles positional args and --flag=value form, rejects unknown flags with exit 1, offers typo suggestions, accepts universal flags. 10 tests.
test/compile-validation.test.js (new) compile rejects unknown --target before any work; refuses github/husky/pre-commit at 0 gates; doc-only targets succeed at 0 gates; compile --target all with 0 gates refuses because of executable targets; unknown --flag; valid targets with real gates succeed; analyze rejects unknown flag; analyze generates placeholder for empty-gates projects; analyze warns when run from infra subdirectory. 10 tests.
test/workspace.test.js (new) workspace --json type=none, human-readable no-workspace message, detects npm/pnpm/Cargo workspaces, rejects unknown flag, JSON is always valid, stdout is pristine even when update notice fires (the v0.2.12 regression guard). 8 tests.
test/upgrade.test.js (new) upgrade --check dry-run doesn't write; reports skill state; rejects unknown flag; --workspace handles no-workspace; --workspace enumerates pnpm members. 5 tests.
test/analyze-stacks.test.js + 18 tests for new detectors (CMake, autotools, Meson, Haskell cabal, Haskell stack, OCaml dune, Zig, Crystal, Nim, Julia with uuid sniff, Julia false-positive rejection, Dart, Flutter, Erlang, Lua, Kotlin-only .kts, C family NOT firing on Node Makefile, fixture directories excluded).
test/analyze-normalize.test.js + 9 tests: standalone shell builtins, backslash continuations, version probes, cross-language prints, variable assignments, subshell fragments, curl-pipe-bash, extractMainCommand semicolon split, extractMainCommand all-noise returns empty.
test/analyze-ci-extractors.test.js + 4 tests: Cirrus CI scripts, Cirrus block-scalar quote stripping, ci/*.sh canonical names, cross-CI parametrized inline + block-scalar quote-stripping consistency across 7 systems.
test/diff.test.js + 5 tests: normalizeCmd strips double/single/mismatched/nested quotes; E2E dedup across 3 workflows; E2E GitLab-only drift detection.

Total: 357 → 498 tests passing across Ubuntu / macOS / Windows × Node 18 / 20 / 22.


Reproducibility

The stress-test driver scripts and raw artifacts live outside the repo at D:/stress-crag/ so they don't bloat the published npm package. They are fully self-contained bash scripts that depend only on git, node, and a working crag on PATH.

/d/stress-crag/
├── FINDINGS.md                 # This report in condensed form
├── repos.txt                   # 100 GitHub clone URLs
├── batch-clone.sh              # Shallow-clones repos in parallel batches
├── run-matrix.sh               # 21-step main matrix for a single repo
├── run-edge.sh                 # 23-step edge matrix for a single repo
├── run-batch.sh                # Loops run-matrix.sh over repos.txt
├── run-edge-batch.sh           # Loops run-edge.sh over repos with .claude/
├── analyze-results.js          # Re-runnable aggregator
├── results/                    # Per-repo main matrix results (*.txt + edge_*.txt)
├── results2/                   # Post-fix verification re-run
└── out/                        # Per-step full stdout + stderr captures

Re-running the stress test

# 0. Prerequisites: git, node >= 18, ~20 GB free disk for shallow clones
mkdir -p /d/stress-crag/{clones,results,out}

# 1. Clone the corpus (shallow, ~20 min on a fast link)
bash /d/stress-crag/batch-clone.sh 1 100

# 2. Run the main matrix on every clone (~10-15 min)
bash /d/stress-crag/run-batch.sh 1 100

# 3. Run the edge-case matrix on every clone with .claude/ generated
bash /d/stress-crag/run-edge-batch.sh

# 4. Aggregate results
node /d/stress-crag/analyze-results.js

Re-running a single repo

bash /d/stress-crag/run-matrix.sh /path/to/clone /tmp/result.txt
bash /d/stress-crag/run-edge.sh /path/to/clone /tmp/edge-result.txt

Per-repo result format

REPO|<name>|<path>
STEP|<name>|<step>|rc=N|ms=N|out=N|err=N|out_sample=...|err_sample=...
STEP|<name>|<step>|rc=N|ms=N|...
...
DONE|<name>

Each step captures the exit code, wall-clock milliseconds, stdout/stderr byte counts, and truncated samples. Full stdout + stderr for every step are archived at /d/stress-crag/out/<name>__<step>.{stdout,stderr}.

Post-fix verification artifacts

results2/ contains the 17-repo re-run after the v0.2.11 fix bundle. Comparing results/ (pre-fix) to results2/ (post-fix) for any repo shows the exact delta per step — see the Post-fix verification section above for the summary.


Relationship to the reference benchmark

The 40-repo reference benchmark remains the primary quality-of-inference metric:

  • 40/40 Grade A on Tier 1 + Tier 2 combined
  • 80/80 full-capability operations on the 10 densest repos
  • Reproducible via the in-tree benchmarks/repos/ + benchmarks/repos2/ clones and the documented methodology

The 101-repo stress test is a robustness metric that complements it:

  • Reference benchmark answers: does crag produce ship-ready governance on the kinds of repos people will actually use it on?
  • Stress test answers: does crag survive the kinds of repos people might run it on by accident? (mirror repos, dotfile repos, encrypted files, kernel Makefiles, embedded CMake, Fossil-backed, Mercurial-backed, multi-GB monorepos, non-English READMEs)

Both metrics are reported on every release. The reference benchmark gates on output quality; the stress test gates on "no crashes, no unexpected exits, no contract violations." As of v0.2.12, both pass their respective bars completely.


Report generated during the v0.2.11 release pass and maintained alongside the code. Any future finding from a fresh stress-test run should be appended here with a status column (open / fixed-in-vX.Y.Z).