fix(pipeline-status): match full receipt pattern in check_memory to prevent PR-number collisions #20

Closed
opened 2026-08-07 13:50:29 +03:00 by slaid098 · 0 comments
Owner

Контекст

Зачем: pipeline-status oracle фазу 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 коллизируют с:

  1. Старыми GitHub ADR-ссылками вида (PR#18) в opencode-config.md (frozen-файл) — например - ADR-001: CI bootstrap + output-based skip (PR#18).
  2. Подстроками больших чисел — 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 намеренно — тогда малых номеров не было).

Задача

  1. В /root/workspace/opencode-config/.opencode/scripts/pipeline-status.py функция check_memory (строки 702–723): заменить substring-поиск на regex-поиск полного receipt-формата.
    • Удалить строку 715 pattern = f"PR#{pr_number}" и условие 717 if 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 допустимо — используется только для лога).
    • В NOT_DONE-возврате (строки 720–723) pattern переименовать в f"PR#{pr_number}" inline, либо завести pattern = f"PR#{pr_number}" только для detail-сообщений (regex и detail независимы).
  2. Обновить paired doc в /root/workspace/opencode-config/.opencode/agents/reviewer.md секция ## Known deterministic links (строки 411–413):
    • Заменить запись pipeline-status.py (MEMORY phase expects PR#N literal) ↔ memory-syncer.md (must write PR#N without 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).
    • Также обновить запись строки 406–408 (memory-syncer.md ↔ check_memory / get_memory_files — expects PR#N literal) — заменить «expects PR#Nliteral in memory files» на «expects- [YYYY-MM-DD, PR#N]` receipt pattern (regex, full match) in memory files».
  3. Тесты — добавить 3 новых кейса в /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 контракт).
  • DONE: найден литерал - [YYYY-MM-DD, PR#{pr_number}] (regex full match: - \[\d{4}-\d{2}-\d{2}, PR#N\]).
  • NOT_DONE: ни в одном memory-файле нет receipt; detail f"PR#{pr_number} не найден в {len(files)} файл(ах)" (формат сохранён).
  • Writer memory-syncer.md НЕ меняется — он уже пишет - [YYYY-MM-DD, PR#N] <content> (строки 69–83 в memory-syncer.md).
  • get_memory_files() multi-file scan (PR#245 fix) НЕ меняется — regex применяется к тому же списку файлов.

Инварианты

  • Regex case-sensitive: PR# с заглавной P и R (memory-syncer пишет PR# заглавными).
  • Граница ] обязательна — именно она отличает receipt от ADR-ref (PR#18) (где после #18 идёт )) и от подстроки PR#180 (где после #18 идёт 0).
  • Префикс - [ обязателен — отличает receipt от произвольного PR#18] в тексте.
  • Дата в ISO формате 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.

Граничные случаи

  • Memory-файл содержит PR#18 как подстроку PR#180 → regex не матчит (после 18 идёт 0, не ]) → NOT_DONE. ✓
  • Memory-файл содержит (PR#18) ADR-ref → regex не матчит (нет - [date, префикса, после 18 идёт )) → NOT_DONE. ✓
  • Receipt того же номера в memory-файле ДРУГОГО репо → уже отсечён get_memory_files() (фильтрация по {repo}.md / {repo}-NNN.md через rotation-regex; MEMORY_DIR / org / repo без host-сегмента с PR#12). В пределах одного репо PR-номера монотонны. ✓
  • Несколько receipt одного PR в разных rotation-файлах → вернуть первый по mtime (новейший) — поведение сохранено, цикл for f in files возвращает на первом совпадении, get_memory_files() сортирует newest-first.
  • get_memory_files() кидает RuntimeError/ValueError → NOT_DONE с текстом ошибки — без изменений (строки 706–707).
  • Пустой список файлов → NOT_DONE «memory files не найдены» — без изменений.

Влияние на связанные компоненты

Paired writer↔reader детерминированные связи (из reviewer.md ## Known deterministic links):

  1. pipeline-status.py:check_memory (READER — меняется) ↔ memory-syncer.md (WRITER — НЕ меняется, уже пишет корректный формат - [YYYY-MM-DD, PR#N]). Проверить, что writer-формат соответствует новому regex — соответствует (date YYYY-MM-DD, затем , PR#N]).
  2. reviewer.md секция ## Known deterministic links (строки 406–413) — ОБНОВИТЬ обе записи (см. шаг 2 в Задаче): заменить «expects PR#N literal» → «expects - [YYYY-MM-DD, PR#N] receipt pattern». Это paired doc-update в том же PR.
  3. run-pipeline/SKILL.md (строки 14–21) — НЕ меняется: поведение не изменилось, oracle теперь корректно возвращает DONE/NOT_DONE; guard Status: COMPLETE работает как прежде.
  4. Исторический прецедент PR#245 / issue #244 — ссылка в этом issue (см. Контекст). Тесты PR#245 (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

  • Ретроспективный memory-sync для Forgejo PR #1–#17 (пользователь решил: только fix, без retro). Только MEMORY-фаза этого fix-PR будет первым корректным receipt'ом.
  • Миграция/чистка старых GitHub ADR-ref из opencode-config.md (frozen-файл, иммутабелен per rotation policy).
  • Изменение writer-формата memory-syncer.md.
  • Изменение rotation-логики get_memory_files().
  • Перевод detail-сообщений на английский (формат сохранён как есть).

Критерии приемки

  • 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#N literal.
  • 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).
## Контекст Зачем: `pipeline-status` oracle фазу 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 коллизируют с: 1. Старыми GitHub ADR-ссылками вида `(PR#18)` в `opencode-config.md` (frozen-файл) — например `- ADR-001: CI bootstrap + output-based skip (PR#18)`. 2. Подстроками больших чисел — `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 намеренно — тогда малых номеров не было). ## Задача 1. В `/root/workspace/opencode-config/.opencode/scripts/pipeline-status.py` функция `check_memory` (строки 702–723): заменить substring-поиск на regex-поиск полного receipt-формата. - Удалить строку 715 `pattern = f"PR#{pr_number}"` и условие 717 `if 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 допустимо — используется только для лога). - В NOT_DONE-возврате (строки 720–723) `pattern` переименовать в `f"PR#{pr_number}"` inline, либо завести `pattern = f"PR#{pr_number}"` только для detail-сообщений (regex и detail независимы). 2. Обновить paired doc в `/root/workspace/opencode-config/.opencode/agents/reviewer.md` секция `## Known deterministic links` (строки 411–413): - Заменить запись `pipeline-status.py` (MEMORY phase expects `PR#N` literal) ↔ `memory-syncer.md` (must write `PR#N` without 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)`. - Также обновить запись строки 406–408 (`memory-syncer.md` ↔ `check_memory / get_memory_files` — expects `PR#N` literal`) — заменить «expects `PR#N` literal in memory files» на «expects `- [YYYY-MM-DD, PR#N]` receipt pattern (regex, full match) in memory files». 3. Тесты — добавить 3 новых кейса в `/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 контракт). - DONE: найден литерал `- [YYYY-MM-DD, PR#{pr_number}]` (regex full match: `- \[\d{4}-\d{2}-\d{2}, PR#N\]`). - NOT_DONE: ни в одном memory-файле нет receipt; detail `f"PR#{pr_number} не найден в {len(files)} файл(ах)"` (формат сохранён). - Writer `memory-syncer.md` НЕ меняется — он уже пишет `- [YYYY-MM-DD, PR#N] <content>` (строки 69–83 в `memory-syncer.md`). - `get_memory_files()` multi-file scan (PR#245 fix) НЕ меняется — regex применяется к тому же списку файлов. ## Инварианты - Regex case-sensitive: `PR#` с заглавной `P` и `R` (memory-syncer пишет `PR#` заглавными). - Граница `]` обязательна — именно она отличает receipt от ADR-ref `(PR#18)` (где после `#18` идёт `)`) и от подстроки `PR#180` (где после `#18` идёт `0`). - Префикс `- [` обязателен — отличает receipt от произвольного `PR#18]` в тексте. - Дата в ISO формате `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. ## Граничные случаи - Memory-файл содержит `PR#18` как подстроку `PR#180` → regex не матчит (после `18` идёт `0`, не `]`) → NOT_DONE. ✓ - Memory-файл содержит `(PR#18)` ADR-ref → regex не матчит (нет `- [date, ` префикса, после `18` идёт `)`) → NOT_DONE. ✓ - Receipt того же номера в memory-файле ДРУГОГО репо → уже отсечён `get_memory_files()` (фильтрация по `{repo}.md` / `{repo}-NNN.md` через rotation-regex; `MEMORY_DIR / org / repo` без host-сегмента с PR#12). В пределах одного репо PR-номера монотонны. ✓ - Несколько receipt одного PR в разных rotation-файлах → вернуть первый по mtime (новейший) — поведение сохранено, цикл `for f in files` возвращает на первом совпадении, `get_memory_files()` сортирует newest-first. - `get_memory_files()` кидает `RuntimeError`/`ValueError` → NOT_DONE с текстом ошибки — без изменений (строки 706–707). - Пустой список файлов → NOT_DONE «memory files не найдены» — без изменений. ## Влияние на связанные компоненты Paired writer↔reader детерминированные связи (из `reviewer.md` `## Known deterministic links`): 1. `pipeline-status.py:check_memory` (READER — меняется) ↔ `memory-syncer.md` (WRITER — НЕ меняется, уже пишет корректный формат `- [YYYY-MM-DD, PR#N]`). Проверить, что writer-формат соответствует новому regex — соответствует (date `YYYY-MM-DD`, затем `, PR#N]`). 2. `reviewer.md` секция `## Known deterministic links` (строки 406–413) — ОБНОВИТЬ обе записи (см. шаг 2 в Задаче): заменить «expects `PR#N` literal» → «expects `- [YYYY-MM-DD, PR#N]` receipt pattern». Это paired doc-update в том же PR. 3. `run-pipeline/SKILL.md` (строки 14–21) — НЕ меняется: поведение не изменилось, oracle теперь корректно возвращает DONE/NOT_DONE; guard `Status: COMPLETE` работает как прежде. 4. Исторический прецедент PR#245 / issue #244 — ссылка в этом issue (см. Контекст). Тесты PR#245 (`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 - Ретроспективный memory-sync для Forgejo PR #1–#17 (пользователь решил: только fix, без retro). Только MEMORY-фаза этого fix-PR будет первым корректным receipt'ом. - Миграция/чистка старых GitHub ADR-ref из `opencode-config.md` (frozen-файл, иммутабелен per rotation policy). - Изменение writer-формата `memory-syncer.md`. - Изменение rotation-логики `get_memory_files()`. - Перевод detail-сообщений на английский (формат сохранён как есть). ## Критерии приемки - [ ] `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#N` literal. - [ ] `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).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
slaid098/opencode-config#20
No description provided.