#!/usr/bin/env python3 """Verify every `path:line` evidence citation in docs/architecture against the repository. The architecture package's central claim is "we verified this by direct inspection", so a citation that lands on the wrong line is not a cosmetic defect: it is the one thing a reviewer spot-checks. This gate makes a stale anchor a failed build instead of a credibility problem discovered by the reader. Two levels of checking: 1. EVERY citation — the cited file exists and every cited line number is within the file. Catches citations to deleted files and off-the-end line numbers. 2. CURATED anchors — for the load-bearing anchors listed in ANCHORS below, the cited line must still match an expected pattern. Catches citation rot: the source moved and the anchors went stale. 3. CLAIM rules — a sentence making a specific claim must not cite an anchor that belongs to a different claim. This is the level that catches the defect this script was written for. Levels 1 and 2 cannot see it: `js/data.js:126` is a real line and really does hold `salary: int(90,190)*1000`, so citing it for the *ATS score* passes both — while being exactly wrong, because the score is at `:123`. Several adjacent lines in `js/data.js` each anchor a different argument in this package, so a one-line slip silently reattributes a claim. Lines that intentionally discuss a citation correction will name both the wrong and the right anchor and would trip level 3. Wrap those in: ...prose about the old and new anchors... stdlib only, no dependencies, no build step. Run from anywhere: python3 tools/check_evidence_citations.py Exit status 0 = clean, 1 = at least one bad citation. """ from __future__ import annotations import os import re import sys REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DOCS = os.path.join(REPO, 'docs', 'architecture') # Only extensions that exist in this repository today. Citations to planned files # (.sql migrations, .tsx components, .yml workflows) are intentionally not matched — # they cannot be verified and are design intent, not evidence. CITATION = re.compile( r'(?P(?:[\w.-]+/)*[\w.-]+\.(?:js|html|css|py))' r':(?P\d+(?:\s*[-,]\s*\d+)*)' ) # path:line -> regex the cited line must match. Keep the reason in the comment. ANCHORS: dict[str, str] = { # --- the candidate object literal: four adjacent lines, four different claims --- 'js/data.js:117': r'candidates\.push\(\{', 'js/data.js:118': r"id: 'CAN-' \+ \(5001 \+ i\)", # CAN- reference code 'js/data.js:120': r'jobId: job\.id', # single jobId on candidate 'js/data.js:121': r'experience: int\(1, 14\)', # experience in whole years 'js/data.js:122': r'location: pick\(locations\), stage, status: stage', # stage as string # The most-quoted fact in the package, and the recruiter scalar, share this line. 'js/data.js:123': r'aiScore: int\(52, 98\).*recruiter: job\.recruiter', 'js/data.js:126': r'salary: int\(90, 190\) \* 1000', # money as bare integer # --- the job object literal --- 'js/data.js:94': r'jobs\.push\(\{', 'js/data.js:95': r"id: 'JOB-' \+ \(1001 \+ i\)", # JOB- reference code 'js/data.js:96': r'recruiter: rec\.name, recruiterId: rec\.id', # recruiter scalar on job 'js/data.js:99': r'salaryMin: int\(80, 160\) \* 1000', # job salary range 'js/data.js:100': r'skills: pickN\(skillsPool, int\(4, 7\)\)', # requirements as flat array # --- the inbox / raw-intake object literal --- 'js/data.js:297': r'inbox\.push\(\{', 'js/data.js:298': r"id: 'APP-' \+ \(30001 \+ i\)", # APP- reference code 'js/data.js:300': r'resumeStatus: rs, atsScore: int\(48, 97\)', # --- other load-bearing anchors --- 'js/data.js:10': r'function rand\(\)', # seeded LCG 'js/data.js:237': r"new Date\('2026-07-09'\)", # hardcoded today 'js/data.js:492': r'window\.DB = \{', 'js/data.js:504': r"money: n => '\$'", # hardcoded currency symbol 'js/data.js:510': r'getRecruiterByName', # name string as identity 'js/candidates.js:18': r"new Date\('2026-07-09'\)", # client-side relevance blend 'js/candidates.js:68': r'\$\{UI\.ava|render: c =>', # unescaped interpolation sink 'js/candidates.js:121': r'onclick="Candidates\.openProfi', # inline handler with interpolated id 'js/offers.js:129': r'\+f\.base <= 0', # offer validation is > 0 only 'js/rbac.js:78': r'r\.matrix\[mod\]\[pi\] = !r\.matrix\[mod\]\[pi\]', # matrix gates nothing 'js/ui.js:251': r'window\.UI = \{', # primitive export list 'js/charts.js:339': r'window\.Charts = \{', # chart engine export list 'js/charts.js:9': r'getComputedStyle', # charts read CSS custom properties 'index.html:264': r'