Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
2 changes: 1 addition & 1 deletion docs/EVAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 46 additions & 29 deletions docs/INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions tests/test_doc_links.py
Original file line number Diff line number Diff line change
@@ -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)