mirror of
https://github.com/Imbad0202/academic-research-skills.git
synced 2026-09-14 13:51:17 +08:00
34a461dc17
* docs(#105): contamination signals backfill migration design Minimal-scope design per user-chosen open Q resolution (2026-05-15): - Q1 backoff-only, no resumable checkpoint - Q2 scalar `contamination_signals_backfilled_at` ISO-8601 field - Q3 directory-scan only, no batch mode - Q4 ruamel.yaml round-trip (preserve comments + key order) 3 pieces: resolver module / CLI migration tool / additive schema field. 8 files touched (5 new + 3 extend); 8 files explicitly frozen. v3.7.3 spec §3.2 R-L3-2-B already promised this batch operation exists; #105 delivers it. No code change to bibliography_agent / orchestrator / adapters — migration is downstream + offline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(#105): v3.7.3 contamination_signals backfill migration tool Delivers the deferred batch operation v3.7.3 §3.2 R-L3-2-B promised: bibliography_agent computes contamination_signals at ingest time; this tool re-runs the check post-hoc on pre-v3.7.3 literature_corpus[] entries so legacy corpora get the v3.7.3 protection layer. Three pieces (per design 2026-05-15): - scripts/contamination_signals.py: pure resolvers + emission rules + SemanticScholarClient Protocol (dependency-injected; tests mock). - scripts/migrate_literature_corpus_to_v3_7_3.py: CLI with --dry-run, --verbose, single-passport + directory-scan (non-recursive). Uses ruamel.yaml round-trip to preserve comments + key order. Idempotent. - shared/contracts/passport/literature_corpus_entry.schema.json: new optional contamination_signals_backfilled_at ISO-8601 string field (strictly additive; existing entries stay valid). Open Q resolutions (user-chosen, minimal scope): - Q1 backoff-only, no resumable checkpoint (YAGNI) - Q2 scalar timestamp (upgrade path to structured if v3.7.4 needs) - Q3 directory-scan only, no --input-list - Q4 ruamel.yaml (preserve user passport formatting) Spec discipline enforced: - Manual exemption: semantic_scholar_unmatched OMITTED on obtained_via=manual (matches existing schema allOf rule #4) - API degradation: field OMITTED, not set to False (absence ≠ negative confirmation) - Idempotency: skip if contamination_signals already present; re-runs are byte-equivalent - No retroactive computation in bibliography_agent (offline batch only) Tests (TDD discipline): - 25 resolver unit tests (Signal 1: 15 cases / Signal 2: 6 / emission: 4) - 9 migration tool tests (dry-run / full / idempotency / insufficient / empty / 3× directory / round-trip preservation) - 3 schema tests for the additive field Regression: 1053 #108 + 17 #111 + 37 new = 1107 unittest + 201 pytest adapters + check_spec_consistency all green. Files explicitly NOT touched: - bibliography_agent.md (ingest behavior frozen) - pipeline_orchestrator_agent.md (finalizer unchanged) - Existing scripts/adapters/* (migration is downstream) Closes #105 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(#105): /simplify pass — 5 findings fixed + PREPRINT_VENUES lint Three-agent /simplify (reuse / quality / efficiency) findings: F1 (reuse) Reuse existing now_iso() instead of local _now_utc_iso: scripts/adapters/_common.py:164 already provides the canonical RFC-3339 helper used by 3 adapters. Replace local duplicate with `from adapters._common import now_iso`. Single source of truth for timestamp format. F2 (reuse) PREPRINT_VENUES drift risk across 2 sources of truth: bibliography_agent.md prose list (line 265-274) and Python contamination_signals.PREPRINT_VENUES frozenset. Added scripts/check_preprint_venues_consistency.py lint extracting both and asserting set equality. Wired into spec-consistency.yml CI. Detection now structural, not visual. F3 (quality) _is_insufficient missing venue check: Original only checked year. An entry with year=2024 but missing venue would pass through, get preprint_post_llm_inflection=false emitted (since venue not in PREPRINT_VENUES) — half-truth violating spec §3.2's "computed and clean" vs "not computed" distinction. Fix: skip when EITHER year OR venue is missing. Added negative test test_insufficient_data_missing_venue_skipped. F4 (quality) skipped_manual_unmatched_omit counter naming: Counter lived in skip family but manual entries DO get patched (with semantic_scholar_unmatched field omitted, not the entry). Renamed to manual_unmatched_omitted; updated test reference. F5 (quality) main() crash on NotImplementedError: _build_default_ss_client raises NotImplementedError — every CLI invocation hits this. Wrapped in try/except returning exit-2 with stderr message instead of raw traceback. Honest deferral now user-facing. False positives skipped: SemanticScholarClient Protocol signature (real client needs full entry); ruamel.yaml factor-on-2nd-use; schema description bloat (spec traceability); 4 TestCase classes (orthogonal surfaces); fixture duplication (different shapes). Tests: 434 unittest (1 new venue-missing case) + 201 pytest adapters + 2 lints (spec_consistency + preprint_venues) all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#105): codex R1 [P1]+[P2]×2 — wire SS client + venue + recovery Codex round-1 review caught three real issues that survived /simplify: R1-1 [P1] Wire CLI to real Semantic Scholar client: The original _build_default_ss_client() raised NotImplementedError — every CLI invocation crashed before reading the passport. Migration guide documented an unrunnable tool. Fix: - New scripts/semantic_scholar_client.py: minimal SemanticScholarClient implementing the protocol at deep-research/references/ semantic_scholar_api_protocol.md (DOI-first then title-similarity with Levenshtein 0.70 threshold, 429 → 2s backoff × 3, S2_API_KEY env var optional, 5xx / network failures raise SemanticScholarUnavailable). - 10 new SS client tests covering DOI match / title-mismatch DOI hit / title search above & below threshold / 429 retry recovery / 429 exhaustion → unavailable / 5xx → unavailable / 404 → no-match / network error → unavailable / CLI wiring constructs real client. R1-2 [P2] Do not treat missing optional venue as unmigratable: Original _is_insufficient skipped entries missing venue. But venue is schema-OPTIONAL — compute_preprint_signal correctly returns False on missing venue (venue not in PREPRINT_VENUES), which is a DEFINED emission ("computed; not a preprint venue"), not half-truth. Skipping prevented Signal 2 (SS lookup) from running on schema-valid entries. Fix: _is_insufficient now only checks year (the unconditional AND gate). Test test_missing_venue_does_not_skip_entry confirms. R1-3 [P2] Allow re-runs to complete partial SS backfills: Original idempotency rule was "skip if contamination_signals present". This blocked recovery from an earlier API-degraded run where only preprint_post_llm_inflection got written. Re-running with a healthy API would never fill in semantic_scholar_unmatched. Fix: new _is_complete() helper distinguishes COMPLETE (every computable field present, accounting for manual exemption) from PARTIAL. Partial entries get the missing fields merged in WITHOUT overwriting the original backfilled_at timestamp. 2 new tests: - test_partial_fill_recovery_fills_unmatched_field - test_manual_entry_with_only_preprint_signal_is_complete Removed test_insufficient_data_missing_venue_skipped (behavior reversed per R1-2; new test_missing_venue_does_not_skip_entry asserts the correct migration semantics). Tests: 447 unittest (+13 from previous /simplify pass) + 201 pytest adapters + spec consistency + preprint_venues all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#105): codex R2 [P2]×2 + [P3] — source_pointer / DOI fallback / partial no-op R2-1 [P2] Honor source_pointer hints for preprints: v3.7.3 spec §3.2 Vector 1 says "venue (or, when venue is absent, inference from source_pointer)" — but resolver required explicit venue. Schema-valid legacy entries with year>=2024, no venue, and a preprint-URL source_pointer (https://arxiv.org/abs/2401...) were incorrectly emitting preprint_post_llm_inflection=false. Fix: new _POINTER_VENUE_HINTS table (10 substring patterns mirroring the 10- venue closed list) + _infer_venue_from_pointer() fallback. Explicit venue still takes precedence. 6 new resolver tests. R2-2 [P2] DOI 404 / mismatch falls back to title search: v3.7.3 Vector 2 says unmatched=true only when NEITHER DOI nor title yields a hit. Original SS client returned no-match immediately on DOI 404 or DOI_MISMATCH, falsely marking legitimate DOI-bearing entries as unmatched. Fix: lookup() now falls through to title search after DOI-miss before declaring unmatched. 3 new SS client tests (404 → title-hit, DOI_MISMATCH → title-hit, double-404 → unmatched). R2-3 [P3] Partial-fill recovery only counts when a field is added: With persistent API degradation, build_signals_object() on a partial entry returns only fields already present, so the merge loop adds nothing. Original code still incremented patched + set mutated=True, causing dry-run misreport + non-dry-run byte-identical rewrite. Fix: track added_any; on no-op partial, increment skipped_already_migrated and continue (no mutation). 1 new test verifying byte-identical output + correct report counts. Tests: 456 unittest (+9 R2 cases) + 201 pytest adapters + spec_consistency + preprint_venues all green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#105): codex R3 [P2]+[P3] — wire migration tests into CI + verbose flag R3-1 [P2] Run new migration tests in CI: spec-consistency.yml previously ran only the PREPRINT_VENUES lint; the new contamination_signals / migrate / SS client unittest modules were uncovered. Future regressions could land while required checks still pass. Fix: add explicit "Run #105 migration unit tests" step running the three new test modules with -v output. R3-2 [P3] Wire the advertised --verbose flag: CLI exposed --verbose but never read args.verbose. Output was identical to non-verbose, making the documented usage misleading. Fix: - migrate_passport gains `verbose: bool = False` kwarg - migrate_directory plumbs it through - main() passes args.verbose into both - Verbose mode emits per-entry stderr lines tagged with passport path + citation_key + decision (skip/patch with reason) - 1 new test verifies all 3 sample entries surface in --verbose stderr Tests: 457 unittest (+1 verbose case) + spec_consistency green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#105): codex R4 [P2]×2 — title normalize + response-read timeout R4-1 [P2] Strip punctuation before title matching: Protocol §"Query Patterns" Pattern 1 says title matching is "case- insensitive, stripped of punctuation". Raw lowercased comparison penalized punctuation differences ('R.A.G.' vs 'RAG' scored below the 0.70 threshold, falsely marking legitimate hits as unmatched). Fix: - New _normalize_title(): lowercase + translate punctuation to spaces + collapse whitespace runs. - _similarity() now normalizes both inputs before SequenceMatcher. - 3 new tests (acronym crosses threshold, trailing punctuation OK, whitespace collapse verified). R4-2 [P2] Wrap response read timeouts as unavailable: resp.read() can raise OSError (socket.timeout subclasses OSError) or TimeoutError outside the URLError handler. Previously this propagated through compute_ss_unmatched_signal and aborted the migration instead of the documented "omit field on degradation" behavior. Fix: - Add `except (OSError, TimeoutError)` branch that wraps as SemanticScholarUnavailable with diagnostic message. - 2 new tests verify OSError + TimeoutError each get wrapped. Tests: 462 unittest (+5 R4 cases) + spec_consistency green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#105): codex R5 [P2] — record provenance on partial-fill recovery R5-1 [P2] Record provenance when filling partial signals: When migration fills missing fields on an entry whose existing contamination_signals was written by ingest-time bibliography_agent during S2 degradation (no backfilled_at present), the post-hoc mutation must be distinguishable from ingest-time data. Original code added fields silently. Fix: when added_any and entry lacks backfilled_at, set now_iso(). When entry already has the timestamp (R1-3 case: prior migration run filled it), preserve the original — that's the canonical backfill record. 2 new tests verify: - Ingest-time partial without backfilled_at gets one on fill - Pre-existing backfilled_at is preserved across re-runs Deferred to follow-up issue per architectural-inflection discipline (memory feedback_isms_v1_2_cascade_stop_at_r3): R5-2 (SS client throttle to 1 req/s) + R5-3 (latch SS client unavailable after URLError to skip remaining batch). Both are SS-client maturity issues — real but tooling-side accumulation, not contract drift. Will file as separate issue for #105 follow-up. Tests: 464 unittest (+2 R5 cases) + spec_consistency green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
124 lines
4.1 KiB
Python
124 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""#105 lint — PREPRINT_VENUES 10-server list must agree across files.
|
|
|
|
v3.7.3 spec §3.2 Vector 1 fixes the closed list at 10 preprint servers
|
|
(gemini review F6 / codex F13 expansion from initial 6). The list lives
|
|
in two places:
|
|
|
|
1. `deep-research/agents/bibliography_agent.md` § "Signal 1 —
|
|
preprint_post_llm_inflection" — bullet list (prose source of truth
|
|
the bibliography_agent reads at ingest time).
|
|
2. `scripts/contamination_signals.py` `PREPRINT_VENUES` frozenset — the
|
|
migration tool's Python constant.
|
|
|
|
If these drift, post-hoc migration would surface different CONTAMINATED-
|
|
PREPRINT advisories than ingest-time computation. This lint extracts
|
|
both lists, compares as sorted sets, and fails on mismatch.
|
|
|
|
Exit codes:
|
|
0 = lists agree
|
|
1 = mismatch (prints both sets + the diff)
|
|
2 = could not parse either file (file move / heading rename / regex break)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
PROSE_PATH = REPO_ROOT / "deep-research" / "agents" / "bibliography_agent.md"
|
|
PYTHON_PATH = REPO_ROOT / "scripts" / "contamination_signals.py"
|
|
|
|
|
|
# Venue strings in the spec carry mixed casing + spaces (e.g.,
|
|
# "Research Square", "OSF Preprints"); normalize for comparison.
|
|
def _normalize(s: str) -> str:
|
|
return " ".join(s.split())
|
|
|
|
|
|
def _extract_prose_venues(text: str) -> set[str]:
|
|
"""Pull the bullet list under § Signal 1.
|
|
|
|
The prose block looks like:
|
|
2. The entry's `venue` field ...:
|
|
- arXiv
|
|
- bioRxiv
|
|
- medRxiv
|
|
...
|
|
- TechRxiv
|
|
|
|
We slice from "preprint_post_llm_inflection" heading to "Otherwise
|
|
set to" sentinel, then pull every `- <name>` line whose name comes
|
|
before any inline comment.
|
|
"""
|
|
start = text.find("preprint_post_llm_inflection")
|
|
end = text.find("Otherwise set to", start) if start >= 0 else -1
|
|
if start < 0 or end < 0:
|
|
raise RuntimeError(
|
|
"could not locate Signal 1 section in bibliography_agent.md "
|
|
"(heading or 'Otherwise set to' sentinel changed)"
|
|
)
|
|
block = text[start:end]
|
|
venues: set[str] = set()
|
|
for line in block.splitlines():
|
|
m = re.match(r"^\s*-\s+([A-Za-z0-9][A-Za-z0-9 .]*?)(\s*\(.*)?$", line)
|
|
if m:
|
|
venues.add(_normalize(m.group(1)))
|
|
return venues
|
|
|
|
|
|
def _extract_python_venues(text: str) -> set[str]:
|
|
"""Pull the items from the `PREPRINT_VENUES = frozenset({ ... })` literal."""
|
|
m = re.search(
|
|
r"PREPRINT_VENUES\s*=\s*frozenset\(\{([^}]+)\}\)",
|
|
text,
|
|
flags=re.DOTALL,
|
|
)
|
|
if not m:
|
|
raise RuntimeError(
|
|
"could not locate PREPRINT_VENUES literal in contamination_signals.py"
|
|
)
|
|
body = m.group(1)
|
|
venues = {
|
|
_normalize(s.strip().strip('"').strip("'"))
|
|
for s in body.split(",")
|
|
if s.strip().strip('"').strip("'")
|
|
}
|
|
return venues
|
|
|
|
|
|
def main() -> int:
|
|
try:
|
|
prose = _extract_prose_venues(PROSE_PATH.read_text(encoding="utf-8"))
|
|
py = _extract_python_venues(PYTHON_PATH.read_text(encoding="utf-8"))
|
|
except (FileNotFoundError, RuntimeError) as e:
|
|
print(f"PREPRINT_VENUES lint ERROR: {e}", file=sys.stderr)
|
|
return 2
|
|
|
|
if prose == py:
|
|
print(
|
|
f"PREPRINT_VENUES lint OK: {len(prose)} venues agree across "
|
|
f"bibliography_agent.md and contamination_signals.py."
|
|
)
|
|
return 0
|
|
|
|
only_prose = sorted(prose - py)
|
|
only_py = sorted(py - prose)
|
|
print("PREPRINT_VENUES lint FAILED — venue list drift detected:", file=sys.stderr)
|
|
if only_prose:
|
|
print(f" only in bibliography_agent.md: {only_prose}", file=sys.stderr)
|
|
if only_py:
|
|
print(f" only in contamination_signals.py: {only_py}", file=sys.stderr)
|
|
print(
|
|
"Reconcile by updating both files in lockstep. Spec §3.2 Vector 1 + "
|
|
"schema description in literature_corpus_entry.schema.json must also agree.",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|