PLAN: embedeval context-diagnose
April 18, 2026 ยท View on GitHub
Project: embedeval Task: New CLI command that tells a team which FAILURE-FACTORS categories (and by extension, which High-strength factor IDs) their CLAUDE.md / context pack fails to cover, based on their own benchmark run vs. the expert reference. Priority: High (sole remaining EmbedEval product play before maintenance mode) Created: 2026-04-19
๐ฏ Executive Summary
TL;DR: Map every failed check in a team tracker to its FAILURE-FACTORS category and surface categories where the team trails the expert pack, along with the High-strength factor IDs they should encode into CLAUDE.md.
What We're Doing
New subcommand embedeval context-diagnose --team X --expert Y that:
- Parses
docs/LLM-EMBEDDED-FAILURE-FACTORS.mdto build acheck_name โ category_lettermap (the**EmbedEval checks mapped:**lines already encode this โ no human curation needed). - Loads the team + expert trackers, tallies each tracker's failed checks per category.
- Flags categories where team failure rate exceeds expert failure rate by โฅ10pp (configurable).
- For each flagged category, lists the High-strength factor IDs from
expert-coverage.mdso the user knows which principles to add.
Why It Matters
After the context-quality mode session, the whole stack now tells a team that their context is weak (Lift/Gap, per-case effect, harmful breakdown). But it does not tell them where โ which specific embedded principles to encode. Factor-level diagnosis closes that loop. It is also the one diagnostic no competing tool can produce: semgrep doesn't understand context, hiloop catches code bugs not context gaps.
This feature locks in EmbedEval's thought-leadership angle ("measure & diagnose team context") so the project can go into maintenance while hiloop takes over commercial work.
Key Decisions
- D1 โ Category-level diagnosis only, factor-level deferred to v2. Reason: the
**EmbedEval checks mapped:**lines in FAILURE-FACTORS are per-category. Per-factor mapping would need human curation of ~100 check names. Ship value with zero curation cost; factor-level only if v1 gets real usage. - D2 โ Compare team โ expert, NOT bare โ team. Reason: the right question is "does team's CLAUDE.md cover the principles expert.md covers?" Bare is the baseline; the measurement we want is the residual gap to the ceiling.
- D3 โ New command, not a flag on
context-compare. Reason: different intent (diagnostic, not comparison) and different output shape (per-category failed_check aggregation, not pass-rate delta). Separate UX surface is clearer for the sales pitch ("run diagnose โ get action items"), even though both share tracker loading. - D4 โ Extract the factor parser into a reusable module. Reason:
scripts/build_expert_pack.pyalready parses factor tables; extending it with**EmbedEval checks mapped:**parsing while keeping script + new diagnose module in sync argues for movingparse_factorstosrc/embedeval/failure_factors.py. Both callers import from there. - D5 โ Unmapped checks log a warning, don't fail. Reason: new checks may be added to case static.py before FAILURE-FACTORS gets updated. Graceful degradation beats hard failure; drift shows up in stderr.
Estimated Impact
- Complexity: Medium (new module + CLI + parser refactor; no schema changes)
- Risk Level: Low (additive, no existing behavior changes)
- Files Changed: ~7 files
- Estimated Time: 10-12h (1.5-2 days)
โ ๏ธ REVIEW CHECKLIST โ Verify before /execute
Critical Decisions to Verify
- D1 scope call โ category-level enough for v1, or do we need factor-level from day one? (My position: category ships in 1.5 days, factor needs ~3 days of curation and hasn't been validated by a user.)
- D2 comparison direction โ diagnose against expert (ceiling) vs. against bare (baseline). Different questions, different outputs.
- D3 separate command vs. flag โ
context-diagnoseas a new command vs.context-compare --diagnose-factors. Duplication risk is low because the diagnosis output shape is different enough. - Threshold for "needs coverage" โ default 10pp gap feels right but is unvalidated. Configurable via
--gap-threshold.
Code Impact to Review
- New module
src/embedeval/failure_factors.pyโ check that movingparse_factorsfromscripts/build_expert_pack.pykeeps the drift CI passing. - New module
src/embedeval/context_diagnose.pyโ factor rollup, category aggregation. -
src/embedeval/cli.pyโ addcontext-diagnosesubcommand following the same pattern ascontext-compareandharmful-inspect. - No schema changes โ
CaseResult.failed_checksandTrackerDatastay as-is.
Testing Coverage
- Mapping parser: every
**EmbedEval checks mapped:**line yields the right category letter for every listed check. - Unknown check in tracker logs warning but doesn't crash diagnosis.
- Threshold boundary: gap exactly at threshold โ flagged or not (spec the exact semantics).
- E2E: uart mock run โ diagnose โ JSON schema check.
- Regression:
build_expert_pack.pystill works afterparse_factorsmove.
Business Logic
- Does category-level output actually give users enough to act? Example output: "Category D: 8 failed checks (expert: 1). High-strength factors in D: D1, D2, D4, D5. Start with D5." Is this the right unit of advice?
- Does the command work without a bare tracker? (Decision: yes โ diagnose is team-vs-expert, bare is optional context.)
- Output when team == expert (well-covered): what does success look like? ("No gaps found. Your context matches the expert reference ceiling.")
๐ Prior Work
Related Documents
- [[plans/PLAN-context-quality-mode]] โ parent feature.
context-diagnoseis the factor-level extension of whatcontext-comparestarted. - [[plans/PLAN-per-case-effect-classification]] โ per-case direction of effect.
context-diagnoseis the category aggregate of the same signal. docs/LLM-EMBEDDED-FAILURE-FACTORS.mdโ the source of truth for factor taxonomy andcheck โ categorymapping.src/embedeval/context_packs/expert-coverage.mdโ machine-generated factor-by-category reference thatcontext-diagnosepoints users back to.src/embedeval/harmful_inspect.pyโ precedent for "new CLI command that reads trackers and produces diagnostic output". Same architectural shape.scripts/build_expert_pack.pyโ already parses factor tables; will be refactored to share a common factor-parsing module.
What Worked Before
harmful-inspectshows the pattern works: read trackers, classify, emit JSON + table, pure offline analysis. No new storage, no schema migrations, ~300 lines of code.build_expert_pack.pyshows**EmbedEval checks mapped:**is reliably parseable โ the drift CI has been green.
Known Blockers / Pitfalls
CaseResult.failed_checksis populated fromCheckDetail.check_name. These names are the same tokens used in FAILURE-FACTORS' mapping lines (volatile_error_flag,dma_config_called, etc.). Verified empirically incases/threading-001/checks/static.py.- Some checks may appear in multiple categories' mapping lines (shared-use checks). Spec says: a check belongs to the first category it appears in (by alphabetical letter), and we emit a test that asserts no duplicates exist unless intentional.
Decisions to Reuse
- Pydantic v2 frozen models with
@computed_fieldfor serialized derived values (CategoryComparison pattern). - CLI command structure mirrors
context-compareandharmful-inspect:--bare/--team/--expertwith--output-json. _resolve_modelhelper pattern fromharmful_inspect.pyโ shared across commands that load multiple trackers.
๐ Problem Analysis
What
After running EmbedEval with their team's CLAUDE.md, the user sees Lift/Gap numbers but has no actionable mapping to "which principles is my CLAUDE.md missing?". The FAILURE-FACTORS doc spells out 42 factors across 6 categories and maps checks to categories, but nobody wires this into the run output.
Why
Three strategic reasons:
- Closes the context-quality loop: measurement without prescription stops at "you're below expert". Diagnosis converts that into "here are the factor IDs to add to CLAUDE.md".
- EmbedEval's unique value: hiloop catches code bugs, semgrep catches patterns, neither can tell a team where their implicit-knowledge encoding is weak. Factor-level diagnosis can, because the measurement substrate is the team's own LLM output.
- Enables hiloop sales motion: "EmbedEval CQM + diagnose first โ only the residual Gap is a hiloop problem". A concrete funnel from free research tool to paid verification product.
Success Criteria
-
embedeval context-diagnose --team X --expert Yemits a table of categories ranked by gap, with High-strength factor IDs listed per flagged category. -
--output-jsonwrites the full breakdown (every category, every failed check, every factor ID). - Unmapped checks produce a warning with the unmapped names but don't crash.
- CI drift gate still passes (refactored
parse_factorsstays in sync with expert-coverage.md). -
--gap-threshold <pp>configurable (default: 10.0). - โฅ10 new unit tests + 1 e2e test.
-
mypy --strictandruffclean on all changed files.
๐ Code Review
Current State
scripts/build_expert_pack.pyhasparse_factors(markdown)returninglist[Category]. It parses the| A1 | ... |rows but does not parse the**EmbedEval checks mapped:**trailer lines โ we need to add that.src/embedeval/harmful_inspect.pyis a clean template for a new CLI-oriented analysis command. Reuse its structure (enum + pydantic model + classifier + formatter + CLI plumbing).src/embedeval/test_tracker.py::CaseResult.failed_checksis already populated with check names that match FAILURE-FACTORS vocabulary. No migration needed.
Affected Components
| File | Change |
|---|---|
src/embedeval/failure_factors.py | NEW โ extracted Factor, Category, parse_factors, plus new parse_check_category_map |
src/embedeval/context_diagnose.py | NEW โ CategoryDiagnosis, CoverageDiagnosis, diagnose_coverage, format_diagnosis |
src/embedeval/cli.py | New context-diagnose subcommand |
scripts/build_expert_pack.py | Import Factor, Category, parse_factors from failure_factors module โ no duplicate parsing logic |
tests/test_failure_factors.py | NEW โ parser tests (categories, factors, check-category map) |
tests/test_context_diagnose.py | NEW โ diagnose module unit tests |
tests/test_context_quality_mode_e2e.py | Extend with context-diagnose e2e |
docs/CONTEXT-QUALITY-MODE.md | New section after "Per-case effect classification" |
Dependencies
- No new third-party libs. Pure stdlib
refor parsing; Pydantic v2 for schema; Typer for CLI (already in deps).
๐๏ธ Technical Design
Data Flow
docs/LLM-EMBEDDED-FAILURE-FACTORS.md
โ
โผ
failure_factors.parse_check_category_map() โ dict[str, str] (check โ category letter)
โ
results/team/test_tracker.json โโ โ
โโโบ context_diagnose.diagnose_coverage(trackers, map)
results/expert/test_tracker.json โ โ
โผ
CoverageDiagnosis
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โผ โผ โผ
stdout table --output-json warnings (stderr)
Schema
class CategoryDiagnosis(BaseModel):
category: str # "D"
category_title: str # "Memory Model & Concurrency"
team_failed_checks: list[str] # sorted, unique
expert_failed_checks: list[str]
team_failure_rate: float # team_failed / total_checks_in_category
expert_failure_rate: float
gap: float # team_failure_rate - expert_failure_rate
needs_coverage: bool # gap > threshold
high_strength_factors: list[str] # ["D1", "D2", "D4", "D5"] for flagged categories
factor_names: dict[str, str] # {"D1": "volatile misuse", ...}
class CoverageDiagnosis(BaseModel):
model: str
gap_threshold: float
per_category: list[CategoryDiagnosis] # sorted by gap desc
unmapped_checks: list[str] # checks in tracker with no factor mapping
Parser Extension
failure_factors.parse_check_category_map(markdown) finds every line matching:
**EmbedEval checks mapped:** `check_a`, `check_b`, `check_c`, ...
inside each ## X. Title section, and emits {check_a: X, check_b: X, ...}. The parser state machine already in parse_factors tracks the current category letter; the new parser reuses that.
Output Sample
Context Coverage Diagnosis (model: claude-code://sonnet)
Gap threshold: 10pp (categories above this need CLAUDE.md improvement)
Category Team Expert Gap High factors to cover
-------------------------------------------------------------------------------
D. Memory Model & Concurrency 53% 7% +46pp D1, D2, D4, D5
E. Error Handling & Safety Patterns 38% 12% +26pp E1, E2, E3, E4, E7
B. Temporal & Real-Time Constraints 20% 10% +10pp B1, B3, B4
-------------------------------------------------------------------------------
A. Hardware Awareness Gap 18% 15% +3pp (within threshold)
C. Memory & Resource Constraints 12% 10% +2pp (within threshold)
F. Toolchain, SDK & Platform Knowledge 8% 7% +1pp (within threshold)
To improve coverage:
D. Memory Model & Concurrency โ add principles for factors D1, D2, D4, D5
See docs/LLM-EMBEDDED-FAILURE-FACTORS.md#d-memory-model--concurrency
E. Error Handling & Safety Patterns โ add principles for factors E1, E2, E3, E4, E7
See docs/LLM-EMBEDDED-FAILURE-FACTORS.md#e-error-handling--safety-patterns
Unmapped checks (warning): 3
new_check_001, new_check_002, new_check_003
These check names aren't mapped in FAILURE-FACTORS.md. Update it, or
the checks don't affect any category's diagnosis.
Why NOT factor-level in v1
The **EmbedEval checks mapped:** lines group checks by category, not by factor. Going factor-level means:
- Manually annotating ~100 check names with factor IDs (
volatile_error_flagโ D1,memory_barrier_presentโ D2, etc.). - Shipping a
check_factor_map.yamlas a new hand-curated artifact. - Accepting the curation drift risk (a new check added to static.py has no factor ID until someone updates the map).
For v1, category-level already gives users the actionable pointer ("add principles for D category, here are the 4 factors"). If users report that's too coarse, factor-level in v2 takes 1 more day.
๐ Implementation Plan
Phase 1 โ Extract failure_factors module (2-3h)
- Create
src/embedeval/failure_factors.py. - Move
Factor,Category,_CATEGORY_RE,_ROW_RE,parse_factorsfromscripts/build_expert_pack.pyto the new module. - Add new
parse_check_category_map(markdown) -> dict[str, str]that iterates sections and captures**EmbedEval checks mapped:**lines. - Update
scripts/build_expert_pack.pyto import from the new module. - Verify
uv run python scripts/build_expert_pack.py --checkstill passes.
Phase 2 โ context_diagnose module (3-4h)
- Create
src/embedeval/context_diagnose.pywithCategoryDiagnosis,CoverageDiagnosis,diagnose_coverage,format_diagnosis. - Compute
team_failed_checks/expert_failed_checksby iteratingtracker.results[model][case].failed_checksand de-duplicating within each tracker. - Failure rate denominator: total checks mapped to the category (sum over all mapped check names in that tracker's model, whether pass or fail). If a check name never appears in any case result, it's not in the denominator.
- Gap = team_rate โ expert_rate (negative gap = team is better โ no coverage problem, keep in output but
needs_coverage=False). - Collect unmapped checks (checks in tracker that aren't in the check โ category map) into
CoverageDiagnosis.unmapped_checksand emit a singlelogger.warningat the end. -
high_strength_factorsis sourced fromparse_factorsoutput filtered toFactor.strength == "High"within the category letter.
Phase 3 โ CLI (1-2h)
- Add
context-diagnosecommand tocli.pywith--team(required),--expert(required),--bare(optional, reserved),--model,--gap-threshold(default 10.0),--output-json. - Error out when
--teamand--expertshare the samecontext_pack_hash(same warning pattern ascontext-compare).
Phase 4 โ Tests (2h)
tests/test_failure_factors.py (new):
-
test_parse_factors_extracts_all_six_categoriesโ 42 factors, 6 categories. -
test_parse_check_category_map_covers_known_checksโ assertvolatile_error_flag โ D,dma_config_called โ A, etc. -
test_parse_check_category_map_handles_multi_line_trailerโ a category whose mapping line wraps across multiple lines (unlikely per current format but regression-proofed). -
test_no_duplicate_check_mapping_across_categoriesโ fail if a check appears in 2+ categories' mapping lines.
tests/test_context_diagnose.py (new):
-
test_diagnose_flags_category_above_thresholdโ synthetic tracker with 5 D-category checks all failing in team, 0 in expert โ D flagged, gap 100%. -
test_diagnose_does_not_flag_category_below_thresholdโ 5% gap < 10pp default โneeds_coverage=False. -
test_diagnose_negative_gap_is_not_flaggedโ team better than expert โ not flagged. -
test_diagnose_returns_high_strength_factors_per_flagged_categoryโ D flagged โhigh_strength_factorsincludes D1, D2, D4, D5 (from FAILURE-FACTORS). -
test_diagnose_unmapped_check_logs_warning_not_crashโ checknonexistent_thingin tracker โ appears inunmapped_checks, diagnosis still succeeds. -
test_diagnose_json_export_has_full_schemaโ round-trip viamodel_dump_json, assert every field present. -
test_diagnose_sorts_by_gap_descendingโ largest gap first.
tests/test_context_quality_mode_e2e.py (extend):
-
test_context_diagnose_cli_produces_json_with_full_schemaโ uart ร mock ร bare/team/expert โembedeval context-diagnoseCLI โ JSON schema check.
Phase 5 โ Docs (1h)
- Add "Diagnosing context coverage gaps" section to
docs/CONTEXT-QUALITY-MODE.md, after "Per-case effect classification" and before "Inspecting harmful cases". - Example output + workflow ("run context-compare โ see Gap โ run context-diagnose โ see factor IDs โ add to CLAUDE.md โ re-run").
- Update
README.mdif there's a CLI command list (there isn't โ skip).
๐งช Testing Strategy
Unit Tests
Covered per phase above. Targets:
- Parser: every category produces the right check list; no duplicates; all 42 factors parsed.
- Aggregation: correct gap calc, correct threshold behavior, correct sorting.
- Serialization:
CaseEffect-style string enum serialization;model_dump_jsonround-trip. - Error path: unmapped check doesn't crash; empty tracker errors cleanly.
Integration / E2E
- Extend existing
test_context_quality_mode_e2e.pywith one new test that runs the fullembedeval context-diagnoseCLI path with mock model + uart category. Validates schema, doesn't validate content (mock is context-independent).
Manual Testing
embedeval run --model mock --category isr-concurrency --output-dir runs/team(no pack โ simulates weak team).embedeval run --model mock --category isr-concurrency --context-pack expert --output-dir runs/expert.embedeval context-diagnose --team runs/team --expert runs/expertโ confirm table rendering.
โ ๏ธ Risks & Mitigation
| # | Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|---|
| R1 | FAILURE-FACTORS format drift breaks the parser | Medium | High | CI drift check already enforces this (build_expert_pack.py --check); test_parse_check_category_map_covers_known_checks adds a content-level regression |
| R2 | Some checks in cases/ aren't in any FAILURE-FACTORS mapping line | High | Low | unmapped_checks warning surfaces the gap; doesn't block diagnosis; follow-up task updates FAILURE-FACTORS |
| R3 | Category-level too coarse, users want factor-level | Medium | Medium | Ship v1 and listen for signal; factor-level v2 is ~1 day of work on top of v1 |
| R4 | Moving parse_factors out of scripts/build_expert_pack.py breaks the drift CI | Low | Medium | Phase 1 keeps the script's public behavior identical; drift test re-runs as part of the move |
| R5 | Team vs. expert comparison misleading when trackers cover different case sets | Medium | Medium | Reuse the case-count mismatch warning from context_compare.compare_runs โ emit at diagnosis time too |
| R6 | Duplicate check name appears in multiple categories' "checks mapped" lines | Low | Low | Unit test enforces no duplicates; document policy (first-alphabetical wins if ever needed) |
โ Success Criteria (Recap)
-
embedeval context-diagnose --team X --expert Yprints a ranked table and exits 0. -
--output-jsonround-trips a fullCoverageDiagnosispayload. -
--gap-threshold <float>tunable; default 10.0. - Unmapped checks warn, don't crash.
-
parse_factorsextraction to new module leavesbuild_expert_pack.py --checkgreen. - โฅ10 new unit tests + 1 e2e test all pass.
-
uv run mypy src/anduv run ruff check src/clean. -
docs/CONTEXT-QUALITY-MODE.mdhas a "Diagnosing context coverage gaps" section. - Full test suite passes (1364 + 10 โ 1374).
๐ Estimated Effort
| Phase | Scope | Time |
|---|---|---|
| Phase 1 | failure_factors module extract + check-category parser | 2-3h |
| Phase 2 | context_diagnose module (aggregation, warnings, schema) | 3-4h |
| Phase 3 | CLI wiring | 1-2h |
| Phase 4 | Tests (unit + e2e) | 2h |
| Phase 5 | Docs | 1h |
| Total | 10-12h (1.5-2 days) |
๐ซ Out of Scope (NON-GOALS)
- Factor-level mapping. Requires human curation of ~100 check names. Revisit only if v1 sees real usage and users ask for more specificity.
- Principle text auto-suggestion from expert.md. Expert.md sections don't have machine-readable factor-ID tags. v2 could add a
factor_ids:YAML frontmatter to each expert.md section; not now. - Hiloop rule targeting. JSON output is enough for a sister tool to consume; active integration (emit hiloop-compatible rule IDs) is hiloop's side to pick up when it wants the flywheel closed.
- CI regression gate for Lift drop. The CI recipe in
docs/CONTEXT-QUALITY-MODE.mdalready points at context-compare's JSON; a gate on diagnose output is a separate follow-up. - Per-model category prior (e.g., "Haiku is known to be weak in D, correct for that"). This is measurement normalization; out of scope.
๐ Follow-up Work (After This PLAN)
- Factor-level diagnosis (v2) โ requires
src/embedeval/context_packs/check_factor_map.yaml(hand-curated). About 1 day if users ask. --suggest-principlesโ emit a ready-to-paste CLAUDE.md snippet per flagged category, sourced from the relevantexpert.mdprinciple paragraph. Requires section-level factor_id tagging in expert.md.- Hiloop integration memo โ write down the handshake so hiloop can consume
context-diagnose --output-jsonto scope which rule packs to recommend for a given team's weak categories. - EmbedEval goes into maintenance mode โ with this feature shipped, the public surface is stable. Only additions after this: new TCs and new check definitions as hiloop telemetry reveals gaps.
๐ References
plans/PLAN-context-quality-mode.mdโ parent measurement featureplans/PLAN-per-case-effect-classification.mdโ sibling diagnosticdocs/LLM-EMBEDDED-FAILURE-FACTORS.mdโ factor + check taxonomydocs/CONTEXT-QUALITY-MODE.mdโ user-facing docs the new section will joinscripts/build_expert_pack.pyโ will be refactored to sharefailure_factorsmodule
Status: Draft v1 โ 2026-04-19
Next: Review the โ ๏ธ REVIEW CHECKLIST above. Decide on D1/D2/D3 and threshold default. Then /execute context-diagnose runs Phase 1โ5.