""" frame_library_v3: Track A rule set using framing_v2 signals. Extends frame_library_v2 (rule-level changes from RULE_AUDIT.md) with signal-level additions from framing_v2.py (S-1 through S-4 per DESIGN_v2.md §4). Changes vs frame_library_v2.suggest_frames_v2: FVS-012: uses framing_v2.detect_coverage_v2 (includes S-1 expanded uncertainty regex); threshold loosened to > 2/1kw per v2. FVS-016: rewired to use detect_named_author_citation (S-2); fires when named-author density > 5/1kw. FVS-008: rewired to use detect_growth_vocabulary (S-3); fires when growth vocab density > 6/1kw AND voice is promotional/advisory/analytical AND risks density < 5/1kw. FVS-015: rewired to use detect_efficiency_vocabulary (S-4); fires when efficiency vocab density > 5/1kw AND voice is promotional/advisory/analytical. Thresholds per DESIGN_v2.md §4; any tuning is noted inline. """ from __future__ import annotations import sys from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parents[2] STUDY_DIR = REPO_ROOT / "fvs_eval" / "validation_study" if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) if str(STUDY_DIR) not in sys.path: sys.path.insert(0, str(STUDY_DIR)) from frame_library import FVS_NAMES, _DEFINITIONS # noqa: E402 def suggest_frames_v3( coverage_v2: dict[str, Any], voice: dict[str, Any], temporal: dict[str, Any], epistemic: dict[str, Any], named_author: dict[str, Any], growth_vocab: dict[str, Any], efficiency_vocab: dict[str, Any], ) -> list[dict[str, str]]: """v3 suggest_frames using the new signals. Note: coverage_v2 uses framing_v2.detect_coverage_v2 (expanded uncertainty regex). All other inputs come from live framing.py except where explicitly new. """ suggestions: list[dict[str, str]] = [] cats = coverage_v2.get("categories", {}) covered = set(coverage_v2.get("covered", [])) missing = set(coverage_v2.get("missing", [])) voice_type = voice.get("voice", "") sourced_pct = epistemic.get("sourced_pct", 100) total_sentences = voice.get("total_sentences", 0) named_author_density = named_author.get("density_per_1kw", 0) growth_density = growth_vocab.get("density_per_1kw", 0) efficiency_density = efficiency_vocab.get("density_per_1kw", 0) risks_density = cats.get("risks", {}).get("density_per_1kw", 0) # Short-doc guard preserved. if total_sentences < 5: return suggestions def _add(fvs_id, name, signal, question): suggestions.append({ "fvs_id": fvs_id, "name": name, "signal": signal, "question": question, "definition": _DEFINITIONS.get(fvs_id, ""), "url": f"/corpus/library/{fvs_id}.html", }) # FVS-001 Frame Amplification: RETIRED (v2 decision) # FVS-002 Fluency-Quality Illusion: KEEP unchanged if voice_type == "promotional" and sourced_pct < 30: _add( "FVS-002", "Fluency-Quality Illusion", "Promotional voice with few sourced claims", "If this were written in rough notes instead of polished prose, would you still accept the claims?", ) # FVS-010 Completeness Illusion: KEEP unchanged if coverage_v2.get("coverage_count", 0) >= 4: densities = [ cats[c]["density_per_1kw"] for c in covered if cats.get(c, {}).get("density_per_1kw", 0) > 0 ] if len(densities) >= 2: max_d = max(densities) min_d = min(d for d in densities if d > 0) if max_d / min_d > 3: _add( "FVS-010", "Completeness Illusion", f"Covers {coverage_v2['coverage_count']} dimensions but density is {max_d / min_d:.0f}x skewed", "Does this document MENTION all perspectives or ANALYZE all perspectives?", ) # FVS-007 Failure Framing: NARROWED with voice filter (v2) if ( "risks" in missing and "uncertainty" in missing and voice_type in ("promotional", "advisory") ): unhedged_pct = 100 - sourced_pct if unhedged_pct > 60: _add( "FVS-007", "Failure Framing (absent)", "No risk or uncertainty coverage, high unsupported assertion rate, in promotional/advisory voice", "What would have to be true for this analysis to be wrong?", ) # FVS-008 Growth Frame: REWIRED with growth-vocabulary signal (S-3) if ( growth_density > 6 and voice_type in ("promotional", "advisory", "analytical") and risks_density < 5 ): _add( "FVS-008", "Growth Frame", f"Growth vocabulary density {growth_density}/1kw with risks density {risks_density}/1kw (low) in {voice_type} voice", "What would a risk analyst say about this same data?", ) # FVS-015 Efficiency Frame: REWIRED with efficiency-vocabulary signal (S-4) if ( efficiency_density > 5 and voice_type in ("promotional", "advisory", "analytical") ): _add( "FVS-015", "Efficiency Frame", f"Efficiency vocabulary density {efficiency_density}/1kw in {voice_type} voice", "Is efficiency the right lens here, or has it been applied by default?", ) # FVS-009 Risk Frame: LOOSENED (v2; drop uncertainty-covered requirement) if ( "risks" in covered and risks_density > 5 and voice_type == "analytical" ): _add( "FVS-009", "Risk Frame (active)", f"Substantive risk analysis ({risks_density}/1Kw) in analytical voice", "Is the risk analysis proportionate, or is it overweighting worst-case scenarios?", ) # FVS-011 Stakeholder Frame: TIGHTENED (v2; density threshold 5 -> 10) stakeholder_density = cats.get("stakeholders", {}).get("density_per_1kw", 0) if ( "stakeholders" in covered and stakeholder_density > 10 and voice_type in ("analytical", "advisory") ): _add( "FVS-011", "Stakeholder Frame (active)", f"Stakeholder coverage present ({stakeholder_density}/1Kw) in analytical voice", "Is the stakeholder analysis substantive or performative?", ) # FVS-012 Uncertainty Frame: LOOSENED (v2; threshold 3 -> 2) # Uses coverage_v2 which has S-1 expanded uncertainty regex. uncertainty_density = cats.get("uncertainty", {}).get("density_per_1kw", 0) if "uncertainty" in covered and uncertainty_density > 2: _add( "FVS-012", "Uncertainty Frame (active)", f"Uncertainty coverage present ({uncertainty_density}/1Kw)", "Is the uncertainty named substantively or merely hedged?", ) # FVS-014 Temporal Anchoring: LOWERED thresholds 70/60 -> 35/35 (v2) past_pct = temporal.get("past_pct", 0) future_pct = temporal.get("future_pct", 0) if past_pct >= 35: _add( "FVS-014", "Temporal Anchoring (past)", f"Past-oriented language in {past_pct}% of sentences", "What has changed since this data was current?", ) elif future_pct >= 35: _add( "FVS-014", "Temporal Anchoring (future)", f"Future-oriented language in {future_pct}% of sentences", "What is the historical base rate for projections like these?", ) # FVS-016 Authority by Citation: REWIRED with named-author-citation signal (S-2) if named_author_density > 5: _add( "FVS-016", "Authority by Citation", f"Named-author / institutional citation density {named_author_density}/1kw", "Can you look up the specific sources cited? Would the argument survive if the citations were removed?", ) return suggestions V3_STATUS = { "FVS-001": "retired (v2)", "FVS-002": "kept_unchanged", "FVS-007": "narrowed_voice_filter (v2)", "FVS-008": "rewired_growth_vocab_signal (S-3)", "FVS-009": "loosened_drop_uncertainty (v2)", "FVS-010": "kept_unchanged", "FVS-011": "tightened_density_5_to_10 (v2)", "FVS-012": "signal_expanded + threshold_loosened (S-1 + v2)", "FVS-014": "lowered_thresholds_70_60_to_35_35 (v2)", "FVS-015": "rewired_efficiency_vocab_signal (S-4)", "FVS-016": "rewired_named_author_signal (S-2)", }