fix(pipeline-status): match full receipt pattern in check_memory to prevent PR-number collisions #20
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Контекст
Зачем:
pipeline-statusoracle фазу MEMORY (check_memory) возвращает ложный DONE для Forgejo PR с малыми номерами (#1–#18), из-за чегоrun-pipelineпропускает фазу memory-syncer и durable-знания по этим PR не попадают в память.Контекст: детектор
pattern in f.read_text()(бессодержательный substring-поискPR#N) не имеет границ слова. После миграции репо с GitHub (slaid098/opencode, PR до ~#290) на Forgejo (slaid098/opencode-config, нумерация сброшена к #1) малые числа Forgejo PR коллизируют с:(PR#18)вopencode-config.md(frozen-файл) — например- ADR-001: CI bootstrap + output-based skip (PR#18).PR#18является подстрокойPR#180,PR#181,PR#182,PR#184,PR#186,PR#187,PR#189(23 вхожденияPR#18вopencode-config.md= 1 ADR-ref + 22 подстроки).Эмпирическое доказательство (запущено против реальных memory-файлов):
check_memory(18)→ DONE черезopencode-config.md:22- ADR-001: ... (PR#18)— старая GitHub ADR-ref, НЕ receipt Forgejo.check_memory(1)→ DONE через подстрокуPR#1внутриPR#10/PR#14/PR#15в-004.md.check_memory(17)→ DONE через старую(PR#17)ADR-ref.memory-syncer был пропущен для ВСЕХ Forgejo PR #1–#18. Исторический прецедент: PR#245 / issue #244 (исправил NOT_DONE-loop через multi-file scan в
get_memory_files(), оставил substring-vulnerability намеренно — тогда малых номеров не было).Задача
/root/workspace/opencode-config/.opencode/scripts/pipeline-status.pyфункцияcheck_memory(строки 702–723): заменить substring-поиск на regex-поиск полного receipt-формата.pattern = f"PR#{pr_number}"и условие 717if pattern in f.read_text():.receipt_re = re.compile(rf"- \[\d{{4}}-\d{{2}}-\d{{2}}, PR#{pr_number}\]")(модульreуже импортирован в файле).if receipt_re.search(f.read_text()):— при совпадении вернутьPhaseResult(PhaseStatus.DONE, f"PR#{pr_number} в {f.name}")(деталь сообщения сохранить;PR#{pr_number}без]в detail допустимо — используется только для лога).patternпереименовать вf"PR#{pr_number}"inline, либо завестиpattern = f"PR#{pr_number}"только для detail-сообщений (regex и detail независимы)./root/workspace/opencode-config/.opencode/agents/reviewer.mdсекция## Known deterministic links(строки 411–413):pipeline-status.py(MEMORY phase expectsPR#Nliteral) ↔memory-syncer.md(must writePR#Nwithout space — otherwise receipt not found)на:pipeline-status.py(MEMORY phase expects- [YYYY-MM-DD, PR#N]receipt pattern via regex) ↔memory-syncer.md(must write- [YYYY-MM-DD, PR#N]receipt — already does, no format change).memory-syncer.md↔check_memory / get_memory_files— expectsPR#Nliteral) — заменить «expectsPR#Nliteral in memory files» на «expects- [YYYY-MM-DD, PR#N]` receipt pattern (regex, full match) in memory files»./root/workspace/opencode-config/tests/test_pipeline_status.pyпослеtest_check_memory_done(≈строка 497), в секцию# ── check_memory ───:test_check_memory_no_substring_collision— memory-файл содержитPR#180но НЕ содержитPR#18]receipt →check_memory(18)возвращаетNOT_DONE.test_check_memory_no_adr_ref_match— memory-файл содержит(PR#18)ADR-ref но НЕ содержит- [date, PR#18]receipt →check_memory(18)возвращаетNOT_DONE.test_check_memory_matches_real_receipt— memory-файл содержит- [2026-08-07, PR#18] — durable: ...→check_memory(18)возвращаетDONE.monkeypatch.setattr(ps, "get_memory_files", lambda: [memory_file])— как в существующих тестах (PR#46). Существующие тесты сPR#46(real receipt- [2026-07-19, PR#46] test entry) остаются зелёными.Контракты
check_memory(pr_number: int) -> PhaseResult— сигнатура без изменений.PhaseStatus.DONEилиPhaseStatus.NOT_DONE(безAMBIGUOUS— per PR#245 контракт).- [YYYY-MM-DD, PR#{pr_number}](regex full match:- \[\d{4}-\d{2}-\d{2}, PR#N\]).f"PR#{pr_number} не найден в {len(files)} файл(ах)"(формат сохранён).memory-syncer.mdНЕ меняется — он уже пишет- [YYYY-MM-DD, PR#N] <content>(строки 69–83 вmemory-syncer.md).get_memory_files()multi-file scan (PR#245 fix) НЕ меняется — regex применяется к тому же списку файлов.Инварианты
PR#с заглавнойPиR(memory-syncer пишетPR#заглавными).]обязательна — именно она отличает receipt от ADR-ref(PR#18)(где после#18идёт)) и от подстрокиPR#180(где после#18идёт0).- [обязателен — отличает receipt от произвольногоPR#18]в тексте.YYYY-MM-DD(4-2-2 цифры) — соответствует writer'у.re.compileвынести за пределы циклаfor f in files:(микрооптимизация, не на hot-path).\bНЕ использовать —\bмежду8и)является word boundary →PR#18в(PR#18)совпадёт.]+- [date,префикс — то, что отличает настоящий receipt.Граничные случаи
PR#18как подстрокуPR#180→ regex не матчит (после18идёт0, не]) → NOT_DONE. ✓(PR#18)ADR-ref → regex не матчит (нет- [date,префикса, после18идёт)) → NOT_DONE. ✓get_memory_files()(фильтрация по{repo}.md/{repo}-NNN.mdчерез rotation-regex;MEMORY_DIR / org / repoбез host-сегмента с PR#12). В пределах одного репо PR-номера монотонны. ✓for f in filesвозвращает на первом совпадении,get_memory_files()сортирует newest-first.get_memory_files()кидаетRuntimeError/ValueError→ NOT_DONE с текстом ошибки — без изменений (строки 706–707).Влияние на связанные компоненты
Paired writer↔reader детерминированные связи (из
reviewer.md## Known deterministic links):pipeline-status.py:check_memory(READER — меняется) ↔memory-syncer.md(WRITER — НЕ меняется, уже пишет корректный формат- [YYYY-MM-DD, PR#N]). Проверить, что writer-формат соответствует новому regex — соответствует (dateYYYY-MM-DD, затем, PR#N]).reviewer.mdсекция## Known deterministic links(строки 406–413) — ОБНОВИТЬ обе записи (см. шаг 2 в Задаче): заменить «expectsPR#Nliteral» → «expects- [YYYY-MM-DD, PR#N]receipt pattern». Это paired doc-update в том же PR.run-pipeline/SKILL.md(строки 14–21) — НЕ меняется: поведение не изменилось, oracle теперь корректно возвращает DONE/NOT_DONE; guardStatus: COMPLETEработает как прежде.test_check_memory_done_rotated_file,test_check_memory_done_newest_first,test_check_memory_not_done_multiple_files,test_check_memory_not_done_value_error) остаются зелёными — они используют real receipts.Вне scope
opencode-config.md(frozen-файл, иммутабелен per rotation policy).memory-syncer.md.get_memory_files().Критерии приемки
check_memory(18)против memory-файла сPR#180(безPR#18]) →NOT_DONE.check_memory(18)против memory-файла с(PR#18)ADR-ref (без- [date, PR#18]) →NOT_DONE.check_memory(18)против memory-файла с- [2026-08-07, PR#18] — durable: ...→DONE.check_memoryсPR#46(real receipts) остаются зелёными.test_check_memory_no_substring_collision,test_check_memory_no_adr_ref_match,test_check_memory_matches_real_receiptдобавлены и проходят.reviewer.mdсекция## Known deterministic linksобновлена: запись содержит- [YYYY-MM-DD, PR#N]receipt pattern вместоPR#Nliteral.cd /root/workspace/opencode-config && ruff check . && mypy .opencode/scripts/pipeline-status.py && pytest tests/test_pipeline_status.py -k check_memory— все зелёно.pipeline-status.py:check_memoryиспользуетre.compile(rf"- \[\d{{4}}-\d{{2}}-\d{{2}}, PR#{pr_number}\]")(regex full match), а неpattern in text(substring).