docs+chore: repo hygiene -- v2.0 manifests, ADRs, changelog, doctor, guides #20

Merged
admin merged 1 commits from docs/repo-hygiene-v2-standard into main 2026-08-11 12:32:19 +00:00
32 changed files with 1569 additions and 81 deletions
+12
View File
@@ -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
+50
View File
@@ -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 },
},
},
};
+15
View File
@@ -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"
}
}
+25
View File
@@ -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
+69
View File
@@ -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
+37
View File
@@ -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
+65
View File
@@ -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`.
+32
View File
@@ -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: {}
+13
View File
@@ -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).
+63
View File
@@ -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
+12 -2
View File
@@ -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).
+7 -6
View File
@@ -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.
+4 -4
View File
@@ -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";
+41
View File
@@ -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.
+26
View File
@@ -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.
+63 -20
View File
@@ -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 `<issuer>/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"`
@@ -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
@@ -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
`<issuer>/.well-known/openid-configuration` (falling back to
`<issuer>/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
@@ -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)
+34
View File
@@ -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.
+150
View File
@@ -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.
+4 -1
View File
@@ -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",
+8
View File
@@ -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"
}
+250
View File
@@ -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
+6
View File
@@ -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 |
|---|---|
+50 -7
View File
@@ -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.
+40
View File
@@ -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.
+46 -2
View File
@@ -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.
+46 -1
View File
@@ -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.
+154
View File
@@ -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);
}
+53 -16
View File
@@ -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
}
+47 -22
View File
@@ -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
}