diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..830ca35 --- /dev/null +++ b/.actrc @@ -0,0 +1,12 @@ +# BACKLOG P6 (CI-16): lets a developer run `act` to execute .gitea/workflows +# (GitHub-Actions-compatible syntax) locally before pushing, instead of +# discovering a workflow bug only after a Gitea runner picks it up. +# https://github.com/nektos/act +# +# ubuntu-latest here maps to a real Docker image (act's default is a minimal +# image missing tools this repo's workflows expect, e.g. curl for the +# Trivy/gitleaks install steps) -- catthehacker's full image matches what +# GitHub-hosted (and this fleet's siax-runner-*) ubuntu-latest actually has. +-P ubuntu-latest=catthehacker/ubuntu:full-latest +-W .gitea/workflows +--container-architecture linux/amd64 diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs new file mode 100644 index 0000000..0e01be8 --- /dev/null +++ b/.dependency-cruiser.cjs @@ -0,0 +1,50 @@ +/** + * BACKLOG P6 (ARK-14): import-boundary check. Advisory today (run manually / + * `pnpm run deps:check`) -- not wired as a blocking CI gate in this pass, + * so a first real run surfacing findings doesn't turn a green PR red + * unexpectedly. Wiring it into CI is a reasonable next step once the + * initial finding set (if any) has been triaged. + * + * Boundaries encoded, matching CLAUDE.md / docs/architecture/ARCHITECTURE.md: + * - `services/scrapers` and `services/api` are Python -- not traversed by + * this JS/TS dependency graph at all; no rule needed, they're simply + * out of dependency-cruiser's file-type scope. + * - `src/` (the capability layer, blocking-typechecked) must not import + * from `app/` or `components/` (the v0-generated UI layer, advisory-only) + * -- that would pull UI-layer assumptions into the tested backend layer. + * - Nothing may import from `tests/` (test helpers stay test-only). + */ +module.exports = { + forbidden: [ + { + name: 'no-src-importing-app-or-components', + comment: + 'src/ is the blocking-typechecked capability layer (tsconfig.check.json); app/ and components/ are the advisory v0-generated UI layer. src/ importing from them would pull unverified UI-layer code into the tested backend layer.', + severity: 'error', + from: { path: '^src' }, + to: { path: '^(app|components)' }, + }, + { + name: 'no-importing-tests', + comment: 'Test helpers stay test-only -- application code must not depend on tests/.', + severity: 'error', + from: { pathNot: '^tests' }, + to: { path: '^tests' }, + }, + { + name: 'no-circular', + comment: 'Circular imports make the module graph hard to reason about and can hide load-order bugs.', + severity: 'warn', + from: {}, + to: { circular: true }, + }, + ], + options: { + tsPreCompilationDeps: true, + tsConfig: { fileName: 'tsconfig.json' }, + exclude: { path: 'node_modules|\\.next|services/(api|scrapers)|services/infra' }, + reporterOptions: { + text: { highlightFocused: true }, + }, + }, +}; diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..5e3d14d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,15 @@ +{ + "name": "0pt", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", + "postCreateCommand": "corepack enable && corepack prepare pnpm@9 --activate && pnpm install --frozen-lockfile", + "postStartCommand": "node scripts/siax-doctor.mjs || true", + "forwardPorts": [3000], + "customizations": { + "vscode": { + "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "editorconfig.editorconfig"] + } + }, + "remoteEnv": { + "AUTH_ENFORCE": "false" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7ddd7a6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# BACKLOG P6 (REPO-13). Matches .prettierrc.json's 2-space indent -- this is +# a floor for editors that don't run Prettier on save, not a replacement for +# it (quote style/semicolons/line width stay Prettier's job, not +# EditorConfig's). Note: `pnpm run format` currently reports ~300 +# pre-existing style violations across this repo (verified 2026-08-11, +# out of this pass's scope) -- this file governs new edits, not a +# retroactive reformat. +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.py] +indent_size = 4 + +[Makefile] +indent_style = tab diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f3ae77a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,69 @@ +# Changelog + +> BACKLOG P1-3. Format loosely follows [Keep a Changelog](https://keepachangelog.com/). +> No git tags/releases exist yet for this repo — this file starts the practice +> retroactively from real `git log` history rather than inventing version +> numbers that were never cut. Dates are commit dates on `main`, not calendar +> "release" dates (there have been none). + +## [Unreleased] + +- fix(security): security headers (CSP/HSTS/X-Frame-Options/etc.) + CI + security gates actually enforce (Trivy `--exit-code 1`, gitleaks) instead + of advisory-only. Actions pinned to commit SHA. SBOM + license inventory + as CI artifacts. (BACKLOG P0-13, P0-16, P0-19, P1-14, P1-15) +- fix(health): `/api/health` reports real per-dependency status instead of a + hardcoded `ok: true`; new `/api/ready` readiness endpoint. (BACKLOG P0-3) +- docs/chore: repo-hygiene pass — schema-valid `siax.repo.json`/`siax.config.json` + (standard v2.0), ADRs, this CHANGELOG, `siax:doctor` script, devcontainer, + `docs/architecture/ARCHITECTURE.md` §5 "Ytor och paritet", CODEOWNERS, + stale-link cleanup. (BACKLOG P1-1, P1-2, P1-3, P1-19, P1-20, P1-21, P2-5, P3-5) + +## 2026-08-11 — Evasion-stack decision provenance + +- Attributed and dated the account-owner's keep-decision on `services/scrapers/*` + across `SECURITY.md`, `CONTRIBUTING.md`, `prompts/002-compliance-audit.md`, + `renovate.json`, `.gitea/workflows/security.yml`, `docs/slo.md`. + +## 2026-08-09/10 — Golden Standard compliance pass (FAS 0–10) + +- `docs/compliance/{INVENTORY,RELATIONS,OSS-INVENTORY,GOLDEN-STANDARD-REPORT,BACKLOG,SLUTRAPPORT}.md` + written; `siax.repo.json` classification recorded. +- `fix(security)`: `GET /api/orders` and `GET /api/admin/metrics` now require + auth (were live 200 anonymous — the routes sit outside `middleware.ts`'s + `/api/0pt/:path*` matcher). +- `fix(deploy)`: `siax.config.json` `deploy.target` flipped from `vercel` to + `coolify` (config-only; the live instance is still on Vercel until a + Coolify app is provisioned and DNS is cut over). +- `feat(contract)`: OpenAPI 3.1 SSOT added (`contracts/openapi.yaml`); + `@vercel/analytics` and `next/font/google` removed in favor of self-hosted + `@fontsource-variable/*` (AR-4). +- `ci(workflows)`: CD/security/nightly split under `.gitea/workflows/`. +- README/SECURITY/CONTRIBUTING/AGENTS + runbooks (deploy, incident, + backup-restore, oncall) + SLO doc added. + +## 2026-08 — Auth + capability hardening ("Wave 2/3") + +- `fix(auth)`: `AUTH_ENFORCE` default flipped to fail-closed on `/api/0pt/*`. +- Zitadel Bearer-token verification wired into `middleware.ts` + (`src/auth/zitadel.ts`, `src/auth/context.ts`). +- P0 capability layer built out: `0pt.query_source`, document + capture/extraction, Iceberg write-boundary, external writeback, dataset + quality flow docs. +- CI quality gate added (typecheck/lint/test), `.env.example` committed + (previously blocked by a blanket `.gitignore` `.env*` rule). + +## 2026-07-13 — Scraper stack (kept, see BACKLOG P3-3 / SECURITY.md) + +- `services/scrapers/*` (WAF-bypass, proxy rotation, ClickHouse pipeline) was + added on the Gitea line of history. This is the subject of the dated, + attributed account-owner decision referenced throughout this repo's + compliance docs — not reopened or altered by this changelog entry. + +## Earlier + +- Initial Next.js scaffold, order/catalog/portal UI, FastAPI engine + scaffold, i18n (EN/SV) and accessibility passes, lakehouse write-path + ownership ADR (`ADR-0030`, in the `n0d` repo). + +[Unreleased]: https://git.siax.io/sax3l/0pt/compare/main...HEAD diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2e77499 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# BACKLOG P6 / DX-11. Thin wrappers around the real pnpm scripts -- this +# file is a convenience entry point, not a second source of truth for what +# each step does (see package.json for the actual commands). +.PHONY: dev doctor typecheck test lint build install db-up db-down + +install: + corepack enable + corepack prepare pnpm@9 --activate + pnpm install --frozen-lockfile + +dev: install + pnpm run dev + +# Optional local Postgres (compose.dev.yaml, BACKLOG DX-02) -- `make dev` +# above works without this via the in-memory fallback (lib/db.ts). Run this +# first, then `DATABASE_URL=postgres://opt:opt-dev-only-not-a-secret@localhost:5432/opt make dev` +# for real persistence locally. +db-up: + docker compose -f compose.dev.yaml up -d + +db-down: + docker compose -f compose.dev.yaml down + +doctor: + pnpm run siax:doctor + +typecheck: + pnpm run typecheck + +test: + pnpm run test + +lint: + pnpm run lint + +build: + pnpm run build diff --git a/ONBOARDING.md b/ONBOARDING.md new file mode 100644 index 0000000..d255f06 --- /dev/null +++ b/ONBOARDING.md @@ -0,0 +1,65 @@ +# Onboarding — 0pt + +BACKLOG P6 (DX-12). For a human or an agent picking this repo up cold. + +## 1. Read, in order + +1. `CLAUDE.md` — the contract for how this project is built. Non-negotiable + constraints (legal sourcing only, Postgres-backbone, provenance-per-field). +2. `AGENTS.md` — agent-specific ground rules. +3. `docs/architecture/ARCHITECTURE.md` — what the code actually does today + (as opposed to `docs/01-11`, which describe target state — see that + doc's own "Läsanvisning"). +4. `README.md` "Known issues" — the live gaps, dated and evidenced. +5. `prompts/000-INDEX.md` — run `001-repo-health.md` before touching anything. + +## 2. Get it running + +```sh +corepack enable +corepack prepare pnpm@9 --activate +pnpm install --frozen-lockfile +pnpm run dev # http://localhost:3000, in-memory fallback, no DB needed +``` + +Or `make dev` (same thing). Want real Postgres locally instead of the +in-memory fallback? `make db-up` first (BACKLOG DX-02, `compose.dev.yaml` — +**unverified in CI, smoke-test before relying on it**), then re-run with +`DATABASE_URL` pointed at it (see the Makefile comment). + +`pnpm run siax:doctor` (or `make doctor`) — runs the self-diagnostic subset +of the SIAX Golden Standard v2.0 checks locally (`scripts/siax-doctor.mjs`, +control AGENT-04). Run it after cloning and after any structural change. + +## 3. Before you change anything + +- `AUTH_ENFORCE` is fail-closed by default — don't set `AUTH_ENFORCE=false` + outside local dev, and never commit it set that way anywhere. +- `services/scrapers/*`, `services/infra/docker-compose.data-stack.yml`, and + `live/src/workers/adaptive_router.py`'s `curl_cffi` entries are frozen — + see `SECURITY.md`. Don't touch, remove, or comment on that code without an + explicit, current instruction from the account owner. +- Run `prompts/003-security-review.md`'s checklist before pushing anything + touching auth, payments, or the platform clients. + +## 4. Before you push + +```sh +pnpm run typecheck # blocking in CI +pnpm run test # blocking in CI +pnpm run build # blocking in CI +pnpm run lint # advisory, but fix what you can +``` + +CI (`.gitea/workflows/ci.yml`) re-runs all of these plus a blocking +Trivy/gitleaks scan (`security.yml`, BACKLOG P0-16/P0-19) — a red `trivy` +job on an otherwise-unrelated PR usually means a pre-existing dependency +finding, not something your change caused; check the run's date against +when that job started being blocking before assuming you broke it. + +## 5. Who to ask + +`CODEOWNERS` → `@sax3l` (Simon Axelsson). Two remotes exist for this repo +(`git.siax.io/sax3l/0pt` and `github.com/sax3l/0pt`) with independently +diverged history — which is authoritative is explicitly Simon's call, not +something to assume; see `CLAUDE.md`. diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..d036472 --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,32 @@ +# BACKLOG P1-19 (DX-02): local-only Postgres for developers who want real +# persistence instead of the in-memory fallback (lib/db.ts / lib/orders-store.ts +# already degrade gracefully without this -- `pnpm dev` / `make dev` work +# without it, this is additive). Modeled on the already-proven Postgres +# service in infra/docker-compose.yml (same image, same sql/ init mount) -- +# not a copy of the full VPS stack (no NocoDB/Metabase/Caddy/worker here, +# those are production concerns, not local dev needs). +# +# UNVERIFIED (2026-08-11): written and reviewed, but not smoke-tested end to +# end in this pass -- no Docker daemon was available in the environment this +# was authored in. Verify with `docker compose -f compose.dev.yaml up` before +# relying on it. +services: + db: + image: pgvector/pgvector:pg16 + environment: + POSTGRES_USER: opt + POSTGRES_PASSWORD: opt-dev-only-not-a-secret + POSTGRES_DB: opt + ports: + - "5432:5432" + volumes: + - opt-dev-pgdata:/var/lib/postgresql/data + - ./sql:/docker-entrypoint-initdb.d:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U opt -d opt"] + interval: 5s + timeout: 5s + retries: 10 + +volumes: + opt-dev-pgdata: {} diff --git a/deploy/k8s/README.md b/deploy/k8s/README.md new file mode 100644 index 0000000..016b4c7 --- /dev/null +++ b/deploy/k8s/README.md @@ -0,0 +1,13 @@ +# Kubernetes manifests — reference only, NOT the deploy path + +BACKLOG P6 (CD-23). **0pt's actual deploy target is Coolify** (ADR-0001, +`siax.config.json`), which runs the committed `Dockerfile` directly — it +does not consume Kubernetes manifests. `deploy/k8s/deployment.yaml` in this +directory is a reference example only, for the scenario where a future SIAX +Cloud node runs k8s instead of/alongside Coolify. It is not applied by any +CI/CD path in this repo, and nothing in `.gitea/workflows/` references it. + +Do not `kubectl apply` this against a real cluster without first replacing +the placeholder image reference and reviewing resource limits against actual +measured usage (none exists yet — 0pt has no live traffic history to size +against, see BACKLOG P0-2/D5). diff --git a/deploy/k8s/deployment.yaml b/deploy/k8s/deployment.yaml new file mode 100644 index 0000000..b1369e6 --- /dev/null +++ b/deploy/k8s/deployment.yaml @@ -0,0 +1,63 @@ +# Reference only -- see deploy/k8s/README.md. Not applied by any CI/CD path. +apiVersion: apps/v1 +kind: Deployment +metadata: + name: 0pt-web + labels: + app: 0pt-web +spec: + replicas: 2 + selector: + matchLabels: + app: 0pt-web + template: + metadata: + labels: + app: 0pt-web + spec: + containers: + - name: 0pt-web + image: REPLACE_ME/0pt-web:REPLACE_ME_TAG + ports: + - containerPort: 3000 + env: + - name: NODE_ENV + value: "production" + # Real values come from Infisical (BACKLOG P0-1), never from a + # committed manifest -- see siax.config.json's `env` list for the + # names this app reads. + envFrom: + - secretRef: + name: 0pt-web-env + livenessProbe: + httpGet: + path: /api/health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 15 + readinessProbe: + httpGet: + path: /api/ready + port: 3000 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + # Placeholder -- no live traffic history exists to size against + # (BACKLOG P0-2/D5). Replace before any real use. + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "1" + memory: "512Mi" +--- +apiVersion: v1 +kind: Service +metadata: + name: 0pt-web +spec: + selector: + app: 0pt-web + ports: + - port: 80 + targetPort: 3000 diff --git a/docs/02-architecture.md b/docs/02-architecture.md index 278e3ff..a414965 100644 --- a/docs/02-architecture.md +++ b/docs/02-architecture.md @@ -162,11 +162,21 @@ Flagged so it is not repeated: `src/services/quality-check-service.ts`, `src/ser | Trigger.dev | the existing queue already schedules jobs; Resend covers mail | | FlareSolverr / stealth browsers / undetected-* / proxy-fingerprint rotation / fake-account creation | **prohibited** — see CLAUDE.md §1. Replace with compliant fetching. (The Gitea-only scraper stack described under "Two diverged git remotes" above is exactly this — removed there, never present on GitHub `main`.) | -## Hosting (cheapest coherent) +## Hosting (cheapest coherent) — **SUPERSEDED** (BACKLOG P2-5) + +> **SUPERSEDED 2026-08-11.** This section describes the original Vercel+Neon +> hosting plan. `siax.config.json` flipped `deploy.target` from `"vercel"` to +> `"coolify"` on 2026-08-09 to match every other SIAX Cloud OS repo — that +> decision supersedes the Vercel prescription below. The live instance is +> **still actually running on Vercel today** (no Coolify app has been +> provisioned yet — see `README.md`'s "Deploy" section and +> `docs/architecture/ARCHITECTURE.md` §5/§8 for the current, verified state); +> this section is kept for history, not as the current target. Do not read +> "Vercel (Hobby/free)" below as still-current guidance. - **Vercel** (Hobby/free) — Next.js + custom domain `0pt.app`. - **One small VPS** (e.g. Hetzner ~€5/mo) running `docker-compose` (`infra/docker-compose.yml`): FastAPI, workers, NocoDB, Metabase, (Postgres or use Neon), optional Meilisearch later. Per-service managed platforms (Railway) get expensive once you have 4–5 always-on services; a single VPS + Vercel + R2 lands ≈ <€10/mo to start. - **Cloudflare R2** — object storage (env-guarded, presign not yet wired — see Components above). - **Postgres** — on the VPS, or **Neon** free tier if you want it managed. -Full config, env, security, CI/CD: `docs/08-infrastructure.md`. Env var inventory: `.env.example` exists on branch/PR [`feature/0pt-env-example`](https://github.com/sax3l/0pt/pull/3) (unmerged as of this pass) — until it merges to `main`, `docs/08-infrastructure.md` is the source of truth for env var names. +Full config, env, security, CI/CD: `docs/08-infrastructure.md`. Env var inventory: [`.env.example`](../.env.example) (present in this repo today — the linked GitHub PR that originally tracked it has long since merged; removed the stale cross-repo link per BACKLOG P3-5). diff --git a/docs/08-infrastructure.md b/docs/08-infrastructure.md index 44686b3..d67373b 100644 --- a/docs/08-infrastructure.md +++ b/docs/08-infrastructure.md @@ -137,12 +137,13 @@ dash.0pt.app { reverse_proxy metabase:3000 } ## Environment variables -**`.env.example` now exists** — added on branch `feature/0pt-env-example` -(`github.com/sax3l/0pt/pull/3`, **not yet merged to `main`** as of this pass). It was previously -impossible to commit: `.gitignore`'s blanket `.env*` rule silently matched `.env.example` too, so it -was never added despite `docs/09-build-plan.md` Phase 0 requiring it since day one. The same PR adds -`!.env.example` to `.gitignore` to fix that. README's "Env inventory: `.env.example`" link is -therefore currently a broken/future reference on `main` until that PR merges. +**`.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 landed a while ago). It was previously impossible to +commit: `.gitignore`'s blanket `.env*` rule silently matched `.env.example` +too, so it was never added despite `docs/09-build-plan.md` Phase 0 requiring +it since day one; `.gitignore` now has `!.env.example` to fix that. README's +"Env inventory: `.env.example`" link is a real, current reference. The table below is grep-verified against `main` this pass, not transcribed from `.env.example`'s own comments — in one case (`ST0RE_URL`, see note) that turned up a discrepancy worth flagging. diff --git a/docs/09-build-plan.md b/docs/09-build-plan.md index 6746657..f566f65 100644 --- a/docs/09-build-plan.md +++ b/docs/09-build-plan.md @@ -29,10 +29,10 @@ Phased, in dependency order. **Finish a phase to its acceptance criteria before **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**, on branch `feature/0pt-env-example` - (`github.com/sax3l/0pt/pull/3`), **not yet merged to `main`**. It was blocked for the life of this - project by `.gitignore`'s blanket `.env*` rule silently matching it too; the same PR fixes that - with `!.env.example`. Until that PR merges, `main` still has no `.env.example`. + - `.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"; diff --git a/docs/api/DEPRECATION_POLICY.md b/docs/api/DEPRECATION_POLICY.md new file mode 100644 index 0000000..d3b1ac3 --- /dev/null +++ b/docs/api/DEPRECATION_POLICY.md @@ -0,0 +1,41 @@ +# API deprecation policy — 0pt + +BACKLOG P6 (API-24). There is exactly one prior breaking-ish change in this +repo's history to learn from: `services/api/orchestrator/main.py` moved from +`src/orchestrator` (docs/09-build-plan.md Phase 0) — internal, pre-contract, +no external consumers yet. This policy is written before it's needed for a +real consumer, on purpose (BACKLOG's own framing: "cheaper to write the +policy than to clean up after breaking someone"). + +## What counts as breaking + +- Removing a path, operation, or required request/response field from + `contracts/openapi.yaml`. +- Narrowing an accepted value (enum shrink, type tightening) on a request field. +- Changing a response field's meaning without changing its name/type. +- Changing default auth/tenant behavior on an existing route (e.g. what + BACKLOG P0-4 fixed — the reverse of a break, but the same class of change). + +Not breaking: adding an optional field, adding a new path, widening an +accepted value, adding a new enum member a client can ignore. + +## Process + +1. Mark the field/path `deprecated: true` in `contracts/openapi.yaml` with an + `x-siax-deprecated-since` date and a one-line replacement pointer. +2. Keep the deprecated shape working for **at least one release cycle** + after the replacement ships (see `CHANGELOG.md` once real releases/tags + exist — BACKLOG P1-3). +3. Announce in `CHANGELOG.md`'s `[Unreleased]` section before merge, not + after. +4. Only remove once nothing in this repo's own consumers (Protosell/FiveCRM, + VehIQ per `CLAUDE.md`) is verified to depend on the old shape — "verified" + means checked against those repos, not assumed silent. + +## What this repo does NOT have yet + +No consumers are known to be live against this contract yet +(`docs/architecture/ARCHITECTURE.md` §2 "Beroenden nedåt": "Inga sådana +anrop finns i det här repot" — unverified downstream). This policy exists +so the first real breaking change has a process to follow, not because one +has happened. diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 0000000..3b7d3f3 --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,26 @@ +# API docs — 0pt + +BACKLOG P1-20 (DOC-07/08). This directory did not exist before this pass. + +**Source of truth:** [`contracts/openapi.yaml`](../../contracts/openapi.yaml) +(OpenAPI 3.1, `0pt Data OS API`). Route handlers in `app/api/*` realize +these contracts; `x-siax-status` on each path says whether it's `live` or +contract-first (target). + +This directory does **not** duplicate the contract as hand-written prose — +that would drift the moment one of them is edited and not the other (exactly +the kind of stale-doc problem this whole compliance pass exists to fix). +Instead: + +| Question | Where to look | +|---|---| +| What routes exist, request/response shapes | [`contracts/openapi.yaml`](../../contracts/openapi.yaml) directly, or render it with any OpenAPI viewer | +| Which routes are live vs. target-only | `x-siax-status` field per path in the same file | +| Risk level + auth requirement per route | `docs/architecture/ARCHITECTURE.md` §3 "API-yta" | +| Capability → surface (API/CLI/MCP/UI) coverage | `docs/architecture/ARCHITECTURE.md` §5 "Ytor och paritet" | +| The separate FastAPI engine contract (`services/api`) | `docs/04-api.md` | +| The edge-native P0 capability contract (`app/api/0pt/*`) | `docs/12-edge-capability-api.md` | + +There are genuinely **two** API surfaces documented separately (edge Next.js +routes vs. the FastAPI engine) — see `docs/02-architecture.md` "Two API +surfaces" for why, before assuming a route lives in the file you expected. diff --git a/docs/architecture/ARCHITECTURE.md b/docs/architecture/ARCHITECTURE.md index 788ebc0..b018e6a 100644 --- a/docs/architecture/ARCHITECTURE.md +++ b/docs/architecture/ARCHITECTURE.md @@ -1,11 +1,11 @@ # 0PT — Arkitektur **Kategori:** platform (K1) **Underkategori:** data-os **Status:** under utveckling (deployad frontend i drift, capability-lagret ej produktionsklart) -**Senast verifierad mot kod:** 2026-08-07 (commit `6531384`, main) · **Klassificeringsuppdatering:** 2026-08-10 (`siax.repo.json`) +**Senast verifierad mot kod:** 2026-08-07 (commit `6531384`, main) · **Klassificeringsuppdatering:** 2026-08-10 (`siax.repo.json`) · **v2.0-standardmigrering (§5 tillagd, §6–§9 omnumrerade; §6 Deploy-fältet nedan uppdaterat mot `lib/health-checks.ts`):** 2026-08-11 — scoped till dessa avsnitt, ingen fullständig radrad-omverifiering av hela dokumentet gjord denna pass > **Läsanvisning.** Detta dokument beskriver vad koden i repot **gör**, inte vad > `README.md`, `CLAUDE.md` eller `docs/01–11` säger att den borde göra. Där de -> avviker står avvikelsen utskriven — se §7. Påståenden märkta *(live-verifierat +> avviker står avvikelsen utskriven — se §8. Påståenden märkta *(live-verifierat > 2026-08-07)* är kontrollerade mot den körande instansen på `https://0pt.app`. > Allt annat är läst ur källkoden. Där något inte gick att belägga står **OKÄNT**. @@ -29,7 +29,7 @@ proveniens (källa, färskhet, konfidens) per fält. | consumes | CL0UD · ST0RE · N0D · AUD0 · INF0 | | replacesSaaS | Apollo · Cognism · ZoomInfo · Clearbit (målet för Data OS: ersätta paid enrichment-SaaS) | | decoupled | `false` | -| designSystem | `null` (eget mono/editorial-system, konsumerar inte @siax/ui — öppen fråga §8.8) | +| designSystem | `null` (eget mono/editorial-system, konsumerar inte @siax/ui — öppen fråga §9.8) | Ersätter krav på betald enrichment-SaaS (Apollo/Cognism/ZoomInfo/Clearbit) med egen compliant sourcing + provenance — målet är att Datahub/VehIQ/data-avdelningar @@ -63,7 +63,7 @@ Tor-rotation, `cloudscraper`, `curl_cffi` TLS-fingerprintbyte, gratis proxypool, nodriver ("undetected browser") och "fake identity generation". `services/infra/docker-compose.data-stack.yml` startar `flaresolverr` och `tor-proxy` som tjänster. Commit `4a164c5` (Simon Axelsson, 2026-07-13) heter -ordagrant *"refactor: remove compliance restrictions"*. Se §7 lucka **S9**. +ordagrant *"refactor: remove compliance restrictions"*. Se §8 lucka **S9**. --- @@ -74,8 +74,8 @@ ordagrant *"refactor: remove compliance restrictions"*. Se §7 lucka **S9**. | Beroende | Klient | Env-var | Beteende när env saknas | |---|---|---|---| | Zitadel (IdP) | `src/auth/zitadel.ts` | `ZITADEL_ISSUER` (default `https://id-customers.siax.io`) | Använder default-issuern; JWKS hämtas via OIDC-discovery, fallback `/oauth/v2/keys` | -| CL0UD (policy) | `src/clients/cl0ud-client.ts` | `CL0UD_URL` | **Fail-open** — returnerar alltid `allow` utan nätanrop (§7 S5) | -| AUD0 (audit) | `src/clients/aud0-client.ts` | `AUD0_URL` | **No-op** — inga auditevent skrivs (§7 S6) | +| CL0UD (policy) | `src/clients/cl0ud-client.ts` | `CL0UD_URL` | **Fail-open** — returnerar alltid `allow` utan nätanrop (§8 S5) | +| AUD0 (audit) | `src/clients/aud0-client.ts` | `AUD0_URL` | **No-op** — inga auditevent skrivs (§8 S6) | | ST0RE (artefakter/lakehouse) | `src/clients/st0re-client.ts`, `st0re-lakehouse-client.ts` | `ST0RE_URL` | Returnerar deterministisk mockartefakt | | N0D (jobb) | `src/clients/n0d-client.ts` | `N0D_URL` | Returnerar deterministiskt mockjobb | | Egen data-engine | `lib/engine.ts` | `ENGINE_URL`, `INTERNAL_TOKEN` | `engineFetch` returnerar `null`, edge räknar lokalt | @@ -86,7 +86,7 @@ ordagrant *"refactor: remove compliance restrictions"*. Se §7 lucka **S9**. | Odoo | `src/adapters/odoo-bills-adapter.ts` | `ODOO_BASE_URL` | Adapter inaktiv | Mönstret är genomgående: **saknad env ⇒ tyst degradering, inte fel.** Det är -bekvämt lokalt och farligt i produktion (§7 S5, S6). +bekvämt lokalt och farligt i produktion (§8 S5, S6). ### Beroenden nedåt (vem konsumerar 0pt) @@ -98,7 +98,7 @@ här. Betrakta konsumentlistan som odokumenterad tills motparten visats. - **BigQuery** — hård estate-regel. Ej överträdd. - **Hogia** — skrivning förbjuden. Ingen referens finns. -- **Protosell-tenantdata** — tenantisolering, se §7 S2. +- **Protosell-tenantdata** — tenantisolering, se §8 S2. - Nya SaaS-beroenden utan dokumenterat skäl (`CLAUDE.md` §4, "one tool per job"). --- @@ -116,10 +116,10 @@ Risknivå = SIAX L0–L5-taxonomin, bedömd av mig utifrån vad handlern gör. | Registrera dataset↔Iceberg-mappning | `POST /api/0pt/lakehouse/mappings` | L3 | Nej i kod; `tenant_id` läses ur body | | Lista mappningar | `GET /api/0pt/lakehouse/mappings` | L2 | Nej; `tenant_id` ur query | | Hämta mappning | `GET /api/0pt/lakehouse/mappings/{mappingId}` | L2 | Nej | -| Utför Iceberg-write | `POST /api/0pt/lakehouse/mappings/{mappingId}/writes` | **L4** | Nej i kod (CL0UD fail-open, §7 S5) | +| Utför Iceberg-write | `POST /api/0pt/lakehouse/mappings/{mappingId}/writes` | **L4** | Nej i kod (CL0UD fail-open, §8 S5) | | Registrera rate-policy | `POST /api/0pt/sources/rate-policies` | L2 | Nej | | Lista/hämta rate-policy | `GET /api/0pt/sources/rate-policies[/{id}]` | L1 | Nej | -| Kör källfråga | `POST /api/0pt/sources/{sourceId}/query` | **L4** | Nej; `connection_config` tas ur body (§7 S10) | +| Kör källfråga | `POST /api/0pt/sources/{sourceId}/query` | **L4** | Nej; `connection_config` tas ur body (§8 S10) | | Ta emot dokument | `POST /api/0pt/documents/capture` | L3 (PII) | Nej | | Lista dokument | `GET /api/0pt/documents/capture` | L3 (PII) | Nej | | Extrahera dokument | `POST /api/0pt/documents/{documentId}/extraction` | L3 (PII) | Nej | @@ -141,11 +141,11 @@ Risknivå = SIAX L0–L5-taxonomin, bedömd av mig utifrån vad handlern gör. | Läs/ändra order | `GET,PATCH /api/orders/{id}` | L3 | Nej | | **Adminmetrik (orderpipeline, belopp)** | `GET /api/admin/metrics` | **L3** | **Nej** *(live-verifierat: 200 anonymt)* | | Deposition (Stripe checkout) | `POST /api/payments/{order_id}/deposit` | **L4** | Nej | -| **Frigör escrow-slutbetalning** | `POST /api/payments/{order_id}/release` | **L4** | **Nej** — sätter dessutom status `delivered` (§7 S4) | +| **Frigör escrow-slutbetalning** | `POST /api/payments/{order_id}/release` | **L4** | **Nej** — sätter dessutom status `delivered` (§8 S4) | | Stripe-webhook | `POST /api/webhooks/stripe` | L3 | Signaturkontroll via `STRIPE_WEBHOOK_SECRET` | | Prenumerationer | `GET,POST /api/subscriptions` | L2 | Nej | | Filuppladdning | `GET,POST /api/uploads` | L3 | Nej (`501` utan R2) | -| Ta emot kund-DB-grant | `POST /api/connections/db-grant` | **L4** | Nej (`501` utan `GRANT_ENC_KEY`; §7 S7) | +| Ta emot kund-DB-grant | `POST /api/connections/db-grant` | **L4** | Nej (`501` utan `GRANT_ENC_KEY`; §8 S7) | | Dev-login | `POST /api/auth/login` | L2 | Ja — kräver `AUTH_SECRET` + matchande `DEV_LOGIN_KEY` | | Logout / me | `POST /api/auth/logout`, `GET /api/auth/me` | L1 | Cookiebaserat | @@ -156,7 +156,7 @@ Risknivå = SIAX L0–L5-taxonomin, bedömd av mig utifrån vad handlern gör. `PATCH /orders/{oid}`, `POST /orders/{oid}/run`, `GET /orders/{oid}/status`. **Ingen autentisering alls** — grep efter `Depends`, `auth`, `token`, `X-Internal-Token` i filen ger noll träffar, trots att `lib/engine.ts` skickar -`X-Internal-Token`. Se §7 S8. +`X-Internal-Token`. Se §8 S8. --- @@ -208,7 +208,7 @@ Två konsekvenser, båda dokumenterade i koden själv: - `dim_contact`: namn, roll, e-post, telefon, LinkedIn — persondata under GDPR. - `dim_ubo`: verklig huvudman — persondata, potentiellt känslig. -- `db_grants.conn_encrypted`: kunds DB-anslutningssträng (se §7 S7). +- `db_grants.conn_encrypted`: kunds DB-anslutningssträng (se §8 S7). - Dokumentcapture (`app/api/0pt/documents/capture`) tar emot godtyckliga dokument, inkl. fakturor/mailbilagor via Graph-adaptern — persondata. @@ -220,7 +220,50 @@ inget sådant jobb, ingen TTL, ingen `DELETE`-väg och ingen radering finns i ko --- -## 5. Deploy +## 5. Ytor och paritet + +**Migrering till standard v2.0 (BACKLOG P1-20/DOC-02):** denna sektion är ny — +v1.0-mallen hade 8 rubriker, v2.0 har 9 (Deploy/Drift/Säkerhet/Öppna frågor +numrerade om §6–§9 nedan, tidigare §5–§8). + +Källa: `cl0ud packages/capability-registry/src/registry.ts` (samma verifiering +som `docs/02-architecture.md`s "Six-surface capability model"-avsnitt, ej +omgjord här). 14 `0pt.*`-kapabiliteter är registrerade. Registret har fält för +`api`/`cli`/`mcp`/`ui`/`job`; "CL0UD-konsol" nedan är en separat fråga (om +kapabiliteten är inbäddad i den delade cl0ud-konsolen) som INTE går att +verifiera från detta repo — markerad OKÄNT genomgående, inte gissad. + +"Egen konsol" är **ANTAGANDE baserat på sidnamn/routestruktur under `app/`**, +inte spårat anrop-för-anrop mot varje handler — verifiera innan den läses som +hårt bevisad. + +| Kapabilitet | API | CLI | MCP | Egen konsol | CL0UD-konsol | +|---|---|---|---|---|---| +| `0pt.order_create` | ✅ | ❌ | ❌ | ✅ (`app/bestall/*`) | OKÄNT | +| `0pt.order_payment` | ✅ | ❌ | ❌ | ✅ (`app/bestall/forskott`) | OKÄNT | +| `0pt.estimate_compute` | ✅ | ❌ | ❌ | ✅ (`app/bestall/beskriv`, ANTAGANDE) | OKÄNT | +| `0pt.catalog_list` | ✅ | ❌ | ❌ | ✅ (`app/katalog`) | OKÄNT | +| `0pt.source_registry_list` | ✅ | ❌ | ❌ | ✅ (`app/kallor`, `app/admin/sources`) | OKÄNT | +| `0pt.query_source` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.vehicle_lookup` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.screen_sanctions` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.document_ocr` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.lakehouse_mapping_manage` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.source_rate_policy_manage` | ✅ | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.lakehouse_write` | ✅ (+job) | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.write_iceberg_table` | ✅ (+job) | ❌ | ❌ | ❌ (ingen dedikerad sida funnen) | OKÄNT | +| `0pt.write_external_record` | ❌ (job-only, `apiEnabled: false`) | ❌ | ❌ | ❌ | OKÄNT | + +**Luckor, sammanfattat:** CLI = 0/14, MCP = 0/14 (matchar BACKLOG P1-5/P1-6: +ingen CLI-yta, ingen MCP-yta alls finns i detta repo ännu). API = 13/14 (allt +utom det renodlade job-kapabiliteten). Egen konsol ≈ 5/14, koncentrerat till +order-/katalog-/källflödet; kapabiliteter under `app/api/0pt/*` (lakehouse, +rate-policy, källfråga, sanktionsscreening, dokument-OCR) har ingen synlig UI +alls — matchar BACKLOG P1-8 ("`/api/0pt/*`-kapabiliteterna saknar UI"). + +--- + +## 6. Deploy | Fält | Verifierat läge | |---|---| @@ -233,11 +276,11 @@ inget sådant jobb, ingen TTL, ingen `DELETE`-väg och ingen radering finns i ko | **CI** | `.gitea/workflows/ci.yml`. `quality`-jobb: typecheck (blockerande, endast `src/` + `tests/`), vitest (blockerande, 17 testfiler), eslint (rådgivande), `next build` (blockerande). `security`-jobb: Trivy `--exit-code 0` (rådgivande). Kör på `ubuntu-latest`. | > **Konflikt att fatta beslut om:** estate-projektet *GCP/Vercel-exit* säger att -> flottan ska bort från Vercel. 0pt:s enda körande instans ligger på Vercel. Se §8. +> flottan ska bort från Vercel. 0pt:s enda körande instans ligger på Vercel. Se §9. --- -## 6. Drift +## 7. Drift ### Hälsokontroll `GET /api/health` → `{ok, service:"0pt-web", db, queue, ts}`. Den svarar **alltid @@ -277,7 +320,7 @@ Om/när Postgres provisioneras måste detta lösas separat — jfr estate-blocke --- -## 7. Säkerhet +## 8. Säkerhet ### Vad som faktiskt finns @@ -396,7 +439,7 @@ bindning till en användarpost. Kommentaren erkänner att det är ett scaffold; --- -## 8. Öppna frågor +## 9. Öppna frågor 1. **Varför kör inte middlewaren i produktion (S1)?** Måste rotorsaksanalyseras. Ingen dokumentation får påstå att `/api/0pt/*` är skyddat innan en anonym @@ -408,7 +451,7 @@ bindning till en användarpost. Kommentaren erkänner att det är ett scaffold; 3. **Vercel eller flottan?** *(config-delen löst 2026-08-09, trafik-delen kvarstår.)* 0pt:s enda **körande** instans ligger fortfarande på Vercel — den ändringen är medvetet INTE gjord här (skulle kräva en egen cutover-plan: verifiera att inget - riktigt kunddata/trafik ligger bakom `0pt.app` — vilket är fallet idag, se §5/§6: + riktigt kunddata/trafik ligger bakom `0pt.app` — vilket är fallet idag, se §6/§7: `/api/health` visar `db/queue: not_configured`, ordrar lever bara in-memory — provisionera en Coolify-app, peka DNS om, och avveckla Vercel-projektet, i den ordningen). Det som ÄR gjort: `siax.config.json` deklarerar nu `target: "coolify"` diff --git a/docs/architecture/decisions/0001-deploy-target-coolify-not-vercel.md b/docs/architecture/decisions/0001-deploy-target-coolify-not-vercel.md new file mode 100644 index 0000000..a7fb772 --- /dev/null +++ b/docs/architecture/decisions/0001-deploy-target-coolify-not-vercel.md @@ -0,0 +1,43 @@ +# ADR-0001: Deploy target is Coolify (SIAX Cloud fleet), not Vercel + +- **Status:** Accepted (config declares it; migration not yet executed) +- **Date decided:** 2026-08-09 (commit `ad042d8`, "fix(deploy): declare Coolify as the deploy target, not Vercel") +- **Date of this record:** 2026-08-11 (BACKLOG P1-2 — this ADR documents an + already-made decision retroactively; it does not introduce a new one) + +## Context + +0pt was originally scaffolded and deployed on Vercel (Hobby/free tier, +project `0pt`, scope *Simon Axelsson's projects*). SIAX Cloud's platform +mandate is to replace paid SaaS with self-hosted fleet infrastructure +(Hetzner + Coolify), matching every other SIAX Cloud OS repo (`n0d`, +`st0re`, `aud0`, ...). + +## Decision + +`siax.config.json`'s `deploy.target` is `"coolify"`, not `"vercel"`. A +Dockerfile written for Coolify (multi-stage, non-root `node` user, `HEALTHCHECK` +against `/api/health`) and `.gitea/workflows/cd.yml` (POSTs a restart to a +Coolify app UUID on push to `main`) both exist and are committed. + +## Consequences + +- **This is a config-and-tooling change, not a traffic cutover.** As of this + record, no Coolify application has been provisioned for this repo — + `cd.yml`'s `COOLIFY_APP_ID` is a literal placeholder that fails the job + loudly on purpose (see the guard step) rather than posting to a bogus + endpoint. The live instance of `0pt.app` is still served by Vercel. +- Flipping the config was deliberately decoupled from the actual cutover: it + states intent (BACKLOG P0-2 / P3-1 own the "provision the Coolify app, cut + DNS, decommission Vercel" work, gated on a dump-before-write blocker per + the standard's "safe to run now" classification). +- Until the cutover happens, docs describing Vercel as *where 0pt actually + runs today* are accurate; docs prescribing Vercel as *the plan going + forward* are stale (see BACKLOG P2-5 — `docs/02-architecture.md`'s + "Hosting" section is marked SUPERSEDED for exactly this reason). + +## See also + +- `docs/architecture/ARCHITECTURE.md` §6 "Deploy", §9 "Öppna frågor" +- `README.md` "Deploy" section (kept current with the live/target split) +- `docs/compliance/BACKLOG.md` P0-2, P1-17, P3-1 diff --git a/docs/architecture/decisions/0002-auth-zitadel-bearer-not-nextauth.md b/docs/architecture/decisions/0002-auth-zitadel-bearer-not-nextauth.md new file mode 100644 index 0000000..0d94ecf --- /dev/null +++ b/docs/architecture/decisions/0002-auth-zitadel-bearer-not-nextauth.md @@ -0,0 +1,57 @@ +# ADR-0002: `/api/0pt/*` authenticates via fleet Zitadel Bearer tokens, not NextAuth/Auth.js + +- **Status:** Accepted and implemented +- **Date decided:** ~2026-07 ("Wave 2" — commit `1d76d50`, + "feat(auth,foundation): Wave 2 flag-gated Zitadel JWT verify + validation + + eslint + tests"), fail-closed default landed commit `1c1e2cb` + ("fix(auth): make the AUTH_ENFORCE default fail-closed on `/api/0pt/*`") +- **Date of this record:** 2026-08-11 (BACKLOG P1-2 — retroactive) + +## Context + +0pt's capability surface (`/api/0pt/*`) needs to authenticate requests +against a real identity, and needs that identity to carry a SIAX tenant so +`x-0pt-verified-tenant` can be trusted downstream. SIAX Cloud already runs a +fleet-wide Zitadel IdP (`id-customers.siax.io`) for exactly this purpose — +building a second, repo-local identity provider would duplicate it and +create a second source of truth for "who is this". + +## Decision + +`middleware.ts` verifies inbound `Authorization: Bearer` tokens against the +fleet Zitadel IdP (`src/auth/zitadel.ts`): JWKS discovered from +`/.well-known/openid-configuration` (falling back to +`/oauth/v2/keys`), tenant derived from a documented claim-key +precedence (`urn:zitadel:iam:org:id` → `urn:zitadel:iam:user:resourceowner:id` +→ `tenant_id` → `tenant`). No NextAuth/Auth.js dependency was added. + +Enforcement is fail-closed by default: `AUTH_ENFORCE` unset (or any value +except the literal string `"false"`) requires a valid, verified token — 401 +on missing/invalid, 403 when a client-supplied `X-Tenant-Id` disagrees with +the token's own tenant claim. `AUTH_ENFORCE=false` is a local-dev-only escape +hatch, and is refused at boot when `NODE_ENV=production` +(`assertAuthConfigSafe()` throws rather than serving anonymously). + +A separate, older HMAC session cookie (`lib/auth.ts`, with a `DEV_LOGIN_KEY` +dev-login shortcut) exists in parallel and is explicitly **not** the auth +system this ADR is about — it predates the Zitadel work, has no MFA/CSRF/ +revocation, and is tracked as its own gap (BACKLOG P0-14, gated on the +account owner's decision about what replaces it). + +## Consequences + +- 0pt does not maintain its own user/credential store for the capability + layer — identity is entirely delegated to the fleet Zitadel instance. +- A live-verified gap was found and is tracked separately: a 2026-08-10 + probe found `GET 0pt.app/api/0pt` returning 200 anonymously despite the + code being fail-closed (BACKLOG P0-4/P0-5, root cause unresolved as of + this record — do not assume the deployed instance enforces this until an + anonymous request gets a real 401 there). +- `middleware.ts` sits outside `tsconfig.check.json`'s blocking type-check + scope (BACKLOG P0-5) — a real gap, not addressed by this ADR. + +## See also + +- `docs/architecture/ARCHITECTURE.md` §8 "Säkerhet" +- `tests/auth/{context,zitadel}.test.ts` +- `docs/compliance/BACKLOG.md` P0-4, P0-5, P0-14 diff --git a/docs/architecture/decisions/0003-lakehouse-format-iceberg-pointer.md b/docs/architecture/decisions/0003-lakehouse-format-iceberg-pointer.md new file mode 100644 index 0000000..5b09914 --- /dev/null +++ b/docs/architecture/decisions/0003-lakehouse-format-iceberg-pointer.md @@ -0,0 +1,47 @@ +# ADR-0003: Lakehouse table format is Apache Iceberg (pointer to `n0d` ADR-0030) + +- **Status:** Accepted (decision owned by `n0d`, not by this repo) +- **Date decided:** documented in the `n0d` repo as ADR-0030; referenced in + this repo since commit `2545218` ("Document lakehouse write-path ownership + decision (ADR-0030)") +- **Date of this record:** 2026-08-11 (BACKLOG P1-2 — this is a **pointer** + ADR, not a duplicate decision; it exists so `docs/architecture/decisions/` + is a complete index of decisions that shape this repo, even the ones made + elsewhere) + +## Context + +BACKLOG P1-2 asks for ADRs to exist in this repo. The lakehouse-format +decision itself was made once, ecosystem-wide, in `n0d` (the repo that owns +the platform's write path across all K1 data producers) — re-deciding it +here would create a second, divergent source of truth for a cross-repo +architectural choice. + +## Decision + +Apache Iceberg is the canonical SIAX Cloud lakehouse table format. 0pt is a +**write-path owner** within that decision: it maps its own datasets to +Iceberg tables and requests writes through the documented boundary (N0D +executes, 0pt requests — see `docs/ICEBERG_WRITE_BOUNDARY.md`). This repo +does not re-litigate the Iceberg-vs-alternatives choice; it implements its +own slice of it. + +## Consequences + +- `docs/10-lakehouse-decision.md` and `docs/11-lakehouse-write-plan.md` are + this repo's own follow-up documentation against the `n0d`-owned decision — + read them alongside this pointer, not instead of it. +- `docs/ICEBERG_WRITE_BOUNDARY.md` records the implementation status, + including which parts are live-verified vs. contract-only. That document, + not this ADR, is the source of truth for "is the write path actually + working" — this ADR only records *which format was chosen and by whom*. +- Any future change to the ecosystem-wide format decision would originate in + `n0d`, not here; this repo would need its own follow-up ADR to record how + it adapts. + +## See also + +- `docs/10-lakehouse-decision.md` +- `docs/11-lakehouse-write-plan.md` +- `docs/ICEBERG_WRITE_BOUNDARY.md` +- `n0d` repo, ADR-0030 (external — not vendored into this repo) diff --git a/docs/architecture/decisions/README.md b/docs/architecture/decisions/README.md new file mode 100644 index 0000000..acf34ec --- /dev/null +++ b/docs/architecture/decisions/README.md @@ -0,0 +1,34 @@ +# Architecture Decision Records — 0pt + +BACKLOG P1-2. One file per decision, numbered sequentially, never renumbered +or deleted (superseded decisions get a new ADR that says so, per the +convention below). + +| ADR | Title | Status | +|---|---|---| +| [0001](0001-deploy-target-coolify-not-vercel.md) | Deploy target is Coolify, not Vercel | Accepted (migration not yet executed) | +| [0002](0002-auth-zitadel-bearer-not-nextauth.md) | `/api/0pt/*` auth is fleet Zitadel Bearer, not NextAuth | Accepted and implemented | +| [0003](0003-lakehouse-format-iceberg-pointer.md) | Lakehouse format is Apache Iceberg (pointer to `n0d` ADR-0030) | Accepted (owned by `n0d`) | + +## Deliberately not yet written here + +Two decisions the original BACKLOG (P1-2) named explicitly are **not** +written as ADRs in this pass, on purpose: + +- **Payments provider (Stripe vs. Revolut vs. Hyperswitch).** Gated on the + account owner's decision (BACKLOG P1-23) — writing an ADR that presents a + choice as "accepted" before it's actually been decided would misrepresent + the repo's own state, which is exactly what this whole documentation pass + is trying to stop doing. +- **The `services/scrapers/*` keep-vs-remove question.** Explicitly out of + scope for this pass — see `SECURITY.md` and the dated, attributed + provenance record already maintained for that decision elsewhere in this + repo's compliance docs. Not duplicated here. + +## Convention + +New ADR: `NNNN-short-kebab-title.md`, next sequential number, status one of +`Proposed` / `Accepted` / `Superseded by ADR-NNNN` / `Rejected`. Record the +date the decision was actually made (from `git log`/commit history where +possible), not just the date the ADR file was written, when they differ — +see 0001–0003 for the pattern. diff --git a/docs/guides/P6-GUIDES.md b/docs/guides/P6-GUIDES.md new file mode 100644 index 0000000..ffe077a --- /dev/null +++ b/docs/guides/P6-GUIDES.md @@ -0,0 +1,150 @@ +# P6 guides — 0pt + +BACKLOG P6: 19 GUIDE-level items, all marked S (small effort), all "NU" +(safe to write now). Several have their own concrete artifact instead of a +guide entry here — see the table. This file covers the rest: real, +actionable "how to build this when it's prioritized" guidance grounded in +0pt's actual code, not generic advice that could apply to any repo. + +| Control | Item | Where | +|---|---|---| +| REPO-13 | `.editorconfig` | [`.editorconfig`](../../.editorconfig) | +| ARK-14 | dependency-cruiser | [`.dependency-cruiser.cjs`](../../.dependency-cruiser.cjs), `pnpm run deps:check` | +| API-24 | deprecation policy | [`docs/api/DEPRECATION_POLICY.md`](../api/DEPRECATION_POLICY.md) | +| CD-23 | K8s manifest | [`deploy/k8s/`](../../deploy/k8s/) (reference only, not the deploy path) | +| CI-16 | `act` | [`.actrc`](../../.actrc) | +| DX-11 | Makefile | [`Makefile`](../../Makefile) | +| DX-12 | onboarding | [`ONBOARDING.md`](../../ONBOARDING.md) | +| CLI-13 | `doctor` CLI subcommand | see §1 below | +| DOC-18 | documentation site | see §2 below | +| UI-30 | PWA | see §3 below | +| IAM-19 | impersonation | see §4 below | +| PAY-11 | Hyperswitch ADR | see §5 below | +| AI-18 | assistant | see §6 below | +| OBS-21 | status page | see §7 below | +| TEST-20 | chaos testing | see §8 below | +| NET-19 | IPv6 | see §9 below | +| SCALE-13 | read replicas | see §10 below | +| OSS-10 | upstream contribution | see §11 below | +| AGENT-13 | nightly agent | see §12 below | + +--- + +## 1. CLI-13 — `doctor` CLI subcommand + +There is no CLI surface at all yet (BACKLOG P1-5 — L effort, not attempted +this pass). `pnpm run siax:doctor` / `make doctor` +(`scripts/siax-doctor.mjs`) already covers the underlying check logic +(AGENT-04). When P1-5 builds `apps/cli`, wire a `siax 0pt doctor` subcommand +that shells out to (or re-imports) `scripts/siax-doctor.mjs` rather than +re-implementing the checks — one source of truth for "is this repo +compliant", not two that can drift. + +## 2. DOC-18 — documentation site + +Not built. When prioritized: `contracts/openapi.yaml` already exists as a +real OpenAPI 3.1 SSOT, so the lowest-effort path is a static OpenAPI +renderer (e.g. Redocly CLI or Scalar) pointed at that file, deployed as a +static site — not a hand-authored docs site that would drift from the +contract. `docs/*.md` could be published alongside via any static-site +generator that reads plain Markdown (Docusaurus, Starlight); pick whichever +matches what other SIAX Cloud repos already use, per BACKLOG's "one tool per +job" rule — check `cl0ud`/`n0d` first before introducing a new one here. + +## 3. UI-30 — PWA + +Not built. Would need: a `manifest.json` (icons already exist at +`public/icon*.png`/`icon.svg` — reuse them, don't regenerate), a service +worker (Next.js has no built-in PWA plugin as of this repo's Next 16; a +minimal hand-written SW covering `/api/health`-style offline detection would +be more honest than a cargo-culted `next-pwa` config, given this app's +`ignoreBuildErrors: true` posture already has enough unverified surface). Low +priority — 0pt's UI is transactional (order flow, portal), not the kind of +app users install to a home screen. + +## 4. IAM-19 — impersonation + +Not built. Needed pattern once an admin portal exists with real customer +data: a support/admin session that can view-as a specific tenant/user, +**always** logged to AUD0 (BACKLOG P0-18 — AUD0 client exists but is +currently no-op without `AUD0_URL`) with the impersonator's real identity +attached, never silently. Do not build a "become this user" auth shortcut +without that audit trail landing first — an impersonation feature with no +audit log is a privilege-escalation bug wearing a support-tool costume. + +## 5. PAY-11 — Hyperswitch ADR + +**Proposed, not accepted — payments provider is explicitly gated on the +account owner's decision (BACKLOG P1-23).** This entry exists so the option +is on record, not to decide it. Hyperswitch is an open-source payment +orchestration layer (self-hostable) that could sit in front of +Stripe/Revolut/others rather than 0pt integrating each provider directly +(`lib/stripe.ts` today). Trade-off to weigh when P1-23 is actually decided: +self-hosting Hyperswitch adds an operational dependency (another service to +run/monitor) in exchange for provider-agnosticism; direct integration +(current state) is simpler but couples this repo to Stripe's specific API +shape. Not evaluated further here — that's the ADR this guide is explicitly +not writing. + +## 6. AI-18 — assistant + +Not built. `app/api/ai/parse` exists today as a heuristic (no model call, +per `CLAUDE.md`/`docs/02-architecture.md`'s "honest heuristic" framing) — a +real assistant feature should route through INF0 (the fleet AI gateway, +`INF0_URL` — documented target, not yet read by any code in this repo) per +the platform mandate, never a raw provider key committed here. + +## 7. OBS-21 — status page + +Not built. Lowest-effort honest option once `/api/health` + `/api/ready` +are actually monitored (BACKLOG P1-11, not yet wired to Prometheus/GlitchTip +per that item): a self-hosted status page (e.g. Upptime, which is +just-a-GitHub/Gitea-Actions-cron + static site, no server to run) polling +those two endpoints. Do not stand up a status page before the endpoints it +would display are meaningfully monitored — an unmonitored status page that +says "operational" is worse than no status page. + +## 8. TEST-20 — chaos testing + +Not built, and low-value until BACKLOG P0-12 lands (real DB/persistence — +right now there's no durable state to chaos-test; killing the process loses +nothing that wasn't already going to be lost on the next cold start). Once +persistence exists: start with the cheapest chaos test that matches 0pt's +actual documented failure mode — kill the Postgres connection mid-request +and assert `/api/ready` flips to 503 within one health-check interval (this +is now directly testable thanks to BACKLOG P0-3's real dependency probes). + +## 9. NET-19 — IPv6 + +Not evaluated. `docs/architecture/ARCHITECTURE.md` doesn't currently record +whether the Coolify target node(s) or `0pt.app`'s DNS have AAAA records — +check `z0ne` (SIAX's own DNS product) once a real Coolify app exists for +this repo; premature to configure before there's a fleet IP to point at. + +## 10. SCALE-13 — read replicas + +Not applicable yet — there is no primary database in production to +replicate (BACKLOG P0-12: state lives in in-memory `Map`s, zero DB +provisioned). Revisit once P0-12 lands and real read/write load is +measured; provisioning a replica against a database that doesn't exist yet +would be pure speculation. + +## 11. OSS-10 — upstream contribution + +No dependency in this repo has been identified as needing a patch upstream +rather than a workaround here (checked: no local patches in +`pnpm-lock.yaml`'s `patchedDependencies`, none found). Nothing to +contribute upstream today; revisit if/when a real workaround is added for +an upstream bug — file the issue/PR there instead of carrying the +workaround silently forever. + +## 12. AGENT-13 — nightly agent + +`nightly.yml` already re-runs the full quality + (now blocking) security +gate daily (BACKLOG P0-16). A step beyond that — an agent that reads the +nightly failure and opens a triage PR — is AGENT-06 territory (BACKLOG P1-19 +already scopes AGENT-06 as not attempted this pass: it needs real +OpenHands/Aider/SWE-agent infrastructure via INF0, which doesn't exist yet +for this repo). Do not build a bespoke nightly-agent script here ahead of +that shared infrastructure landing — it would be exactly the kind of +one-off tool the "one tool per job" rule (`CLAUDE.md` §4) warns against. diff --git a/package.json b/package.json index 8bdf905..fc5ff37 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "test:watch": "vitest", "format": "prettier --check .", "format:write": "prettier --write .", - "typecheck:app": "tsc --noEmit -p tsconfig.json" + "typecheck:app": "tsc --noEmit -p tsconfig.json", + "siax:doctor": "node scripts/siax-doctor.mjs", + "deps:check": "depcruise src app components lib --config .dependency-cruiser.cjs" }, "dependencies": { "@fontsource-variable/inter": "^5.2.5", @@ -78,6 +80,7 @@ "@types/node": "^22", "@types/react": "^19", "@types/react-dom": "^19", + "dependency-cruiser": "^18.2.0", "eslint": "^9.15.0", "postcss": "^8.5", "prettier": "^3.9.6", diff --git a/packages/schemas/package.json b/packages/schemas/package.json new file mode 100644 index 0000000..e8989f4 --- /dev/null +++ b/packages/schemas/package.json @@ -0,0 +1,8 @@ +{ + "name": "@siax/0pt-schemas", + "version": "0.1.0", + "private": true, + "description": "BACKLOG P1-21: makes packages/schemas a real workspace package (it was previously loose JSON Schema files with no package.json, so pnpm-workspace.yaml's `packages/*` glob matched an empty package). Zero build step -- these files are consumed by path, not imported as JS.", + "files": ["*.schema.json"], + "license": "UNLICENSED" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe83c30..4001eef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,9 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.3(@types/react@19.2.17) + dependency-cruiser: + specifier: ^18.2.0 + version: 18.2.0 eslint: specifier: ^9.15.0 version: 9.39.5(jiti@2.7.0) @@ -211,6 +214,8 @@ importers: specifier: ^3.2.7 version: 3.2.7(@types/node@22.20.0)(jiti@2.7.0)(lightningcss@1.32.0) + packages/schemas: {} + packages: '@alloc/quick-lru@5.2.0': @@ -1681,16 +1686,32 @@ packages: '@vitest/utils@3.2.7': resolution: {integrity: sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==} + acorn-jsx-walk@2.0.0: + resolution: {integrity: sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-loose@8.5.2: + resolution: {integrity: sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==} + engines: {node: '>=0.4.0'} + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} hasBin: true + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -1786,6 +1807,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1865,6 +1890,11 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dependency-cruiser@18.2.0: + resolution: {integrity: sha512-xMDoLD0no6pDInR8/4rIIqZ4mERDnsjezk8PkNORYSfBLvjCOogUxaruepmi1uQtZQlYUgdT2u7G3jTlgKqNjw==} + engines: {node: ^22||^24||>=26} + hasBin: true + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1895,6 +1925,14 @@ packages: resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} engines: {node: '>=10.13.0'} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2012,6 +2050,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -2020,6 +2061,10 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + global-directory@4.0.1: + resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==} + engines: {node: '>=18'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2031,6 +2076,10 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2047,6 +2096,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + ini@4.1.1: + resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + input-otp@1.4.1: resolution: {integrity: sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==} peerDependencies: @@ -2057,6 +2110,14 @@ packages: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + interpret@3.1.1: + resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} + engines: {node: '>=10.13.0'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2065,6 +2126,14 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-installed-globally@1.0.0: + resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} + engines: {node: '>=18'} + + is-path-inside@4.0.0: + resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} + engines: {node: '>=12'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -2094,9 +2163,18 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -2203,6 +2281,9 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2273,6 +2354,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -2311,6 +2395,10 @@ packages: engines: {node: '>=14'} hasBin: true + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2404,15 +2492,31 @@ packages: react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + rechoir@0.8.0: + resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} + engines: {node: '>= 10.13.0'} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + safe-regex@2.1.1: + resolution: {integrity: sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2436,6 +2540,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sonner@1.7.4: resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} peerDependencies: @@ -2452,6 +2559,10 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2485,6 +2596,10 @@ packages: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + tailwind-merge@3.6.0: resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} @@ -2531,6 +2646,14 @@ packages: peerDependencies: typescript: '>=4.8.4' + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -2672,6 +2795,11 @@ packages: jsdom: optional: true + watskeburt@6.0.0: + resolution: {integrity: sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==} + engines: {node: ^22.13||^24||>=26} + hasBin: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -4033,12 +4161,28 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + acorn-jsx-walk@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn-loose@8.5.2: + dependencies: + acorn: 8.18.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.18.0 + acorn@8.17.0: {} + acorn@8.18.0: {} + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 @@ -4137,6 +4281,8 @@ snapshots: color-name@1.1.4: {} + commander@15.0.0: {} + concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -4199,6 +4345,27 @@ snapshots: deep-is@0.1.4: {} + dependency-cruiser@18.2.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + acorn-jsx-walk: 2.0.0 + acorn-loose: 8.5.2 + acorn-walk: 8.3.5 + commander: 15.0.0 + enhanced-resolve: 5.24.5 + ignore: 7.0.6 + interpret: 3.1.1 + is-installed-globally: 1.0.0 + json5: 2.2.3 + picomatch: 4.0.5 + prompts: 2.4.2 + rechoir: 0.8.0 + safe-regex: 2.1.1 + semver: 7.8.5 + tsconfig-paths-webpack-plugin: 4.2.0 + watskeburt: 6.0.0 + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -4227,6 +4394,13 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.3 + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} esbuild@0.28.1: @@ -4373,18 +4547,28 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-nonce@1.0.1: {} glob-parent@6.0.2: dependencies: is-glob: 4.0.3 + global-directory@4.0.1: + dependencies: + ini: 4.1.1 + globals@14.0.0: {} graceful-fs@4.2.11: {} has-flag@4.0.0: {} + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + ignore@5.3.2: {} ignore@7.0.6: {} @@ -4396,6 +4580,8 @@ snapshots: imurmurhash@0.1.4: {} + ini@4.1.1: {} + input-otp@1.4.1(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -4403,12 +4589,25 @@ snapshots: internmap@2.0.3: {} + interpret@3.1.1: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-installed-globally@1.0.0: + dependencies: + global-directory: 4.0.1 + is-path-inside: 4.0.0 + + is-path-inside@4.0.0: {} + isexe@2.0.0: {} jiti@2.7.0: {} @@ -4429,10 +4628,14 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + kleur@3.0.3: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -4517,6 +4720,8 @@ snapshots: dependencies: brace-expansion: 1.1.16 + minimist@1.2.8: {} + ms@2.1.3: {} nanoid@3.3.15: {} @@ -4580,6 +4785,8 @@ snapshots: path-key@3.1.1: {} + path-parse@1.0.7: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -4608,6 +4815,11 @@ snapshots: prettier@3.9.6: {} + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -4704,8 +4916,21 @@ snapshots: tiny-invariant: 1.3.3 victory-vendor: 36.9.2 + rechoir@0.8.0: + dependencies: + resolve: 1.22.12 + + regexp-tree@0.1.27: {} + resolve-from@4.0.0: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -4737,6 +4962,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.2 fsevents: 2.3.3 + safe-regex@2.1.1: + dependencies: + regexp-tree: 0.1.27 + scheduler@0.27.0: {} semver@7.8.5: {} @@ -4781,6 +5010,8 @@ snapshots: siginfo@2.0.0: {} + sisteransi@1.0.5: {} + sonner@1.7.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -4792,6 +5023,8 @@ snapshots: std-env@3.10.0: {} + strip-bom@3.0.0: {} + strip-json-comments@3.1.1: {} strip-literal@3.1.0: @@ -4811,6 +5044,8 @@ snapshots: dependencies: has-flag: 4.0.0 + supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.6.0: {} tailwindcss-animate@1.0.7(tailwindcss@4.3.1): @@ -4842,6 +5077,19 @@ snapshots: dependencies: typescript: 5.9.3 + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.24.5 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + tslib@2.8.1: {} tw-animate-css@1.3.3: {} @@ -4996,6 +5244,8 @@ snapshots: - tsx - yaml + watskeburt@6.0.0: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/prompts/000-INDEX.md b/prompts/000-INDEX.md index 2687ca9..bf3826c 100644 --- a/prompts/000-INDEX.md +++ b/prompts/000-INDEX.md @@ -1,7 +1,13 @@ # Prompts — index +**Kontroll-ID:** AGENT-02 (denna katalog + index), AGENT-03 (varje enskild +prompt nedan har mål/kontext/förutsättningar/omfattning/kontrakt/ +acceptanskriterier/kontroll-ID/förbjudna åtgärder) + > Operativa prompt-mallar för agentsessioner mot 0pt. Konvention: `NNN-roll.md`. > Alla mallar respekterar CLAUDE.md/AGENTS.md + docs/compliance (AR-5). +> Självdiagnos: `pnpm siax:doctor` (AGENT-04, `scripts/siax-doctor.mjs`) +> kör den maskinellt mätbara delmängden av dessa krav lokalt. | Fil | Användning | |---|---| diff --git a/prompts/001-repo-health.md b/prompts/001-repo-health.md index fe3ade0..042777e 100644 --- a/prompts/001-repo-health.md +++ b/prompts/001-repo-health.md @@ -1,22 +1,65 @@ # Prompt: Repo-hälsa (0pt) +**Kontroll-ID:** AGENT-03 (denna mall), AGENT-02 (`prompts/`-strukturen som helhet) + > Använd före annat arbete: verifiera att utgångsläget är sant innan ändringar. +## Mål + +Fastställa repots verkliga läge (branch, CI, live `/api/health`) innan någon +kod ändras, så att arbetet bygger på fakta, inte på vad ett dokument påstår. + +## Kontext + +`main` på detta repo har varit mål för flera parallella sessioner (se +`docs/compliance/BACKLOG.md`s löpande historik). Antaganden om "senaste +läget" blir fel snabbt — kör alltid denna prompt först. + +## Förutsättningar + +- Läs-åtkomst till `git.siax.io/sax3l/0pt` (klon eller `git fetch`). +- Nätåtkomst till `https://0pt.app` för live-proben (annars: notera EJ + VERIFIERAT i output, gissa aldrig). + +## Omfattning + +Läsning + rapportering endast. Denna prompt gör inga kodändringar. + ## 1. Status - `git status` + `git log --oneline -5` — ren main? - Senaste CI-run (Gitea Actions API): - `/repos/sax3l/0pt/actions/runs?limit=5` — ci.yml ska vara grön; - cd.yml/deploy.yml röd = avsiktlig guard (placeholder UUID). -- `curl -s https://0pt.app/api/health` — ok/true + db/queue-läge. + `/repos/sax3l/0pt/actions/runs?limit=5` — `ci.yml` ska vara grön; + `cd.yml` röd = avsiktlig guard (placeholder UUID). +- `curl -s https://0pt.app/api/health` — `ok`/beroendestatus (se BACKLOG P0-3: + fältet rapporterar numera riktig per-dependency-status, inte bara + `DATABASE_URL`/`REDIS_URL`-närvaro). ## 2. Luckor att minnas -- S1: `/api/0pt/*` svarade 200 anonymt (2026-08-10) — påstå aldrig "skyddad". -- S2: tenant tas ur body — nya handlers läser `x-0pt-verified-tenant`. -- Bypass-stacken `services/scrapers/*` — medvetet behållen, rör ej. +- S1: `/api/0pt/*` svarade 200 anonymt (2026-08-10) — påstå aldrig "skyddad" + utan en färsk 401-verifiering. +- S2: tenant tas ur body i äldre handlers — nya handlers läser + `x-0pt-verified-tenant`. +- Bypass-stacken `services/scrapers/*` — medvetet behållen, rör ej (se + `SECURITY.md`). + +## Kontrakt / maskinellt verifierbara acceptanskriterier + +- [ ] `git log -1 --format=%H` rapporterad i output. +- [ ] CI-status för senaste `ci.yml`-run på `main` rapporterad (grön/röd + run-ID). +- [ ] `/api/health`-svarets `ok`-fält + per-dependency-status citerat ordagrant + (inte omskrivet) med tidsstämpel för när proben kördes. +- [ ] Status på S1–S3 märkt VERIFIERAT/ANTAGANDE/EJ VERIFIERAT — aldrig + olabellat. ## 3. Output -Rapportera: HEAD-SHA, CI-status, /api/health-svar, status på S1–S3 +Rapportera: HEAD-SHA, CI-status, `/api/health`-svar, status på S1–S3 (VERIFIERAT/ANTAGANDE/EJ VERIFIERAT). + +## Förbjudna åtgärder + +- Skriv aldrig till `main` från denna prompt. +- Påstå aldrig ett kontrollresultat utan att ha kört den bakomliggande + kommandot/proben denna session. diff --git a/prompts/002-compliance-audit.md b/prompts/002-compliance-audit.md index db0ebca..5a30d38 100644 --- a/prompts/002-compliance-audit.md +++ b/prompts/002-compliance-audit.md @@ -1,7 +1,33 @@ # Prompt: Compliance-audit (0pt) +**Kontroll-ID:** AGENT-03 (denna mall), AGENT-04/AGENT-05 (denna prompt är den +manuella/agent-drivna motsvarigheten till `pnpm siax:doctor` + framtida +C0NF0RM-rapportering — se `scripts/siax-doctor.mjs`) + > Uppföljning av GOLDEN-STANDARD-REPORT. Körs när backlog-objekt stängts. +## Mål + +Hålla `docs/compliance/GOLDEN-STANDARD-REPORT.md` och `docs/compliance/BACKLOG.md` +i synk med repots faktiska läge — aldrig med önskeläget. + +## Kontext + +Poängen i GOLDEN-STANDARD-REPORT.md är avsedd att jämföras mot andra SIAX +Cloud-repon. En falskt hög poäng (ändrad utan nytt bevis) gör den jämförelsen +värdelös för hela flottan, inte bara för detta repo. + +## Förutsättningar + +- `docs/compliance/GOLDEN-STANDARD-REPORT.md` och `docs/compliance/BACKLOG.md` + existerar och lästa i sin helhet innan ändring. +- Det påstådda beviset (testfil, commit-SHA, live-svar) finns och går att peka på. + +## Omfattning + +Endast `docs/compliance/**`. Kör aldrig kodändringar från denna prompt — de +hör hemma i den ändring som faktiskt stänger backlog-posten. + ## 1. Uppdatera - Läs `docs/compliance/GOLDEN-STANDARD-REPORT.md` — sätt status ✅/⚠️/❌/➖ @@ -17,3 +43,17 @@ attribuerade beslut (Simon Axelsson, 2026-08-11, se SECURITY.md), INTE det gamla oattribuerade "medvetet användarbeslut" — ingen ändring av C6-status. - Inga fabricerade förbättringar: om inget faktiskt ändrats → ingen poängändring. + +## Kontrakt / maskinellt verifierbara acceptanskriterier + +- [ ] Varje ✅/⚠️→bättre statusändring har en bevisrad (filväg/SHA/kommando) + i samma commit. +- [ ] Poängräkningen i "## Poäng" summerar korrekt om man räknar om den + (✅×1 + ⚠️×0.5) / (antal kontroller − antal ➖). +- [ ] C6 oförändrad ➖ om inte en ny, daterad, attribuerad ägarinstruktion citeras. + +## Förbjudna åtgärder + +- Ändra aldrig C6:s status eller motivering utan en ny, explicit, + daterad instruktion från kontoägaren i denna session. +- Höj aldrig en kontrolls poäng utan ett konkret, citerat bevis. diff --git a/prompts/003-security-review.md b/prompts/003-security-review.md index c9c0574..ce5a845 100644 --- a/prompts/003-security-review.md +++ b/prompts/003-security-review.md @@ -1,19 +1,63 @@ # Prompt: Security review (0pt) +**Kontroll-ID:** AGENT-03 (denna mall) + > Säkerhetsgranskning av en ändring före push. Kör ALLTID vid ändringar i > auth, klienter (cl0ud/aud0/n0d/st0re), payments eller middleware. +## Mål + +Fånga regressions i auth/tenant/hemlighetshantering innan de når `main`, +komplement till (inte ersättning för) CI:s blockerande Trivy/gitleaks-steg +(`.gitea/workflows/security.yml`). + +## Kontext + +Två av repots historiskt allvarligaste fynd (S1 anonym `/api/0pt/*`, S3 +anonym `/api/orders`) hade upptäckts av denna checklista om den körts före +push. CI:s automatiska scanning (BACKLOG P0-16/P0-19) täcker beroenden och +hemligheter i historiken — den täcker inte logikfel som "tenant_id läst ur +body". + +## Förutsättningar + +- Diffen som ska granskas är känd (branch/PR-nummer). +- `pnpm run typecheck` och `pnpm run test` går att köra lokalt. + +## Omfattning + +Granskning av EN ändring/PR. Ändrar inget i sig själv — output är en rapport, +inte en fix. + ## 1. Checklista -- [ ] Inga hemligheter i diffen (grep: sk_/whsec/private key/AIza/AKIA…). +- [ ] Inga hemligheter i diffen (grep: sk_/whsec/private key/AIza/AKIA…) — + CI:s gitleaks-steg är facit, men kör lokalt först. - [ ] Inga nya betald-SaaS-/Google/Vercel-bärare (AR-4). - [ ] `tenant_id` inte tagen ur request-body (S2-regeln). - [ ] Inga ändringar i `services/scrapers/*` (bypass-stacken, medvetet beslut). - [ ] Auth-fail-closed kvarstår; ingen `AUTH_ENFORCE=false`-produktionsväg. - [ ] Inga `console.log` av hemligheter; ingen `NEXT_PUBLIC_*`-hemlighet. - [ ] Tester täcker nya grenar; typecheck+test gröna. -- [ ] Trivy-fynd i diffen triagerade och dokumenterade. +- [ ] Trivy-fynd i diffen triagerade och dokumenterade (BACKLOG P0-16: Trivy + är numera blockerande i CI — ett rött `trivy`-jobb är förväntat tills + befintliga beroende-fynd åtgärdas separat, inte en regression du orsakat + om det redan var rött på `main`). + +## Kontrakt / maskinellt verifierbara acceptanskriterier + +- [ ] Varje checklisterad punkt har VERIFIERAT/ANTAGANDE/EJ VERIFIERAT + + risknivå (L0–L5) i output — ingen punkt lämnas tom. +- [ ] Om `services/scrapers/*` finns i diffen: granskningen stoppar och + eskalerar, gör ingen egen bedömning. ## 2. Output `VERIFIERAT/ANTAGANDE/EJ VERIFIERAT` per punkt + risknivå (L0–L5) + rekommendation. + +## Förbjudna åtgärder + +- Godkänn aldrig en diff som rör `services/scrapers/*`, + `services/infra/docker-compose.data-stack.yml` eller + `live/src/workers/adaptive_router.py`s `curl_cffi`-rader — eskalera i stället. +- Fatta aldrig auktoritativt beslut om C6/bypass-stacken från denna prompt. diff --git a/prompts/004-deploy-cutover.md b/prompts/004-deploy-cutover.md index f8e4cd8..f5629f2 100644 --- a/prompts/004-deploy-cutover.md +++ b/prompts/004-deploy-cutover.md @@ -1,8 +1,35 @@ # Prompt: Deploy-cutover (0pt) +**Kontroll-ID:** AGENT-03 (denna mall) + > Mål: 0pt.app på Coolify, sedan Vercel-avveckling. Följ > `docs/runbook/deploy.md` steg för steg. +## Mål + +Flytta produktionstrafik från Vercel till den provisionerade Coolify-appen +utan dataförlust eller nedtid, och utan att avveckla Vercel förrän paritet +är bevisad. + +## Kontext + +`siax.config.json` deklarerar redan `deploy.target: "coolify"` (ADR-0001) +— detta är avsiktligt bara en konfigurationsavsikt, inte en genomförd +flytt. Ingen Coolify-app finns provisionerad för detta repo än +(`.gitea/workflows/cd.yml`s `COOLIFY_APP_ID` är en medveten platshållare). + +## Förutsättningar + +- Coolify-åtkomst (cloud.siax.io) för att skapa appen. +- Infisical-projekt för 0pt (BACKLOG P0-1 — **kräver en Infisical-token som + denna prompts session inte nödvändigtvis har**; verifiera innan steg 1). +- `COOLIFY_TOKEN`-repo-secret-behörighet. + +## Omfattning + +Infra-provisionering + DNS + en skyddad-sökväg-PR (`cd.yml`). Rör aldrig +`services/scrapers/*` eller andra frysta sökvägar under denna cutover. + ## 1. Gate (blockerare) - Coolify-app existerar? (cloud.siax.io → projekt) @@ -15,10 +42,28 @@ 1. Provisionera app (dockerfile, repo sax3l/0pt, branch main). 2. PR: fyll UUID + verifiera status-check (skyddade filer). 3. Kör deploy, följ Gitea-run → grön. -4. `curl https://0pt.app/api/health` och en marknadssida. +4. `curl https://0pt.app/api/health` **och** `curl https://0pt.app/api/ready` + (BACKLOG P0-3 — readiness failar stängt om databasen inte är uppe; kräv + 200 på båda, inte bara `/api/health`) och en marknadssida. 5. DNS-ompekning + Vercel-avveckling först när paritet certifierats. ## 3. Regel Radera aldrig Vercel-projektet före certifierad paritet. Dokumentera varje steg i `docs/compliance/EVIDENCE/` med datum + SHA. + +## Kontrakt / maskinellt verifierbara acceptanskriterier + +- [ ] `/api/health` OCH `/api/ready` svarar 200 på den nya Coolify-URL:en + innan DNS pekas om. +- [ ] Varje steg i sekvensen har en motsvarande rad i + `docs/compliance/EVIDENCE/` med datum + SHA innan nästa steg påbörjas. +- [ ] Vercel-projektet finns kvar (overifierat raderat) tills steg 5 är klart. + +## Förbjudna åtgärder + +- Radera eller pausa Vercel-projektet före certifierad paritet. +- Fyll `COOLIFY_APP_ID` eller `COOLIFY_TOKEN` med gissade/platshållarvärden + som INTE är den verkliga provisionerade appens uppgifter. +- Rör `services/scrapers/*`, `services/infra/docker-compose.data-stack.yml` + under denna cutover — helt orelaterat till deploy-målet. diff --git a/scripts/siax-doctor.mjs b/scripts/siax-doctor.mjs new file mode 100644 index 0000000..49aca5c --- /dev/null +++ b/scripts/siax-doctor.mjs @@ -0,0 +1,154 @@ +#!/usr/bin/env node +// siax-doctor (BACKLOG P1-19, control AGENT-04): "Repot är +// self-diagnostiserande: `pnpm siax:doctor` kör hela standarden lokalt och +// skriver ut avvikelser med kontroll-ID." +// +// This is a repo-local, zero-dependency subset -- NOT the published +// @siax/doctor package from sax3l/siax-standard. That package was +// deliberately not taken on as a dependency here: at the time this was +// written it wasn't verified reachable/stable from this repo's frozen +// lockfile install, and a doctor script that can silently fail to install +// is worse than no doctor script. This checks the same class of +// machine-checkable facts (required files, manifest shape) using only +// Node's built-in `fs`/`path` -- no network, no registry. +// +// Exit code 0 = every check passed. Exit code 1 = at least one check failed +// (see the printed control-ID to find the BACKLOG/standard item). +// +// NOTE for reviewers: this script was authored alongside two sibling PRs +// (fix/honest-health-check adds app/api/ready/route.ts; +// ci/security-headers-and-scanning adds CODEOWNERS) that this branch does +// not itself contain. Run this on a checkout with all three merged for a +// clean 0-failure result -- a couple of FAILs on this branch alone, for +// exactly those two files, is expected and not a bug in either PR. +import { existsSync, readFileSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); + +/** @type {{id: string, label: string, check: () => boolean | string}[]} */ +const checks = []; + +function file(path) { + return join(ROOT, path); +} + +function requireFile(id, label, path) { + checks.push({ + id, + label, + check: () => (existsSync(file(path)) ? true : `missing: ${path}`), + }); +} + +function requireJsonField(id, label, path, fieldPath) { + checks.push({ + id, + label, + check: () => { + if (!existsSync(file(path))) return `missing file: ${path}`; + let data; + try { + data = JSON.parse(readFileSync(file(path), "utf8")); + } catch (e) { + return `invalid JSON in ${path}: ${e.message}`; + } + let cur = data; + for (const key of fieldPath.split(".")) { + if (cur == null || !(key in cur)) return `missing field "${fieldPath}" in ${path}`; + cur = cur[key]; + } + return true; + }, + }); +} + +function requireTextMatch(id, label, path, pattern, patternLabel) { + checks.push({ + id, + label, + check: () => { + if (!existsSync(file(path))) return `missing file: ${path}`; + const text = readFileSync(file(path), "utf8"); + return pattern.test(text) ? true : `${path} does not contain ${patternLabel}`; + }, + }); +} + +// -- REPO-* / DOC-* : required root files -------------------------------- +requireFile("DOC-01", "README.md exists", "README.md"); +requireFile("REPO-08", "LICENSE exists", "LICENSE"); +requireFile("REPO-13", ".editorconfig exists", ".editorconfig"); +checks.push({ + id: "REPO-15/P0-19", + label: "no committed .env (real secrets)", + check: () => (existsSync(file(".env")) ? "a real .env file is committed -- should never happen" : true), +}); +requireFile("P1-15", "CODEOWNERS exists", "CODEOWNERS"); +requireFile("P1-3", "CHANGELOG.md exists", "CHANGELOG.md"); +requireFile("SEC-*", "SECURITY.md exists", "SECURITY.md"); +requireFile("AGENT-01", "AGENTS.md exists", "AGENTS.md"); +requireFile("DOC-01", "CONTRIBUTING.md exists", "CONTRIBUTING.md"); + +// -- DOC-02/DOC-04 : ARCHITECTURE.md at the exact standard path ----------- +requireFile("DOC-02", "docs/architecture/ARCHITECTURE.md exists at the standard path", "docs/architecture/ARCHITECTURE.md"); +requireTextMatch( + "DOC-02/v2.0", + "ARCHITECTURE.md has the v2.0 '## 5. Ytor och paritet' section", + "docs/architecture/ARCHITECTURE.md", + /^## 5\. Ytor och paritet/m, + "'## 5. Ytor och paritet'", +); +requireTextMatch( + "DOC-04", + "ARCHITECTURE.md has a 'Senast verifierad mot kod' date field", + "docs/architecture/ARCHITECTURE.md", + /Senast verifierad mot kod:\*\*\s*\d{4}-\d{2}-\d{2}/, + "a dated 'Senast verifierad mot kod' field", +); + +// -- P1-1 / REPO-03/04/06 : manifests -------------------------------------- +requireFile("P1-1/REPO-04", "siax.repo.json exists", "siax.repo.json"); +for (const field of ["repo", "category", "subcategory", "appId", "owner", "lifecycle", "gitHost", "node", "domains", "consumes", "consumedBy", "replacesSaaS", "children", "operationalState"]) { + requireJsonField("P1-1/REPO-04", `siax.repo.json has required field "${field}"`, "siax.repo.json", field); +} +requireFile("P1-1/REPO-06", "siax.config.json exists", "siax.config.json"); +for (const field of ["framework", "build", "start", "port", "healthcheck", "environments"]) { + requireJsonField("P1-1/REPO-06", `siax.config.json has required field "${field}"`, "siax.config.json", field); +} + +// -- AGENT-02/03 : prompts ------------------------------------------------- +requireFile("AGENT-02", "prompts/000-INDEX.md exists", "prompts/000-INDEX.md"); + +// -- DX-03 : devcontainer -------------------------------------------------- +requireFile("DX-03", ".devcontainer/devcontainer.json exists", ".devcontainer/devcontainer.json"); + +// -- P1-2 : ADRs ------------------------------------------------------------- +requireFile("P1-2", "docs/architecture/decisions/ has at least one ADR", "docs/architecture/decisions/README.md"); + +// -- CI-14 : contract --------------------------------------------------------- +requireFile("A4/DOC", "contracts/openapi.yaml exists", "contracts/openapi.yaml"); + +// -- CD-04/OBS-04 : health/ready --------------------------------------------- +requireFile("P0-3/CD-04", "app/api/health/route.ts exists", "app/api/health/route.ts"); +requireFile("P0-3/OBS-04", "app/api/ready/route.ts exists (readiness, not just liveness)", "app/api/ready/route.ts"); + +// -------------------------------------------------------------------------- + +let failures = 0; +console.log("siax-doctor -- 0pt (repo-local subset, see scripts/siax-doctor.mjs header)\n"); +for (const { id, label, check } of checks) { + const result = check(); + if (result === true) { + console.log(` [PASS] ${id.padEnd(14)} ${label}`); + } else { + failures += 1; + console.log(` [FAIL] ${id.padEnd(14)} ${label} -- ${result}`); + } +} +console.log(`\n${checks.length - failures}/${checks.length} checks passed.`); +if (failures > 0) { + console.log(`${failures} check(s) failed -- see control-IDs above and cross-reference docs/compliance/BACKLOG.md.`); + process.exit(1); +} diff --git a/siax.config.json b/siax.config.json index 0ac39c4..dda7d4a 100644 --- a/siax.config.json +++ b/siax.config.json @@ -1,19 +1,56 @@ { - "name": "0pt", - "category": "platform", - "subcategory": "data-os", - "appId": "0PT", - "owner": "SIAX Technology AB", - "packageName": "@siax/0pt", - "capabilityPrefix": "0pt", - "deploy": { - "target": "coolify", - "node": null, - "domain": "0pt.app", - "healthPath": "/api/health" + "$schema": "https://git.siax.io/sax3l/siax-standard/raw/branch/main/schema/siax.config.schema.json", + "framework": "nextjs", + "build": "pnpm run build", + "start": "pnpm run start", + "port": 3000, + "healthcheck": { + "path": "/api/health", + "readyPath": "/api/ready" }, - "consumes": ["CL0UD", "ST0RE", "N0D", "AUD0", "INF0"], - "replacesSaaS": ["Apollo", "Cognism", "ZoomInfo", "Clearbit"], - "decoupled": false, - "designSystem": null + "environments": { + "prod": { + "domain": "0pt.app" + } + }, + "env": [ + "DATABASE_URL", + "POSTGRES_URL", + "POSTGRES_PRISMA_URL", + "REDIS_URL", + "AUTH_SECRET", + "AUTH_URL", + "DEV_LOGIN_KEY", + "INTERNAL_TOKEN", + "ZITADEL_ISSUER", + "AUTH_ENFORCE", + "ZITADEL_TENANT_CLAIM", + "ENGINE_URL", + "CL0UD_URL", + "N0D_URL", + "AUD0_URL", + "INF0_URL", + "ST0RE_URL", + "STRIPE_SECRET_KEY", + "STRIPE_WEBHOOK_SECRET", + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + "R2_ENDPOINT", + "R2_BUCKET", + "R2_ACCESS_KEY_ID", + "R2_ACCOUNT_ID", + "R2_SECRET_ACCESS_KEY", + "DOC_INTELLIGENCE_ENDPOINT", + "DOC_INTELLIGENCE_KEY", + "DOC_INTELLIGENCE_API_VERSION", + "GRANT_ENC_KEY", + "ODOO_BASE_URL", + "RESEND_API_KEY", + "METABASE_SITE_URL", + "METABASE_SECRET_KEY", + "NOCODB_BASE_URL", + "NOCODB_API_TOKEN", + "NEXT_PUBLIC_APP_URL", + "NEXT_PUBLIC_API_URL" + ], + "decoupled": false } diff --git a/siax.repo.json b/siax.repo.json index 4e83641..1fa3c72 100644 --- a/siax.repo.json +++ b/siax.repo.json @@ -1,34 +1,59 @@ { - "$schema": "https://docs.siax.io/schemas/siax.repo.schema.json", - "name": "0pt", + "$schema": "https://git.siax.io/sax3l/siax-standard/raw/branch/main/schema/siax.repo.schema.json", + "repo": "0pt", + "standardVersion": "2.0", "category": "platform", "subcategory": "data-os", "appId": "0PT", "capabilityPrefix": "0pt", "packageName": "@siax/0pt", "owner": "SIAX Technology AB", + "codeowners": ["@sax3l"], + "lifecycle": "active", + "operationalState": "running", + "gitHost": "git.siax.io", "node": null, "domains": [ - "0pt.app", - "api.0pt.app" + { + "fqdn": "0pt.app", + "env": "prod", + "purpose": "Next.js edge plane -- marketing, catalog, order flow, customer/admin portal, capability API", + "registrar": "EJ VERIFIERAT fran detta repo -- se docs/architecture/ARCHITECTURE.md SS6/SS9 (BACKLOG P0-15)" + }, + { + "fqdn": "api.0pt.app", + "env": "prod", + "purpose": "Target API host (Caddy) -- documented target, not verified live (contracts/openapi.yaml server list)", + "registrar": "EJ VERIFIERAT fran detta repo -- se docs/architecture/ARCHITECTURE.md SS6/SS9 (BACKLOG P0-15)" + } ], - "deploy": { - "target": "coolify", - "healthPath": "/api/health" + "consumes": ["CL0UD", "ST0RE", "N0D", "AUD0", "INF0"], + "consumedBy": [], + "children": [ + { + "path": "app", + "purpose": "Next.js edge plane -- UI + capability/order/payment API route handlers", + "lifecycle": "active" + }, + { + "path": "services/api", + "purpose": "FastAPI data engine (orchestrator + workers) -- legal sourcing chain, estimates/orders/screening", + "lifecycle": "active" + }, + { + "path": "services/scrapers", + "purpose": "Scraper stack including WAF-bypass code -- kept per a dated, attributed account-owner decision under active review; NOT part of the compliant sourcing chain (see SECURITY.md, CLAUDE.md SS1)", + "lifecycle": "active" + }, + { + "path": "packages/schemas", + "purpose": "Shared JSON Schema definitions for 0pt data contracts (dataset/iceberg/lineage/signal/rate-policy)", + "lifecycle": "active" + } + ], + "replacesSaaS": ["Apollo", "Cognism", "ZoomInfo", "Clearbit"], + "contracts": { + "openapi": "contracts/openapi.yaml" }, - "consumes": [ - "CL0UD", - "ST0RE", - "N0D", - "AUD0", - "INF0" - ], - "replacesSaaS": [ - "Apollo", - "Cognism", - "ZoomInfo", - "Clearbit" - ], - "decoupled": false, - "designSystem": null + "decoupled": false }