# Task 4 evidence — source provenance collector and approval queue

## Scope

- Added only `/home/cube/.hermes/profiles/physique-coach/workspace/source_collector` plus this evidence file.
- The collector has fixed canonical endpoints for Naver RSS, YouTube Atom, and the supplied Instagram profile URL.
- Tests use only local XML/HTML fixtures through an injected `Fetcher`; no live request, credential, login, cookie, or access-control bypass is present.

## TDD evidence

1. Red: before production files existed, `python3 -m pytest -q` exited `2` with `ModuleNotFoundError: No module named 'source_collector'`.
2. Green: after implementation, `python3 -m pytest -q` reported `7 passed in 0.01s`.
3. Regression red/green: verifier cases first failed for semantic non-RSS 200 and a reappearing item; after feed-shape validation, durable candidate-ID dedupe, and a queue mutation lock, `python3 -m pytest -q` reported `10 passed in 0.02s`.

## Acceptance checks

| Scenario | Observable result |
| --- | --- |
| New Naver RSS item | One concise review candidate with canonical RSS attribution; feed description/body is not persisted. |
| New YouTube Atom item | One concise review candidate with canonical channel-feed attribution. |
| Repeated item | `unchanged`; queue remains one candidate. |
| HTTP 500 after success | `fetch_failed`; previous `item_ids`, digest, and `last_good_at_kst` remain. |
| Malformed XML | `parse_failed`; no candidate and no raw response body in state. |
| Instagram challenge/login/CAPTCHA fixture | `blocked_pending`; no candidate, no scraping/login attempt. |
| Doctrine immutability | Fixture SOUL and knowledge checksums remain byte-identical after collection. |
| Semantic non-RSS 200 | `parse_failed`; successful `item_ids`, digest, and `last_good_at_kst` are retained. |
| Reappearing prior item | `unchanged`; no duplicate review candidate and `candidate_ids` stays unique. |
| Concurrent collectors | Two forked collectors (Naver + YouTube) leave both candidates in one queue. |

## Storage guarantees

- Per-source JSON state is written with a same-directory temporary file, `fsync`, `0600` permissions, and `os.replace`.
- State contains only `{source, attempted_at_kst, status, item_ids, digest, last_good_at_kst, candidate_ids, parser_version}`.
- Candidates are approval-only (`pending_review`); no collector code writes SOUL or knowledge assets.
- Queue read-modify-write is protected by profile-local POSIX `flock` on `.review-queue.lock`, then committed atomically; this prevents concurrent collector processes from dropping candidates.

## Commands run

```text
cd /home/cube/.hermes/profiles/physique-coach/workspace/source_collector
python3 -m pytest -q
python3 /home/cube/.codex/plugins/cache/sisyphuslabs/omo/4.17.0/skills/programming/scripts/python/check-no-excuse-rules.py source_collector/models.py source_collector/collector.py tests/test_collector.py
awk '!/^[[:space:]]*$/ && !/^[[:space:]]*(//|#|--)/' <each changed Python file> | wc -l
```

The regression suite passed five consecutive runs (`10 passed in 0.02s` on the final run); no-excuse audit reported `no violations in 3 file(s)`. Pure LOC: `models.py` 64, `collector.py` 224, tests 147. `ruff` / `basedpyright` were not installed in the active runtime, so they were not run.
