Be terse. Prefer fragments over full sentences. Skip filler and preamble. Sacrifice grammar for density. Lead with the answer or action.
Web service for mapping political and corporate power: people, organizations, roles, and their temporal relationships.
TDD required. Red → Green → Refactor. No production code without a failing test first.
Python ≥3.12, uv, pytest, ruff; Node ≥22, npm, vitest + ESLint + Prettier (JS only); bats + shellcheck (shell); pre-commit (git hooks)
SocratiCode is the preferred semantic-search tool here once indexed (manifest
.socraticodecontextartifacts.json). Its MCP tools are deferred — schemas
load only after the ToolSearch prefetch that
.claude/hooks/socraticode-reminder.sh prints each session.
Negative rule. Use SocratiCode MCP tools first for semantic questions
("where is X", "how does Y work", "what depends on Z"). Reach for grep/rg
only on exact strings (error messages, log lines, known symbols). Reserve the
Explore subagent for path-pattern walks (*.py under src/api/), not semantic
search.
| Goal | Tool |
|---|---|
| Where is X defined / how does Y work / what touches Z | codebase_search |
| Exact string or regex (errors, log lines, known symbols) | grep / rg |
| Imports/dependents of a file · blast radius of a change | codebase_graph_query / codebase_impact |
Full tool table, prefetch hook, per-tool guidance: docs/SOCRATICODE.md.
src/api/ — FastAPI app (ASGI, routes, auth, schemas)
admin/ — Jinja2 + HTMX admin dashboard
public/ — JSON API (X-API-Key auth, server-to-server)
src/core/ — Shared domain logic (db, schema.sql, normalizers, ingestion); `ingestion/mapping/` is the dbt-duckdb project, the only place usa-wa ontology lives (#497)
src/static/ — Static assets; vendor/ is SHA-pinned, excluded from linting
tests/ — Mirrors src/; js/ for Vitest
docs/ — Reference docs, by subject; index at the end of this file
scripts/ — One-off operational scripts
infra/ — systemd units + terraform
Full conventions → docs/ADMIN.md
Accessibility rules and their test tiers → docs/ACCESSIBILITY.md
- JS required (#287): the admin is an HTMX app, not progressively enhanced. Never add
method="post" action=…to anhx-postcontrol to "support no-JS" — the 303 fallbacks serve non-HTMX clients, not browsers. Guard:test_js_required_policy.py - Auth:
user: AdminUser = Depends(get_admin_user)on every route - Archive model:
archived_at TIMESTAMPTZ— NULL = active, non-NULL = archived; hard delete requires archived (409 otherwise). Unarchive is fallible onroles/role_assignments(#424) — a freed slot can be reoccupied, so savepoint + warning flash, never a bareUPDATE - Every mutation route: HTMX partial via
is_htmx(request)plus awith_flash(url, key)RedirectResponsefallback — CI-enforced bytest_mutation_fallback_sweep.py - Flash:
flash_trigger(level, body); alwaysmarkupsafe.escape()DB-derived values - Status filters (#306), dup-count invalidation, citation counts (#341): each carries a rule and a sweep test — read
docs/ADMIN.mdbefore touching a list or a merge path
Full conventions → docs/CONVENTIONS.md
- Auth deps from
src.api.public.depsonly:require_api_key/require_key(read),require_scope("scope:id")(write); 403 missing/insufficient, 401 invalid. Write txns open viastamped_transaction(db, key_id), never baredb.transaction()(#491, sweep-enforced) - All routes: Pydantic
response_model+operation_id; nodict[str, Any]returns - Lists:
{"data": [...], "meta": {...}}, fetchlimit+1forhas_more; every paginatedORDER BYmust end with a unique column (#297) - Timestamps:
datetime+@field_serializer→TimestampStrviafmt_ts(), never hand-built (#440) - Conditional GET (#292/#392) lives entirely in
src/api/public/etag.py— a route callsconditional_response(...)and never readsif-none-matchitself - Observation semantics — assignments (#311/#391), events (#321/#322), citations (#319): identity vs payload, refine-in-place,
op="retract",source_key_idsame-or-NULL gate. Each is enforced by a sweep test; readdocs/OBSERVATIONS.mdbefore changing one
Full conventions → docs/SCHEMA.md
- PKs: ULIDs via
generate_id()fromsrc.core.db updated_at: maintained by DB triggers — never set manually- Route handlers acquire connections via
Depends(get_db)only — neversrc.core.db.acquire()(breaks test isolation). Sole route exception:GET /ready(#343) - Display names: always use
v_org_display_names/v_person_display_namesviews; never join name tables directly for display - Raw
person_namesaccess: AND-appendvisibility='public'or callvisible_names_filter()fromsrc.core.db. Lint enforces. - Integration tests: require
TEST_DATABASE_URL; never run against the production DB - Integration test fixtures acquire from the session-scoped
db_pool; endpoint tests use the lifespan-less rollback client (#288) - Every inline
CHECK/FK/ON DELETEchange ships an idempotent reconciliationDOblock, placed before anyset_updated_at()trigger on that table (#307/#312/#315/#392); dailypower-map-schema-parity.timerguards it. Seeds of a UNIQUE-natural-key lookup stage rows in_seed_<table>+reconcile_seeded_slugs()first (#458) - Temporal and provenance invariants — org lifespan (#307), assignment/event/citation observations, org parent (#334), RA→RA edges (#301), canonical person name (#308), merge identity & signals (#324/#327/#467), entity search (#316), role-type vocabulary (#266) — each has exact rules in the
docs/SCHEMA*.mdfamily,docs/OBSERVATIONS.md,docs/API_ASSIGNMENTS.md, ordocs/MERGE.md. Read them before changing any of them.
Single VM; port split:
| Port | Process | Managed by |
|---|---|---|
| 8000 | Production API (--workers 2) |
systemd (power-map.service) |
| 8001 | Dev server (--reload) |
manual, always from a worktree |
All development work must be done in a git worktree — never edit the main checkout directly. brainstorming is the entry point that triggers worktree setup via using-git-worktrees. After teardown, run git worktree prune. Sole exemption (#505): a spike — a read-only or throwaway probe answering a feasibility question, keeping no code. Keeping anything re-classifies the task, and that lands in a worktree.
Worktree setup (required after creation, #450): run
bash scripts/worktree-setup.sh <worktree-path>Gives the worktree its own .venv and its own node_modules (#554), initialises the skills-vendor/ submodules, and symlinks the gitignored .env and data/cannabis_observer; refuses (exit 2) against the main checkout. Never share a venv with the main checkout — that is production's working directory, and its units' uv run / ExecStartPre=uv sync rewrite a shared venv mid-suite, taking the browser tier with it. Full rules → docs/COMMANDS.md § Worktree setup.
exe.dev proxy: dev server at https://power-map.exe.xyz:8001/.
Only the hazards are here; every command itself is in docs/COMMANDS.md.
bash scripts/apply-schema.shtargets PRODUCTION from any directory — main checkout only, and it refuses in a linked worktree (exit 2, #398). From a worktree,--test.sudo systemctl restart power-mapalso applies any pending schema change.- The dev server on 8001 always runs from a worktree, never the main checkout.
/health+/readyare unauthenticated probes (#343); apool_timeoutfrom/readymeans the egress IP likely rotated out of DO Trusted Sources →docs/RUNBOOK_DB_TRIAGE.md(#410).
Scheduled timers all surface failure through systemctl --failed — roster, cadences and scripts in docs/COMMANDS.md § Scheduled timers.
Operational scripts are dry run by default (#402/#399): DATABASE_URL resolves to production from any directory, so a scripts/ writer gates its write behind --execute and echoes its target first. Uniform flags, the scripts/_dsn.py resolver, and the AST sweep enforcing them → docs/RUNBOOKS.md § Operational scripts.
/etc/power-map/.env then .env, later winning. Every uv run here depends on the env_args idiom → docs/COMMANDS.md § Environment.
Skills in skills/ (agentskills.io) and .claude/skills/ (Claude Code). Reference: docs/SKILLS.md
Commit Messages:
#<number> [type]: <description> # with issue
[type]: <description> # without issue
Types: feat, fix, refactor, docs, test, chore
Logging:
from src.core.logging import get_logger
logger = get_logger(__name__)Entry points only: call configure_logging() once.
Date & Time:
- All UTC
- ISO 8601 on the API wire (#440 guards it; JSON logs use
+00:00):YYYY-MM-DDTHH:MM:SS.ffffffZ,YYYY-MM-DDdates
Version bumps: update pyproject.toml and package.json together — the check-version-sync pre-commit hook enforces this.
General:
- Imports explicit and at file top — never inline in a function
- Docstrings for public modules, classes, functions
- Test structure mirrors source (
src/foo.py→tests/test_foo.py) - Small, focused functions
Each line says what a task would need the doc for — load the one that matches, not the tree.
Domain & data
- docs/SCHEMA.md — tables, column conventions, display-name views, links schema; routes on to SCHEMA_INDEXES and SCHEMA_VALIDITY
- docs/OBSERVATIONS.md — identifier types; writes: identity vs payload, refine-in-place,
op="retract",source_key_idgate; routes on to ANCILLARY - docs/NAMES.md — person and org names: the canonical/display pointer, visibility rules, structured parts, readings, locale/script tables, org display-identity invariants
Public API
- docs/PUBLIC_API.md — auth, scopes, rate limits, pagination, conditional requests; routes to CHANGE_FEED
- docs/API_ENTITIES.md — one-table index over the resource docs below; load one, not the set
- API_PEOPLE — people, identify, name reads
- API_ORGS — orgs, hierarchy, lifespan
- API_ROLES — roles and the role-type catalog
- API_ASSIGNMENTS — assignments, RA→RA relationships
- API_EVENTS — entity events
- API_JURISDICTIONS — jurisdictions and districts
- docs/CONVENTIONS.md — request/response contracts every route follows, the API request log, ingestion
Admin dashboard
- docs/ADMIN.md — server side: auth, archive model, HTMX partials, flash, status filters; routes on to ADMIN_PANELS and ADMIN_NAMES
- docs/ADMIN_OVERLAY.md — the curation overlay: editing a field a dataset owns, and what survives a re-apply
- docs/HTMX.md — interaction patterns: swaps, redirects, flash, pagination, inline edit, guarded deletes, live header sync
- docs/UI.md — components and table/list conventions: buttons, badges, modals, page headers, empty states, the row-key contract
- docs/FORMS.md — the hand-built composite controls: typeahead, address confirm, paired dates
- docs/MERGE.md — the server-side merge data contract, duplicate detection, and the merge-bar pattern across people, orgs and roles
- docs/STYLE.md — visual system: brand, colour, dark mode, CSS tokens, layout, breakpoints, i18n, performance
- docs/ACCESSIBILITY.md — WCAG 2.1 AA markup rules and the a11y test tiers
Operating it
- docs/COMMANDS.md — everyday commands: setup, env files, provisioning, deploy, the dev loop, linting, timers, the ship gate
- docs/TESTING.md — each test tier, the integration marker, the endpoint-test client, Vitest, the browser a11y sweep, the ship gate
- docs/RUNBOOKS.md — data operations: importer, seeds, role sweep, TTL prune, operational-script dry-run rules
- docs/RUNBOOK_DESIRED_STATE.md — the dataset-subscription chain: pull, build, apply, and what a nightly run blocks on
- docs/AUDITS.md — the recurring integrity audits and uptime guards, and which carry systemd timers
- docs/RUNBOOK_DB_TRIAGE.md — DB unreachable:
/readyreasons, egress-IP drift - docs/RUNBOOK_DB_MIGRATION.md — DB cutover checklist, maintenance window, rollback
- docs/SKILLS.md — vendored skill inventory, submodule refresh, hook command form, index health (the daily
unresolved %line is not a defect) - docs/SOCRATICODE.md — the exploration policy's other half: full tool table, prefetch, per-tool notes, graph health
- docs/CONTEXT.md — the rules this file obeys: its token budget, index lines that stay pointers, why a count carries a command or no number