Module Architecture - How Wire is Built
Wire is fourteen modules. If you've opened the source and wondered why something lives where it does, or why a change in one place didn't affect another, this page explains the boundaries.
Wire's modules aren't organized by feature. They're organized by reason to change. `analyze.py` changes when routing logic improves. `content.py` changes when a new operation is needed. `chief.py` changes when batch logic evolves. None of these changes touch each other. That separation is intentional, and it has a cost implication most people don't expect: one module makes zero API calls. Which part of the architecture matters most to you right now?
The linter runs after the build, not during content creation. That timing is deliberate. If your build is failing on a lint rule, the problem is in the rendered HTML, not in your markdown source. One design detail catches most people off guard: `_real_tags()` filters out tags inside `<code>` and `<pre>` blocks. Without that filter, a code example containing an H2 would trigger a heading hierarchy violation. Is the failure you're seeing a false positive on a code block, or a real structural problem in the page?
`analyze.py` makes zero API calls. Keyword routing, BM25 scoring, and amendment brief generation all run locally. When `chief.py` builds briefs for 500 pages, that step costs nothing. The Claude API is only called in `content.py`, after the brief is already built. At $0.06 per page, the paid step receives specific instructions, not an open-ended prompt. The dependency graph enforces this: imports are unidirectional, so no module can accidentally reverse the analyze-first, act-second flow. Does that separation change how you were planning to extend Wire?
`chief.py` coordinates multi-page operations but never calls Claude directly. It calls `analyze.py`, then `content.py`, then `db.py`. The cascade filter is the part most people miss: merge blocks differentiate, differentiate blocks refine, refine blocks reword. If a page is flagged for merging, it won't also get a reword pass in the same run. Chief also handles resume on interruption, so a batch of 200 pages that fails at page 140 picks up where it stopped. Which part of that flow broke for you?
Every content operation in `content.py` follows the same pattern: gather context, build prompt, call Claude, validate output, save through the sanitize pipeline. The `improve()` method is the exception that proves the rule. It doesn't build its own prompt. It receives a pre-built amendment brief from `analyze.py` and passes it to Claude as a complete set of instructions. That means `improve()` is only as good as the brief. If the output isn't what you expected, the problem is usually upstream in the analysis step, not in the content operation itself.
GSC reads and GSC fetches are separate functions by design. `get_search_terms()` reads from the local SQLite database. `fetch_and_store()` hits the API. Reading is free and fast. Fetching is rate-limited and logged. The database lives at `.wire/gsc.db` with three tables: Content, Keyword, and Snapshot. The functions most people end up in are `find_overlaps()` and `keeper_score()`, which feed directly into the routing logic in `analyze.py`. If your keyword data looks stale, the question is whether you've run a fetch recently, not whether the read functions are broken.
Wire consists of fourteen modules. Each has a single responsibility. They share data through function calls and dataclasses, not through global state or message passing.
Module Map
wire/
tools.py # Shared infrastructure (Claude API, I/O, dataclasses)
build.py # Static site generator (replaces MkDocs)
lint.py # Build-time HTML content linter (44 rules)
qa.py # Automated QA (SEO, links, accessibility, reports)
discovery.py # Discovery reading system (guided content)
content.py # Content operations (create, refine, seo, merge)
news.py # News gathering and evaluation (junior/senior)
chief.py # Batch orchestrator (data, audit, reword, etc.)
analyze.py # Local analysis (zero API cost)
gsc.py # Google Search Console integration
db.py # SQLite database operations
schema.py # Frontmatter validation contract
wp.py # WordPress migration tool
migrate.py # Backfill created dates from git history
build_dev.py # Dev server with file watching
wire/build.py - Static Site Generator
Single-pass markdown-to-HTML pipeline. Replaces MkDocs entirely.
python -m wire.build --site . --serve
# Builds, lints, and starts dev server at localhost:8000
Pipeline: wire.yml config → collect pages → parse frontmatter → render markdown (mistune) → inject discovery layer → apply Jinja2 template → write HTML → copy assets → generate sitemap.xml + robots.txt → auto-lint → optional QA.
Custom renderer: WireRenderer(mistune.HTMLRenderer) handles link sculpting (first 3 external links get dofollow, rest masked), heading anchor generation, code block language classes, and raw HTML passthrough.
Theming: CSS custom properties injected from wire.yml theme: section. Clients change colors and fonts through config, with no CSS editing.
theme:
primary_color: "#059669"
accent_color: "#f59e0b"
font_family: "'Inter', sans-serif"
Performance: ThreadPoolExecutor with 8 workers. Dirty builds skip unchanged files. This site (17 pages) builds in 0.1 seconds.
CLI flags: --dirty (incremental), --serve (dev server), --qa (run QA), --no-lint (skip lint), --port N.
wire/lint.py - Build-Time Content Linter
44 rules across 11 groups. Every rule is a hard failure with a specific fix. Runs automatically after every build. See the full rule reference for what each check does and why it exists.
python -m wire.lint --site site/ --rules RULE-01,RULE-05
Groups: Metadata (RULE-01 to RULE-11), Canonicalization (RULE-12, 13, 15, 16), URL Structure (RULE-17 to 22), Sitemap (RULE-23 to 26), robots.txt (RULE-28 to 31), Internal Links (RULE-33 to 35), Security (RULE-37), Images (RULE-40 to 42), Hreflang (RULE-43, 44, 45), Structured Data (RULE-46 to 48), Indexability (RULE-49, 50).
Key design: _real_tags(soup, tag_name) filters out tags inside <code> and <pre> blocks. Without this, code examples trigger false positives for missing H1, heading hierarchy violations, and image alt text.
Cross-page checks: Duplicate title detection, duplicate H1 detection, orphan page detection (no inbound links), sitemap coverage. These require the full site context, not just a single page.
wire/qa.py - Automated Quality Assurance
HTML-based QA that replaces manual browser review. Checks every page for SEO metadata, broken links, accessibility basics, and HTML quality.
python -m wire.qa --site site/ --full
# QA: 17 pages checked. All passed.
# Report: qa/report.html
Checks: check_seo() (title, description, H1, canonical, viewport), check_links() (broken internal links, masked link validation, empty anchors), check_accessibility() (image alt, target=_blank without noopener, missing lang), check_html_quality() (DOM node count, inline styles).
Report: generate_report_html() produces a visual HTML report with color-coded pass/fail. Engineers open one file. If green, ship it.
Sampling: Full mode checks every page. Default mode samples 20 random pages plus the homepage. Keeps QA fast for large sites.
wire/discovery.py - Discovery Reading System
Interactive guided reading layer for long-form content. Increases dwell time without changing the article itself.
docs/guides/getting-started/
index.md # Full article (unchanged, always in DOM)
steps.md # Discovery layer (optional companion file)
Parser: parse_steps() converts :::hook/:::step/:::choices markdown syntax into a JSON structure. render_discovery_html() generates DOM elements with data-* attributes.
Behavior: Hook text and two buttons appear above the article. "Guide me through this" fades in interactive step cards. "Read the full article on this" scrolls to the matching section with a highlight. All content is in the DOM on load. Works without JavaScript.
Integration: wire.build auto-detects steps.md files and injects the discovery HTML before the article content. The discovery.js script handles transitions. See the discovery system guide for authoring steps.
wire/content.py - Content Operations
Every operation that creates or modifies a single page.
Commands: create(), refine(), expand(), compare(), consolidate(), seo(), seo_light(), crosslink(), merge(), differentiate(), improve().
Each method follows the same pattern: gather context → build prompt → call Claude → validate output → save through the sanitize pipeline. The improve() method combines multiple operations into one Claude call based on a pre-built amendment brief from analyze.py.
wire/news.py - News Intelligence
Junior-senior pattern for evaluating article relevance.
analyze_article() performs junior evaluation. Claude reads one article and decides if it is relevant. Returns ArticleEvaluation or None.
combine() handles senior synthesis. All relevant junior reports combined into an executive summary.
gather_news() orchestrates the pipeline: web search, fetch articles, junior evaluation, senior synthesis, save as pending news file.
wire/chief.py - Batch Orchestrator
Coordinates multi-page operations across entire topics.
Commands: data, audit, deduplicate, news, refine, reword, consolidate, crosslink, sanitize, enrich, newsweek.
Chief handles progress tracking (resume on interruption), cascade filtering (merge blocks differentiate blocks refine blocks reword), input validation, and reporting. It calls content.py, news.py, analyze.py, and db.py but never accesses Claude directly.
wire/analyze.py - Local Analysis
Zero API calls. All functions use GSC database queries and local math.
keyword_routing() decides how to handle a keyword: add section, add substantial content, mention with link, or create new page. Uses three signals: impression ratio, page breadth, and BM25 semantic fit.
build_amendment_brief() produces a complete improvement brief for one page. Checks gates (recently touched, no data, overlap-blocked) before analysis runs.
wire/gsc.py + wire/db.py - Data Layer
GSC: get_search_terms() reads from database only. fetch_and_store() hits the API. Clean separation: reading is free, fetching is rate-limited.
Database: SQLite at .wire/gsc.db. Three tables (Content, Keyword, Snapshot). Key functions: find_overlaps(), find_content_gaps(), find_dead_pages(), keeper_score(), trending_keywords().
wire/wp.py - WordPress Migration
Downloads WordPress pages and converts to Wire-compatible markdown. Extracts metadata from JSON-LD/Yoast/OG tags. Converts HTML to markdown with markdownify. Downloads images to local assets. See the migration guide.
Why Twelve Modules, Not One
Wire's module count is a design choice, not accidental complexity. Each module has exactly one reason to change.
analyze.py changes when keyword routing logic improves. content.py changes when a new content operation is needed. chief.py changes when batch orchestration logic evolves. db.py changes when the database schema changes. None of these changes affect each other.
This separation has a concrete benefit: the analysis module (analyze.py) makes zero API calls. Keyword presence analysis, BM25 scoring, keyword routing, and amendment brief generation all run locally. When Wire builds an amendment brief for 500 pages, it costs nothing. Only the content generation step in content.py hits the Claude API. At $0.06 per page, the paid step is precise. Claude receives a pre-built brief with specific instructions, not an open-ended prompt.
The module boundaries also enforce Wire's core principle: analyze first, act second. chief.py calls analyze.py before calling content.py. It calls db.py before calling gsc.py. The dependency graph flows from free operations to paid ones. No module can accidentally reverse this flow because the imports are unidirectional.
The static site generator (build.py) is deliberately separate from the content pipeline (content.py). Content operations produce markdown. Build operations produce HTML. Mixing these, as many CMS platforms do, creates situations where build failures corrupt content and content errors break builds. Wire's separation means a build failure never affects your markdown source files.
The linter (lint.py) runs after the build, not during content creation. This is intentional. Content creation uses Claude with styleguide rules (Layer 1 prevention). The linter checks the rendered HTML output (Layer 3 detection). Running the linter during content creation would be redundant; the styleguide already prevents the issues the linter checks for. Running it after build catches problems introduced by template rendering, markdown edge cases, or manual edits that bypassed Wire's pipeline.