Skip to content

feat(detections): add 3 semantic labels (log_content, stack_trace, diff_patch) - #13

Merged
bfirestone merged 18 commits into
mainfrom
three-head-part-deux
Apr 11, 2026
Merged

feat(detections): add 3 semantic labels (log_content, stack_trace, diff_patch)#13
bfirestone merged 18 commits into
mainfrom
three-head-part-deux

Conversation

@bfirestone

Copy link
Copy Markdown
Contributor

Summary

Adds the first cross-cutting semantic detection labels to the training annotation pipeline:

  • log_content — embedded log output, with strict density + schema rules (uppercase severity, multi-line density, CSV log records)
  • stack_trace — programmatic tracebacks across Python/Java/.NET/JS/Go/Rust/Ruby, multi-frame with file:line locators required
  • diff_patch — unified diffs / git patches with required structural markers (hunk header / diff --git / adjacent ---/+++)

Removes det_log_lines (redundant with sub_type=log_lines; detection head never trained so removal is free).

Validated via 5k stratified audit + targeted positive injection across 3 annotator models (gemini-3-flash, sonnet-4.6, gpt-5.4-mini, ~$15-20). All 3 new labels have inter-annotator agreement ≥0.9986 (threshold 0.995).

Architectural note

First introduction of detection-only labels — labels that are NOT mirrored by a ContentSubType. Documented via a new SEMANTIC_LABELS: frozenset[str] module constant with invariants enforced by tests (semantic labels must not appear in ROUTING_TABLE, must be a subset of DETECTION_LABELS).

Scope

Training workstream only (training/). No Rust/src/ changes per the workspace boundary rule.

IN scope:

  • Prompt edits to annotate_detections.py (label set, SYSTEM_PROMPT, JSON template)
  • SEMANTIC_LABELS constant
  • Regression-guard tests (7 per new label + architectural invariants + Phase 0 repair of 3 stale iter15 tests)
  • Audit infrastructure: build_audit_sample.py + audit_semantic_labels.py
  • 5k audit execution + analysis + post-audit prompt refinement

OUT of scope (next branches):

  • Full 90k annotation run + retrain with detection head
  • Rust port (copy artifacts, smoke test CLI)

Test plan

  • uv run pytest training/tests/test_annotate_detections.py -v — 51 passed, 1 skipped
  • uv run pytest training/tests/test_build_audit_sample.py -v — 3 passed
  • uv run pytest training/tests/test_audit_semantic_labels.py -v — 3 passed
  • 5k audit ran via trainr data annotate-detections against 3 models
  • Inter-annotator agreement ≥0.995 on all 3 new labels (actual: 0.9986-0.9999)
  • Effective recall ≥0.90 on injected positives for all 3 new labels (100% after adjusting for injection-regex overmatch and applying CSV log refinement)
  • Manual spot-check of disagreement rows — no rule violations found; one CSV ambiguity identified and fixed pre-merge
  • Targeted post-fix verification on 24 CSV log rows — 24/24 fire unanimously across all 3 models

Audit findings

See docs/accuracy_runs/2026-04-10-iteration-16.md for full analysis. Headlines:

  • Raw Criterion 2 recall looked bad (stack_trace 62.5%, log_content 84%) but manual spot-check revealed that 9 of 9 stack_trace "misses" were injection-regex overmatches (rustc test directives, Turkish Azure docs, HTML renderings) — the annotators correctly refused to fire.
  • One real log_content ambiguity caught and fixed: CSV log records with timestamp,level,message columns. The v2 prompt listed nginx/syslog/JSON schemas but not CSV. Fixed in c1ec175; re-annotation of the 24 affected rows shows 100% unanimous firing post-fix.
  • Pre-existing label agreement floor: 12 existing labels fall below 0.995 (worst: plain at 0.9391, markdown at 0.9507). These are likely pre-existing — the 3 new labels added to the same prompt have tighter agreement than most existing labels, so prompt length isn't the cause. Filed as follow-up for rigorous A/B testing if needed.

Key gotcha discovered

python -m trainr.core.annotate_detections silently exits 0 with no output because the module lacks an if __name__ == "__main__": main() guard. The working invocation is uv run trainr data annotate-detections via the click CLI. Filed as a follow-up plan fix.

Spec + plan

  • Spec: docs/superpowers/specs/2026-04-10-semantic-detection-labels-design.md
  • Plan: docs/superpowers/plans/2026-04-10-semantic-detection-labels.md
  • Audit report: docs/accuracy_runs/2026-04-10-iteration-16-audit-report.md
  • Iteration doc: docs/accuracy_runs/2026-04-10-iteration-16.md

…f_patch

Design spec for adding 3 cross-cutting semantic detection labels and removing
det_log_lines. Prompt-only change; validated via 5k stratified audit + targeted
positive injection before committing to the $400-600 full 90k annotation run.

Locks in v2 label definitions after adversarial + standard review of 4
checkpoint questions (density threshold, minimum frames, anti-markdown-list
rule, co-fire guidance).
Bite-sized TDD plan for iter16: prep (fix 3 stale iter15 tests),
phased label changes (remove det_log_lines, add log_content, stack_trace,
diff_patch with regression-guard tests), audit tooling (build sample,
compute metrics), execution (build sample → 3-model annotate → report →
gate → iterate), iteration report + PR.
- Update label count to 40 (was 29, pre-iter15).
- Refresh expected label list to include iter15's 9 language labels.
- Invert routing-table coverage assertion: ROUTING_TABLE keys must be
  in DETECTION_LABELS, not the reverse. Sub_types without an entry
  fall through to the default model by design, and iter16 introduces
  detection-only semantic labels that will never have routing entries.
- Skip test_routing_uses_correct_api_keys when ROUTING_TABLE has no
  anthropic entries (iter15 migrated everything to openrouter).
log_lines remains a ContentSubType. The detection label was redundant
with the sub_type column and will be replaced by a proper cross-cutting
det_log_content in subsequent commits. No trained model depends on
det_log_lines (detection head has never been trained).
Documents which detection labels are NOT mirrored by a ContentSubType.
Empty in this commit — populated in subsequent phases with log_content,
stack_trace, and diff_patch.
First detection-only label — fires on embedded log output anywhere in a
row, distinct from sub_type=log_lines which means the row IS primarily
logs. Strict density+schema definition with uppercase-severity rule to
keep false positives bounded at the ~0.1% target class prevalence.
Two precision refinements to the log_content definition block from
code-quality review:

- JSON log records: clarify that a record may span multiple lines when
  pretty-printed and should be counted as ONE density unit regardless
  of its line count (resolves a contradiction with the top-level
  'consecutive lines' density rule).
- logfmt: require the timestamp/severity co-occurrence to be within
  the same line (via ts=/time=/level=/severity= keys) rather than the
  ambiguous 'co-occurring' wording which could be read as adjacent.
Multi-frame programmatic traceback detection across Python, Java/JVM,
.NET, JS/Node, Go, Rust, and Ruby. Requires two or more frames with
file:line or package.Class.method locators — single-line 'Error: foo'
messages and prose 'at line 5' do not qualify. Co-fires with log_content
when embedded in log output.
Three precision refinements from code-quality review:

- Ruby: use real traceback punctuation (backtick+apostrophe) instead of
  plain single quotes so LLM annotators can match actual Ruby output.
- Frames-vs-lines: clarify that a frame may span multiple source lines
  (Python emits two lines per frame) to prevent confusion with
  log_content's line-counting rule.
- Test brittleness: anchor dotnet_format test on 'File.cs:line 42' and
  cofire test on the full '"stack_trace": 1 AND "log_content": 1'
  string, since bare substrings '.NET' and 'fire BOTH' pre-existed in
  other parts of SYSTEM_PROMPT and would pass even if stack_trace were
  deleted.
Unified diff / git patch detection with strict required-marker rule: at
least one of (@@ hunk header, diff --git header, adjacent --- / +++ pair)
must be present. Line prefixes alone are insufficient — prevents markdown
bullet lists from dominating false positives at 0.1% class prevalence.
…ules

Two precision refinements to the diff_patch definition from code-quality
review, carrying forward the same tightening pattern applied to
log_content in Phase 3:

- 'adjacent lines' disambiguated inline to 'immediately consecutive lines
  with no blank line between them' — a small LLM could otherwise count
  ---/blank/+++ as adjacent. Phrase 'adjacent lines' retained for the
  existing regression-guard test.
- Email-patch header rule: replaced 'co-occurring' (ambiguous, same bug
  we caught in log_content's logfmt rule) with 'within the same
  contiguous block as'. Also labelled the literal 'Mon Sep 17 00:00:00
  2001' constant explicitly as a git-format-patch sentinel to prevent
  small models from treating it as a date placeholder.
Composes the iter16 5k audit sample: stratified sample via the existing
stratified_sample() helper, plus targeted injection of known-positive
candidates for log_content, stack_trace, and diff_patch. Injected rows
are tagged with an audit_source column so the downstream audit can
exclude them from the agreement metric and measure per-label recall on
them separately.
Two low-cost improvements to the error surface for the upcoming Phase 8
run on the real 90k corpus:

- Emit a WARNING on stderr when an injection-regex label matches zero
  rows. A silent missed label would otherwise look identical to a
  successfully-matched label in the final report.
- Validate that the input corpus has required 'sub_type' and 'text'
  columns and raise ValueError with a clear message at the entry point,
  rather than failing deep inside stratified_sample() or polars string
  ops with a confusing downstream error.
Computes per-label inter-annotator agreement on stratified rows
(injected positives excluded — they'd inflate the stat) and per-new-label
recall on injected positives using majority vote across 3 annotator
models. Emits a markdown report with pass/fail verdicts against the
two quantitative pass criteria from the iter16 spec.
Three defensive improvements to audit_semantic_labels.py before the
Phase 8 run on real annotated parquets:

- NaN handling in format_report: a NaN score (no data) now renders as
  'N/A (no data)' / 'N/A (no injected rows)' rather than silently
  comparing False against the threshold and producing a confusing FAIL.
  Missing recall data still counts as criterion-2 failure (can't certify
  recall we couldn't measure).
- Schema drift guards in compute_agreement_across_models: explicit
  ValueError if the 3 model parquets have mismatched det_* columns or
  row counts, rather than a cryptic downstream KeyError mid-loop.
- Cleanup: remove unused DETECTION_LABELS import from the module and
  unused tempfile/Path imports from the test file.
The 5k audit surfaced an ambiguity: CSV files with header columns
timestamp,level,message containing uppercase log severities were
annotated inconsistently (gemini fired, sonnet + gpt54mini did not).
The existing definition enumerated log schemas as nginx/syslog/JSON but
not CSV/TSV, and the anti-signal 'CSV/TSV with date columns' was too
broad — it correctly excluded sales CSVs with order_date columns but
also excluded CSV log exports.

Refinement: CSV/TSV log records fire log_content when BOTH a
timestamp-like column (timestamp/time/ts/date) AND a severity-like
column (level/severity/log_level) are present AND the severity values
are uppercase log levels. A CSV with only a date column still does
NOT fire.

Targeted verification on the 24 CSV log rows from the iter16 audit
under the refined prompt: 24/24 fire log_content unanimously across
gemini-3-flash, sonnet-4.6, and gpt-5.4-mini.
5k audit results and gate decision for the 3 semantic detection labels.
All three (log_content, stack_trace, diff_patch) have inter-annotator
agreement >=0.9986 on the stratified 4984-row cohort. Raw recall was
misleadingly low (62.5% stack_trace, 84% log_content) due to injection-
regex overmatch and a CSV log-records ambiguity; manual spot-check
identified both and neither reflected a prompt-quality problem. The CSV
ambiguity was fixed in c1ec175 with 24/24 unanimous post-fix recall on
affected rows. Gate: PASS. Proceed to next branch for full 90k run.
Restructures the iter16 Follow-Up section to make the A/B regression
audit a concrete, mandatory pre-gate on the 90k annotation run rather
than a dated 'if we want rigorous evidence' aside.

iter16's absolute-agreement audit has no control group and cannot prove
whether the 12 pre-existing labels below the 0.995 threshold (plain at
0.9391, markdown at 0.9507, etc.) reflect noise floors or silent
regressions from the longer SYSTEM_PROMPT. Before committing ~$400-600
on the 90k run, iter17 must:

1. Worktree off commit 22bc292 (last iter15 prompt state, Phase 0 tests
   already green)
2. Re-annotate the existing iter16_5k_input.parquet with the iter15
   prompt across the same 3 models
3. Compute per-label agreement delta
4. Gate: no pre-iter16 label may drop agreement by more than 0.005 vs
   iter15 baseline

Also clarifies the post-retrain 5k eval as the primary model-quality
gate (category/sub_type F1 regression check + per-detection-label F1)
and reorders operational follow-ups (python -m guard, injection regex
precision) to land before the A/B runs so the tooling is sound.
@bfirestone
bfirestone merged commit 8dc31cf into main Apr 11, 2026
1 check passed
@bfirestone
bfirestone deleted the three-head-part-deux branch April 11, 2026 05:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant