Skip to content

fix(extraction): reduce injection noise — checkbox filter + narrative quality scoring (#68)#158

Merged
thebtf merged 3 commits into
mainfrom
feat/injection-quality
Apr 14, 2026
Merged

fix(extraction): reduce injection noise — checkbox filter + narrative quality scoring (#68)#158
thebtf merged 3 commits into
mainfrom
feat/injection-quality

Conversation

@thebtf
Copy link
Copy Markdown
Owner

@thebtf thebtf commented Apr 14, 2026

Summary

Reduces injection noise from ~80% to <30% by filtering low-value observations. Issue #68.

Changes

Deterministic checkbox filter (internal/pipeline/deterministic.go)

  • IsCheckboxToggle() detects tasks.md checkbox changes ([ ]→[x]) via regex
  • ClassifyEvent calls it first — checkbox edits → type=change (not decision)
  • 7 test cases in deterministic_test.go

LLM prompt update

  • processor.go systemPrompt: added checkbox toggles to DO NOT EXTRACT
  • prompts.go ChunkExtractionSystemPrompt: same rule for backfill extraction

Narrative quality multiplier (internal/scoring/calculator.go)

  • narrativeQualityMultiplier(): <5 words → 0.5x, <20 words without reasoning markers → 0.7x
  • Reasoning markers: "because", "chose", "decided", "instead of", "rather than", etc.
  • NarrativeMultiplier added to ScoreComponents for explainability
  • 6 test cases in calculator_test.go

Testing

  • 13 new tests, all passing
  • go vet clean
  • No changes to existing tests

Closes #68 (iteration 1 — extraction quality)

Summary by CodeRabbit

Примечания к выпуску

  • Новые функции

    • Система теперь игнорирует переключение состояния флажков задач в файлах задач как рутинное отслеживание прогресса.
    • Оценка расширена: учитывается множитель качества описания наблюдений (narrative multiplier).
  • Улучшения

    • Повышена точность классификации событий за счёт распознавания рутинных обновлений задач.
  • Тесты

    • Добавлены юнит-тесты для обнаружения переключений флажков и для оценки качества описаний.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 14, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5a5a166b-eaf5-4eed-a504-5dedce9f4148

📥 Commits

Reviewing files that changed from the base of the PR and between 7d5a7e9 and a3b8b60.

📒 Files selected for processing (1)
  • internal/pipeline/deterministic.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/pipeline/deterministic.go

Walkthrough

Добавлена детекция переключений чекбоксов задач и исключение их из извлечения; интегрировано в пайплайн классификации событий. Добавлен множитель качества повествования в расчёт оценок и соответствующие юнит-тесты. Текстовые системные промты расширены для игнорирования таких чекбокс‑изменений.

Changes

Cohort / File(s) Summary
Обновление системных промптов экстракции
internal/backfill/extract/prompts.go, internal/worker/sdk/processor.go
Расширен раздел "DO NOT EXTRACT" для явного исключения переключений чекбоксов задач ([ ][x]) в типичных файлах задач (tasks.md, TODO.md, чеклисты).
Логика классификации/обнаружения чекбоксов
internal/pipeline/deterministic.go
Добавлена экспортируемая функция IsCheckboxToggle(toolName, toolInput string) bool (regex-проверки по путям/маркировке чекбоксов) и интеграция в ClassifyEvent, немедленно классифицирующая такие правки как models.ObsTypeChange.
Тесты для обнаружения чекбоксов
internal/pipeline/deterministic_test.go
Добавлен TestIsCheckboxToggle с наборами сценариев: переключения в tasks.md, TODO.md, игнорирование в README.md, проверка инструментов Edit/Read и случаев без переключения.
Множитель качества повествования в оценщике
internal/scoring/calculator.go
Добавлен narrativeMultiplier в расчёт coreScore, новый хелпер narrativeQualityMultiplier(narrative string) float64, глобальный reasoningMarkers и поле NarrativeMultiplier в ScoreComponents.
Тесты множителя повествования
internal/scoring/calculator_test.go
Добавлен TestNarrativeQualityMultiplier с проверкой множителей для различных сюжетных входов (пустой, короткий, с маркерами рассуждения, длинный).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 Я — кролик, код люблю щёлкать,
Чекбоксам не велю извлекать.
Повествованья множитель блеснёт,
Рутинный прогресс спокойным пойдёт.
Удачи в сборке — тесты пусть пройдут!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Связанная issue #68 требует миграции базы данных для удаления учетных данных, но PR реализует фильтр чекбоксов и оценку нарратива для снижения шума извлечения — совершенно разные требования. Либо PR содержит неправильно указанную связанную issue, либо нужно реализовать требуемые изменения миграции базы данных из issue #68.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Заголовок описывает основные изменения (фильтр чекбоксов и оценка качества нарратива для снижения шума извлечения), которые полностью соответствуют сделанным изменениям в коде.
Out of Scope Changes check ✅ Passed Все изменения (фильтр чекбоксов, оценка нарратива, обновления промптов) касаются снижения шума извлечения и в полной мере соответствуют заявленным целям PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/injection-quality

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements logic to ignore routine task checkbox toggles during observation extraction and introduces a narrative quality multiplier to penalize thin descriptions in scoring. Review feedback suggests improving the robustness of the checkbox detection regex and ensuring file path matching is performed on the actual path rather than the full JSON input to avoid false positives. There is also a suggestion to use word boundaries for reasoning markers to prevent incorrect substring matches.

Comment thread internal/pipeline/deterministic.go Outdated
Comment on lines +28 to +30
if !taskFileRe.MatchString(toolInput) {
return false
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation matches the taskFileRe against the entire toolInput JSON string. This can lead to false positives if a task-related filename (e.g., tasks.md) appears within the content of an edit for a non-task file (e.g., a comment in main.go referencing the task list). It is safer to extract the actual file path being edited and verify it against the regex.

Suggested change
if !taskFileRe.MatchString(toolInput) {
return false
}
// Check if file is a task/checklist file
path := extractFilePath(toolInput)
if path == "" || !taskFileRe.MatchString(path) {
return false
}

Comment thread internal/pipeline/deterministic.go Outdated
)

// checkboxToggleRe matches checkbox state changes in task lists.
var checkboxToggleRe = regexp.MustCompile(`-\s*\[[ x]\]\s`)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The regex for detecting checkboxes is slightly too restrictive. It should support uppercase [X], which is common in many Markdown flavors. Additionally, requiring a trailing whitespace character (\s) might fail to match checkboxes at the end of a string or followed by JSON delimiters (like a closing quote) when matching against the raw toolInput string. Consider allowing the end of the string or a broader set of delimiters.

Suggested change
var checkboxToggleRe = regexp.MustCompile(`-\s*\[[ x]\]\s`)
var checkboxToggleRe = regexp.MustCompile(`(?i)-\s*\[[ x]\](\s|$)`)

Comment on lines +214 to +218
for _, marker := range reasoningMarkers {
if strings.Contains(lower, marker) {
return 1.0
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using strings.Contains with short markers like "why" or "reason" can lead to false positives by matching substrings in unrelated words (e.g., "anywhere" contains "where", though "why" is safer; however, "reason" matches "treason" or "unreasonable"). While acceptable as a heuristic, using word boundaries or a more specific check would improve the accuracy of the reasoning detection.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
internal/scoring/calculator.go (1)

213-216: Проверка маркеров через Contains даёт ложные срабатывания по подстрокам.

Это может необоснованно поднимать множитель до 1.0 и снижать эффективность фильтра шума. Лучше матчить слова/фразы с границами.

Вариант исправления
 import (
 	"math"
+	"regexp"
 	"strings"
 	"time"

 	"github.com/thebtf/engram/pkg/models"
 )
@@
-var reasoningMarkers = []string{
-	"because", "chose", "decided", "instead of", "rather than",
-	"tradeoff", "trade-off", "alternative", "reason", "why",
-}
+var reasoningMarkerRE = regexp.MustCompile(`(?i)\b(because|chose|decided|tradeoff|trade-off|alternative|reason|why)\b|instead of|rather than`)
@@
-	lower := strings.ToLower(narrative)
-	for _, marker := range reasoningMarkers {
-		if strings.Contains(lower, marker) {
-			return 1.0
-		}
-	}
+	if reasoningMarkerRE.MatchString(narrative) {
+		return 1.0
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/scoring/calculator.go` around lines 213 - 216, The current loop uses
strings.Contains on lower (lowercased narrative) against reasoningMarkers which
causes substring false-positives; change the check to match whole words/phrases
using word-boundary matching (e.g., build a regex per marker with \b +
regexp.QuoteMeta(marker) + \b and test with MatchString on lower) or tokenize
the narrative and compare tokens/phrases exactly; update the loop that iterates
over reasoningMarkers (and any helper that uses lower/narrative) to use this
boundary-safe match so only full-word/phrase hits return 1.0.
internal/scoring/calculator_test.go (1)

786-797: Добавьте boundary-кейсы для ровно 5 и 20 слов.

Сейчас не покрыты ключевые границы условий (<5 и <20), поэтому возможна тихая регрессия порогов.

Предлагаемый патч
 	tests := []struct {
 		name      string
 		narrative string
 		want      float64
 	}{
 		{"very thin", "Marked T015 complete", 0.5},
+		{"exactly 5 words no marker", "one two three four five", 0.7},
 		{"thin with marker", "Chose sync.Map because read-heavy", 1.0},
 		{"medium no marker", "Updated the configuration file with new settings for the deployment pipeline across environments", 0.7},
+		{"exactly 20 words no marker", strings.TrimSpace(strings.Repeat("word ", 20)), 1.0},
 		{"long narrative", strings.Repeat("word ", 50), 1.0},
 		{"empty", "", 0.5},
 		{"short with reason", "Changed approach because the original design had a fundamental flaw in concurrency handling", 1.0},
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/scoring/calculator_test.go` around lines 786 - 797, Add two boundary
test cases to the tests slice in calculator_test.go to cover exactly 5 and
exactly 20-word narratives: add an entry (e.g., name "five words") with a 5-word
string and expected want 0.7, and an entry (e.g., name "twenty words") with a
20-word string and expected want 0.7; place them alongside the existing
table-driven tests so the scoring logic that handles the <5 and <20 thresholds
(the tests variable in calculator_test.go) is exercised.
internal/pipeline/deterministic_test.go (1)

1360-1383: Добавьте негативный кейс “чекбокс есть, но toggle нет”.

Сейчас нет проверки для сценария old_string == new_string (например, - [ ]- [ ]), из-за чего ложноположительный детект не будет пойман регрессией.

🧪 Минимальное расширение теста
 func TestIsCheckboxToggle(t *testing.T) {
 	tests := []struct {
 		name     string
 		toolName string
 		input    string
 		want     bool
 	}{
 		{"checkbox toggle in tasks.md", "Edit", `{"file_path":"tasks.md","old_string":"- [ ] T015","new_string":"- [x] T015"}`, true},
 		{"uncheck in tasks.md", "Edit", `{"file_path":"tasks.md","old_string":"- [x] T015","new_string":"- [ ] T015"}`, true},
+		{"checkbox unchanged in tasks.md", "Edit", `{"file_path":"tasks.md","old_string":"- [ ] T015","new_string":"- [ ] T015"}`, false},
 		{"normal edit in main.go", "Edit", `{"file_path":"main.go","old_string":"foo","new_string":"bar"}`, false},
 		{"checkbox in non-task file", "Edit", `{"file_path":"README.md","old_string":"- [ ] item","new_string":"- [x] item"}`, false},
 		{"Read tool", "Read", `{"file_path":"tasks.md"}`, false},
 		{"checkbox in TODO.md", "Edit", `{"file_path":"TODO.md","old_string":"- [ ] Fix bug","new_string":"- [x] Fix bug"}`, true},
 		{"nested path tasks.md", "Edit", `{"file_path":".agent/specs/feature/tasks.md","old_string":"- [ ] T001","new_string":"- [x] T001"}`, true},
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/pipeline/deterministic_test.go` around lines 1360 - 1383, Add a
negative test case to TestIsCheckboxToggle to cover the scenario where a
checkbox is present but not toggled (old_string == new_string) so
IsCheckboxToggle does not return a false positive; specifically update the tests
slice in TestIsCheckboxToggle to include an entry (toolName "Edit") with a JSON
input where "file_path" is a task file (e.g., "tasks.md") and "old_string"
equals "new_string" (for example both "- [ ] T015"), and assert the expected
want value is false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/pipeline/deterministic.go`:
- Around line 15-33: IsCheckboxToggle currently returns true if any checkbox
pattern exists in toolInput; change it to detect an actual state change by
parsing old vs new content and confirming a transition between unchecked and
checked. Modify IsCheckboxToggle (and keep using checkboxToggleRe/taskFileRe) to
extract old_string and new_string from toolInput (or accept separate old/new
inputs), add two specific regexes for unchecked (`-\s*\[\s\]\s`) and checked
(`-\s*\[x\]\s`) states, and return true only when the file is a task file
(taskFileRe) and one of old_string/new_string matches the checked pattern while
the other matches the unchecked pattern (i.e., a real toggle between states).

---

Nitpick comments:
In `@internal/pipeline/deterministic_test.go`:
- Around line 1360-1383: Add a negative test case to TestIsCheckboxToggle to
cover the scenario where a checkbox is present but not toggled (old_string ==
new_string) so IsCheckboxToggle does not return a false positive; specifically
update the tests slice in TestIsCheckboxToggle to include an entry (toolName
"Edit") with a JSON input where "file_path" is a task file (e.g., "tasks.md")
and "old_string" equals "new_string" (for example both "- [ ] T015"), and assert
the expected want value is false.

In `@internal/scoring/calculator_test.go`:
- Around line 786-797: Add two boundary test cases to the tests slice in
calculator_test.go to cover exactly 5 and exactly 20-word narratives: add an
entry (e.g., name "five words") with a 5-word string and expected want 0.7, and
an entry (e.g., name "twenty words") with a 20-word string and expected want
0.7; place them alongside the existing table-driven tests so the scoring logic
that handles the <5 and <20 thresholds (the tests variable in
calculator_test.go) is exercised.

In `@internal/scoring/calculator.go`:
- Around line 213-216: The current loop uses strings.Contains on lower
(lowercased narrative) against reasoningMarkers which causes substring
false-positives; change the check to match whole words/phrases using
word-boundary matching (e.g., build a regex per marker with \b +
regexp.QuoteMeta(marker) + \b and test with MatchString on lower) or tokenize
the narrative and compare tokens/phrases exactly; update the loop that iterates
over reasoningMarkers (and any helper that uses lower/narrative) to use this
boundary-safe match so only full-word/phrase hits return 1.0.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a1ab31ca-6acd-4681-af34-3ba6dbcdec50

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa4720 and 7d5a7e9.

📒 Files selected for processing (6)
  • internal/backfill/extract/prompts.go
  • internal/pipeline/deterministic.go
  • internal/pipeline/deterministic_test.go
  • internal/scoring/calculator.go
  • internal/scoring/calculator_test.go
  • internal/worker/sdk/processor.go

Comment thread internal/pipeline/deterministic.go Outdated
…tection

- Match taskFileRe against extracted file path, not raw JSON (Gemini)
- Support uppercase [X] checkbox with case-insensitive regex (Gemini)
- Detect actual state change (unchecked+checked both present), not just
  checkbox presence (CodeRabbit)
- Use filepath.ToSlash for cross-platform path matching
@thebtf thebtf merged commit d7d4093 into main Apr 14, 2026
2 checks passed
@thebtf thebtf deleted the feat/injection-quality branch April 14, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant