Files
0pt/docs/09-build-plan.md
T
admin 328f8a46cb
CI (SIAX Cloud) / security (push) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / quality (push) Successful in 44s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / quality (pull_request) Successful in 42s
docs+chore: repo hygiene -- v2.0 manifests, ADRs, changelog, doctor, guides
BACKLOG P1-1: siax.repo.json/siax.config.json rewritten to be schema-valid
against sax3l/siax-standard's actual v2.0 schemas (lifecycle,
operationalState, gitHost, domains-as-objects, consumedBy, children,
contracts -- validated with jsonschema, not just eyeballed). config.json
restructured to the real deploy-descriptor shape (framework/build/start/
port/healthcheck/environments) instead of duplicating repo.json's identity
fields, which the schema doesn't allow.

BACKLOG P1-2: 3 ADRs for decisions already made and evidenced elsewhere in
this repo (Coolify-not-Vercel, Zitadel-not-NextAuth, Iceberg-format pointer
to n0d ADR-0030) -- retroactive documentation, not new decisions. The
Stripe/Revolut and services/scrapers/* questions are explicitly NOT written
as ADRs here; both are account-owner-gated or actively under review
elsewhere (see docs/architecture/decisions/README.md).

BACKLOG P1-3: CHANGELOG.md from real git log history (no tags/releases
exist yet -- this doesn't invent version numbers that were never cut).

BACKLOG P1-19: `pnpm siax:doctor` (scripts/siax-doctor.mjs, AGENT-04) --
zero-dependency, checks required files + manifest shape, prints
control-IDs. .devcontainer/devcontainer.json (DX-03). compose.dev.yaml
(DX-02 -- UNVERIFIED, no Docker available in this environment to smoke-test,
flagged in the file itself). All 4 prompts + the index rewritten to AGENT-03
shape (goal/context/prerequisites/scope/contract/acceptance-criteria/
control-ID/forbidden-actions).

BACKLOG P1-20: docs/architecture/ARCHITECTURE.md migrated to the v2.0
9-heading template -- new §5 "Ytor och paritet" (capability x
API/CLI/MCP/console coverage table, grounded in the same registry facts
docs/02-architecture.md already established), §6-9 renumbered from the old
§5-8 (all internal cross-references fixed, CLAUDE.md's own §-numbers left
untouched). docs/api/ added (points at contracts/openapi.yaml as the one
SSOT instead of duplicating it as drift-prone prose).

BACKLOG P1-21: packages/schemas now has a real package.json (was loose JSON
files with no package -- pnpm-workspace.yaml's packages/* glob matched an
empty package). Verified `pnpm install --frozen-lockfile` and the full
typecheck/test/build suite still pass with 2 real workspace projects.
apps/ restructuring explicitly NOT attempted (BACKLOG's own effort estimate
is "L" / large -- moving the whole app tree is a bigger, riskier change than
belongs in this pass). Import-boundary check added via dependency-cruiser
(ARK-14) -- installed for real, config tested against this codebase (179
modules, 310 deps, 0 violations), wired as `pnpm run deps:check` (advisory,
not yet a CI gate).

BACKLOG P2-5: docs/02-architecture.md's "Hosting" section marked SUPERSEDED
(the Vercel plan was superseded by the Coolify deploy.target flip,
2026-08-09 -- ADR-0001). README.md/.env.example were checked and are
already accurately framed (live-on-Vercel vs. config-target-is-Coolify) --
not touched.

BACKLOG P3-5: 2 stale github.com/sax3l/0pt/pull/3 links removed (the PR
merged long ago, .env.example is real and current) -- docs/09-build-plan.md,
docs/08-infrastructure.md. The "two diverged remotes, unresolved which is
canonical" language elsewhere (CLAUDE.md, AGENTS.md, SECURITY.md, etc.) is
deliberately NOT touched -- that's a live, correctly-flagged, account-owner-
owned open question per the Gitea-exclusivity mandate, not a stale link.

BACKLOG P6 (19 items): concrete artifacts for the ones that are actually a
file (.editorconfig, dependency-cruiser, deprecation policy, K8s reference
manifest, .actrc, Makefile, ONBOARDING.md); docs/guides/P6-GUIDES.md for the
rest (CLI-13, DOC-18, UI-30, IAM-19, PAY-11, AI-18, OBS-21, TEST-20, NET-19,
SCALE-13, OSS-10, AGENT-13) -- each grounded in this repo's actual code/gaps,
not generic advice.

NOT attempted (out of this task's scope, noted for the record):
- P2-1 (cl0ud app-registry.ts mismatch) -- lives in a different repo
  (sax3l/cl0ud), not cloned/authorized for this task's single-repo PR flow.
- The CI "doctor step" half of P1-15 -- deliberately left out of this PR's
  .gitea/workflows/ci.yml to avoid a 3-way merge conflict with
  ci/security-headers-and-scanning, which already restructured that file;
  add it as a follow-up once that PR merges.

This branch was authored independently of, but shares docs/architecture/
ARCHITECTURE.md and docs/slo.md with, fix/honest-health-check and
ci/security-headers-and-scanning -- expect a small, normal merge conflict
if this is rebased after either of those merge first (both already rebased
cleanly against each other and against main as of this commit).
2026-08-11 14:18:00 +02:00

246 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 09 · Build plan
Phased, in dependency order. **Finish a phase to its acceptance criteria before the next.** Each phase = small, reviewable commits. Reconcile with existing code rather than rewriting wholesale.
> **Status callouts added 2026-07-19.** Each phase below now carries a `Status:` line, grep/read-verified
> against `main` this pass. These are conservative on purpose: a phase is only called **Done** when this
> pass found real, working code satisfying its Accept criteria; anything else is **Partial** (with the
> specific evidence and gap named) or **Not started**. No phase is upgraded to Done without a code
> citation. Where a referenced doc (e.g. `docs/design/...`) doesn't exist, that's flagged rather than
> silently worked around. See `docs/08-infrastructure.md` for the full env/security grounding these
> statuses draw on.
## Phase 0 — Foundation & repo
- Monorepo structure (`docs/08`). Migrate existing `src/orchestrator` + `src/workers` into `services/api`.
- Postgres schema + migrations (`docs/03`); `pgvector` enabled. `.env.example`.
- `docker-compose` up (postgres, redis, api, worker, nocodb, metabase, caddy). `/health` green.
- CI skeleton (lint/typecheck/build).
- **Accept:** `docker compose up` runs the whole stack locally; migrations apply; `/health` returns ok for db + queue.
- **Status: Partial.**
- Repo structure exists but diverged from the `apps/web`/`packages/shared` layout `docs/08` originally
sketched — it settled on `app/` (Next.js, repo root), `lib/`, `src/` (platform clients/services), and
`services/api/` (FastAPI). `docs/08-infrastructure.md` now documents the structure as-built.
- Migration to `services/api/orchestrator` + `services/api/workers` is **done** (verified: those
directories exist and contain `main.py`/`adaptive_router.py`/`pipeline.py`/etc.; no `src/orchestrator`
or `src/workers` remain at the old paths).
- Postgres schema is real: `sql/001_init.sql` (`CREATE EXTENSION vector`, `CREATE EXTENSION pgcrypto`,
17 tables incl. `orgs`, `users`, `orders`, `jobs`, `audit_log`, `bridge_provenance`, `db_grants`,
`payments`) + `sql/004_iceberg_write_boundary.sql`.
**No `sql/002_*` or `sql/003_*` exists anywhere in this repo's history** — some other docs
(`docs/ICEBERG_WRITE_BOUNDARY.md` etc.) cite a `sql/002_p0_dataset_quality.sql` that was never built;
see `docs/08-infrastructure.md`'s "Schema state" section.
- `.env.example` — **now exists and is merged to `main`** (BACKLOG P3-5: the
stale link to the unmerged `github.com/sax3l/0pt/pull/3` that used to sit
here has been removed — that PR has long since landed, `.env.example` is
a real, current, gitignore-exempted file at the repo root today).
- `infra/docker-compose.yml` and `infra/Caddyfile` are real, committed files (not sketches) covering
exactly the services listed. **This pass found no evidence they've ever actually been run** — no
CI, no deploy log, no reachable host checked. "File exists and looks correct" ≠ "verified to run";
treat the Accept criterion ("docker compose up runs... /health returns ok") as unverified.
- CI skeleton — **not built.** There is no `.github/workflows/` directory in this repo at all. `pnpm
lint`/`typecheck`/`test` exist as local scripts only.
## Phase 1 — Design system in code
- Tailwind + shadcn; `globals.css` (grain + reveal keyframes) and `useReveal` verbatim (`docs/05`).
- Build **DataArtifact** primitive (table/map/schema/chart/grid) and **ProvenanceBadge** (`docs/design` prompts 5.1, 2.11).
- **Accept:** a sample page renders mono tokens, a DataArtifact animates on scroll, reduced-motion respected, focus rings visible.
- **Status: Partial — real components exist; scroll-motion/reduced-motion/focus-ring behavior not
independently exercised this pass.**
- Tailwind + shadcn/radix stack: confirmed real in `package.json` (`tailwindcss`, `@radix-ui/*`,
`class-variance-authority`, `tailwind-merge`, `tailwindcss-animate`).
- `hooks/use-reveal.ts`, `components/data-artifact.tsx`, `components/provenance-badge.tsx` all exist
and are used across `app/katalog`, `app/kallor`, `app/bestall`, `app/order/[id]`, and most
`components/sections/*` files (grep-confirmed).
- `docs/design/0pt-v0-build-brief.md` and `docs/design/0pt-design-coverage-and-visuals.md` (the
"prompts 5.1, 2.11" this bullet references) **do not exist anywhere in this repo** — confirmed via
full-repo search; there is no `docs/design/` directory at all. `CLAUDE.md`'s own doc index has
already been corrected to flag this as a gap rather than list them as real. Treat any reference to
specific numbered "build-brief prompts" in this build plan as pointing at a doc that was never
written — not this file's scope to author that content.
- Reduced-motion handling, focus-ring visibility, and actual scroll-triggered animation behavior were
not executed/verified in this pass (no browser run performed) — code presence is confirmed, runtime
behavior is not.
## Phase 2 — Marketing + catalog (placeholder data)
- Landing components top-to-bottom (build-brief 2.1–2.10): Header, Hero, Philosophy, Catalog teaser, Engine, Visualisation gallery, Coverage, Editorial, Proof, Footer.
- Full **Catalog** page + **Dataset detail** + **Sourcing-map** page (coverage doc 5.4). Postgres full-text search.
- All with loading/empty/error states + mobile.
- **Accept:** marketing site + catalog browse work end-to-end on placeholder catalog rows; one continuous scroll reads well; DoD met per screen.
- **Status: Partial.**
- Landing section components are real: `components/sections/{hero,philosophy,gallery,editorial,
pricing,technology,testimonials,footer,featured-products,collection}-section.tsx` all exist.
- `app/katalog/page.tsx`, `app/katalog/[id]/page.tsx`, `app/kallor/page.tsx` all exist.
- **Postgres full-text search was not found** — grepped for `to_tsvector`/`websearch_to_tsquery`
across the whole repo, zero matches. Catalog browsing today reads from `lib/data.ts` placeholder
data, not a full-text query against Postgres. This Accept criterion's "catalog browse... on
placeholder catalog rows" half is plausible; the Postgres full-text search half is not built.
- Loading/empty/error states + mobile + DoD-per-screen were not independently audited screen-by-screen
this pass — `components/states.tsx` (`LoadingSkeleton`, `ErrorState`) exists and is imported widely,
which is evidence toward this but not a full verification.
## Phase 3 — Auth & portal shell
- Auth.js + Postgres adapter; sign in/up/verify/forgot. `orgs`/`users`.
- Portal shell + Dashboard ("Mina beställningar") with empty state.
- **Accept:** a user can sign up, land in an empty dashboard, sign out; routes protected.
- **Status: Not done — this is the most consequential gap in the whole repo, not a minor loose end.**
- **There is no Auth.js integration.** `lib/auth.ts` is a from-scratch, ~40-line HMAC-SHA256 session
scaffold (Web Crypto, no dependency) whose own comment states: "Production auth = Auth.js + Postgres
adapter + OAuth (docs/02) — wired once the database is provisioned." `app/api/auth/login/route.ts`
is a **dev-only** login gated by a shared `DEV_LOGIN_KEY`, not sign-up/verify/forgot.
- `orgs` and `users` tables exist in `sql/001_init.sql` (schema is ready), but nothing in the app
reads/writes them for real session-bound identity today.
- `app/portal/page.tsx` and sibling portal pages exist (shell is real), but routes are **not**
protected by any real session check tied to that schema.
- **Concretely, this means the tenant/org isolation several `0pt.*` API routes assume does not exist
yet.** `app/api/0pt/lakehouse/mappings/route.ts`, `.../writes/route.ts`,
`app/api/0pt/sources/rate-policies/route.ts`, `app/api/0pt/sources/[sourceId]/query/route.ts`, and
`src/services/document-capture-service.ts` all read `tenant_id`/`project_id` straight from the
request body — anyone can claim any tenant. `app/api/orders/route.ts`'s own `GET` handler carries
the literal comment `// org scoping added with auth`. This is an open security gap, not a
two-tier design choice — see `docs/08-infrastructure.md`'s "Security & compliance" section for the
full list. **Do not build any doc or feature that assumes tenant-scoped auth already exists.**
## Phase 4 — Order lifecycle end-to-end (test mode)
- **Configurator** (2.12) wired to `POST /estimates` (real estimate math in FastAPI).
- **Deposit checkout** (2.13) → Stripe **test** Checkout → webhook → order `IN_PRODUCTION`.
- **Production tracker** (2.14) reading `GET /orders/{id}/events` (SSE) — pipeline may be **simulated** at this phase.
- **Validation & delivery** (2.15): tabs render from a sample delivery; **accept** → `POST /payments/release` (test) → `DELIVERED`; **revise** leaves escrow untouched.
- AI-assist (`POST /ai/parse`) prefilling the configurator.
- **Accept:** a full custom order can be configured, deposit paid (test), watched, validated, accepted, and "delivered" — state machine in `docs/01` traversed; escrow split correct.
- **Status: Partial — the payment/escrow spine is genuinely real; the SSE tracker is not.**
- `app/api/estimates/route.ts` exists.
- Deposit checkout is **real, working code**: `app/api/payments/[order_id]/deposit/route.ts` calls
Stripe's Checkout Sessions API directly (not a stub) and `app/api/webhooks/stripe/route.ts` verifies
the webhook signature and advances order status via `setStatus`.
- `app/api/payments/[order_id]/release/route.ts` is also real: it enforces the escrow rule in code —
balance release is rejected with `409 not_accepted` unless `order.status === "accepted"` — matching
the "escrow split correct" accept criterion's intent.
- **`GET /api/orders/{id}/events` (SSE) does not exist.** Only `GET`/`PATCH /api/orders/{id}` were
found (`app/api/orders/[id]/route.ts`); no `/events` sub-route, and no `EventSource`/
`text/event-stream`/`ReadableStream` usage anywhere in `app/` (grep-verified). If a production
tracker is live today, it's polling `GET /api/orders/{id}`, not the SSE stream this bullet describes
— or it hasn't been built. Either way, the SSE contract as written is not implemented.
- `app/api/ai/parse/route.ts` is real: a genuine (non-fabricated) keyword/regex heuristic that
prefills a config object, with a documented fallback to `engineFetch("/ai/parse", ...)` when
`ENGINE_URL` is set. This one holds up as described.
## Phase 5 — Real data engine (Nordic slice)
- Compliant connectors first: **GLEIF + Brreg + Companies House** (verify access terms; record in `sources.yaml`). Remove any FlareSolverr/bypass path (`docs/06`).
- Pipeline stages produce real `dim_company` + `fact_financials` with `bridge_provenance`; QA threshold drops low-confidence rows.
- Production tracker now reflects **real** job metrics.
- **Accept:** a Nordic company dataset is produced for a real order, every field has provenance, quality report generated, level-0 fields shown honestly (not fabricated).
- **Status: Partial.**
- `services/api/sources/sources.yaml` and `connectors.py` are real: GLEIF, Brreg, Companies House,
PRH, and CVR are all registered, each with an explicit `licence: "... — VERIFY"` tag rather than an
assumed-clean claim — matches the "verify access terms" instruction.
- `services/api/workers/adaptive_router.py`'s own comment confirms: "There are NO bypass/stealth/
proxy-rotation tiers here, by design (CLAUDE.md §1)" — the FlareSolverr/bypass removal is done, at
least on this repo's `main` (see `docs/08-infrastructure.md`'s "Known open question" about the
diverged Gitea remote, which independently added such a stack on its own `main` — unresolved which
remote is canonical).
- `services/api/workers/pipeline.py` has a real, working confidence-threshold QA drop: `delivered =
[r for r in deduped if r["confidence"] >= threshold]`, plus an `avg_confidence` figure in its output.
This is a genuine, narrow QA mechanism inside the Python engine.
- **This is not the same thing as the "quality report" / quality-check layer once described in
`docs/ICEBERG_WRITE_BOUNDARY.md`, `docs/11-lakehouse-write-plan.md`, and
`docs/P0_QUERY_SOURCE_FLOW.md`.** Those three docs used to cite `src/services/quality-check-service.ts`,
`src/services/quality-report-service.ts`, `src/types/quality-{check,report}.ts`, and
`src/data-platform/services/catalog-service.ts` as already-built and tested. **None of those files
has ever existed in this repo's git history, on any branch** — and all three docs have since been
corrected to say so explicitly (verified this pass), rather than repeating the original claim. So:
the narrow Python-side confidence filter is real; the broader TS/lakehouse "quality report" this
Accept criterion likely also intends is not built (see `docs/P0_DATASET_QUALITY_FLOW.md` for the
honest target spec). Whether `pipeline.py`'s `avg_confidence` output satisfies "quality report
generated" is a judgment call for whoever runs this phase — flagging rather than deciding it here.
- Whether an actual Nordic dataset has been produced end-to-end for a real order was not verified
this pass (would require running the stack).
## Phase 6 — Delivery surfaces & integrations
- Exports: CSV / Parquet / JSON via R2 presigned URLs.
- Embedded **NocoDB** ("Min data") and **Metabase** dashboards via signed URLs in mono chrome.
- Integrations: **FiveCRM push**, webhooks, Google Sheets.
- File **uploads** (Uppy → R2) for matching/enrichment.
- **Accept:** a delivered dataset is downloadable, viewable in embedded NocoDB + Metabase (no separate login), and pushable to FiveCRM; uploads work.
- **Status: Not done — this phase is UI-shell-only today.**
- CSV/Parquet/JSON exports via R2 presigned URLs: not found. `app/api/uploads/route.ts` (the closest
R2-touching code) is a `501` stub with a hardcoded `put_url: null` — no presign logic exists yet,
for uploads or exports.
- `components/embed.tsx` (`EmbeddedView`) is real — a sandboxed iframe shell — but the server-side
signed-URL generator for Metabase/NocoDB (`METABASE_SECRET_KEY`/`NOCODB_API_TOKEN`) that would feed
it a real `src` was not found anywhere in this pass.
- "FiveCRM" appears only as a **destination label string** in `lib/data.ts`, `lib/services.ts`,
`app/portal/integrationer/page.tsx` (explicitly described in that file's own comment as
"presentationella toggles" — i.e. presentational, not wired), and as one of several regex-matched
destination names in `app/api/ai/parse/route.ts`'s heuristic. **No actual FiveCRM API push exists.**
- `uppy` is not a dependency in `package.json` (checked directly) — the Uppy → R2 upload flow this
bullet describes has no corresponding package installed, let alone implemented.
## Phase 7 — Subscriptions / feeds
- `subscriptions`; recurring jobs (weekly/daily) delivering **deltas**; webhooks on change.
- Freshness monitoring; Resend emails (confirmation, "din data är klar", feedback).
- **Accept:** an order converts to a feed; a scheduled run produces a delta + fires a webhook + email.
- **Status: Partial — data layer real, scheduling and email are not.**
- `lib/subscriptions-store.ts` is real CRUD against the `subscriptions` table (with an in-memory
fallback when no DB is configured) — `createSubscription`/`listSubscriptions` genuinely insert/read.
- No recurring-job scheduler (cron, queue consumer producing deltas, etc.) was found on the TS side of
this repo. `services/api/workers/queue.py` exists on the Python side but this pass did not trace
whether it implements actual delta-producing scheduled runs — flagged as unverified rather than
assumed either way.
- **Resend is not wired at all** — grepped for `resend`/`Resend` across the whole repo, zero matches
outside `RESEND_API_KEY` being listed as an env var. No confirmation/"din data är klar"/feedback
email has been implemented.
## Phase 8 — Admin, hardening, compliance
- Admin: order queue + scope/quote + escrow release; **source registry & freshness** monitoring; worker/job monitoring; customer + catalog management.
- Customer **DB-access grant** flow (encrypted, read-only, time-boxed) + revoke.
- Security pass: RLS, presigned-only files, signed embeds, secret rotation, `audit_log` everywhere; **DPA** published; legal pages.
- Finalize all loading/empty/error states + mobile across the app.
- **Accept:** admin can run the business; DB-grant flow works and self-revokes; security checklist passes; DoD met app-wide.
- **Status: Partial — UI shell real, the security substance is not.**
- Admin pages exist: `app/admin/{page,jobs/page,sources/page,customers/page,verktyg/page}.tsx`. This
pass did not verify whether they read real backend data or placeholders.
- `sql/001_init.sql` defines `audit_log`, `db_grants`, and `payments` tables — schema is ready.
- **The DB-access grant flow is a stub, not a working feature.**
`app/api/connections/db-grant/route.ts` returns `501` without `GRANT_ENC_KEY`, and with it set
returns a hardcoded `201 { grant_id, status: "pending" }` — **no AES-GCM encryption, no read-only
validation, and no self-revoke logic exist in code today**, despite the route's own comment
describing that target behavior. See `docs/08-infrastructure.md`'s "Security & compliance" section.
- RLS enforcement, presigned-only files (blocked on the Phase 6 R2 gap above), signed embeds (blocked
on the Phase 6 NocoDB/Metabase gap above), and a published DPA were not found as implemented in this
pass. Treat the "security checklist passes" accept criterion as not met.
## Phase 9 — Launch
- Domain `0pt.app` (Vercel) + subdomains (api/data/dash via Caddy). Sentry + uptime live.
- Soft launch: Protosell internally + a handful of existing dealer relationships.
- **Accept:** a real external customer completes an order start→delivery in production; feedback loop running.
- **Status: Domain is live; soft-launch/customer criterion not verified.**
- **Verified this pass** (network check performed): `https://0pt.app` returns HTTP 200 and serves this
repo's own Next.js app (title "0pt — your data", SIAX branding); `GET /api/health` returns
`{"ok":true,"service":"0pt-web","db":"not_configured","queue":"not_configured",...}`, an exact shape
match for `app/api/health/route.ts`. README's "Live: https://0pt.app" is confirmed, and the response
confirms the live deployment has no `DATABASE_URL`/`REDIS_URL` set — see `docs/08-infrastructure.md`'s
"Hosting topology" section. This does not confirm which git remote/commit is deployed (see the
diverged-remotes note elsewhere in this doc), or that api/data/dash subdomains or Caddy are live —
only the root Vercel deployment was checked.
- **Sentry is not wired** — no `sentry` package in `package.json`, no Sentry config file anywhere in
the repo (grep-confirmed). `@vercel/analytics` **is** genuinely wired (`app/layout.tsx` renders
`<Analytics />`), which is a real but much smaller piece of the "Sentry + uptime live" bullet.
- No evidence either way on the soft-launch/external-customer criterion — out of this doc's ability to
verify from code alone.
---
## Working conventions (for Claude Code)
- Tech docs + identifiers **English**; UI copy **Swedish**.
- Conventional Commits; one concern per PR; keep diffs reviewable.
- No secrets in code; update `.env.example` when adding env. (`.env.example` itself currently lives on
`feature/0pt-env-example` / PR #3 — merge that first, or this instruction has nothing to update yet.)
- Add/extend tests for engine logic (estimate math, fusion/confidence, dedup) and API contract.
- When a source's access/licence is unclear, **verify and record in `sources.yaml`** before relying; if disallowed for bulk, route to licensed/partner or mark level 0 — never bypass, never fabricate.
- Touch the design tokens/motion only via `docs/05`; keep red ≤ once per screen; no photography.
- If anything here conflicts with existing code, **flag it** in the PR description and propose the reconciliation rather than guessing.
- **Two git remotes claim to be this repo** (`github.com/sax3l/0pt` and `git.siax.io/sax3l/0pt`), and
they have independently diverged (see `CLAUDE.md`, `README.md`). Don't assume the checkout you're in
is the only copy, and don't silently pick one as canonical — that's Simon's decision, flag it if it
becomes relevant to the phase you're working on.