Autonomous daily run of the internal-link-opportunity scheduled task for Sam Aguiar Injury Lawyers (aguiarinjurylawyers.com, WordPress on Cloudways). Goal: add contextually-correct internal links from unlinked pillar-topic mentions in live published content, repair broken internal links, avoid overlap with the prior few days, and ship a structural improvement to the matcher. Last prior run was 2026-06-01 (no runs 06-02 or 06-03), so saturation had a 3-day gap to recover.
reaudit_db.py, authenticated context=edit raw read): 30/30 live in the WP database, 21/21 pages fully live. Public cache-busted spot-checks on 4 pages confirmed live rendering after cache purge.context_qa2.py classified every insert by enclosing container; all 30 sit in editorial prose (<p>/<li>/<td>/<blockquote>). First single-pass clean run since the non-prose bug class appeared on 05-31./practice-areas/ hub (page id 48) had reacquired two dead internal links; repointed /practice-areas/spinal-cord-injury/ to /practice-areas/spinal-cord/ and /practice-areas/brain-injury/ to /practice-areas/tbi-cases/. DB-verified both dead paths gone and both live targets present.app_purge_cache(server_id=1615235, app_id=6360875).Inserts by pillar: underinsured-motorist 9, tbi-cases 5, drunk-driving 3, distracted-driving 3, hit-and-run 2, spinal-cord 2, premises-liability 1, dram-shop 1, uber-accident 1, wrongful-death 1, uninsured-motorist 1, personal-injury 1. underinsured-motorist is now the dominant anchor (location pages mention it in prose but rarely hand-link it).
The standing fix direction recommended to Sam on 2026-06-01 is now implemented. Background: two runs in a row (05-31 results-widget labels, 06-01 form-dropdown labels) the matcher linked non-prose UI text because the old logic was a deny-list (SKIP_RE): link unless explicitly skipped. Each new non-prose template surface required a new deny rule and was only caught by post-run QA.
Fix in scan.py: find_unlinked_match now calls a new nearest_block_tag(content, pos) and only inserts when the match's nearest enclosing BLOCK element is in PROSE_TAGS = {p, li, td, summary, blockquote, dd}. Inline formatting tags (strong/em/span/a/label, set _INLINE_TAGS) are transparent, so an anchor inside a strong tag within a paragraph still qualifies. SKIP_RE kept as a cheap first filter (now belt-and-suspenders). The allow-list self-defends against future non-prose surfaces (tabs, accordions, nav) with no new deny rule. Unit-tested 10 cases pre-run (prose accept; option/select/widget-div/bare-div/h2 reject), all passed. First production run: 30/30 prose, 0 reverts.
/Users/samaguiar/Documents/Projects/routines-build/outputs/2026-06-04/internal-link-opportunity/
scan.py carries the prose allow-list (nearest_block_tag, PROSE_TAGS, _INLINE_TAGS). Copy THIS forward; do not regress to a deny-list-only scan.py.run.py all-in-one runner (MAX_INSERTS=30, MAX_PER_POST=2).reaudit_db.py DB ground-truth verifier (authoritative).context_qa2.py independent prose-container QA (run every run).broken_href_targeted.py targeted dead-href repointer.before/, after/ per-post snapshots (one PUT to revert).results/FINAL-SUMMARY.json, run-log.json, reaudit-db.json, context-qa.json, broken-href-fixes.json./Users/samaguiar/Documents/Claude/wp-content-backups/<slug>-<ts>-pre-edit.html.routines-build/skills/internal-link-opportunity/SKILL.md (allow-list documented as landed).project-internal-link-routine.md.