diff --git a/README.md b/README.md index 0cd4697c..ea976f58 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,7 @@ See [Run Modes and Costs](docs/RUN_MODES.md) for the full mode matrix, platform | Standard Strategic Overview plus AI Strategy | `primr "Company" https://company.com` | | Strategic Overview only | `primr "Company" https://company.com --no-ai-strategy` | | Separate Microsoft and private-infrastructure strategy artifacts | `primr "Company" https://company.com --platform ms` | -| Enable routed utility-stage pilot | `primr "Company" https://company.com --inference hybrid` | +| Enable routed utility-stage pilot (single-company; may meter host billing) | `primr "Company" https://company.com --inference hybrid --acknowledge-host-agent-may-bill` | | Site corpus and extracted insights only | `primr "Company" https://company.com --mode scrape` | | DNS intelligence only, no model keys required | `primr recon company.com` | | Agent Skills pack for downstream hosts | `primr skills "Company" https://company.com` | @@ -426,8 +426,12 @@ Important keys: | `GEMINI_API_KEY` | Low-cost writing, utility, premium mode, and Gemini-backed stages | | `OPENAI_API_KEY` | Optional OpenAI fallback provider | | `ANTHROPIC_API_KEY` | Optional Anthropic fallback provider | +| `AZURE_OPENAI_API_KEY` | Optional Azure AI Foundry provider (with `AZURE_OPENAI_BASE_URL`) | +| `AWS_BEARER_TOKEN_BEDROCK` | Optional AWS Bedrock provider (or the AWS credential chain; needs `primr[bedrock]`) | | `OLLAMA_BASE_URL` | Optional local OpenAI-compatible endpoint for local eval and utility paths | +`primr keys test` validates any configured provider credential with a live check. + See [API Key Setup](docs/API_KEYS.md) and [Configuration Reference](docs/CONFIG.md). ## Agent and Tool Integration diff --git a/ROADMAP.md b/ROADMAP.md index 05426d8a..5cd02c5e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1728,7 +1728,7 @@ What gets built: - Cost estimator reflects local inference as $0.00 API cost while tracking runtime - `primr doctor --local` validates Ollama is running, models are pulled, VRAM is sufficient - Graceful degradation: if a local model can't handle a stage, fall back to cloud (hybrid) or skip (local) with clear logging -- Progress display shows which backend each stage is using: `Analysis (local: qwen3:30b)` vs `Analysis (cloud: grok-4.3)` +- Progress display shows which backend each stage is using: `Analysis (local: qwen3:32b)` vs `Analysis (cloud: grok-4.3)` Validation approach: - Run the eval harness on the standard company corpus: cloud baseline vs hybrid vs local diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d620b502..ec4cad18 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1263,7 +1263,7 @@ Playwright tiers now perform adaptive lazy-load scrolling (up to 20 steps by def | Model | Role | Pricing (per 1M tokens) | |-------|------|-------------------------| | Grok 4.3 | Default mode: reasoning stages (analysis, workbook, cross-validation) | $1.25 in / $2.50 out · $0.20 cached | -| Grok 4.1 fast | XAI-only utility and writing fallback when Gemini is not configured | $0.20 in / $0.50 out | +| Grok 4.20 non-reasoning | XAI-only utility and writing fallback when Gemini is not configured | $2.00 in / $6.00 out | | Grok 4.20 | Legacy flagship - kept registered for resume of in-flight runs and as a fallback in the analysis chain | $2.00 in / $6.00 out | | Gemini 3.1 Flash-Lite | Default routed writing and utility path when `XAI_API_KEY` and `GEMINI_API_KEY` are both configured | See provider pricing in the estimator | | Gemini 3.1 Pro | `--premium` mode: section writing, analysis | $2/$12 (≤200k) · $4/$18 (>200k) | diff --git a/docs/EVAL.md b/docs/EVAL.md index aa041663..ccdb1398 100644 --- a/docs/EVAL.md +++ b/docs/EVAL.md @@ -70,7 +70,7 @@ Optional LLM-judge overlays on staged reports: ```bash # Cloud judge (requires spend cap) -primr --eval --eval-id eval-2026-02-r1 --eval-llm-judge --eval-judge-provider grok --eval-judge-model grok-4-1-fast-reasoning --eval-judge-max-cost 0.25 +primr --eval --eval-id eval-2026-02-r1 --eval-llm-judge --eval-judge-provider grok --eval-judge-model grok-4.3 --eval-judge-max-cost 0.25 # Local judge against an Ollama/OpenAI-compatible endpoint primr --eval --eval-id eval-2026-03-local --eval-llm-judge --eval-judge-provider local --eval-judge-model qwen3:30b --eval-judge-base-url http://localhost:11434/v1 diff --git a/docs/INTERNALS.md b/docs/INTERNALS.md index 47635ab5..f554de2f 100644 --- a/docs/INTERNALS.md +++ b/docs/INTERNALS.md @@ -66,14 +66,14 @@ Report sections are defined in YAML configuration files, not hardcoded in Python - **Customizable**: Users can modify sections without changing code - **Extensible**: New strategy reports can be added by creating new YAML files -**As of December 2025, the Strategic Company Overview uses 21 sections** defined in `src/primr/prompts/company_overview.yaml`: +**The Strategic Company Overview uses 23 sections** defined in `src/primr/prompts/company_overview.yaml`: | Part | Sections | |------|----------| | 1 - Foundational | Executive Summary, Products and Services, Target Customers, Competitive Differentiation, Financial Profile, Company History, Leadership and Organization | -| 2 - Industry | Industry Dynamics, Competitive Landscape | +| 2 - Industry | Industry Dynamics, Industry Outlook, Competitive Landscape | | 3 - Strategic | Business Model, SWOT Analysis, Strategic Tensions, Constraints and Degrees of Freedom | -| 4 - Patterns | Narrative Gap Analysis, Fragilities, Patterns Worth Exploring, Discovery Questions, Engagement Opportunities | +| 4 - Patterns | Narrative Gap Analysis, Fragilities, Patterns Worth Exploring, Discovery Questions, Board Perspective, Engagement Opportunities | | 5 - Frameworks | Porter's Five Forces, Value Chain Analysis, Strategic Positioning Hypothesis | **AI Strategy Report** uses 15 sections defined in `src/primr/prompts/strategies/ai_strategy.yaml`. It starts with business strategy and value, then treats Azure, AWS, GCP, private infrastructure, or agnostic selection as an evaluation emphasis rather than a predetermined answer. @@ -98,17 +98,28 @@ This enables downstream parsing and structured data extraction. ### Tier Selection -The scraping engine uses a simple escalation strategy: +The scraping engine escalates through nine tiers, **browser-first** (a real +browser defeats most modern bot defenses; plain HTTP is the last resort, and a +vision tier sits in the middle as a safety net). The canonical order lives in +`src/primr/data/scraping/tier_registry.py`: ```python def scrape(url): - for tier in [requests, httpx, playwright, playwright_aggressive]: - content, error = tier.scrape(url) + tiers = [ + "playwright", "playwright_aggressive", "patchright", # browser + "curl_cffi", "drissionpage_stealth", "drissionpage", # stealth HTTP / browser + "vision", # screenshot + Gemini extraction + "httpx", "requests", # plain HTTP fallback + ] + for tier in tiers: + content, error = run_tier(tier, url) if content and not is_soft_blocked(content): return content return None ``` +See [ARCHITECTURE](ARCHITECTURE.md) for the full tier table and per-tier costs. + Each tier is tried in order. Escalation happens on: - HTTP errors (4xx, 5xx) - Connection errors @@ -148,21 +159,21 @@ def detect_soft_block(text, url): ### Browser Fingerprinting -Four browser profiles are maintained: - -| Profile | User Agent | Platform | Timezone | -|---------|------------|----------|----------| -| Windows Chrome | Chrome/122 on Win10 | Win32 | America/New_York | -| Mac Chrome | Chrome/122 on macOS | MacIntel | America/Los_Angeles | -| Windows Firefox | Firefox/123 on Win10 | Win32 | America/Chicago | -| Mac Safari | Safari/17.2 on macOS | MacIntel | America/Denver | - -Each profile includes: -- User agent string -- Platform identifier -- Vendor string -- Timezone -- Screen dimensions +Five HTTP profiles are maintained in +`src/primr/data/scraping/profiles.py`: + +| Profile | User Agent | Platform | +|---------|------------|----------| +| `chrome_131_windows` | Chrome/131 on Win10 | Win32 | +| `chrome_131_mac` | Chrome/131 on macOS | MacIntel | +| `chrome_130_windows` | Chrome/130 on Win10 | Win32 | +| `edge_131_windows` | Edge/131 on Win10 | Win32 | +| `safari_18_mac` | Safari/18.2 on macOS | MacIntel | + +Each profile carries a user-agent string, platform and vendor identifiers, +client hints, and screen dimensions. Timezone and viewport are decoupled into a +separate `CONTEXT_PROFILES` set (America/New_York, Los_Angeles, Chicago, +Denver) so a fingerprint and its locale can be varied independently. - Color depth - Hardware concurrency - Device memory @@ -219,12 +230,13 @@ const getParameterProxyHandler = { ### Grading Criteria -Each section is graded on four dimensions: +Each section is graded on four equally-weighted criteria that contribute to a +single **0-100** score (there are no separate per-criterion sub-scores): -1. **Clarity & Readability** (0-25): Is the section well-structured? -2. **Completeness** (0-25): Does it cover critical aspects? -3. **Insight Depth** (0-25): Does it provide meaningful business insights? -4. **Accuracy** (0-25): Does it match the company's website information? +1. **Clarity & Readability**: Is the section well-structured? +2. **Completeness**: Does it cover critical aspects? +3. **Insight Depth**: Does it provide meaningful business insights? +4. **Accuracy**: Does it match the company's website information? ### Grading Prompt @@ -543,9 +555,14 @@ src/primr/prompts/ │ ├── formatting.yaml # Formatting rules │ └── personas.yaml # Analyst personas └── strategies/ - ├── ai_strategy.yaml # AI strategy module - ├── cloud_migration.yaml # Cloud migration (placeholder) - └── data_strategy.yaml # Data strategy (placeholder) + ├── ai_strategy.yaml # AI strategy module (default) + ├── ai_first_transformation.yaml # Historical / non-selectable + ├── customer_experience.yaml # Active strategy module + ├── data_fabric_strategy.yaml # Active strategy module + ├── modern_security_compliance.yaml + ├── skills.yaml # Skills-pack strategy + ├── cloud_migration.yaml # Placeholder + └── data_strategy.yaml # Placeholder ``` ### Core Components diff --git a/tests/test_doc_links.py b/tests/test_doc_links.py new file mode 100644 index 00000000..e1a16581 --- /dev/null +++ b/tests/test_doc_links.py @@ -0,0 +1,51 @@ +"""Every relative link in the project's Markdown must resolve. + +The index test (`test_docs_index.py`) guards the doc map specifically; this one +guards *all* Markdown in the repo -- READMEs, guides, skills, design notes, +example docs -- so a renamed or deleted file can never leave a dead link behind +in prose. External links (http/https/mailto) and pure anchors are out of scope; +only on-disk relative targets are checked, which needs no network and is safe +under a shallow CI checkout. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent + +# Build snapshots, vendored trees, and generated output are not source docs. +_EXCLUDED_PARTS = { + ".venv", + "node_modules", + ".agent", + ".git", + "site", + "htmlcov", + "__pycache__", +} + +_LINK = re.compile(r"\[[^\]]*\]\(([^)]+)\)") + + +def _markdown_files() -> list[Path]: + return [ + path for path in REPO_ROOT.rglob("*.md") if not _EXCLUDED_PARTS.intersection(path.parts) + ] + + +def test_all_relative_markdown_links_resolve() -> None: + broken: list[str] = [] + for md in _markdown_files(): + text = md.read_text(encoding="utf-8", errors="replace") + for match in _LINK.finditer(text): + target = match.group(1).strip() + if target.startswith(("http://", "https://", "mailto:", "#", "tel:")): + continue + path_part = target.split("#", 1)[0].strip() + if not path_part: + continue + if not (md.parent / path_part).resolve().exists(): + broken.append(f"{md.relative_to(REPO_ROOT)} -> {target}") + assert not broken, "Broken relative Markdown links:\n" + "\n".join(broken)