audit-retention · git:20261014.62aa70e · 2026-10-14 · sha256 55afcef7fc18ddde
audit-retention git:20261014.62aa70eA
Immutable. This exact content never changes and is served at /api/v1/blob/55afcef7fc18ddde.
--- name: audit-retention description: "Audit which stores grow without bound and what trims them: every table of every role, with what writes it, when a row is stale, the purge that deletes it, its retention, and whether that purge is batched and indexed; then the stores outside the database (log groups, buckets, queues, the cache). Measures the purges on a database of its own, reads staging's retention settings read-only, and writes a report with verdicts and proposed tickets. Never changes anything." allowed-tools: Read, Grep, Glob, Write, Bash(uv run:*), Bash(git:*), Bash(aws:*), Bash(mkdir:*) --- # audit-retention Which tables grow for ever, which purge trims each one, and whether that purge still works when the table is large. The answer comes from the code, checked against plans on a seeded database of the run's own, and against the retention settings staging actually runs with. Read `../_shared/ops-preamble.md`, a path from this skill's folder, before the first step: the profiles, the account check, and the env file are there. ## Input `[--env local|staging|production] [--scale <n>]` `--env` is where the stores outside the database are read: `staging` by default, `local` to read none and say so. `--scale` is the seed's, `1` by default (5,000 people with a personal org each, a team org of 200 members, a million change events); `0.01` is a quick run with the same shape, whose plans prove the wiring: a sequential scan on a small table is the planner's choice, and a quick run's plan findings say "verify at scale 1". The audit reads and runs the checkout, tools and code alike; to audit another commit, run it from a checkout of that commit that has `ops/audit/`. Every command runs from the repository root. ## Role and credential Investigator, read-only. The database half runs on the local stack (`make infra-up`, with `make migrate` run once), in a database the run makes, seeds, and drops; it holds no cloud credential. The cloud half runs under `acme-<env>-investigate` (`acme-staging-investigate`, `acme-production-investigate`), checked with `sts get-caller-identity` before any other `aws` command, as the preamble states. Refuse any profile wider than the investigate role. Every `aws` command carries `--profile acme-<env>-investigate` and `--region` with the value of `.region` in `deployment/cloud/environments.json`, written out in full on each command: a shell's own region answers from another region, and an empty answer there looks like nothing deployed. The skill uses the `aws` command line, not another AWS tool of the session, so the profile check holds. It reads no env file and no token: it calls no route. ## Procedure 1. Name the run: `audit_retention_<yyyymmdd>`, today's date in UTC. When `uv run python ops/audit/auditdb.py list` shows that name taken, another run holds it: add a suffix (`_2`), and never drop a database this run did not make. Make the evidence folder `~/Downloads/acme_retention_<yyyy-mm-dd>/` (`mkdir -p`). Say which commit the report read (`git rev-parse HEAD`). 2. List every live table. `explain.py inventory` on the run's database (step 4) is the live list; the migrations say why each exists (`om/migrations/sql/<role>/*.up.sql`, newest last, a role with no folder has no tables), and the table classes are `om/src/acme/om/*/storage/tables/*.py`. For each table: what writes it (the namespace and the storage method, from `om/src/acme/om/<namespace>/storage/impl/postgres.py`), what it grows with, and when a row is stale. 3. Find the purge of each table. The sweep is `workers/maintenance/src/acme/workers/maintenance/loop.py` (`_sweep_once`, which also purges the outbox and the work items itself); `build_loop` in that folder's `main.py` wires the rest: the per-tenant `purges`, the `across` and `across_batches` purges across tenants, and any `chores` it names. Each calls a manager (`om/src/acme/om/<namespace>/impl/manager.py`; the outbox's is `outbox/impl/relay.py`), which calls its storage's delete. A retention is a field of `settings.py` in that folder (read as `ACME_<FIELD>`), passed into the manager's options in `container.py`, or an options default in the manager when no setting names it; say which. For each purge note its statement, whether it deletes a batch (`delete_batch`, `LIMIT`) or everything at once, and which index serves its `WHERE`. A table no purge reaches is a gap, unless the product keeps its rows as the record (the org row), as one row per tenant (the change event cursor) or one for the system (its size), or as a person's until they are erased (the identity), or it is live data (a living org's members and files); say which. `references/purges.md`, in this skill's folder, lists the purges the sweep runs; a purge the code has and that list misses is read all the same. 4. Measure the purges. Make and seed the run's database: ```bash uv run python ops/audit/auditdb.py create audit_retention_<yyyymmdd> uv run python ops/audit/seed.py audit_retention_<yyyymmdd> --scale <scale> uv run python ops/audit/explain.py reset audit_retention_<yyyymmdd> ``` Write each purge's statement into `~/Downloads/acme_retention_<yyyy-mm-dd>/purges.sql`, in the file format `references/purges.md` gives (read it before writing the file: it says how to get the exact SQL and the ids a statement binds), and run: ```bash uv run python ops/audit/explain.py plans audit_retention_<yyyymmdd> ~/Downloads/acme_retention_<yyyy-mm-dd>/purges.sql uv run python ops/audit/explain.py inventory audit_retention_<yyyymmdd> ``` A plan with a `Seq Scan` on a large table, or rows read far beyond the rows deleted, is a finding with its numbers. A table the seed leaves empty is "not measured", never "fine". Each plan runs in a transaction that is rolled back, so nothing is deleted. 5. The stores outside the database. The Terraform modules under `deployment/terraform/modules/` state each one's retention: log groups (`log_retention_days`), buckets' lifecycle rules, and queues' `message_retention_seconds`; every cache write carries a TTL (`infra/src/acme/infra/cache/valkey.py`). With a cloud `--env`, check what is live, read-only (each command with the profile and the region of the Role section): ```bash aws logs describe-log-groups --profile acme-<env>-investigate --region <region> aws sqs list-queues --queue-name-prefix acme-<env> --profile acme-<env>-investigate --region <region> aws sqs get-queue-attributes --queue-url <url> --attribute-names MessageRetentionPeriod --profile acme-<env>-investigate --region <region> aws s3api list-buckets --profile acme-<env>-investigate --region <region> aws s3api get-bucket-lifecycle-configuration --bucket <name> --profile acme-<env>-investigate --region <region> aws rds describe-db-instances --profile acme-<env>-investigate --region <region> ``` The buckets are the environment's and the account's own (artifacts, audit, state); read all of them. A log group with no `retentionInDays`, or a bucket that keeps current objects for ever, is a finding. The database's size is its `AllocatedStorage` against `FreeStorageSpace` (`aws cloudwatch get-metric-statistics --namespace AWS/RDS --metric-name FreeStorageSpace --dimensions Name=DBInstanceIdentifier,Value=<id> --start-time <a day ago> --end-time <now> --period 3600 --statistics Minimum`), since no role logs in to it. 6. Drop the run's database, whatever happened before: ```bash uv run python ops/audit/auditdb.py drop audit_retention_<yyyymmdd> ``` 7. Write the report, and the proposed tickets in it. Each ticket names the table, the evidence, the fix, and the effort. The skill files none. ## What it never does - Never writes to a shared database or to an environment: it logs in only to the database it made on the local stack, and drops it. - Never modifies a tracked file, never commits, never opens a pull request: it writes a report and proposes tickets. - No `aws` verb that is not `describe`, `get`, or `list`; no secret read. - No row count from a cloud: no role reads its rows, and the report says so rather than guessing. ## Output `~/Downloads/acme_retention_<yyyy-mm-dd>.md`: ```markdown # Acme: which stores grow for ever, and what trims them Read at <commit>; seeded at scale <n>; stores read in <env> under <profile and Arn>, <region>. ## The answer <two paragraphs: what is trimmed, the gaps, the risks, most important first> ## Every table | table | what writes it | grows with | stale when | trimmed by (the storage's file:line) | retention (setting or default) | batched / indexed | verdict | |---|---|---|---|---|---|---|---| Verdicts: fine, risk, gap, kept on purpose, not measured. ## Findings, by impact ### <gap or risk>: <table> **What.** **The harm.** **Evidence** (plan, rows read, file:line). **Fix.** **Effort** S/M/L. **Proposed ticket:** <title>. ## Stores outside the database | store | retention | verdict | ## What I could not verify ```