From 913117e37ded05a5c44a3f3162a89175d82bf736 Mon Sep 17 00:00:00 2001 From: Ahmustufa Date: Tue, 20 Oct 2026 13:46:49 +0500 Subject: [PATCH] docs: add a roadmap, and stop keeping the schedule in two places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ROADMAP.md says what is shipped, what is next and why. The parity plan stays the backlog — what is missing — and its "immediate list" is marked spent, because all four items in it have shipped. That split is the point. The parity plan drifted far enough once that five shipped features still read as outstanding, and work was nearly dispatched to rebuild the webhooks UI, the scheduled-job ledger and the storage seam. Keeping a schedule in the same file as a backlog is what let that happen, so the roadmap is explicitly reconciled against the tree rather than against its own previous contents, and says so at the top. Records three things that were otherwise only in PR descriptions and would have been lost: the merge order for the open stack (#245 consumes an API that only exists in #248), the suppression-expiry question P1.3 must answer in its schema rather than defer, and the two standing environmental hazards — DB-to-host clock skew failing clock-sensitive tests on any branch, and the golangci-lint action cache reporting findings in files a branch never touched. Also sets out QA as a separate Petri agent: what it would look for, why the existing QA agent cannot find it, and that it is a spike before it is infrastructure. The Petri API specifics are explicitly marked unverified. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VECELVAKe8Xcp7GH9wGR5t --- README.md | 1 + ROADMAP.md | 178 ++++++++++++++++++++ docs/competitive-analysis/04-parity-plan.md | 13 +- 3 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 ROADMAP.md diff --git a/README.md b/README.md index 03b42b35..ea9a4a13 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,7 @@ broken — are in [docs/security.md](docs/security.md). | [docs/self-hosting.md](docs/self-hosting.md) | Deploying, env vars, and connecting Gmail / M365 (OAuth setup, scopes, redirect URIs) | | [api/openapi.yaml](api/openapi.yaml) | The REST contract. The SPA's typed client is generated from it | | [CONTRIBUTING.md](CONTRIBUTING.md) | Dev loop, native setup, tests, and what a good PR looks like | +| [ROADMAP.md](ROADMAP.md) | What is shipped, what is next and why, and the standing hazards worth knowing | The same docs ship as a browsable site (`docs/`, Astro/Starlight). The dev stack serves it on . diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 00000000..6e874e39 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,178 @@ +# Roadmap + +**Last reconciled:** 2026-10-20 against `main` @ `5058c66`. + +This is a point-in-time plan, not a backlog. The backlog lives in +[`docs/competitive-analysis/04-parity-plan.md`](docs/competitive-analysis/04-parity-plan.md); +this file says what we are doing next and why, and is re-reconciled against the +tree rather than against its own previous contents. + +That distinction is not pedantry. The parity plan drifted far enough once that +five shipped features still read as outstanding, and work was nearly dispatched +to rebuild the webhooks UI, the scheduled-job ledger and the storage seam — all +of which already existed. **Any plan here is wrong until checked against the +code.** + +--- + +## Where we are + +**The multi-IP sending fleet is architecturally complete.** The execution plane +is now a containment boundary rather than an operational lever: a `role=send` +worker holds no master key, opens no database connection, and refuses to start +if given either. It reaches relational data only through an authenticated HTTP +seam. + +| Shipped | What it gave us | +|---|---| +| Worker roles + role-scoped queues | A worker can run per-message work without the control plane's sweeps | +| IP-derived worker identity | A worker's identity survives restarts and maps to the IP whose reputation it carries | +| Scored placement + rotation | Placement composes additively; a mailbox moves only under an urgency gate | +| Provider signals + decision log | Per-worker auth/throttle counters, and "why is this mailbox here?" is answerable | +| Credential brokering | A stolen worker disk or `.env` decrypts nothing | +| `coreapi` remote transport | The pool is gone on `role=send` | +| IMAP/SMTP hardening + poll backoff | AUTH negotiation, a real test harness, and an outage no longer costs ~60 reconnects/hour | +| Operator CLI, installer, audit log, operator view | An instance can be recovered, installed, audited and inspected | + +--- + +## In flight + +Merge order matters here, because one of these breaks the build if taken out of +turn. + +1. **#248** — optimistic concurrency for step branch writes. Mergeable. +2. **#245** — yes/no condition nodes on the canvas. Mergeable, and **stacks on + #248**: its frontend consumes an API that only exists there. Merging it first + breaks the build. +3. **#244** — configurable retention. Conflicting, and genuinely so (overlapping + edits in `.env.example`, `capabilities_test.go`, `maintenance/cleanup.go`). + Needs a real merge, not a rebuild. +4. Dependabot (**#259**, **#260**) — mergeable. + +--- + +## Next, in order + +### 1. Make the installer usable — **GHCR packages are still private** + +`scripts/install.sh` and `docker-compose.prod.yml` shipped in #199. An anonymous +pull of `ghcr.io/axomble/inroad-api` returns **401**, so the documented +`curl | sh` path cannot work for anyone. This is a settings toggle, not code, +and it is the only item standing between a merged feature and a real user. + +### 2. P1.3 — pre-send email verification + +Design in progress. Decisions already taken: block `invalid`, warn `risky`; +provider verification auto-on-import but opt-in; cache keyed by address, with an +`invalid` verdict writing a suppression so the existing send-time gate (remote +workers included) does the blocking. + +**Open question that must be answered in the schema, not deferred:** nothing in +this repo ever deletes from `suppression`. The verification row carries an +expiry; the suppression it writes does not. An address suppressed because its +domain was briefly NXDOMAIN is therefore suppressed *permanently*, and the only +evidence expires. Either a sweep re-verifies expired `invalid` verdicts and +lifts the suppression, or `undeliverable` suppressions carry their own expiry. + +### 3. P1.5 — the CSV import wizard + +Export ships. Import is a single-click upload with no preview, column mapping or +dedup strategy, and no XLSX. + +### 4. Surface poll failures to users + +Poll backoff persists a failure count and classification, and logs it — visible +to an operator with database or log access, **not to a user**. Pairs with the +mailbox-error tile that is permanently 0 because nothing writes `status='error'` +(and nothing should: `ListActiveMailboxes` and `MailboxExists` both gate on +`active`, so writing it would stop sending too). + +### 5. Fleet remainder + +- `sequence:advance` is still unpinned — a decision about where sender selection + lives, since the mailbox does not exist at enqueue time for an initial step. +- The four `inprocess.WithRemote*` options have no production caller; they drive + the fault-injection suites. Deleting them means migrating those suites to + `*remote.Client` — worth doing, and worth doing separately from the thing they + prove. +- OAuth token refresh still uses `http.DefaultTransport`. Deliberate — it moves + to the control plane — but it should get chosen timeouts regardless. +- `GetInboxPollJob` failures are not backed off, so a revoked refresh token + still hammers the provider's token endpoint every sweep. + +--- + +## QA as a Petri agent + +**Intent:** run exploratory QA as a separate agent in +[Petri](https://github.com/safety-research/petri), alongside — not instead of — +the existing suites. + +### Why a separate agent, and why Petri + +Today's QA agent (`.claude/agents/qa.md`) *executes* tests: 135 Go integration +files, 9 Playwright specs, the Vitest suite. It verifies what we thought to +assert. It cannot find what nobody thought to assert. + +Petri's shape fits the gap: an auditor agent drives a target through many seeded +scenarios **in parallel**, and a judge scores the resulting transcripts. Applied +to a product rather than a model, that is exploratory QA at a scale a human +tester cannot reach — many sessions, varied, each one scored. + +**Inroad is unusually well suited to this** because it already exposes a typed +agent surface: `internal/app/mcpserver` with a documented tool reference. A +Petri auditor can drive the product through its own MCP tools rather than +through a browser, which makes scenarios cheap to seed and transcripts easy to +judge. + +### What it would look for + +The failures this session actually produced are the template — every one was +invisible to the existing suites because it was never asserted: + +- **Capabilities silently not registered.** A wrapper satisfying an interface + but not the concrete type compiled, started, and registered no test-send + handler, no breaker, no dead-letter capture. Nothing failed. +- **A sentinel that never matched.** Two `errors.New` values meant an + `errors.Is` check had never once been true in production; every operator undo + was dead-lettered instead of ending quietly. +- **Behaviour that looks like success.** A localized junk folder meant warmup + reported a *clean inbox rate* for a mailbox whose mail was going to spam. +- **A connect test and a sender that disagreed** about the same mailbox: the + test passed, every send failed. + +A judge rubric aimed at "did the system report success while doing nothing", +"did two paths disagree about the same object", and "did a failure surface +anywhere a user could see it" would have caught all four. + +### Sequencing + +1. **Spike first.** Stand up Petri against the MCP surface with a handful of + seeds and see whether the transcripts are judgeable. Output is an answer, not + infrastructure. +2. **If it holds:** a seed corpus derived from the parity plan's user journeys, + plus one seed per class of bug above. +3. **Then CI**, nightly rather than per-PR — exploratory runs are not a gate, + they are a net. + +**Unverified:** Petri's current API, its judge configuration, and whether it +drives a non-model target cleanly all need checking against the upstream repo +before any of this is scheduled. Treat the above as intent, not a design. + +--- + +## Standing hazards + +Two environmental issues cost real debugging time and will cost it again: + +- **DB↔host clock skew fails tests on any branch.** Measured between −1711 ms + and −37 ms on one machine in a day. When the sign flips positive, claims defer + and clock-sensitive tests fail — on `main` as readily as on a branch. Prefer + injected clocks; compute scheduling deltas in SQL, not in Go. +- **The `golangci-lint` action cache can report findings in files a branch never + touched**, including paths that no longer exist. `golangci-lint cache clean` + before trusting a local run; re-run the job before trusting a CI one. + +Also worth knowing: the dev Postgres container exits on its own. A wall of +`connection refused` is a dead container, not a regression. diff --git a/docs/competitive-analysis/04-parity-plan.md b/docs/competitive-analysis/04-parity-plan.md index ffca925b..32b569b4 100644 --- a/docs/competitive-analysis/04-parity-plan.md +++ b/docs/competitive-analysis/04-parity-plan.md @@ -157,7 +157,18 @@ to recover if they lock themselves out. Re-score `01-feature-matrix.md` at the end of each sprint. -### The immediate list, in order (as of 2026-09-08 @ `d4f5720`) +> **Superseded 2026-10-20 — see [`ROADMAP.md`](../../ROADMAP.md).** Every item in +> the list below has since shipped: the webhook UI, the blob seam's filesystem +> implementation and factory, the warmup header-loss fallback, and contact +> export. It is kept for the record rather than deleted, because the sequencing +> argument it makes is still the right one; only its contents are spent. +> +> The ordered plan now lives in `ROADMAP.md`, which is reconciled against the +> tree. **This file remains the backlog — what is missing — not the schedule.** +> Keeping the schedule in two places is what let five shipped features read as +> outstanding here for a month. + +### ~~The immediate list, in order (as of 2026-09-08 @ `d4f5720`)~~ — spent Small enough to finish before the next re-score, and each one closes a row rather than starting a surface: