Problem
The probe/source pipeline leaks several untyped or mutable internal shapes that callers can take dependencies on:
DataSource.diff() returns Any. protocols.py does define a @runtime_checkable DataSource Protocol whose diff is declared def diff(self, previous: Any, current: Any) -> Any (src/paperscout/protocols.py lines 34–36). The concrete WG21Index.diff() (src/paperscout/sources.py lines 239–247) has no return annotation at all — it defers to monitor._diff_paper_maps() (which does return a typed DiffResult), so the real value is typed, but the public method/protocol surface is Any. (ISOProber.diff() at lines 377–389 and OpenStdSource.diff() at lines 825–833 are already typed to list[ProbeHit] / list[OpenStdEntry].) Note: an earlier draft asserted protocols.py/DataSource do not exist in this clone — that is incorrect; both exist and the Any-typed diff is real.
- Bare positional 6-tuple probe entry.
_Entry = tuple[str, Tier, str, int, int, str] (src/paperscout/sources.py lines 317–319) is unpacked positionally inside run_cycle — for url, tier, prefix, num, rev, ext in urls (line 432) — and indexed positionally (u[1] at lines 409–410). It is produced across _build_probe_list (516–524), _build_hot_list (562–595, appends at 585/593), and _build_cold_slice (597–635, appends at 623/633).
ProbeHit is a non-frozen mutable dataclass. @dataclass(slots=True) (not frozen=True) at src/paperscout/models.py lines 169–184, so every field (is_recent, last_modified, front_text, …) is silently mutable after construction.
PollResult performs no validation. PollResult is a plain class (not a dataclass) whose __init__ accepts list/dict arguments and stores them as-is with no narrowing/validation (src/paperscout/monitor.py lines 107–120), unlike the adjacent typed DiffResult (lines 40–46) and CycleResult (models.py 203–224, which validates in __post_init__).
Acceptance Criteria
Problem
The probe/source pipeline leaks several untyped or mutable internal shapes that callers can take dependencies on:
DataSource.diff()returnsAny.protocols.pydoes define a@runtime_checkableDataSourceProtocol whosediffis declareddef diff(self, previous: Any, current: Any) -> Any(src/paperscout/protocols.pylines 34–36). The concreteWG21Index.diff()(src/paperscout/sources.pylines 239–247) has no return annotation at all — it defers tomonitor._diff_paper_maps()(which does return a typedDiffResult), so the real value is typed, but the public method/protocol surface isAny. (ISOProber.diff()at lines 377–389 andOpenStdSource.diff()at lines 825–833 are already typed tolist[ProbeHit]/list[OpenStdEntry].) Note: an earlier draft assertedprotocols.py/DataSourcedo not exist in this clone — that is incorrect; both exist and theAny-typeddiffis real._Entry = tuple[str, Tier, str, int, int, str](src/paperscout/sources.pylines 317–319) is unpacked positionally insiderun_cycle—for url, tier, prefix, num, rev, ext in urls(line 432) — and indexed positionally (u[1]at lines 409–410). It is produced across_build_probe_list(516–524),_build_hot_list(562–595, appends at 585/593), and_build_cold_slice(597–635, appends at 623/633).ProbeHitis a non-frozen mutable dataclass.@dataclass(slots=True)(notfrozen=True) atsrc/paperscout/models.pylines 169–184, so every field (is_recent,last_modified,front_text, …) is silently mutable after construction.PollResultperforms no validation.PollResultis a plain class (not a dataclass) whose__init__acceptslist/dictarguments and stores them as-is with no narrowing/validation (src/paperscout/monitor.pylines 107–120), unlike the adjacent typedDiffResult(lines 40–46) andCycleResult(models.py203–224, which validates in__post_init__).Acceptance Criteria
DataSource.diff(protocol) andWG21Index.diffcarry a concrete return type (e.g.DiffResult) instead ofAny/no annotation, somypysees a real type at the source boundary._Entryis replaced by a named, typed structure (e.g. a@dataclass(frozen=True, slots=True)ProbeTargetor aNamedTuple) so probe-list fields are accessed by name;run_cycle,_build_probe_list,_build_hot_list,_build_cold_slice, and_probe_oneare updated to use it.ProbeHitis made immutable (@dataclass(frozen=True, slots=True)) or otherwise prevented from post-construction mutation, with all construction sites insources.pystill passing.PollResult.__init__validates/normalizes its inputs (or becomes a typed dataclass) rather than storing arbitrary rawlist/dictvalues fordp_transitions/per_user_matches.mypy/type checks pass with no newAny-typed public return in the probe/source pipeline.