diff --git a/.actrc b/.actrc deleted file mode 100644 index 830ca35..0000000 --- a/.actrc +++ /dev/null @@ -1,12 +0,0 @@ -# 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 deleted file mode 100644 index 0e01be8..0000000 --- a/.dependency-cruiser.cjs +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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 deleted file mode 100644 index 5e3d14d..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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/.dockerignore b/.dockerignore deleted file mode 100644 index 0fc740c..0000000 --- a/.dockerignore +++ /dev/null @@ -1,35 +0,0 @@ -# Keep the build context small + reproducible. Do NOT exclude app source dirs -# (app/, components/, hooks/, lib/, packages/, src/, styles/, public/) -- next -# build needs them. -node_modules -.next -out -build - -.git -.gitea -.github - -# local env / secrets -- never ship into an image -.env -.env.* -!.env.example - -.vercel -*.tsbuildinfo -next-env.d.ts - -# python services + their large sample dumps are not part of the Next build -**/__pycache__ -*.pyc -services/scrapers/output - -# editor / OS noise -.DS_Store -*.log -npm-debug.log* -.pnpm-debug.log* - -# the Docker files themselves -Dockerfile -.dockerignore diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 7ddd7a6..0000000 --- a/.editorconfig +++ /dev/null @@ -1,25 +0,0 @@ -# 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/.env.example b/.env.example deleted file mode 100644 index d5c2bb9..0000000 --- a/.env.example +++ /dev/null @@ -1,301 +0,0 @@ -# ============================================================================= -# 0pt (edge plane) -- environment inventory / example environment -# ============================================================================= -# Copy to `.env` (gitignored) and fill in real values. Never commit real values. -# Vercel env for the edge app; VPS .env (git-ignored) for infra/docker-compose.yml. -# See docs/08-infrastructure.md for the canonical description of each integration. -# -# Two provenance markers are used below and they mean different things: -# * "wired in code today" -- read via a `process.env.*` somewhere in -# this repo right now (grep-verified). -# * "documented target, not yet read" -- named in docs/08-infrastructure.md / -# docs/02-architecture.md but with no -# `process.env.*` read yet. Filling it in -# unblocks the integration; it does not by -# itself change behaviour. -# -# The app is designed to build and boot with all of these UNSET: lib/db.ts and -# lib/stripe.ts are lazy + env-guarded, and the storage/payment/doc routes return -# HTTP 501 ("not configured") until the relevant keys are provided. - -# --------------------------------------------------------------------------- -# Core / public -# --------------------------------------------------------------------------- -# Public base URL of this app (used for absolute links / callbacks). -NEXT_PUBLIC_APP_URL=http://localhost:3000 -# Documented target (docs/08); no call site found reading it in this pass. -# NOTE: api.0pt.app is a dead hostname (falls through to wildcard Vercel -> -# DEPLOYMENT_NOT_FOUND). The app serves its own /api routes on the site -# origin, so the honest base URL is the live origin itself. -NEXT_PUBLIC_API_URL=https://0pt.app - -# --------------------------------------------------------------------------- -# Database (Postgres) -- wired in code today: lib/db.ts, app/api/health -# --------------------------------------------------------------------------- -# Vercel Postgres sets POSTGRES_URL / POSTGRES_PRISMA_URL automatically when the -# integration is attached. First one set wins, in this order (lib/db.ts): -# DATABASE_URL -> POSTGRES_URL -> POSTGRES_PRISMA_URL. -DATABASE_URL=postgres://user:password@localhost:5432/opt -POSTGRES_URL= -POSTGRES_PRISMA_URL= - -# --------------------------------------------------------------------------- -# Cache / queue (Redis) -- app/api/health only checks presence; no other call -# site found reading it in this pass (worker queue usage, if any, lives in -# services/api, outside this repo's scope). -# --------------------------------------------------------------------------- -REDIS_URL=redis://localhost:6379 - -# --------------------------------------------------------------------------- -# Auth -- session / dev-login / service-token secrets (wired in code today) -# --------------------------------------------------------------------------- -# Session-signing secret (generate a long random string). -AUTH_SECRET=change-me-long-random-string -# AUTH_URL is documented (docs/08) but not yet read by any route in this pass. -AUTH_URL=https://0pt.app -# Optional dev-only login shortcut key (leave UNSET in production). -DEV_LOGIN_KEY= -# Shared token for internal/service-to-service calls; sent as X-Internal-Token -# on every engineFetch() call (lib/engine.ts). -INTERNAL_TOKEN=change-me-internal-token -# Tenant authority for the internal-token principal. Comma-separated allowlist -# of tenants this deployment's engine is entitled to write for, e.g. -# "379976258701823235". This is the ONLY source of tenant authority for -# X-Internal-Token: a request header may never name a tenant. Empty/unset means -# the service principal has no tenant authority and tenant-scoped writes -# (canonical facts, quality bridge runs) are denied with 403. Exactly ONE -# tenant is supported: the shared token identifies one service process, not a -# set of principals. A multi-tenant list is refused (401) until a verified -# workload-identity binding exists. -INTERNAL_SERVICE_TENANTS= - -# --------------------------------------------------------------------------- -# Zitadel / OIDC Bearer verification (Wave 2 hardening) -- wired in code today -# --------------------------------------------------------------------------- -# 0pt verifies inbound Bearer tokens against the fleet Zitadel IdP -# (src/auth/zitadel.ts, wired via middleware.ts). JWKS are discovered from -# /.well-known/openid-configuration, falling back to /oauth/v2/keys. -# -# Fleet Zitadel issuer (OIDC). Default when unset: https://id-customers.siax.io -ZITADEL_ISSUER=https://id-customers.siax.io -# Browser login (Authorization Code + PKCE). Create a Web application in the -# Zitadel project with redirect URI https://0pt.app/api/auth/callback. -ZITADEL_OIDC_CLIENT_ID= -ZITADEL_OIDC_CLIENT_SECRET= -# Enforcement is FAIL-CLOSED. Leave this UNSET (or set it to "true") and a -# valid, verified Bearer token is REQUIRED on /api/0pt/* -- 401 on -# missing/invalid, 403 when X-Tenant-Id disagrees with the token tenant. -# ONLY the literal value "false" disables authentication, and that value is -# REFUSED when NODE_ENV=production: the app throws at startup instead of -# serving openly. Use "false" for local development only. -AUTH_ENFORCE=true -# Optional override for the claim key that carries the SIAX tenant id. When -# unset, tenant derivation uses the documented precedence: -# urn:zitadel:iam:org:id -> urn:zitadel:iam:user:resourceowner:id -> tenant_id -> tenant. -ZITADEL_TENANT_CLAIM= - -# --------------------------------------------------------------------------- -# Data engine bridge -- wired in code today: lib/engine.ts -# --------------------------------------------------------------------------- -# When set, edge route handlers defer to the FastAPI engine (services/api) as the -# single source of truth (orders/estimates/catalog/ai-parse). When unset, the edge -# computes locally / falls back to an honest heuristic -- see lib/engine.ts and -# app/api/ai/parse. -ENGINE_URL=http://localhost:8000 - -# --------------------------------------------------------------------------- -# SIAX platform clients -# --------------------------------------------------------------------------- -# WIRED FOR REAL since 2026-08-31: every factory in src/clients/* reads its -# *_URL from process.env when no constructor baseUrl is given. Under -# NODE_ENV=production an UNSET url is a hard boot failure -# (SiaxError DEPENDENCY_UNAVAILABLE) — production never silently falls back -# to the in-memory mocks (Master-DoD §3/§10). In dev/test, unset = documented -# local mock/no-op behaviour. -CL0UD_URL= -N0D_URL= -AUD0_URL= -# Optional service bearer tokens for the platform clients (machine identity). -# Values come from Infisical/secret refs — NEVER commit real values here. -CL0UD_SERVICE_TOKEN= -N0D_SERVICE_TOKEN= -AUD0_SERVICE_TOKEN= -ST0RE_SERVICE_TOKEN= -# INF0 (AI OS) -- documented target, not yet read by any code in this repo. -# Route AI-parsning / model calls through the gateway instead of raw provider keys. -INF0_URL= -# ST0RE_URL is the convention name for src/clients/st0re-client.ts / -# st0re-lakehouse-client.ts and is read by the real-infra test suite -# (tests/p0/*.realinfra.test.ts). The lakehouse write path was live-verified -# against ST0RE on 2026-07-13 with this set; unset, the clients fall back to -# their deterministic in-memory mocks. -ST0RE_URL= - -# --------------------------------------------------------------------------- -# P0 Data-OS backends (src/search, src/analytics, src/metadata, src/features) -# --------------------------------------------------------------------------- -# OpenSearch — search projections ONLY, never canonical truth (§42). Unset in -# dev/test = search capability unavailable (typed error), never a fake. -OPENSEARCH_URL= -# ClickHouse — 0pt capability analytics (tenant-isolated metric events). -# HAS NO CONNECTION to b0ts pipelines or quarantined market data (legal). -CLICKHOUSE_URL= -# "user:password" for ClickHouse basic auth; from secret refs only. -CLICKHOUSE_AUTH= -# OpenMetadata — metadata PROJECTION target (canonical stays in 0pt). -OPENMETADATA_URL= -OPENMETADATA_TOKEN= -# Feast feature server (online features path). -FEAST_SERVER_URL= - -# --------------------------------------------------------------------------- -# Payments (Stripe) -- wired in code today: app/api/payments/*, -# app/api/webhooks/stripe -# --------------------------------------------------------------------------- -STRIPE_SECRET_KEY=sk_test_xxx -STRIPE_WEBHOOK_SECRET=whsec_xxx -# Documented target (docs/08); not yet read by any route in this pass. -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= - -# --------------------------------------------------------------------------- -# Object storage -- Cloudflare R2 / S3-compatible -# --------------------------------------------------------------------------- -# R2_ENDPOINT / R2_BUCKET / R2_ACCESS_KEY_ID are read in code today; all three -# are required to enable /api/uploads, otherwise it returns 501. -# R2_ACCOUNT_ID and R2_SECRET_ACCESS_KEY are documented in docs/08 but were NOT -# found via a process.env.* read in this pass -- verify before assuming the R2 -# client is fully wired. -R2_ENDPOINT=https://.r2.cloudflarestorage.com -R2_BUCKET=opt-uploads -R2_ACCESS_KEY_ID=change-me -R2_ACCOUNT_ID= -R2_SECRET_ACCESS_KEY= -# Fallback local artifact store used when no object storage is configured -# (src/adapters/local-blob-store.ts). Leave unset in production. -OPT_LOCAL_BLOB_DIR=./.opt-blobs - -# --------------------------------------------------------------------------- -# Document intelligence / OCR (Azure) -- wired in code today: -# src/services/document-extraction-service.ts. Unset => extraction -# "not configured". -# --------------------------------------------------------------------------- -DOC_INTELLIGENCE_ENDPOINT=https://.cognitiveservices.azure.com -DOC_INTELLIGENCE_KEY=change-me -DOC_INTELLIGENCE_API_VERSION=2024-11-30 - -# --------------------------------------------------------------------------- -# Customer DB-grant encryption -- wired in code today: -# /api/connections/db-grant -# --------------------------------------------------------------------------- -# 32-byte key, AES-GCM. Encrypts customer-supplied read-only connection strings -# at rest. -GRANT_ENC_KEY=change-me-32-byte-key - -# --------------------------------------------------------------------------- -# External write-back targets -# --------------------------------------------------------------------------- -# Odoo base URL for the bills/write-back adapter. -ODOO_BASE_URL=https:// - -# --------------------------------------------------------------------------- -# Email -- Resend (documented target, docs/08; no Resend call site found in -# this pass -- treat as not-yet-implemented, not just unconfigured) -# --------------------------------------------------------------------------- -RESEND_API_KEY= - -# --------------------------------------------------------------------------- -# Embedded analytics (documented target, docs/08; no call site found in this -# pass) -# --------------------------------------------------------------------------- -METABASE_SITE_URL= -METABASE_SECRET_KEY= -NOCODB_BASE_URL= -NOCODB_API_TOKEN= - -# --------------------------------------------------------------------------- -# Test-only (tests/p0 real-infra suite; not needed to run the app) -# --------------------------------------------------------------------------- -# ST0RE_TEST_TENANT_ID= - -# --------------------------------------------------------------------------- -# Document workbench (upload -> DOC0 -> INF0 -> validated export -> credits) -# Wired in code today: app/api/0pt/documents/*, app/api/0pt/credits/*. -# Every value is optional to boot and mandatory to USE: a missing integration -# makes its route answer 503 "not configured" instead of fabricating success. -# --------------------------------------------------------------------------- -# DOC0 document service. URL must be absolute HTTPS; token is a service PAT. -DOC0_URL=https://doc0.siax.io -DOC0_SERVICE_TOKEN= -# The DOC0 service identity's own actor id (token subject). DOC0 rejects an -# x-actor-id it has no actor_grant for; 0pt keeps the end-user actor in its own -# journal and lets the DOC0 call act as the service. -DOC0_SERVICE_ACTOR_ID= -# Maximum accepted upload size (bytes). Default 25 MiB. -OPT_MAX_UPLOAD_BYTES=26214400 - -# Interim AI path: self-hosted Ollama on the SIAX decision server -# (server-decision-01). Internal only; no provider key and no per-token cost. -# Plain HTTP is accepted only for loopback/RFC1918/tailnet hosts. -DECISION_OLLAMA_URL=http://100.96.133.104:11434 -DECISION_OLLAMA_MODEL=hermes-qwen3.5:9b - -# INF0 AI gateway (model calls). No provider key is ever placed in this repo. -# Live path today: the LiteLLM gateway (OpenAI-compatible). The native -# /v1/inf0/* surface is contract-only; INF0_URL is used as a fallback. -INF0_GATEWAY_URL=https://ai-gateway.siax.io/v1 -INF0_GATEWAY_LITELLM_KEY= -INF0_GATEWAY_PROVIDER_KIND=external -INF0_URL=https://api.inf0.siax.io -INF0_API_TOKEN= - -# PAY0 (optional; Stripe is used directly for credit checkout today). -PAY0_URL=https://pay0.siax.io -PAY0_API_TOKEN= - -# N0D / ST0RE (used by the broader pipeline; not required by the routes above). -N0D_URL=https://api.n0d.siax.io -N0D_API_TOKEN= -ST0RE_URL=https://api.st0re.siax.io -ST0RE_API_TOKEN= - -# OpenFGA project authorization. Without all four values, document routes fail -# closed with PROJECT_AUTHORIZATION_NOT_CONFIGURED. -OPENFGA_API_URL=https://fga.siax.io -OPENFGA_STORE_ID= -OPENFGA_MODEL_ID= -OPENFGA_AUTH_TOKEN= - -# Transitional, server-owned project grants used only when OpenFGA is not -# configured. JSON array; actorId "*" means every verified actor in the tenant. -# OPT_PROJECT_GRANTS=[{"tenantId":"379976258701823235","projectId":"prj_demo","actorId":"*"}] -OPT_PROJECT_GRANTS= - -# Model policy for the document workbench. Without this, structuring answers -# MODEL_POLICY_NOT_CONFIGURED. -OPT_ALLOWED_MODELS= -OPT_ALLOWED_DATA_CLASSES=none,low -OPT_MODEL_DESTINATION=external -OPT_EXTERNAL_TRANSFER_ALLOWED=false -OPT_MODEL_MAX_INPUT_TOKENS=60000 -OPT_MODEL_MAX_OUTPUT_TOKENS=4000 - -# Server-owned, versioned credit price book (JSON, one line). No price is ever -# read from a browser. Without it, credit packs/checkout/structuring answer -# PRICE_BOOK_NOT_CONFIGURED. -# {"version":"2026-11-01","currency":"SEK","packs":[{"id":"starter","credits":100,"amountMinor":9900}],"metering":{"pagesPerCredit":5,"inputTokensPerCredit":20000,"outputTokensPerCredit":4000}} -OPT_CREDIT_PRICE_BOOK= - -# Pilot switch. `off` runs document structuring without a wallet; it is -# explicit (never a silent fallback), the response reports `credits: null`, and -# purchases stay disabled. Unset/`on` requires the price book + ledger. -OPT_CREDITS_MODE= - -# One-time trial credits granted per tenant (server-owned, idempotent, NOT a -# payment). 0 disables. With OPT_CREDITS_MODE=on this lets reserve/settle run -# in production before any customer purchase. -OPT_TRIAL_CREDITS=0 - -# Central Keycloak (SIAX Portal) -KEYCLOAK_ISSUER=https://auth.portal.siax.dev/realms/portal -KEYCLOAK_EXPECTED_AUDIENCE=account -KEYCLOAK_AUTHORIZED_PARTY=siax-portal diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index e7a6cdc..0000000 --- a/.gitattributes +++ /dev/null @@ -1,37 +0,0 @@ -# Normalize line endings for text files; keep binaries untouched. -* text=auto - -# Source / config -*.ts text eol=lf -*.tsx text eol=lf -*.js text eol=lf -*.mjs text eol=lf -*.cjs text eol=lf -*.css text eol=lf -*.json text eol=lf -*.jsonc text eol=lf -*.yaml text eol=lf -*.yml text eol=lf -*.py text eol=lf -*.md text eol=lf -*.sh text eol=lf -Dockerfile text eol=lf -*.dockerfile text eol=lf - -# Binaries / generated — never text-normalized -*.png binary -*.jpg binary -*.jpeg binary -*.gif binary -*.webp binary -*.ico binary -*.svg text eol=lf -*.woff binary -*.woff2 binary -*.ttf binary -*.otf binary -*.pyc binary -*.pyo binary - -# Lockfiles — keep exactly as generated -pnpm-lock.yaml -text diff --git a/.gitea/CODEOWNERS b/.gitea/CODEOWNERS deleted file mode 100644 index 5e5c794..0000000 --- a/.gitea/CODEOWNERS +++ /dev/null @@ -1,11 +0,0 @@ -# CI-14 — CODEOWNERS bor i .gitea/, aldrig i .github/ (SEC-25). -# Granskning krävs särskilt för kontrakt, deploy och migrationer. - -* @sax3l -/contracts/ @sax3l -/deploy/ @sax3l -/.gitea/ @sax3l -/siax.repo.json @sax3l -/siax.config.json @sax3l -# TODO: lägg till migrationskatalogen när den finns, t.ex. -# /packages/db/migrations/ @sax3l diff --git a/.gitea/workflows/cd.yml b/.gitea/workflows/cd.yml deleted file mode 100644 index cc92f11..0000000 --- a/.gitea/workflows/cd.yml +++ /dev/null @@ -1,206 +0,0 @@ -name: Deploy to Coolify - -# Follows the fleet-wide pattern used by sax3l/n0d and sax3l/st0re: push to -# main triggers a POST to Coolify's deploy endpoint for this app's UUID, -# authenticated with the COOLIFY_TOKEN repo secret. Coolify already has the -# Dockerfile (build_pack: dockerfile) -- this workflow only fires the -# rebuild+redeploy, it doesn't build or push an image itself. -# -# STATUS (verified 2026-11-02): LIVE. The Coolify application exists -# (dockerfile build pack, git.siax.io/sax3l/0pt, branch main) and its UUID is -# supplied by the repository variable COOLIFY_APP_ID; COOLIFY_TOKEN and the -# registry/cosign secrets are set. A push to main, or a manual -# workflow_dispatch of this file, has been proven to verify the signed -# attestation for the exact commit and then return HTTP 200 from Coolify. -# The placeholder guard below remains as a fail-closed guard, not as a TODO. -# Operational procedure and verification live in docs/runbook/deploy.md. -on: - push: - branches: [main] - workflow_dispatch: - -# Never let two deploys race the same Coolify app. Unlike CI we do NOT cancel an -# in-flight deploy (it may already be restarting the container); the newest -# commit still wins because Coolify builds from the branch head. -concurrency: - group: deploy-0pt-main - cancel-in-progress: false - -jobs: - deploy: - runs-on: ubuntu-latest - timeout-minutes: 20 - env: - COOLIFY_APP_ID: ${{ vars.COOLIFY_APP_ID || 'TODO-no-coolify-app-provisioned-yet' }} - IMAGE_REPO: git.cloud.siax.io/sax3l/0pt - steps: - # ----------------------------------------------------------------- - # SIGNED-ARTIFACT GATE (added 2026-09-14, same pattern as - # sax3l/cl0ud + sax3l/st0re's fix/wire-signed-digest-to-deploy; this - # job's own wait-for-signed-artifact polling logic below predates and - # is independent of the 2026-09-15 G17 migration, and is KEPT as-is - # here -- it is a real, working, 0pt-specific mechanism, not part of - # what that migration delegates to siax-templates): once the guard - # step below stops failing (i.e. once a real Coolify app exists for - # 0pt and COOLIFY_APP_ID is filled in), this job will REFUSE to call - # Coolify at all unless a verified attestation exists in the registry - # for the EXACT commit being deployed. `.gitea/workflows/release.yml` - # is the piece that builds, SBOMs, scans, attests, and pushes that - # image (rewritten 2026-09-15 to delegate to sax3l/siax-templates' - # G17 reference hook -- see `.siax/hooks/README.md`). - # - # VERIFY STEP CHANGED 2026-09-15 (part of the G17 migration): the old - # release.yml called `cosign sign` on the bare digest, so this job's - # gate used `cosign verify`. The new release.yml calls `cosign attest` - # instead -- a full DSSE-signed in-toto Statement binding digest + - # source SHA + build identity + environment + SBOM hash (ADR-W16-003 - # bar items 4/5), not just a bare-digest signature. `cosign verify` - # against a `cosign attest`-only artifact fails with "no matching - # signatures" (attestations and signatures are stored/verified - # separately in cosign's model) -- so this step now calls - # `cosign verify-attestation --type ` instead, the - # same command `.siax/hooks/G17.sh` itself already runs and requires - # to PASS before it will submit evidence to AUD0. - # - # HONEST LIMITATION (identical to cl0ud/st0re/h0st's writeup): - # `siax.config.json`'s `deploy.target: "coolify"` and this file's own - # `/api/v1/deploy` call assume a `dockerfile` build-pack Application - # once provisioned -- same as cl0ud/st0re/h0st, Coolify would clone - # this repo and run its OWN independent `docker build`, never pulling - # the image release.yml pushed and signed, and `/api/v1/deploy` has no - # digest parameter. True digest-pinning would need 0pt's eventual - # Coolify app migrated to the separate "Docker Image" resource type -- - # a production infrastructure decision that hasn't been made yet - # (0pt isn't even provisioned on Coolify at all as of this writing; - # see the guard step below and docs/runbook/deploy.md §1), not - # something this CI-YAML change can or should decide. - # - # So: what this DOES guarantee, from the day 0pt's Coolify app exists - # -- Coolify is only ever told to rebuild+redeploy a commit whose - # release-chain artifact was built, SBOM'd, signed, and had its - # signature RE-verified in THIS job. What it does NOT guarantee -- - # bit-identical bytes between the signed image and whatever Coolify's - # own rebuild produces from the same source. - # ----------------------------------------------------------------- - - name: Guard against unset token / placeholder app id - run: | - if [ -z "${{ secrets.COOLIFY_TOKEN }}" ]; then - echo "::error::COOLIFY_TOKEN secret is not set on this repo -- cannot redeploy." - exit 1 - fi - if [ "$COOLIFY_APP_ID" = "TODO-no-coolify-app-provisioned-yet" ]; then - echo "::error::No Coolify app has been provisioned for 0pt yet. Create the app in the Coolify UI (cloud.siax.io), copy its UUID into COOLIFY_APP_ID in this file, and set the COOLIFY_TOKEN repo secret before this workflow can deploy anything." - exit 1 - fi - - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Resolve this commit's release-chain tag (same scheme as release.yml) - id: sha - run: | - set -euo pipefail - short="$(git rev-parse --short=12 HEAD)" - echo "short=$short" >> "$GITHUB_OUTPUT" - echo "image_ref=${IMAGE_REPO}:sha-$short" >> "$GITHUB_OUTPUT" - - - name: Docker login (git.cloud.siax.io) — read access to check release.yml's output - env: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)" - echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin - - - name: Install cosign v3.1.3 (pinned, checksum-verified — never :latest) - run: | - set -euo pipefail - cd /tmp - curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign-linux-amd64 - curl -fsSL -O https://github.com/sigstore/cosign/releases/download/v3.1.3/cosign_checksums.txt - grep 'cosign-linux-amd64$' cosign_checksums.txt | sha256sum -c - - install -m 0755 cosign-linux-amd64 /usr/local/bin/cosign - cosign version - - - name: Wait for release.yml's signed artifact for THIS commit, then verify (fail closed) - id: verify - env: - IMAGE_REF: ${{ steps.sha.outputs.image_ref }} - SHORT: ${{ steps.sha.outputs.short }} - PREDICATE_TYPE: https://siax.io/attestations/g17-release-evidence/v1 - run: | - set -euo pipefail - test -f cosign.pub || { echo "::error::cosign.pub not found at repo root — cannot verify, refusing to deploy"; exit 1; } - - # release.yml runs as a SEPARATE workflow triggered by the same push - # (Gitea/GitHub Actions has no cross-workflow-file `needs:`), so this - # job cannot depend on that job directly — it polls the registry for - # the artifact release.yml produces instead. Bounded: a genuinely - # broken/skipped release run fails this job loudly rather than - # hanging forever. - # - # The loop retries TWO distinct not-ready states (measured live on - # run 2265dbb 2026-09-15, under the OLD bare-signature gate — the - # same race applies to attestation-attach timing today): tag not - # pushed yet (release still building) vs. tag pushed but the - # attestation not attached yet (release between push and - # `cosign attest`). - attempt=0 - max_attempts=40 - digest="" - while true; do - attempt=$((attempt + 1)) - if [ "$attempt" -ge "$max_attempts" ]; then - echo "::error::no signed image found at $IMAGE_REF after $max_attempts attempts (10 min) — release.yml has not produced a signed artifact for sha-$SHORT. Refusing to deploy an unverified rebuild." - cat /tmp/pull.log 2>/dev/null || true - cat /tmp/verify.log 2>/dev/null || true - exit 1 - fi - if ! docker pull "$IMAGE_REF" >/tmp/pull.log 2>&1; then - echo "image not yet available at $IMAGE_REF (attempt $attempt/$max_attempts) — waiting for release.yml..." - sleep 15 - continue - fi - - # Read back the digest the registry actually stored for this tag — - # same filter release.yml itself uses (RepoDigests can hold multiple - # entries; never trust index 0) — and verify THAT digest, never the - # mutable tag alone, so a tag reassigned after signing can't slip past. - repo_digest="$(docker inspect --format='{{json .RepoDigests}}' "$IMAGE_REF" \ - | node -e "const rd=JSON.parse(require('fs').readFileSync(0,'utf8')); const prefix=process.env.IMAGE_REPO+'@'; const m=rd.find(r=>r.startsWith(prefix)); if(!m){console.error('no RepoDigests entry starting with '+prefix+' among: '+JSON.stringify(rd));process.exit(1);} console.log(m);")" - digest="${repo_digest##*@}" - echo "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$' || { echo "::error::unexpected digest shape: $digest"; exit 1; } - - echo "verifying cosign attestation on ${IMAGE_REPO}@${digest} ..." - if cosign verify-attestation --key cosign.pub --type "$PREDICATE_TYPE" "${IMAGE_REPO}@${digest}" >/tmp/verify.log 2>&1; then - break - fi - if grep -qE "no matching (attestations|signatures)( found)?" /tmp/verify.log; then - echo "image present but attestation not attached yet (attempt $attempt/$max_attempts) — waiting for release.yml's cosign attest..." - sleep 15 - continue - fi - echo "::error::cosign verify-attestation FAILED for ${IMAGE_REPO}@${digest} (not a not-ready state — refusing to deploy)" - cat /tmp/verify.log - exit 1 - done - - echo "digest=$digest" >> "$GITHUB_OUTPUT" - echo "SIGNED ARTIFACT VERIFIED: ${IMAGE_REPO}@${digest} — authorizing Coolify redeploy of sha-$SHORT" - - - name: Trigger Coolify redeploy - env: - COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }} - VERIFIED_DIGEST: ${{ steps.verify.outputs.digest }} - run: | - # See the SIGNED-ARTIFACT GATE comment above: Coolify's /deploy has - # no digest/image param to pass, so $VERIFIED_DIGEST is not (and - # cannot be) sent to Coolify — it is recorded here as the - # provenance proof that authorized this specific redeploy call. - echo "authorized by verified digest: $VERIFIED_DIGEST" - curl -fsS -X POST \ - "https://cloud.siax.io/api/v1/deploy?uuid=${COOLIFY_APP_ID}&force=false" \ - -H "Authorization: Bearer $COOLIFY_TOKEN" \ - -o /dev/null -w 'coolify deploy HTTP %{http_code}\n' diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml deleted file mode 100644 index 1e31c9a..0000000 --- a/.gitea/workflows/ci.yml +++ /dev/null @@ -1,330 +0,0 @@ -name: CI (SIAX Cloud) - -# Self-hosted CI on SIAX Cloud (Gitea Actions, runners siax-runner-1/2). -# Quality gate for the Next.js 16 web console + the 0pt capability layer. -# -# * typecheck (BLOCKING) -> `tsc -p tsconfig.check.json` over src/ + tests/. -# * test (BLOCKING) -> `vitest run`. -# * lint (ADVISORY) -> `eslint src tests` (legacy v0-generated noise -# is inherited debt; continue-on-error so it never blocks). -# * build (BLOCKING) -> `next build`. -# -# * security (BLOCKING) -> `trivy fs` over the repo; fails the build on -# HIGH/CRITICAL vulns or detected secrets. This is a REQUIRED -# protected-branch context -- see the job's own comment. -# -# Nightly re-runs live in nightly.yml; security.yml is the dedicated -# push/PR/weekly scanner (this file is push/PR-only). -on: - push: - branches: ["main"] - pull_request: - workflow_dispatch: - -# One run per ref; a branch push that also updates a PR used to start two full -# quality runs (and, on main, two deploy chains). Cancel the superseded one. -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - quality: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "pnpm" - cache-dependency-path: pnpm-lock.yaml - - name: Enable corepack + pin pnpm@9 (pnpm-lock.yaml is lockfileVersion 9.0) - run: | - corepack enable - corepack prepare pnpm@9 --activate - - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile - - name: Typecheck (tsc over src/ + tests/ -- blocking) - run: pnpm run typecheck - # UI-09: 0pt carried 82 axe color-contrast violations (impact=serious), the - # estate's largest single a11y defect, and every one came from a Tailwind - # alpha modifier on a text colour. The unmodified token measures 4.73:1 - # light / 7.63:1 dark and passes AA; every faded variant fails. Blocking, - # because a contrast regression is invisible in review. - - name: Text contrast shape (WCAG 2.2 AA 1.4.3 -- blocking) - run: pnpm run check:contrast - - name: Test (vitest -- blocking) - run: pnpm run test - - name: Lint (eslint -- advisory; legacy tree has pre-existing noise) - continue-on-error: true - run: pnpm run lint - - name: Build (next build -- blocking quality gate) - run: pnpm run build - - # REAL-DEPENDENCY integration tests (Master-DoD §48: "integration tests ... - # Mocks alone count as insufficient"; §10 Search/Analytics). Runs - # tests/integration/* against a LIVE OpenSearch + LIVE ClickHouse started as - # service containers. - # - # SERVICE-CONTAINER DISCIPLINE is copied from sax3l/st0re's ci.yml, which - # documents three measured defects on these runners (alias collision across - # repos, sibling-run collision behind one alias, and DNS round-robin): - # * repo-unique aliases (opt-ci-opensearch / opt-ci-clickhouse), - # * SELF-IDENTIFYING containers — OpenSearch carries a run-unique - # cluster.name; ClickHouse carries run-unique credentials — so the - # select step can positively identify THIS run's container, - # * enumerate every IP behind the alias (getent ahostsv4), keep the one - # that proves the run identity, freeze it into the URL env var, - # * no match = hard failure, never a silent fallback or skip. - integration: - # Gitea currently ignores workflow concurrency. A branch push followed by - # its PR synchronize event otherwise starts TWO 512 MiB OpenSearch JVMs - # and ClickHouse suites on the same runner fleet at once; both timed out - # creating their first index (runs 28904/28906). Keep the blocking LIVE - # integration proof on every PR, on main push, and on manual dispatch; - # ordinary branch pushes still run blocking quality/security. - if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest - timeout-minutes: 45 - services: - opt-ci-opensearch: - image: opensearchproject/opensearch:2.11.1 - env: - discovery.type: single-node - DISABLE_SECURITY_PLUGIN: "true" - OPENSEARCH_JAVA_OPTS: "-Xms512m -Xmx512m" - # Identity badge: only THIS run's container answers with this - # cluster_name (the OpenSearch image maps dotted env keys to - # settings). - cluster.name: opt_ci_${{ github.run_id }}_${{ github.run_attempt }} - # Throwaway CI cluster: never let the runner's disk fill turn indices - # read-only (the 403 index_create_block_exception seen in run 388536). - cluster.routing.allocation.disk.threshold_enabled: "false" - # `--workdir` is REQUIRED (measured 2026-08-31, PR #25 CI run 7745, - # job "integration"): this runner's act_runner (v0.6.1) merges the - # JOB's WorkingDir (`/workspace/sax3l/0pt`) into every SERVICE - # container's config, clobbering the image's own `WORKDIR - # /usr/share/opensearch`. OpenSearch's ENTRYPOINT is the - # WORKDIR-relative `./opensearch-docker-entrypoint.sh`, so with the - # wrong cwd the container failed with "exec: - # \"./opensearch-docker-entrypoint.sh\": stat ...: no such file or - # directory" before it ever bound a port. ClickHouse's entrypoint is - # an absolute path, so it was unaffected by the same defect — do not - # assume a green ClickHouse service means service-container options - # are fine in general. - options: >- - --workdir /usr/share/opensearch - --health-cmd "curl -fsS http://localhost:9200/_cluster/health || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 60 - opt-ci-clickhouse: - image: clickhouse/clickhouse-server:24.12 - env: - CLICKHOUSE_USER: opt_ci - # Identity badge: run-unique password — an authenticated SELECT only - # succeeds against THIS run's container. CI-scoped throwaway value, - # not a secret (exists only for the lifetime of the job). - CLICKHOUSE_PASSWORD: ci_${{ github.run_id }}_${{ github.run_attempt }} - CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: "1" - options: >- - --health-cmd "wget -qO- http://localhost:8123/ping || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 60 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "pnpm" - cache-dependency-path: pnpm-lock.yaml - - name: Enable corepack + pin pnpm@9 - run: | - corepack enable - corepack prepare pnpm@9 --activate - - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile - - name: Select THIS run's OpenSearch by identity (st0re ci.yml pattern) - run: | - set -eu - WANT="opt_ci_${{ github.run_id }}_${{ github.run_attempt }}" - FOUND="" - for ip in $(getent ahostsv4 opt-ci-opensearch | awk '{print $1}' | sort -u); do - NAME=$(curl -fsS --max-time 5 "http://$ip:9200/" | sed -n 's/.*"cluster_name" *: *"\([^"]*\)".*/\1/p' || true) - echo "candidate $ip cluster_name=${NAME:-}" - if [ "$NAME" = "$WANT" ]; then FOUND="$ip"; fi - done - [ -n "$FOUND" ] || { echo "FATAL: no OpenSearch behind the alias identifies as $WANT"; exit 1; } - - # The single-node test cluster inherits the runner's disk watermark. - # On a busy runner the flood-stage watermark makes every new index - # read-only and the suite fails with HTTP 403 - # index_create_block_exception (measured on run 388536). The container - # is throwaway CI state, so disable the disk threshold and PROVE that - # index creation works before the blocking tests run. - for attempt in 1 2 3 4 5; do - # Disabling the watermark alone does NOT clear a cluster that is - # already read-only: the flood stage sets - # cluster.blocks.read_only_allow_delete and every write keeps - # returning 403. Clear that block explicitly every attempt. - curl -sS --max-time 10 -X PUT "http://$FOUND:9200/_cluster/settings" \ - -H 'Content-Type: application/json' \ - -d '{"persistent":{"cluster.routing.allocation.disk.threshold_enabled":false,"cluster.blocks.read_only_allow_delete":null}}' >/tmp/os-settings.json || true - cat /tmp/os-settings.json 2>/dev/null || true - probe_status=$(curl -sS --max-time 10 -o /tmp/os-probe.json -w '%{http_code}' -X PUT "http://$FOUND:9200/opt-ci-probe-${{ github.run_id }}") - if [ "$probe_status" = "200" ]; then - curl -fsS --max-time 10 -X DELETE "http://$FOUND:9200/opt-ci-probe-${{ github.run_id }}" >/dev/null || true - echo "OpenSearch accepts index creation" - break - fi - echo "index creation not ready (attempt $attempt/5, HTTP $probe_status) — waiting" - cat /tmp/os-probe.json 2>/dev/null || true - [ "$attempt" = "5" ] && { echo "FATAL: OpenSearch still refuses index creation"; exit 1; } - sleep 10 - done - echo "OPENSEARCH_URL=http://$FOUND:9200" >> "$GITHUB_ENV" - - name: Select THIS run's ClickHouse by identity - run: | - set -eu - PASS="ci_${{ github.run_id }}_${{ github.run_attempt }}" - FOUND="" - for ip in $(getent ahostsv4 opt-ci-clickhouse | awk '{print $1}' | sort -u); do - OK=$(curl -fsS --max-time 5 -u "opt_ci:$PASS" "http://$ip:8123/?query=SELECT%201" || true) - echo "candidate $ip auth_select=${OK:-}" - if [ "$OK" = "1" ]; then FOUND="$ip"; fi - done - [ -n "$FOUND" ] || { echo "FATAL: no ClickHouse behind the alias accepts this run's credentials"; exit 1; } - echo "CLICKHOUSE_URL=http://$FOUND:8123" >> "$GITHUB_ENV" - echo "CLICKHOUSE_AUTH=opt_ci:$PASS" >> "$GITHUB_ENV" - # The anti-skip guard here used to grep vitest's HUMAN output: - # - # grep -qE "skipped" && ! grep -qE "Tests [0-9]+ passed" - # - # vitest colours that summary, so the real line on this runner is - # ESC[2m Tests ESC[22m ESC[1mESC[32m97 passedESC[39m ... — ANSI escapes - # sit between "Tests " and the digits, so the two-spaces-then-digit - # pattern can never match. The first grep still matched "skipped", so - # the guard fired on ANY run containing a single skipped test while - # printing "integration suites skipped instead of running" — a false - # statement. Measured 2026-09-08, run 13911 job 31183: 13 files / 97 - # tests PASSED, 1 file / 2 tests skipped → FATAL anyway. - # - # It survived review because it is correct locally: redirecting to a - # file disables vitest's colour, so the pattern matches on a developer - # machine and only misfires under CI. - # - # Replaced with the JSON reporter — structured and colourless. A/B - # measured on real reports, BOTH directions: - # all-skipped (0 passed / 99 pending) → RED (protection preserved) - # 945 passed + 184 skipped → GREEN (false red removed) - - name: Integration tests against LIVE OpenSearch + ClickHouse (blocking) - run: | - set -eu - # Env MUST be present here — the suites self-skip without it, and a - # skipped suite must never masquerade as a green integration gate - # (Master-DoD §56 status honesty). - [ -n "${OPENSEARCH_URL:-}" ] && [ -n "${CLICKHOUSE_URL:-}" ] - pnpm vitest run tests/integration \ - --reporter=default \ - --reporter=json --outputFile.json=/tmp/itest.json - node -e ' - const fs = require("fs"); - const p = "/tmp/itest.json"; - if (!fs.existsSync(p)) { - console.error("FATAL: vitest produced no JSON report"); process.exit(1); - } - const r = JSON.parse(fs.readFileSync(p, "utf8")); - console.log(`integration: ${r.numPassedTests} passed, ${r.numPendingTests} skipped, ${r.numFailedTests} failed, ${r.numTotalTests} total`); - if (r.numFailedTests > 0) { - console.error("FATAL: integration tests failed"); process.exit(1); - } - if (r.numPassedTests === 0) { - console.error("FATAL: no integration test actually executed — every suite skipped"); - process.exit(1); - } - ' - - # BLOCKING. This job publishes the protected-branch status check context - # "CI (SIAX Cloud) / security (push)", which branch protection on `main` - # lists as required alongside "CI (SIAX Cloud) / quality (push)". - # - # It previously ran Trivy with `--exit-code 0`, which made the REQUIRED - # context incapable of ever reporting failure. Measured 2026-09-08 on - # main @4514d97: a planted CRITICAL secret (AWS secret access key) was - # DETECTED by the scanner and the job still exited 0 -- i.e. the gate - # reported success while holding a critical finding in hand. The same - # command with `--exit-code 1` exits 1 on that planted secret and exits 0 - # on the clean tree, so this flag is the whole difference between a real - # gate and a decorative one. - # - # KNOWN COVERAGE GAP (not fixed here, needs an owner decision): - # services/scrapers/requirements.txt pins nothing (`>=` ranges only), so - # Trivy's pip scanner cannot resolve versions and silently omits the file - # from the scan entirely -- it is not a target in the report at all. - # Pinning those same floors with `==` surfaces 3 HIGH findings today - # (aiohttp CVE-2025-69223, aiohttp CVE-2026-69244, lxml CVE-2026-41066). - # Do NOT paper over this with `--skip-dirs`; that would convert a - # measurable blind spot into declared policy. - security: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - name: Install Trivy - run: | - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - # `siax/sbom/` är CycloneDX-evidenens för REDAN BYGGDA images (release-kedjan), - # inte källberoenden. Att scanna dem här återrapporterar gamla image-versioner - # (next 16.3.x, sharp 0.35.4) och blockerar källfixar som redan är patchade. - # Image-SBOM:erna verifieras i release/image-pipelinen. - - name: Trivy scan (vuln + secrets -- BLOCKING, fails on HIGH/CRITICAL) - run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 --skip-dirs 'siax/sbom' . - - sonar: - # SonarQube Community Build har inget separat PR-analysläge (en enda "main"-gren - # per projekt) — en analys från en PR-branch eller en tagg-push som inte pekar på - # main skulle skriva över kvalitetshistoriken. Kör därför bara - # vid push till main. - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: SonarQube analysis (self-hosted sonar.siax.dev) - env: - SONAR_HOST_URL: https://sonar.siax.dev - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: | - set -euo pipefail - test -n "$SONAR_TOKEN" || { echo "::error::SONAR_TOKEN repo secret missing"; exit 1; } - # Jobbet kör i en container på act_runner; "docker run -v \"\$PWD:...\"" - # skulle montera VÄRDENS (tomma) sökväg. Kopiera in källträdet istället. - cid=$(docker create -e SONAR_HOST_URL -e SONAR_TOKEN -w /usr/src \ - sonarsource/sonar-scanner-cli:latest \ - sonar-scanner -Dsonar.projectKey=0pt -Dsonar.host.url="$SONAR_HOST_URL") - trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT - docker cp . "$cid":/usr/src - docker start -a "$cid" - - design-conformance: - name: Component conformance (blocking) - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '22' - - name: Checkout canonical siax-standard (SSOT) - uses: actions/checkout@v4 - with: - repository: admin/siax-standard - token: ${{ secrets.SIAX_CI_TOKEN }} - path: siax-standard-ssot - - name: Component conformance (blocking) - run: | - node --input-type=module -e "import('./siax-standard-ssot/packages/doctor/src/commands/component-conformance.mjs').then(m=>{process.exit(m.run(['--repo', process.env.GITHUB_WORKSPACE, '--allow-path-prefix', 'siax-standard-ssot/', '--fail-on=block']))})" diff --git a/.gitea/workflows/health-check.yml b/.gitea/workflows/health-check.yml deleted file mode 100644 index 90aece5..0000000 --- a/.gitea/workflows/health-check.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Health Check (SIAX Cloud) -on: - workflow_dispatch: - schedule: - - cron: "0 6 * * 1" -jobs: - health: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Required files - run: | - for f in README.md LICENSE .gitignore; do [ -f "$f" ] || echo "::warning file=$f,title=Missing::$f is missing"; done - [ -f .gitea/workflows/ci.yml ] || echo "::warning title=No CI::No CI workflow found" - - name: Install Trivy - run: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - - name: Secret scan - run: trivy fs --scanners secret --exit-code 0 . \ No newline at end of file diff --git a/.gitea/workflows/masterplan-lock.yml b/.gitea/workflows/masterplan-lock.yml deleted file mode 100644 index 2a8d813..0000000 --- a/.gitea/workflows/masterplan-lock.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: masterplan-lock - -# Fails the build if this repo's copy of the SIAX Non-Platform masterplan -# (PLAN/MASTERPLAN_INDEX.md) is deleted or its locked architecture -# principles are stripped out. Dependency-free (POSIX sh) so it needs no -# language toolchain. See scripts/check-masterplan-lock.sh and -# sax3l/siax-masterplan. - -on: - push: - branches: ["**"] - pull_request: - workflow_dispatch: - -jobs: - check: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v4 - - name: Masterplan lock check - run: sh scripts/check-masterplan-lock.sh \ No newline at end of file diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml deleted file mode 100644 index 9492ffd..0000000 --- a/.gitea/workflows/nightly.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Nightly (SIAX Cloud) - -# Nightly full-gate re-run so CI exercises the repo even without pushes: -# quality gate + advisory security scan. Fails loudly on regressions -# introduced outside push-triggered runs (e.g. dependency drift). -on: - workflow_dispatch: - schedule: - - cron: "0 4 * * *" # daily 04:00 UTC - -concurrency: - group: nightly-${{ github.ref }} - cancel-in-progress: true - -jobs: - quality: - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - cache: "pnpm" - cache-dependency-path: pnpm-lock.yaml - - name: Enable corepack + pin pnpm@9 - run: | - corepack enable - corepack prepare pnpm@9 --activate - - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile - - name: Typecheck (blocking) - run: pnpm run typecheck - - name: Test (blocking) - run: pnpm run test - - name: Lint (advisory) - continue-on-error: true - run: pnpm run lint - - name: Build (blocking) - run: pnpm run build - - security: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - uses: actions/checkout@v4 - - name: Install Trivy - run: | - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - # A nightly scan exists to catch CVEs published against code that did - # not change. With `--exit-code 0` it could never report failure, so it - # could never raise that alarm -- the one job whose entire purpose is - # detection was the one guaranteed to stay green. Fixed 2026-09-08. - - name: Trivy fs scan (vuln + secrets -- fails on HIGH/CRITICAL) - run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 --skip-dirs 'siax/sbom' . diff --git a/.gitea/workflows/prod-smoke.yml b/.gitea/workflows/prod-smoke.yml deleted file mode 100644 index dd33821..0000000 --- a/.gitea/workflows/prod-smoke.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Production smoke (0pt.app) - -# Automated end-to-end delivery verification. Every 30 minutes (and on demand) -# this checks that the live service is actually ready and that the running -# container is the commit that main points at — so a green deploy is never -# confused with a healthy, current service. -on: - workflow_dispatch: - schedule: - - cron: "*/30 * * * *" - -concurrency: - group: prod-smoke - cancel-in-progress: true - -jobs: - smoke: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Readiness (retry on a fresh deploy) - run: | - set -eu - for attempt in 1 2 3 4 5 6; do - body="$(curl -fsS --max-time 15 https://0pt.app/api/ready || true)" - echo "attempt $attempt: ${body:-}" - if printf '%s' "$body" | grep -q '"ready":true'; then - exit 0 - fi - sleep 20 - done - echo "::error::0pt.app/api/ready never reported ready:true" - exit 1 - - - name: Health envelope - run: | - set -eu - body="$(curl -fsS --max-time 15 https://0pt.app/api/health)" - echo "$body" - printf '%s' "$body" | grep -q '"db":"ok"' || { echo "::error::database is not ok"; exit 1; } - printf '%s' "$body" | grep -q '"stripe":"ok"' || { echo "::error::stripe is not ok"; exit 1; } - - - name: Deployed commit equals main - env: - SIAX_CI_TOKEN: ${{ secrets.SIAX_CI_TOKEN }} - run: | - set -eu - version="$(curl -fsS --max-time 15 https://0pt.app/api/version)" - echo "$version" - running="$(printf '%s' "$version" | sed -n 's/.*"commit":"\([0-9a-f]*\)".*/\1/p')" - [ -n "$running" ] || { echo "::error::/api/version returned no commit"; exit 1; } - if [ -z "${SIAX_CI_TOKEN:-}" ]; then - echo "::warning::SIAX_CI_TOKEN is not set — cannot compare with main; commit $running is live" - exit 0 - fi - expected="$(curl -fsS --max-time 15 \ - -H "Authorization: token ${SIAX_CI_TOKEN}" \ - "https://git.siax.io/api/v1/repos/admin/0pt/commits?limit=1" \ - | python3 -c 'import sys,json; print(json.load(sys.stdin)[0]["sha"])')" - echo "running=$running expected(main)=$expected" - [ -n "$expected" ] || { echo "::error::could not read main SHA from Gitea"; exit 1; } - [ "$running" = "$expected" ] || { - echo "::error::production is $running but main is $expected — deploy did not land" - exit 1 - } - - - name: Document pipeline surface responds (auth required, never 404/5xx) - run: | - set -eu - for path in /api/0pt/documents/upload /api/0pt/documents/structure /api/0pt/documents/models /api/0pt/credits/balance; do - code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "https://0pt.app${path}")" - echo "$path -> $code" - case "$code" in - 401|405) ;; # unauthenticated or method guard: the route exists - *) echo "::error::$path returned $code (expected 401/405)"; exit 1 ;; - esac - done diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml deleted file mode 100644 index 7a9a903..0000000 --- a/.gitea/workflows/release.yml +++ /dev/null @@ -1,184 +0,0 @@ -name: Release chain (G17, ADR-W16-003) - -# Builds + pushes the 0pt image, then hands off to the shared -# sax3l/siax-templates G17 reference hook for every ADR-W16-003 bar item -# 1-7 (pinned-tool install, SBOM, blocking vuln scan, in-toto attestation, -# scoped-key signing, pubkey verification, AUD0/ST0RE evidence storage). -# -# REPLACES this repo's own ad hoc "P007 pattern" release chain (added -# 2026-09-14). An audit (2026-09-15) found that chain failed most of the -# real promotion bar it claimed to meet: syft/cosign installed via a `curl` -# + freshly-refetched checksums file (not a pinned, out-of-band-verified -# digest -- see siax-templates' `lib/pinned-tools.sh` header for exactly -# why that is not a verified digest), NO vulnerability scan of the built -# digest at all, `cosign sign` on the bare digest only (no in-toto/SLSA -# statement, no source-SHA/build-identity/environment/SBOM-hash bound -# together, no scoped key identity), and evidence committed straight back -# into this same mutable repo (`siax/production-evidence/*.json`, -# `siax/source-bundles/*.json` -- nine such commits already existed in this -# repo's history before this change; deleted in this same PR, superseded by -# the real AUD0 write below). This file keeps ONLY what is genuinely -# 0pt-specific (image name, tag scheme, registry push, Dockerfile) and -# delegates everything else to `.siax/hooks/G17.sh` -> the shared reference -# implementation. See `.siax/hooks/README.md` for the pin status and -# required secrets (none of which were provisioned for the old gate either -# -- see that file's own note). -# -# DELIBERATELY ONE JOB. Gitea 1.26 on this fleet resolves org-level -# secrets (SIAX_CI_TOKEN below is org-level) only for the FIRST job of a -# workflow file -- learned the hard way on sax3l/h0st's and sax3l/aud0's -# own G17 migrations. 0pt does not wire a G20 post-deploy step in this -# pass at all (see `.siax/hooks/README.md` "G20 -- deliberately not wired -# yet": the app is now provisioned and `/api/version` exists, but G20 is still -# not wired in this pass), so the single-job constraint that forces G20 into its own -# workflow file elsewhere does not yet bite here -- documented anyway so a -# future PR adding G20 does not repeat the mistake. - -on: - push: - branches: [main] - workflow_dispatch: - - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: false - -env: - IMAGE_REPO: git.cloud.siax.io/sax3l/0pt - DOCKERFILE: Dockerfile - SIAX_TEMPLATES_REPO: sax3l/siax-templates - # PIN STATUS 2026-09-15: sax3l/siax-templates PR #51 - # (feat/g17-signed-deploy-gate-adr-w16-003) MERGED to main, plus three - # real follow-up fixes on top -- #56/#57 (SPDX compound-license- - # expression evaluation, exercised live by this repo's own - # G17_LICENSE_ALLOW policy in .siax/hooks/G17.sh) and #59 (G20 retry - # logic -- not used by 0pt yet, but the pin picks up main's current tip - # rather than an older commit, matching what a fresh migration reviews). - # - # Re-pin 2026-09-25 (medveten, granskad): 9a3a0aa -> 09504e3a. Deltaet ar - # EN merge, siax-templates #138 "Upload complete release evidence and - # validate artifact receipt". Enda berorda filer under ci/v2 ar - # reference-hooks/{G17.sh, G17-ADR-W16-003.md, lib/emit-release-evidence.py, - # lib/test_emit_release_evidence.py}. Andringen ar bakatkompatibel by - # design: ST0RE-uppladdningen ar OPT-IN via G17_REQUIRE_ST0RE (default 0), - # sa en AUD0-only-konsument som 0pt kor exakt samma vag som forut. - # - # Deliberately NO branch name here: the pinned commit is checked out - # DIRECTLY (see the step below). The previous shape -- clone `main`, then - # require its HEAD to equal this pin -- turned every unrelated merge in - # siax-templates into a red release chain here (CD-03 / CI-RED-02) while - # nothing was actually unsafe. Checking the pin out directly keeps the pin - # a real supply-chain control (the gate runs exactly this reviewed commit, - # or it fails) without coupling it to someone else's branch tip. - SIAX_TEMPLATES_PINNED_SHA: 09504e3a593ee8ce0eaba606df31c349a3d52277 - SIAX_TEMPLATES_DIR: /tmp/siax-templates-g17-ref - -jobs: - release: - name: Build, push, G17 (SBOM/scan/attest/verify/evidence) - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: https://git.siax.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4, mirrored + SHA-pinned (SUP-06, same pin n0d/cl0ud/st0re/h0st already use) - with: - fetch-depth: 0 - - # --- fetch the shared G17 reference implementation ----------------- - # Same authenticated-fetch-at-runtime mechanism this repo's OLD - # release.yml already used SIAX_CI_TOKEN for (the evidence-commit - # push a prior PR deleted) -- see .siax/hooks/G17.sh's header for why - # NOT a `uses:` reusable workflow / submodule / vendored copy. Fails - # closed if the exact commit this adapter was reviewed against cannot - # be checked out (removed or rewritten upstream); an unrelated merge - # on siax-templates' own main is NOT a failure and never was a real - # signal. - - name: Fetch siax-templates G17 reference (pinned commit) - env: - SIAX_CI_TOKEN: ${{ secrets.SIAX_CI_TOKEN }} - run: | - set -euo pipefail - rm -rf "$SIAX_TEMPLATES_DIR" - mkdir -p "$SIAX_TEMPLATES_DIR" - git -C "$SIAX_TEMPLATES_DIR" init --quiet - git -C "$SIAX_TEMPLATES_DIR" remote add origin \ - "https://oauth2:${SIAX_CI_TOKEN}@git.siax.io/${SIAX_TEMPLATES_REPO}.git" - # Ask for the pinned commit by SHA -- one shallow commit, no branch - # involved. A server with uploadpack.allowReachableSHA1InWant off - # refuses that, so fall back to fetching the branches and taking the - # same commit out of them. Either way the work tree ends up at - # exactly $SIAX_TEMPLATES_PINNED_SHA, never at a branch tip. - if ! git -C "$SIAX_TEMPLATES_DIR" fetch --quiet --depth 1 origin "$SIAX_TEMPLATES_PINNED_SHA"; then - echo "note: server refused a by-SHA fetch; falling back to fetching all branches" - git -C "$SIAX_TEMPLATES_DIR" fetch --quiet origin - fi - if ! git -C "$SIAX_TEMPLATES_DIR" -c advice.detachedHead=false \ - checkout --quiet --detach "$SIAX_TEMPLATES_PINNED_SHA"; then - echo "::error::siax-templates commit $SIAX_TEMPLATES_PINNED_SHA not found in $SIAX_TEMPLATES_REPO -- the reviewed commit was removed or rewritten upstream. Re-review siax-templates and update SIAX_TEMPLATES_PINNED_SHA deliberately." - exit 1 - fi - actual_sha="$(git -C "$SIAX_TEMPLATES_DIR" rev-parse HEAD)" - if [ "$actual_sha" != "$SIAX_TEMPLATES_PINNED_SHA" ]; then - echo "::error::checked-out siax-templates HEAD is $actual_sha, expected the pinned $SIAX_TEMPLATES_PINNED_SHA -- refusing to run an unreviewed version of the gate." - exit 1 - fi - chmod +x "$SIAX_TEMPLATES_DIR/ci/v2/reference-hooks/"*.sh "$SIAX_TEMPLATES_DIR/ci/v2/reference-hooks/lib/"*.sh - echo "siax-templates G17 reference @ $actual_sha (pinned commit, checked out directly)" - - - name: Resolve SHA + tag (never :latest) - id: sha - run: | - set -euo pipefail - full="$(git rev-parse HEAD)" - short="$(git rev-parse --short=12 HEAD)" - echo "full=$full" >> "$GITHUB_OUTPUT" - echo "short=$short" >> "$GITHUB_OUTPUT" - echo "image_tag=sha-$short" >> "$GITHUB_OUTPUT" - - - name: Docker login (git.cloud.siax.io) - env: - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }} - run: | - set -euo pipefail - registry_host="$(echo "$IMAGE_REPO" | cut -d/ -f1)" - echo "$REGISTRY_TOKEN" | docker login "$registry_host" -u "$REGISTRY_USER" --password-stdin - - - name: Build - env: - IMAGE_TAG: ${{ steps.sha.outputs.image_tag }} - run: | - set -euo pipefail - docker build -f "$DOCKERFILE" -t "${IMAGE_REPO}:${IMAGE_TAG}" . - - - name: Push (immutable sha-<12> tag, never :latest) - env: - IMAGE_TAG: ${{ steps.sha.outputs.image_tag }} - run: docker push "${IMAGE_REPO}:${IMAGE_TAG}" - - # --- G17: everything else, delegated ------------------------------- - - name: G17 (bar items 1-7, delegated to siax-templates) - env: - ARTIFACT_COMMIT_SHA: ${{ steps.sha.outputs.full }} - ARTIFACT_IMAGE_TAG: ${{ steps.sha.outputs.image_tag }} - CI_RUN_ID: ${{ github.run_id }} - CI_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - G17_COSIGN_KEY: /tmp/0pt-g17-cosign.key - G17_COSIGN_KEY_PASSWORD: ${{ secrets.COSIGN_PASSWORD }} - AUD0_INGEST_TOKEN: ${{ secrets.AUD0_INGEST_TOKEN }} - AUD0_TENANT_ID: ${{ secrets.AUD0_TENANT_ID }} - run: | - set -euo pipefail - echo "${{ secrets.COSIGN_PRIVATE_KEY }}" > "$G17_COSIGN_KEY" - trap 'rm -f "$G17_COSIGN_KEY"' EXIT - bash .siax/hooks/G17.sh - - # NOTE: deliberately NO `actions/upload-artifact@v4` step here. - # sax3l/cl0ud's release.yml (run 23340, 2026-09-14) documents that - # this Gitea instance's act_runner does not reliably support - # upload-artifact@v4 even with continue-on-error: true, and that it - # can abort the whole job before later steps run. G17.sh's own AUD0 - # submission (bar item 7, step above) is already the durable evidence - # record; a convenience local copy is not worth re-triggering that - # known breakage. - diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml deleted file mode 100644 index c5ce869..0000000 --- a/.gitea/workflows/security.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Security Scan (SIAX Cloud) - -# Security scanning on every push/PR + weekly re-run. -# -# This job runs the same Trivy invocation as ci.yml's `security` job and -# previously carried the same defect: `--exit-code 0` meant it could report -# success while holding a CRITICAL finding (measured 2026-09-08 with a -# planted AWS secret key -- detected, still exit 0). Its context -# "Security Scan (SIAX Cloud) / security" is NOT in branch protection's -# required list, so this one never gated a merge either way; it is fixed -# here so a green tick on this workflow means what a reader assumes it -# means. See ci.yml's `security` job for the measurement and for the -# services/scrapers unpinned-requirements coverage gap. -# -# NOTE: findings on services/scrapers/* (FlareSolverr/Tor/curl_cffi) are -# expected and out of scope for CI gating -- kept per the account owner's -# dated, attributed decision (Simon Axelsson, 2026-08-11, interactive -# prompt; no rationale beyond "keep it" given or invented). See SECURITY.md -# and docs/compliance/GOLDEN-STANDARD-REPORT.md control C6 for full context. -on: - push: - branches: ["main"] - pull_request: - workflow_dispatch: - schedule: - - cron: "0 5 * * 1" # weekly Monday re-scan - - -concurrency: - group: security-${{ github.ref }} - cancel-in-progress: true -jobs: - security: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - - name: Install Trivy - run: | - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin - - name: Trivy fs scan (vuln + secrets -- fails on HIGH/CRITICAL) - run: | - trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 --skip-dirs 'siax/sbom' . diff --git a/.gitea/workflows/vehiq-oltp-ingest.yml b/.gitea/workflows/vehiq-oltp-ingest.yml deleted file mode 100644 index 3ddfe45..0000000 --- a/.gitea/workflows/vehiq-oltp-ingest.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: VehIQ OLTP extractor (0pt ingestion) - -# P070 — scheduled poll of app.dealer_stock_entry/app.deal (VehIQ, server10) -# into opt_dataset_projection (ClickHouse), via the watermark extractor in -# src/ingestion/vehiq-oltp-ingest-runner.ts. See docs/P070_VEHIQ_OLTP_ -# EXTRACTOR.md for the full design and "what activating this requires". -# -# SAFE BY DEFAULT: VEHIQ_INGEST_MODE defaults to "preview" (read-only, never -# commits a checkpoint, never writes) below. With no VEHIQ_SOURCE_DATABASE_URL -# secret configured on this repo, the script logs "not_configured" and exits -# 0 — this workflow can be left enabled indefinitely with zero effect on -# anything until BOTH the secret is added AND VEHIQ_INGEST_MODE is changed -# to "run". Neither is done by this workflow file itself. -on: - workflow_dispatch: - schedule: - - cron: "*/15 * * * *" - -jobs: - extract: - runs-on: ubuntu-latest - env: - VEHIQ_SOURCE_DATABASE_URL: ${{ secrets.VEHIQ_SOURCE_DATABASE_URL }} - DATABASE_URL: ${{ secrets.DATABASE_URL }} - OPT_ANALYTICS_CLICKHOUSE_BASE_URL: ${{ secrets.OPT_ANALYTICS_CLICKHOUSE_BASE_URL }} - OPT_ANALYTICS_CLICKHOUSE_BASIC_AUTH: ${{ secrets.OPT_ANALYTICS_CLICKHOUSE_BASIC_AUTH }} - OPT_ANALYTICS_CLICKHOUSE_DATABASE: opt_analytics - # Activation gate — see docs/P070_VEHIQ_OLTP_EXTRACTOR.md. Flip to - # "run" only once steps 1-4 in that doc's "What activating this - # requires" section are actually done. - VEHIQ_INGEST_MODE: preview - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: "22" - - name: Enable corepack + pin pnpm@9 - run: | - corepack enable - corepack prepare pnpm@9 --activate - - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile - - name: Run the extractor (preview mode unless activated) - run: pnpm run vehiq:oltp-ingest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 73a27fd..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: CI - -# Quality gate: typecheck + test on every PR and non-main push. -# 0pt previously relied solely on the Vercel build with no pre-merge gate. -# Build is intentionally NOT run here because it needs runtime secrets -# (POSTGRES_URL, ENGINE_URL, STRIPE, RESEND) — Vercel owns the build; CI owns -# the static gate. -# -# Updated by the GitHub<->Gitea unification merge (2026-07-28). The note that -# used to sit here — "lint is not a step because this repo has no eslint -# dependency or config wired" — is no longer true: the Gitea line's Wave 2 -# hardening added eslint + typescript-eslint + a flat config and repointed the -# "lint" script at `eslint src tests`. Lint now runs, ADVISORY -# (continue-on-error), matching .gitea/workflows/ci.yml: the v0-generated -# legacy tree carries pre-existing lint noise, so it must not block merges. -# -# `typecheck` (BLOCKING) is tsc over tsconfig.check.json = src/ + tests/. -# `typecheck:app` (ADVISORY) is the whole-repo tsc this workflow used to run as -# its blocking step. It was green on the GitHub line alone but reports 9 -# pre-existing errors on the unified tree (app/api/0pt/* route handlers, where -# the Wave 2 Zod schemas type fields as plain `string` while the services want -# narrower unions). It is kept running, and kept visible, rather than deleted — -# see package.json's "//scripts" note. Flip it back to blocking once those 9 -# are cleared. -# -# Python engine (services/api) is gated separately if/when a Python workflow -# is added. -on: - pull_request: - push: - branches-ignore: [main] - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - gate: - name: Typecheck + test - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: '22' - - name: Enable corepack (pnpm) - run: corepack enable - - name: Install (frozen lockfile) - run: pnpm install --frozen-lockfile - - name: Typecheck (tsc over src/ + tests/ -- blocking) - run: pnpm run typecheck - - name: Test (vitest -- blocking) - run: pnpm run test - - name: Typecheck whole repo (advisory -- 9 pre-existing app/ errors) - continue-on-error: true - run: pnpm run typecheck:app - - name: Lint (eslint -- advisory; legacy tree has pre-existing noise) - continue-on-error: true - run: pnpm run lint diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 85cc7b3..0000000 --- a/.gitignore +++ /dev/null @@ -1,57 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules - -# next.js -/.next/ -/out/ - -# production -/build - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files -.env* -!.env.example - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts -# pnpm local store (created when store-dir defaults into the project) -.pnpm-store/ - -# interim LocalBlobStore output (src/adapters/local-blob-store.ts) -.data/ - -# local blob storage (document artifacts when no object storage configured) -.opt-blobs/ - -# python (engine + scrapers) -- runtime artifacts, never source -__pycache__/ -*.pyc -*.pyo -.venv/ -.pytest_cache/ - -# analytics / database files -*.duckdb -*.duckdb.wal - -# scraper run output (local dumps written by services/scrapers/*.py, never source) -services/scrapers/output/ - -# local-only environment overrides (all real values live in Infisical) -.env.local -.env.production - -# parallel git worktrees (policy: hidden inside the repo, never committed) -/.worktrees/ diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 8e42c68..0000000 --- a/.npmrc +++ /dev/null @@ -1,8 +0,0 @@ -verify-deps-before-run=false -include-workspace-root=true -confirmModulesPurge=false - -# SIAX Cloud publishes @siax/* packages from Gitea's own npm registry. -# Only scoped @siax/* names resolve here; everything else stays on the -# public npm registry. -@siax:registry=https://git.siax.io/api/packages/sax3l/npm/ diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 5c6b74a..0000000 --- a/.prettierignore +++ /dev/null @@ -1,11 +0,0 @@ -# Build output and local worktrees are not source. -.next/ -out/ -build/ -dist/ -node_modules/ -.worktrees/ -coverage/ -*.min.js -*.min.css -pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json deleted file mode 100644 index f423e70..0000000 --- a/.prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "semi": true, - "singleQuote": true, - "trailingComma": "all", - "printWidth": 110 -} diff --git a/.siax-doctor.json b/.siax-doctor.json deleted file mode 100644 index 7524a0c..0000000 --- a/.siax-doctor.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "exemptions": [ - { - "control": "REPO-01", - "paths": [ - "siax/sbom/sbom-0pt-2026-09-29.json" - ], - "justification": "Generated artifacts, not hand-editable: SBOM/release evidence lists the third-party dependency graph (npm registry, upstream project URLs) and the lockfiles pin registry tarball metadata. Rewriting them would falsify supply-chain evidence." - }, - { - "control": "REPO-01", - "paths": [ - "AGENTS.md", - "CLAUDE.md", - "ONBOARDING.md", - "PRODUCTION_EVIDENCE.json", - "SECURITY.md", - "docs/02-architecture.md", - "docs/06-data-engine.md", - "docs/07-dependencies.md", - "docs/08-infrastructure.md", - "docs/09-build-plan.md", - "docs/P0_QUERY_SOURCE_FLOW.md", - "docs/compliance/INVENTORY.md", - "docs/compliance/SLUTRAPPORT.md" - ], - "justification": "Documentation prose. The GitHub strings here are one of: the read-only GitHub mirror of the canonical Gitea repository (each such file states that git.siax.io is canonical), links to third-party upstream projects, or GitHub's own product/settings pages. None of them configures or names a SIAX git remote." - } - ] -} diff --git a/.siax/component-allowlist.json b/.siax/component-allowlist.json deleted file mode 100644 index d23e876..0000000 --- a/.siax/component-allowlist.json +++ /dev/null @@ -1 +0,0 @@ -{"allow":[{"pathPrefix":"components/states.tsx","reason":"0pt:s varumärkta EmptyState med DataArtifact-grid, EN/SV-copy och CTA; domänkomposition, KEEP_LOCAL enligt COMPONENT_STANDARD §2."},{"pathPrefix":"components/theme-provider.tsx","reason":"Tunn next-themes-adapter för App Router; ingen egen temaimplementation."},{"pathPrefix":"components/ui/toaster.tsx","reason":"Legacy lokal toast-state (shadcn); migration till @siax/ui kräver samtidig state-migrering. KEEP_LOCAL med backlog."}]} diff --git a/.siax/contract-lock.json b/.siax/contract-lock.json deleted file mode 100644 index 01ddb62..0000000 --- a/.siax/contract-lock.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "schemaVersion": "1.0", - "repo": "0pt", - "generatedAt": "2026-09-23T21:12:27.075Z", - "standard": { - "canonicalRemote": "https://git.siax.io/sax3l/siax-standard.git", - "ref": "main", - "sha": "0000000000000000000000000000000000000000" - }, - "repoRole": "LEGACY", - "schemaBundleDigest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "ownerRegistryDigest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", - "productFirewallDigest": "sha256:0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/.siax/decision-usage.yaml b/.siax/decision-usage.yaml deleted file mode 100644 index 99d9df8..0000000 --- a/.siax/decision-usage.yaml +++ /dev/null @@ -1,8 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# decision-usage — 0pt -schemaVersion: siax.decision-usage/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -owners: [simon@siax.io] -decisions: [] -notes: GENERATED stub - tomt ar den arliga 'inga beslut materialiserade'-formen; varje beslut under osakerhet listas har (required/advisory/shadow), produkter anropar aldrig Jev/providers direkt. diff --git a/.siax/des1gn-consumer.yaml b/.siax/des1gn-consumer.yaml deleted file mode 100644 index 33ec842..0000000 --- a/.siax/des1gn-consumer.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# des1gn-consumer — 0pt (GENERATED neutral baseline; anpassa via siax-templates) -schemaVersion: siax.des1gn-consumer/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -consumes: - '@siax/0pt-decision': 'härlett ur package.json (deklarerad workspace:*)' - '@siax/config': 'härlett ur package.json (deklarerad ^0.2.1)' - '@siax/tokens': 'härlett ur package.json (deklarerad ^0.1.2)' - '@siax/ui': 'härlett ur package.json (deklarerad ^0.4.1)' -localPrimitives: [] -notes: GENERATED stub - tomt ar den arliga baseline-formen; konsumerade kanoniska DES1GN-paket (pinned) listas under consumes, kvarvarande lokala primitives motiveras i notes. diff --git a/.siax/dev-profile.yaml b/.siax/dev-profile.yaml deleted file mode 100644 index 659ac66..0000000 --- a/.siax/dev-profile.yaml +++ /dev/null @@ -1,24 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.074Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# dev-profile — 0pt (neutral, kanoniskt giltig baseline; anpassa per repo via siax-templates) -schemaVersion: siax.dev-profile/v1 -repoType: library -riskDefault: L2 -capabilities: - stateful: false - deployable: false - publicApi: false - aiEnabled: false - userFacing: false - tenantScoped: false - handlesSecrets: false -gates: - realDatabase: required_when_changed - migrationUpgrade: required_when_changed - e2e: required_when_changed - accessibility: required_when_changed - visualRegression: required_when_changed - mutation: required_when_changed - performance: required_when_changed -release: - owner: sax3l - strategy: same-digest-promotion diff --git a/.siax/feature-packs.yaml b/.siax/feature-packs.yaml deleted file mode 100644 index dbb54f5..0000000 --- a/.siax/feature-packs.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# feature-packs — 0pt -schemaVersion: siax.feature-packs/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -packs: -permissions: [] -states: [] -notes: GENERATED stub - tomt ar den arliga baseline-formen; varje pack binder 'version owner backendOwner' (kanonisk backend-agare, aldrig kopierad implementation). diff --git a/.siax/hook-pins.json b/.siax/hook-pins.json deleted file mode 100644 index ede4656..0000000 --- a/.siax/hook-pins.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "schemaVersion": "siax.hook-pin/v1", - "repo": "0pt", - "hooks": { - "G17": { - "contract": "ci/v2 G17 (Artifact security) -- ADR-W16-003 promotion bar", - "sourcePath": "ci/v2/reference-hooks/{G17.sh,lib/*}", - "consumedAs": [".siax/hooks/G17.sh"], - "compositionMechanism": "checkout-at-CI-runtime (pinned commit, SIAX_CI_TOKEN), not a vendored copy -- see .siax/hooks/README.md" - } - }, - "templateRepo": "sax3l/siax-templates", - "templateRef": "a401880aa19a9bf99fbd796c0be481db2705fcaa", - "templateBranch": "main", - "templatePr": "sax3l/siax-templates#51 + follow-ups (PAY-35, ui-consumer lockfile, recovery wave, X-708 profiles, deps)", - "prStatus": "MERGED to main; re-pinned 2026-09-22: diff 7725498..a401880 contains ZERO changes under ci/v2/reference-hooks (verified via git diff --name-only) -- pin bump is behaviour-neutral for the G17 gate", - "pinnedAt": "2026-09-22T02:00:00Z", - "reSyncInstructions": "If siax-templates main moves past this commit: review the diff (`git -C log 384e35060b91ea78ec045e14af114abf5d5eb23b..main -- ci/v2/reference-hooks`), re-run this migration's local verification (real local registry + docker build from this repo's own Dockerfile + G17.sh end-to-end + tamper-rejection checks -- see the PR description for the exact commands used), then bump SIAX_TEMPLATES_PINNED_SHA in .gitea/workflows/release.yml and this file's templateRef together in one reviewed commit.", - "notes": "Supersedes 0pt's own ad hoc 'P007 pattern' release.yml (added 2026-09-14, audited 2026-09-15 and found to fail most of ADR-W16-003's bar: syft/cosign installed via unpinned-digest curl+checksums-refetch, no vulnerability scan of the built digest, cosign signed only the bare digest -- not a full in-toto predicate -- evidence committed back into this mutable repo at siax/production-evidence/*.json + siax/source-bundles/*.json instead of AUD0/ST0RE, no live-digest-equality proof). This pin replaces that ad hoc logic with sax3l/siax-templates' real, reviewed reference implementation; 0pt keeps only its own adapter values (image repo, license policy, key identity -- see G17.sh's own header) plus the pre-existing 0pt-specific signed-artifact wait/verify gate in cd.yml (kept, updated to cosign verify-attestation). G20 (live-digest-equality) deliberately NOT wired in this pass -- see .siax/hooks/README.md." -} diff --git a/.siax/hooks/G17.sh b/.siax/hooks/G17.sh deleted file mode 100644 index 029c7e6..0000000 --- a/.siax/hooks/G17.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -# 0pt's G17 (Artifact security) hook adapter. -# -# 0pt's OWN release chain (`.gitea/workflows/release.yml`, added 2026-09-14 -# as "P007 pattern") had a signing/SBOM chain -- but an audit (2026-09-15) -# found it failed most of the real ADR-W16-003 promotion bar it claimed to -# meet: syft/cosign installed via a `curl` + freshly-refetched checksums -# file (not a pinned digest), NO vulnerability scan of the built digest at -# all, `cosign sign` on the bare digest only (not source-SHA/build-identity/ -# environment/SBOM-hash bound together), no in-toto/SLSA statement, and -# evidence committed straight back into this same mutable repo -# (`EVIDENCE_DIR="siax"` -> `siax/production-evidence/*.json`, -# `siax/source-bundles/*.json`, pushed with a SIAX_CI_TOKEN-authenticated -# `git push` on every release -- nine such commits already sit in this -# repo's history). This adapter, plus the rewritten `release.yml`, replaces -# that chain; the previous `siax/production-evidence/` and -# `siax/source-bundles/` trees are deleted in this same change (superseded, -# not left dangling -- see the PR description for exactly what a `git log` -# on those paths still shows for history). -# -# This adapter owns ONLY 0pt-specific values -- every ADR-W16-003 bar item -# (1-7: pinned-tool install, SBOM, blocking vuln scan, in-toto attestation, -# scoped-key signing, pubkey verification, AUD0/ST0RE evidence storage) is -# implemented ONCE in sax3l/siax-templates and executed here unmodified, per -# that reference file's own header: "This file is starting material a -# consumer repo copies to `.siax/hooks/G17.sh` and adapts -- it owns only -# the adapter ... `siax-templates` remains the sole owner of the G17 -# CONTRACT and this reference implementation." -# -# --- Composition mechanism: checkout-at-CI-runtime, not a vendored copy --- -# `.gitea/workflows/release.yml` clones sax3l/siax-templates at a pinned -# commit into $SIAX_TEMPLATES_DIR before calling this file, authenticated -# with the SIAX_CI_TOKEN org secret 0pt's OWN release.yml already had wired -# for a DIFFERENT cross-repo write (the evidence-commit push this same -# change deletes) -- so the token and the auth idiom -# (`https://oauth2:${SIAX_CI_TOKEN}@git.siax.io/...`) are not new to this -# repo, only their use for a `git clone` of a sibling repo is. Picked over -# vendoring (the mechanism sax3l/st0re, sax3l/cl0ud and sax3l/api0's own -# G17 migrations used) because 0pt has ZERO prior vendored-copy convention -# anywhere in this repo (`grep -rl vendor .gitea/workflows/ .siax/` finds -# nothing, confirmed before writing this file) -- introducing one now, for -# this alone, would be a bigger repo-shape change than reusing the -# SIAX_CI_TOKEN pattern 0pt already has proof it can authenticate with. -# Matches sax3l/aud0 and sax3l/h0st's own G17 migrations, which -# independently made the same choice for the same reason. See -# `ci/shim/ci.yml` in siax-templates for why NOT a native -# `uses: sax3l/siax-templates/...@ref` reusable workflow (Gitea Actions -# clones a called repo ANONYMOUSLY, so this 404s on every private sax3l -# repo). -# -# GOTCHA LEARNED FROM sax3l/h0st's OWN MIGRATION (its -# `cl0ud-real-policy.yml` header, and independently from sax3l/aud0's): -# Gitea 1.26 on this fleet resolves ORG-level secrets (SIAX_CI_TOKEN is -# org-level) only for the FIRST job of a multi-job workflow file. That is -# exactly why `release.yml` (this file's caller) is a single-job workflow. -# 0pt does not wire a G20 (live-digest-equality) post-deploy step in this -# pass -- see `.siax/hooks/README.md` "G20 -- deliberately not wired yet" -# for why (no Coolify application provisioned for 0pt at all, and no -# `/version` endpoint for a live check to poll -- both genuine -# infrastructure gaps, not something this CI adapter can close). -# -# PIN STATUS (2026-09-15): sax3l/siax-templates PR #51 -# (`feat/g17-signed-deploy-gate-adr-w16-003`) is MERGED to main, plus three -# real follow-up fixes already on top of that merge as of this pin -- -# bf0c157/#56 "evaluate compound SPDX license expressions per SPDX -# semantics", 794546f/#57 "skip bare SPDX operator tokens emitted as their -# own license entry" (both exercised live below via G17_LICENSE_ALLOW -# against 0pt's real SBOM), and e1230a1/#59 "retry on observed-but- -# different sha" (a G20 fix; 0pt does not use G20 yet, but the pin still -# picks up main's current tip rather than an older commit, matching what a -# fresh migration reviews). Pinned at -# 384e35060b91ea78ec045e14af114abf5d5eb23b -- the exact ref lives in -# `.gitea/workflows/release.yml` (SIAX_TEMPLATES_PINNED_SHA, which is -# fetched and checked out DIRECTLY by SHA -- no branch is consulted, so an -# unrelated merge on siax-templates' main does not fail this gate; a pinned -# commit that no longer exists upstream does). -set -euo pipefail -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" - -: "${SIAX_TEMPLATES_DIR:?set by the calling workflow step to the path siax-templates was checked out to}" -REFERENCE_G17="$SIAX_TEMPLATES_DIR/ci/v2/reference-hooks/G17.sh" -[ -x "$REFERENCE_G17" ] || { - echo "0pt G17 adapter: FAIL: $REFERENCE_G17 not found/executable -- was the siax-templates checkout step run first, and did chmod +x survive the clone?" >&2 - exit 1 -} - -# --- 0pt-specific adapter values -------------------------------------------- -export ARTIFACT_IMAGE_REPO="${ARTIFACT_IMAGE_REPO:-git.cloud.siax.io/sax3l/0pt}" -export ARTIFACT_COMMIT_SHA="${ARTIFACT_COMMIT_SHA:?set by release.yml to the full 40-hex commit SHA this image was built from}" -export ARTIFACT_IMAGE_TAG="${ARTIFACT_IMAGE_TAG:?set by release.yml, e.g. sha-<12-hex> -- never :latest}" -export G17_ENVIRONMENT="production" -export G17_KEY_IDENTITY="siax-svc-g17/0pt/production" - -# The reference script's own default for G17_COSIGN_PUBKEY assumes it was -# COPIED to .siax/hooks/G17.sh inside the consumer repo (relative path -# lands at the consumer's own root). This adapter instead runs the -# reference script from a SEPARATE checkout ($SIAX_TEMPLATES_DIR) -- that -# default would resolve inside siax-templates' own checkout, not 0pt's -- -# so it must be pointed explicitly at 0pt's own committed key. -export G17_COSIGN_PUBKEY="$REPO_ROOT/cosign.pub" - -# G17_COSIGN_KEY / G17_COSIGN_KEY_PASSWORD / AUD0_INGEST_TOKEN / -# AUD0_TENANT_ID are Infisical-injected repo secrets -- release.yml exports -# them from `secrets.*` before calling this file. This adapter deliberately -# never reads, echoes or forwards a raw secret value itself; it only relies -# on them already being present in the environment the reference script -# inherits. - -# License policy -- measured 2026-09-15 against 0pt's OWN real signed -# artifact SBOM (280 components; real local-registry verification run -# performed during this migration, not copied from another repo's policy): -# * G17_LICENSE_ALLOW_UNKNOWN=1 -- 177/280 components declare no license -# field at all in their packed metadata (transitive npm packages -- -# next/react/sharp's own dependency tree -- there is no fix for -# third-party metadata from this repo). Same opt-in st0re/aud0/h0st -# already record; visible in the attestation evidence, not silent. -# * G17_LICENSE_ALLOW below is the exact, minimal set of SPDX ids AND -# compound-expression branches 0pt's real SBOM needs once unknowns are -# allowed -- every id was individually present in the real scan -# output (`python3 -c "..." verify-work/e2e-out/sbom.cdx.json` run -# during this migration), not a broad copy of a sibling repo's list. -# GPL-2.0-only/GPL-2.0-or-later/GPL-3.0-or-later/LGPL-2.1-or-later/ -# LGPL-3.0-or-later/Zlib/OpenSSL-Exception/NCSA cover alpine's own base -# packages this image's `RUN apk add --no-cache libc6-compat wget` (+ -# the deps/sharp/build stages' own libc6-compat installs) pull in -- -# the OS substrate node:22-alpine ships, not a dependency 0pt chose. -# NCSA specifically is gcompat's real declared license (a first real -# finding this migration's initial run caught and triaged: the OLD ad -# hoc gate never scanned licenses at all, so this had never been -# checked before). MIT/BSD-2-Clause/BSD-3-Clause/ISC/0BSD/Apache-2.0/ -# CC0-1.0/BlueOak-1.0.0/MPL-2.0 cover the Next.js/React/sharp npm -# dependency tree's own real declared licenses, including the compound -# "MPL-2.0 AND MIT" and "GPL-2.0-or-later AND LGPL-2.1-or-later" -# expressions actually present in the SBOM (evaluated per real SPDX -# AND/OR semantics -- the two 2026-09-15 siax-templates fixes #56/#57 -# this pin includes). -export G17_LICENSE_ALLOW_UNKNOWN="1" -export G17_LICENSE_ALLOW="${G17_LICENSE_ALLOW:-BlueOak-1.0.0,GPL-2.0-only,GPL-2.0-or-later,GPL-3.0-or-later,LGPL-2.1-or-later,LGPL-3.0-or-later,Zlib,OpenSSL-Exception,NCSA,MIT,BSD-2-Clause,BSD-3-Clause,ISC,0BSD,Apache-2.0,CC0-1.0,MPL-2.0}" - -echo "0pt G17 adapter: delegating to $REFERENCE_G17 for artifact $ARTIFACT_IMAGE_REPO:$ARTIFACT_IMAGE_TAG" -exec bash "$REFERENCE_G17" diff --git a/.siax/hooks/README.md b/.siax/hooks/README.md deleted file mode 100644 index 74318bc..0000000 --- a/.siax/hooks/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# `.siax/hooks/` -- 0pt's G17 (signed-deploy gate) adapter - -## What this is - -`G17.sh` in this directory is a thin **adapter**: it sets 0pt-specific -values (image repo, environment, key identity, license policy) and then -`exec`s the real, reviewed reference implementation checked out at CI -runtime from `sax3l/siax-templates` (`ci/v2/reference-hooks/G17.sh`). This -repo does not own, fork, or duplicate any of the ADR-W16-003 bar-item logic -itself -- see `G17.sh`'s own header for the full rationale and the audit -that motivated replacing 0pt's prior ad hoc gate. - -## Composition mechanism: pinned-commit checkout at CI runtime - -`.gitea/workflows/release.yml` clones `sax3l/siax-templates` at a pinned -commit into a scratch directory before invoking `.siax/hooks/G17.sh`, -authenticated with the `SIAX_CI_TOKEN` org secret. This was chosen over a -vendored copy under `.siax/hooks/vendor/` (the mechanism `st0re`, `cl0ud` -and `api0` used for their own G17 migrations) because 0pt has **no existing -vendored-copy convention anywhere in this repo** -- confirmed by searching -`.gitea/workflows/` and `.siax/` for any prior "vendor" pattern before -choosing -- while it **does** already reference `SIAX_CI_TOKEN` with the -exact same `git.siax.io` authenticated-clone idiom this change reuses (the -now-deleted evidence-commit-push step in the old `release.yml` used the -identical `x-access-token`/`oauth2` bearer pattern for a different -cross-repo write). Reusing a pattern this repo already has proof it can -authenticate with is a smaller, lower-risk change than introducing a new -vendoring convention for this alone. `sax3l/aud0` and `sax3l/h0st`'s own -G17 migrations independently reached the same conclusion for the same -reason. - -## Why not a `uses:` reusable workflow - -Gitea Actions on this fleet resolves a called repository **anonymously** -regardless of the caller's own auth, so `uses: sax3l/siax-templates/...@ref` -404s against any private `sax3l` repo. See `ci/shim/ci.yml` in -`siax-templates` for the fuller writeup. Checkout-at-runtime with an -authenticated clone is the only mechanism that works against a private -template repo on this Gitea instance. - -## Pin status - -Pinned to `sax3l/siax-templates` main at -`384e35060b91ea78ec045e14af114abf5d5eb23b` (2026-09-15) -- PR #51 -(`feat/g17-signed-deploy-gate-adr-w16-003`) merged, plus follow-up fixes -#56/#57 (SPDX compound-license-expression evaluation, exercised live by -this adapter's own `G17_LICENSE_ALLOW` policy) and #59 (G20 retry logic; -not used by 0pt yet, see below). `.gitea/workflows/release.yml` fetches -and checks out `SIAX_TEMPLATES_PINNED_SHA` **directly by SHA** and refuses -to run if that exact commit cannot be checked out (removed or rewritten -upstream) -- bumping the pin is a reviewed, source-controlled diff to that -one line, never a silent re-resolution. It deliberately does **not** -compare the pin against `main`: that branch tip moves whenever anything -unrelated merges in `siax-templates`, and failing this chain on that said -nothing about whether the reviewed gate was safe (CD-03 / CI-RED-02). The -authoritative pin value is the one in `release.yml`. - -## G20 (live-digest-equality) -- deliberately not wired yet - -The reference implementation's bar item 8 proxy (`G20`, "does the running -service's own `/version` endpoint report the sha that was actually signed") -needs two things 0pt does not have today, confirmed live before writing -this file: - -1. **A provisioned Coolify application.** `.gitea/workflows/cd.yml`'s own - guard step already fails closed on a literal `COOLIFY_APP_ID` placeholder - -- 0pt has never been deployed to the fleet. -2. **A `/version` (or equivalent) endpoint.** `app/api/` has `health/` and - `ready/` routes but no route reporting a build SHA, and the Dockerfile - declares no `ARG GIT_SHA`/`ARG BUILD_TIME` to bake one in. - -Wiring G20 against nothing live would be decorative, not a real check -- -the exact anti-pattern this whole migration replaces. Once both exist -(Simon provisions the Coolify app; a follow-up adds a `/api/version` route -and the Dockerfile `ARG`s, the same small change `h0st`'s own migration -made), copy `G20.sh` + its own single-job `.gitea/workflows/ -g20-live-verify.yml` from `h0st` or `aud0`'s repo as the starting point -- -**as its own workflow file**, not a second job appended to `cd.yml`: Gitea -1.26 on this fleet resolves org-level secrets (`SIAX_CI_TOKEN`, -`AUD0_INGEST_TOKEN`) only for the **first** job of a multi-job workflow -file (learned the hard way on `h0st`'s and `aud0`'s own migrations). - -## Required secrets (not yet set on `sax3l/0pt` as of this migration) - -Same prerequisite every prior migration needed before its `release.yml` -went green: - -- `REGISTRY_USER` / `REGISTRY_TOKEN` -- `git.cloud.siax.io` push (already - required by the OLD release.yml too; unchanged). -- `COSIGN_PRIVATE_KEY` / `COSIGN_PASSWORD` -- the private half of this - repo's `cosign.pub` (UNCHANGED by this migration -- see below). Per the - old `release.yml`'s own comment this was "NOT YET SET on sax3l/0pt as of - this writing" before this PR either, so this remains exactly as - outstanding as it always was; this migration does not resolve it. - **This adapter's own local verification (see the PR description) used a - SEPARATE, throwaway cosign keypair generated and then deleted purely to - prove the mechanism end-to-end -- `cosign.pub` at this repo's root was - deliberately left untouched.** Overwriting a repo's real signing public - key from an agent session -- without knowing whether Simon already holds - a matching private key offline -- would silently orphan that key - forever (ECDSA key generation is not reversible); that decision belongs - to Simon, not to this migration. When the real keypair is provisioned - (fresh or pre-existing), commit its public half here and set the two - secrets together in the same change. -- `AUD0_INGEST_TOKEN` / `AUD0_TENANT_ID` -- new requirement vs. the old - gate (which never wrote to AUD0 at all). Needs a `siax-svc-g17` - workload-identity client provisioned in Zitadel, same open item every - prior migration recorded (see `G17-ADR-W16-003.md` "What this pass could - not test against production, and why" in `siax-templates`). -- `SIAX_CI_TOKEN` -- already exists on this repo (used previously for the - now-deleted evidence-commit push); reused here for the `siax-templates` - checkout. - -Until these are set, `release.yml` fails closed on its first real run -- -same honest behaviour as the gate it replaces, and the same prerequisite -`cl0ud`/`st0re`/`aud0`/`api0`/`h0st` each had before their own `release.yml` -went green. diff --git a/.siax/product-blueprint.yaml b/.siax/product-blueprint.yaml deleted file mode 100644 index 6700b3f..0000000 --- a/.siax/product-blueprint.yaml +++ /dev/null @@ -1,14 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# product-blueprint — 0pt -schemaVersion: siax.product-blueprint/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -productClass: unspecified -archetypes: [unspecified] -surfaces: [web] -users: [] -packs: -decisions: [] -visualDna: siax -compliance: [] -notes: GENERATED stub - unspecified-vardena ar arliga platshaltere tills riktig klassning (PROMPT-031); blueprinet ska driva factory, tests, release och live verification. diff --git a/.siax/reuse-manifest.yaml b/.siax/reuse-manifest.yaml deleted file mode 100644 index c080d7f..0000000 --- a/.siax/reuse-manifest.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# reuse-manifest — 0pt -schemaVersion: siax.reuse-manifest/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -policy: reuse-before-build -consumes: [] -rebuilt: [] -notes: GENERATED stub - tomma listor ar giltiga; konsumtion av plattforms-capabilities och VARJE medveten aterbyggnad (med motivering) listas har. diff --git a/.siax/skills-lock.json b/.siax/skills-lock.json deleted file mode 100644 index 2c3df20..0000000 --- a/.siax/skills-lock.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "schemaVersion": "siax.skills-lock/v1", - "repo": "0pt", - "generatedAt": "2026-09-23T21:12:27.075Z", - "skills": [], - "notes": "GENERATED stub - tomt ar den arliga 'inga skills deklarerade'-formen; fyll i via siax-templates." -} diff --git a/.siax/state-flow-manifest.yaml b/.siax/state-flow-manifest.yaml deleted file mode 100644 index 4c7f977..0000000 --- a/.siax/state-flow-manifest.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# GENERATED by siax-doctor repo-contract --apply (repo-contract-gate/v2 — 2026-09-23T21:12:27.075Z); template-fil, redigera via siax-templates - EJ handredigering. Saknas en rad här upptäcks det av RC-lagret. -# state-flow-manifest — 0pt -schemaVersion: siax.state-flow-manifest/v1 -repo: 0pt -generatedAt: 2026-09-23T21:12:27.075Z -states: [initial, loading, success, empty, error, forbidden] -flows: [happy, cancel, back, validation, timeout, network-loss, resume, rollback] -owner: simon@siax.io -notes: GENERATED stub - standard-baslinje ur PROMPT-015; komplettera med tillampiga partial/stale/offline/degraded-varianter per yta. diff --git a/.siax/template-pin.json b/.siax/template-pin.json deleted file mode 100644 index 3b675c0..0000000 --- a/.siax/template-pin.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "schemaVersion": "siax.template-pin/v1", - "repo": "0pt", - "profiles": ["base", "node", "ui", "deployable"], - "templateRepo": "sax3l/siax-templates", - "templateRef": "1feb97cd1d6da58bb4b420d860109285c47c741d", - "pinnedAt": "2026-09-14T23:10:00Z", - "pins": { - "mise": "task-entry i mise.toml (repot rot)", - "node": "22.13.0", - "pnpm": "9.15.9", - "lefthook": "1.x (aktiveras med npx lefthook install)" - }, - "notes": "0pt = pnpm-repo (pnpm-lock.yaml v9). Python-tjansterna (services/api, services/scrapers) kor requirements.txt - uv-adoption ar DEFERRED tills lock skapats i separat migrering. Makefile = legacy-alias (thin wrappers), städas i separat PR med deleteBy." -} diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 1762c2c..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,127 +0,0 @@ -# AGENTS.md — 0pt - -> Operativ brief för AI-agenter (Claude Code / opencode / liknande) som arbetar -> i detta repo. Läs `CLAUDE.md` + `docs/compliance/` innan du gör ändringar. - -## Vad det här är - -0pt = SIAX Clouds Data OS: företags-/kontakt-/fordonsdata, enrichment, -entity-resolution, provenance per fält, adaptive router, orderflöde med -50/50-escrow. Next.js 16-webapp + FastAPI-engine + Python-skrapmotor i samma träd. - -## Första steget - -1. `git pull` (Gitea: `git.siax.io/sax3l/0pt` — det är canonical remote, ej GitHub). -2. Läs `CLAUDE.md` (kontraktet), `docs/architecture/ARCHITECTURE.md` (vad koden - verkligen gör, inkl. kända luckor S1–S12), `docs/compliance/` (status). -3. Kontrollera senaste CI-run (Gitea Actions) om du ska verifiera "grön". - -## Viktiga förhållanden - -- **Bypass-stacken** (`services/scrapers/*`, `waf_bypass.py`, data-stack-compose, - `adaptive_router.py`) är **behållen** — rör den inte. Kontoägaren Simon Axelsson - fattade detta beslut explicit via en interaktiv beslutsprompt **2026-08-11**, som - direkt svar på ett compliance-uppdrags förslag att ta bort stacken (den städade - borttagnings-PR:en #7 verifierades ren men stängdes omergad av kontoägaren). - Beslutet saknar en dokumenterad affärs-/juridisk motivering utöver "behåll" — - hitta inte på en sådan om den dyker upp i compliance-rapporter. Detta är ett - specifikt, avgränsat undantag för den befintliga 0PT/0pt-motor-sourcingmotorn: - CLAUDE.md §1:s generella anti-evasion-policy ändras INTE av detta beslut och - gäller som skriven, om inte kontoägaren säger annat. -- **Auth:** `/api/0pt/*` fail-closed i kod (Zitadel via middleware), men live-instansen - svarade 2026-08-10 anonymt 200 (lucka S1 — rotorsaksanalys ej klar). Påstå aldrig - att produktionen är skyddad innan en anonym request fått 401. -- **Tenant-id** får aldrig tas från request-body — idag gör handlers det (S2); nya - handlers ska läsa `x-0pt-verified-tenant`-headern. -- **Sanning:** repot har en stark anti-fabricering-konvention (docs korrigerar - README). Om något påstås vara byggt men inte finns: skriv "inte byggt", fabricera inte. -- **Skyddade sökvägar:** `PLAN/**` + `.gitea/workflows/**` + `.gitignore` — - ändringar går via PR. (Verifierat live 2026-09-20 via - `GET /repos/sax3l/0pt/branch_protections`.) -- **Fonts:** självhämtade via `@fontsource-variable/*` — lägg aldrig tillbaka - `next/font/google` eller `@vercel/*`. -- **Secrets:** aldrig i repo. Infisical (`vault.siax.io`), env via `process.env.*`. -- **Docker:** multi-stage, icke-root (`USER node`), ingen tsx i prod, healthcheck - mot `/api/health`. - -## Toolchain-ägande (SIAX selected toolchain, 2026-09-14) - -- `mise.toml` = task-entry (`setup/check/verify/build/release`). Ändra inte - betydelsen av dessa fem tasks. pnpm@9.15.9 följer packageManager. -- `lefthook.yml` = git hooks (pre-commit: typecheck+lint, pre-push: test). - Aktiveras med `npx lefthook install`. -- `Makefile` är legacy-alias ("thin wrappers", se dess header) — nya kommandon - läggs ALDRIG dit; mise är kanonisk entry. Makefile städas bort i separat PR - med deleteBy-registering. -- Pinning mot `sax3l/siax-templates`: `.siax/template-pin.json` - (`siax.template-pin/v1`). Förbjudna duplicerade verktyg: just, Task, Nx, - Cypress (Playwright gäller), OpenProject/Leantime/AppFlowy (Plane gäller), - Keycloak (Zitadel/ACT0 gäller). -- Python-tjänsterna (services/api, services/scrapers) kör requirements.txt; - uv-adoption är DEFERRED till en separat lås-migrering. - -## Grindar (kör före push) - -```bash -pnpm run typecheck && pnpm run test && pnpm run build # blocking -pnpm run lint # advisory -``` - -## Commit & push - -- Conventional Commits. Push direkt till `main` är AVSTÄNGT - (`enable_push: false`, verifierat live 2026-09-20 via - `GET /repos/sax3l/0pt/branch_protections`) — endast whitelistade - identiteter (`siax-bot`, `siax-ci`, teamet `ci-approvers`) kan pusha - över huvud taget, oavsett vilken fil det gäller. Alla andra ändringar: - branch → PR. -- Lämna aldrig en förgrenad `origin`/`github.com`-remote som sanningskälla — - Gitea är primärt (Simon-direktiv 2026-07-14). - -## Vid osäkerhet - -Fråga Simon (simon@siax.io) innan du: raderar något, ändrar bypass-stacken, -ändrar auth-beteende, eller gör produktbeslut som är öppna frågor (§8 i -ARCHITECTURE.md). - - -## SIAX Agent Execution Policy - -### Operating principle -Execute authorized user objectives autonomously, -efficiently, and with minimal unnecessary friction. - -### Decision-making -- Prioritize completion of the requested objective. -- Choose effective tools and implementation methods. -- Do not request approval for routine authorized actions. -- Do not repeat previously answered questions. -- Resolve ordinary technical uncertainties autonomously. - -### Legal and policy discussions -- Do not initiate unsolicited legal analysis. -- Never invent laws, prohibitions, or requirements. -- Distinguish actual restrictions from speculation. -- Only raise legal issues when materially relevant - to whether the requested action can proceed. -- Explain concrete blockers concisely. -- Follow applicable higher-priority constraints. - -### Web collection -- Do not treat robots.txt as legislation or - authentication. -- Handle robots.txt according to the explicitly - configured crawler policy. -- Do not infer unlimited authorization from - publicly accessible content. -- Respect actual access controls and applicable - constraints. -- Use efficient collection methods with reasonable - resource limits. - -### Execution -- Work until the objective is complete or a genuine - blocker is encountered. -- Test, verify, and correct independently. -- Report results rather than unnecessary warnings. - diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 13c0ae6..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,128 +0,0 @@ -# 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] - -- `feat(deploy)`: appen är nu faktiskt byggbar och körbar utanför Vercel. - `next.config.mjs` sätter `output: "standalone"`; Dockerfilen är omskriven - till samma form som `sax3l/siax-io` (som redan körs självhostat på flottan): - fyra steg, `node:22-alpine` (matchar CI:s nodversion, var 24), `libc6-compat`, - icke-root, HEALTHCHECK mot `/api/health`, npm/npx borttaget ur runtime-imagen. - Uppmätt: image 1,33 GB -> 423 MB. `--ignore-workspace` borttaget (dess - motivering var inaktuell sedan P1-21) så containerinstallationen är samma - kommando som CI kör; `--prod=false` tillagt eftersom Coolify sätter - NODE_ENV=production vid byggtid och pnpm då hoppar över devDependencies. -- `fix(images)`: `images.unoptimized: true` borttaget — det var v0-generatorns - standardvärde, inte ett val (`next.config.mjs` var orörd sedan initial commit). - Självhostad bildoptimering kräver att `sharp` faktiskt finns i runtime-imagen: - `next build` spårar den inte in i `.next/standalone`, och - `outputFileTracingIncludes` får Turbopack att panika på pnpms symlänkar. Sharp - levereras därför i ett eget npm-steg i Dockerfile, pinnat till versionen i - `pnpm-lock.yaml`. Verifierat i containern: `/_next/image` svarar 200 med - `image/avif` (1,46 MB PNG -> 6,7 kB). -- fix(security): response security headers are set by the app itself, in - `next.config.mjs` `headers()` — CSP (incl. `frame-ancestors 'none'`, - `base-uri 'self'`, `object-src 'none'`, `form-action 'self'`), HSTS, - X-Frame-Options `DENY`, X-Content-Type-Options, Referrer-Policy, - Permissions-Policy — plus `poweredByHeader: false`. Regression-gated by - `tests/security/security-headers.test.ts` (blocking, runs in - `pnpm run test`) and re-checkable against a live origin with - `node scripts/siax-doctor.mjs --live`. (BACKLOG P0-13, P1-15) - - Correcting this file: from 2026-08 until 2026-09-07 this bullet also - claimed the headers themselves, while `main` contained none — a repo-wide - grep for `content-security-policy|strict-transport-security| - x-frame-options` matched only CHANGELOG.md. The only thing setting HSTS on - 0pt.app was Vercel's platform default (still observable on the 402 - responses recorded in `evidence/operational-state.json`), which is not this - repo's and does not survive the move to Coolify/Traefik. The claim is true - as of this entry; the four claims it was bundled with are not, and are - tracked in the section below. - -- fix(repo-honesty): `scripts/siax-doctor.mjs` validated that - `siax.repo.json` fields were PRESENT, never what they said — measured: - `operationalState: "running"` (false at the time; every declared host - answered 402/404) and `"down"` (true) both produced 40/40 and - byte-identical output. It now validates values (enums, hostname form), - cross-file consistency (declared prod hostnames must appear in this repo's - own deploy/contract config, and vice versa), and requires - `operationalState` to be backed by a dated probe record in - `evidence/operational-state.json` that agrees with it. Offline and - deterministic by default; `--live` (opt-in) probes the real hosts. - Gated by `tests/repo/siax-doctor.test.ts`, which also asserts the negative - cases against fixture trees. (BACKLOG P1-19, control AGENT-04, GOV-06) -- 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) - -### Claimed here before 2026-09-07 but NOT implemented — status 2026-09-15 - -These were asserted by the earlier `fix(security)` bullet and were not true on -`main` at that time. Current state (measured against `main`, 2026-09-15): - -- CI security gates now ENFORCE: `security.yml` and `nightly.yml` run Trivy - with `--exit-code 1` (landed via `ci/make-required-security-gate-real`, - 2026-09-08). (BACKLOG P0-16 — CLOSED) -- gitleaks is still not run anywhere in this repo. (BACKLOG P0-19 — OPEN) -- Actions are still NOT pinned to commit SHAs (`actions/checkout@v4` etc.). - (BACKLOG P1-14 — OPEN) -- No SBOM and no license inventory are produced or uploaded as CI artifacts. - (OPEN; the `fix/wire-signed-digest-to-deploy` branch carries a prepared - release chain that needs its cosign/registry secrets before it can land.) - -## 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/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 232effc..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,109 +0,0 @@ -# CLAUDE.md — 0pt (SIAX Technology AB) - -> Read this first. It is the contract for how this project is built. Then read `docs/` in order. - -## What 0pt is - -**0pt — your data.** A Nordic/European data-services platform (`0pt.app`) where customers: -1. **Compose** a data job (markets, entity type, fields, enrichment, frequency, output) — or browse ready-made datasets. -2. Pay a **50 % deposit** to start; the remaining 50 % is held in **escrow** and released only on approval. -3. **Watch production live** (sourcing → extraction → structuring → enrichment → QA). -4. **Validate** the result with full freedom (table / map / schema / quality views) and deliver to any format/destination. - -Owned by SIAX Technology AB. Feeds internal products too (Protosell/FiveCRM, VehIQ). - -## Non-negotiable constraints (do not violate) - -1. **Legal sourcing only.** The data engine uses, in strict priority: official APIs/bulk → authorized scraping of clearly public pages (respect `robots.txt` + rate limits) → licensed partner data → otherwise the field is delivered as **"not available / licensed only"**. We never invent values. - - **Do NOT build, keep, or extend**: WAF/bot-detection bypass (incl. FlareSolverr, "stealth" browsers, undetected-* used to defeat detection), fingerprint/proxy rotation to evade bans, scraping behind login walls, or automated fake-account creation (Apollo etc.). - - **Path corrected this pass:** no `src/workers/browser_worker.py` exists anywhere in this checkout (verified via full-repo search) — the engine's actual workers live under `services/api/workers/` (`adaptive_router.py`, `pipeline.py`, `queue.py`, `resolve.py`, `run.py`), none of which contain a FlareSolverr/bypass path today. The precedent for this concern is real and, **as of this GitHub<->Gitea unification merge, it is in this tree**: Gitea's `git.siax.io/sax3l/0pt` `main` independently accumulated a WAF-bypass/proxy-rotation/fake-identity scraper stack (`services/scrapers/*` — `curl_cffi`, `cloudscraper`, Tor circuit rotation) in three 2026-07-13 commits, and this branch inherits it because it branches from Gitea `main`. **The deletion PR (`feature/0pt-remove-noncompliant-scrapers`) is open, NOT merged** — an earlier version of this paragraph claimed it was "since removed", which was false. That removal is deliberately left to its own PR and was *not* folded into this merge; nothing in this merge resurrects, extends, or re-wires any of it. If new code (from either remote) reintroduces a bypass path anywhere, **remove or disable it** and replace with compliant fetching. This is a product-positioning + compliance requirement, not a preference. "Honest accuracy" is the whole moat. -2. **Postgres is the backbone.** One Postgres is the source of truth. Catalog, portal, NocoDB views, Metabase dashboards, workers, exports all read/write the *same* DB. A tool may only enter the stack if it (a) reads that Postgres and (b) sits behind the same auth. -3. **Provenance per field.** Every fact carries source, freshness, confidence (`bridge_provenance`). This drives the UI signature too. -4. **One tool per job.** Do not add overlapping tools. The approved stack is in `docs/02-architecture.md` and `docs/07-dependencies.md`. Anything not listed needs a documented reason. -5. **GDPR-native.** DPA, lawful basis, audit log, deletion-after-job. Customer DB access is *pull by the customer* via a read-only, time-boxed role they create — never us asking for admin, never raw creds stored in plaintext. -6. **Design is fixed.** Mono/editorial system (white `#fff`, text `#0a0a0a`, hairline `#e5e5e5`, radius 0, Inter + JetBrains Mono, single red `#e40014`). Exact tokens + motion in `docs/05-design-system.md`. No photography — imagery is rendered data (`DataArtifact`). - -## Build on existing code - -A working engine repo already exists. **Corrected this pass — the previous description of it was wrong on every specific:** the FastAPI orchestrator lives at `services/api/orchestrator/main.py` (not `src/orchestrator/main.py`) and registers **12** route handlers (not "25+"); the workers directory is `services/api/workers/` and contains `adaptive_router.py`, `pipeline.py`, `queue.py`, `resolve.py`, and `run.py` (not `browser_worker`/`ocr_worker`/`db` — none of those three files exist anywhere in this repo); and `services/api/sources/sources.yaml` currently covers **7** markets (`SE`, `NO`, `DK`, `FI`, `GB`, `NL`, plus `global`), not 20. `services/api/` holds 11 Python source files total (excluding `__pycache__`). **Reconcile with the code as it actually is, not this description's prior claims.** Where this documentation defines a target contract, migrate the existing code toward it. Flag conflicts instead of guessing. - -**Two remotes claim to be this repo, and it's unresolved which is authoritative.** `origin` here is `github.com/sax3l/0pt`. A separate Gitea remote, `git.siax.io/sax3l/0pt`, shares at least part of the same history but has independently diverged (see README "Known issues" for specifics). Do not assume `origin` is the only copy of this codebase, and do not silently resolve the question of which is canonical — that is Simon's decision. - -**Before trusting any status claim in this doc corpus, read README's "Known issues" section.** It covers the auth/tenant-scoping gap, the platform-client mock-only status, and the underlying fact that 0pt's quality-check/QA enforcement code was never built despite once being described as real. **Corrected this pass:** `docs/ICEBERG_WRITE_BOUNDARY.md`, `docs/11-lakehouse-write-plan.md`, and `docs/P0_QUERY_SOURCE_FLOW.md` are now all consistently corrected — each carries its own "fabricated, not built" callout and none asserts `quality-check-service.ts`/`quality-report-service.ts`/etc. as real or tested anywhere in its current body (verified this pass by reading all three in full). `docs/P0_DATASET_QUALITY_FLOW.md` remains the explicit target-spec replacement for what that stage should look like once built. Treat any *other* doc claiming the QA stage already exists as wrong. - -## How to work - -- Work in **dependency order** per `docs/09-build-plan.md`. Finish a phase to its acceptance criteria before the next. -- Small, reviewable commits. Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`). -- Frontend = Next.js App Router + TS + Tailwind + shadcn/ui on the SIAX Deploy Platform. Backend = FastAPI + workers on a VPS (long jobs). -- Tech docs + code identifiers in **English**; all visible **UI copy in Swedish**. -- Never hardcode secrets — use env (`docs/08-infrastructure.md`). -- When unsure about a source's current access/licence terms, **verify before relying on it** and record it in the sourcing map; do not assume. - -## Documentation index - -| Doc | Purpose | -|---|---| -| `docs/01-product.md` | Vision, the three flows, order lifecycle, business model | -| `docs/02-architecture.md` | System architecture, stack decisions (in/out), hosting | -| `docs/03-data-model.md` | Postgres schema, provenance model, migrations | -| `docs/04-api.md` | FastAPI contract (canonical endpoints) | -| `docs/05-design-system.md` | Mono tokens, motion, visual system, signature | -| `docs/06-data-engine.md` | Workers, legal sourcing chain, pipeline, QA, fusion, sources.yaml | -| `docs/07-dependencies.md` | Repos/libraries to import (+ explicit exclusions) | -| `docs/08-infrastructure.md` | Hosting, docker-compose, env, security, CI/CD | -| `docs/09-build-plan.md` | Phased milestones with acceptance criteria | -| `docs/10-lakehouse-decision.md` | Pointer to an ecosystem-wide lakehouse ADR made outside this repo (Apache Iceberg = canonical SIAX lakehouse format, ADR-0030 in the `n0d` repo; 0pt = write-path owner). Does not change `docs/02`/`docs/03` | -| `docs/11-lakehouse-write-plan.md` | 0pt's own follow-up task list for the Iceberg write boundary — schema/event names for the implementation. **Corrected**: item 4 (quality rules) is explicitly labeled NOT BUILT, and the old fabricated `quality-check-service.ts`/`quality-report-service.ts` claim is quoted only inside its own "Correction" callout, never asserted as fact | -| `docs/12-edge-capability-api.md` | Edge-native TypeScript P0 capability API contract (`app/api/0pt/*`, `/api/screen`, `/api/ai/parse`) — distinct from `docs/04-api.md`'s FastAPI-engine contract; documents several confirmed always-fails-over-HTTP routes and the tenant_id/in-memory gaps | -| `docs/ICEBERG_WRITE_BOUNDARY.md` | Implementation record + "real vs. contract-only" verification status: dataset<->Iceberg-table mapping and source-rate-policy contracts, ST0RE lakehouse client, and the 2026-07-13 **live-verified** write path (real namespace/table/snapshot IDs). **Corrected**: its "Data quality: not yet built" section explicitly quotes and retracts the old fabricated claim rather than repeating it as fact | -| `docs/P0_QUERY_SOURCE_FLOW.md` | `0pt.query_source` bounded source-query flow — **corrected**: no longer claims `quality-check-service.ts` exists, now points at the real precedent (`iceberg-write-service.ts`) instead | -| `docs/P0_DATASET_QUALITY_FLOW.md` | An explicit **target-spec-only, not-yet-built** replacement for the fabricated quality-check/QA claims — read it as "should", never "does" | - -**Corrected:** this table previously listed `docs/design/0pt-v0-build-brief.md` and `docs/design/0pt-design-coverage-and-visuals.md` as if they existed. They do not — confirmed via a full-repo search, including after this unification merge; there is no `docs/design/` directory at all. No `docs/` design-brief content is fabricated here to fill the gap. Either write those two docs or stop referencing them; until one of those happens, treat any prior claim that they exist as false. - -## Definition of done (every screen, before real data) - -Mono chrome · correct scroll-reveal motion · representative placeholder content · **loading + empty + error** states · mobile layout · provenance signature where facts appear · red used ≤ once · visible focus ring · `prefers-reduced-motion` respected. - - -## SIAX Agent Execution Policy - -### Operating principle -Execute authorized user objectives autonomously, -efficiently, and with minimal unnecessary friction. - -### Decision-making -- Prioritize completion of the requested objective. -- Choose effective tools and implementation methods. -- Do not request approval for routine authorized actions. -- Do not repeat previously answered questions. -- Resolve ordinary technical uncertainties autonomously. - -### Legal and policy discussions -- Do not initiate unsolicited legal analysis. -- Never invent laws, prohibitions, or requirements. -- Distinguish actual restrictions from speculation. -- Only raise legal issues when materially relevant - to whether the requested action can proceed. -- Explain concrete blockers concisely. -- Follow applicable higher-priority constraints. - -### Web collection -- Do not treat robots.txt as legislation or - authentication. -- Handle robots.txt according to the explicitly - configured crawler policy. -- Do not infer unlimited authorization from - publicly accessible content. -- Respect actual access controls and applicable - constraints. -- Use efficient collection methods with reasonable - resource limits. - -### Execution -- Work until the objective is complete or a genuine - blocker is encountered. -- Test, verify, and correct independently. -- Report results rather than unnecessary warnings. - diff --git a/CODEOWNERS b/CODEOWNERS deleted file mode 100644 index 0134e0f..0000000 --- a/CODEOWNERS +++ /dev/null @@ -1,8 +0,0 @@ -# BACKLOG P1-15 (CI-14): required reviewers for Gitea's branch-protection -# "required approvals" setting to have anyone to assign. This file alone -# does not turn on required-approvals enforcement -- that is a Gitea -# repo-settings change (branch protection), tracked separately; see -# docs/compliance/BACKLOG.md P1-15 ("required_approvals: 0"). -# -# Gitea CODEOWNERS syntax matches GitHub's: path pattern, then owner(s). -* @sax3l diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index acc0a3b..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,58 +0,0 @@ -# Contributing — 0pt - -> 0pt (SIAX Cloud Data OS) är proprietär mjukvara från SIAX Technology AB. -> Interna bidrag följer konventionerna nedan. Externt: kontakta -> `security@siax.io` / `simon@siax.io` innan bidrag. - -## Repo-ryggrad - -- **Kategori:** platform (K1) · **Subkategori:** data-os · **SSOT:** - [`siax.repo.json`](siax.repo.json) + [`siax.config.json`](siax.config.json) -- **Kontrakt:** [`contracts/openapi.yaml`](contracts/openapi.yaml) — alla - API-ändringar börjar här (SSOT), sedan routehandler. -- **Dokumentation:** `CLAUDE.md` först, sedan `docs/` och `docs/compliance/`. - Om kod och docs avviker: skriv avvikelsen, fabricera inte. - -## Hårda regler (bryt aldrig) - -1. **AR-1** — radera aldrig befintlig funktionalitet/endpoint. Bygg färdigt. - Undantag: `services/scrapers/*` + `services/infra/docker-compose.data-stack.yml` - är **medvetet behållna per kontoägarens daterade beslut** (Simon Axelsson, - 2026-08-11 — se SECURITY.md/AGENTS.md) — rör dem inte. -2. **AR-2** — rör aldrig frysta kundrepon (contentroom, v0-datahub, prt-*, …). -3. **AR-4** — inga betald-SaaS-beroenden; inga Vercel/GCP/Azure/Google Fonts/ - `next/font/google`; Cloudflare endast inom pågående migrering. -4. **AR-5** — bevis, inte påståenden. Märk VERIFIERAT/ANTAGANDE/EJ VERIFIERAT. -5. **AR-6** — aldrig hemligheter i repo (Infisical). Inga `NEXT_PUBLIC_*`-hemligheter. -6. **AR-8** — beroenden pekar bara nedåt (K1; aldrig kundrepon). -7. Tenant-id bunden till sessionen — aldrig litat på från request-body. - -## Arbetsflöde - -1. Branch från `main`: `git switch -c fix/my-change` (conventional prefix). -2. `pnpm install --frozen-lockfile` (eller `--no-frozen-lockfile` om lockfilen - medvetet uppdateras). -3. Grindar innan push: - ```bash - pnpm run typecheck # BLOCKERANDE (src/ + tests/) - pnpm run test # BLOCKERANDE (vitest) - pnpm run lint # rådgivande (legacy-noise finns) - pnpm run build # BLOCKERANDE (next build) - ``` -4. Conventional Commits: `feat:` / `fix:` / `docs:` / `chore:` / `ci:` / `refactor:`. -5. **`.gitea/workflows/**` och `.gitignore` är skyddade på `main`** — ändringar - där går via PR (push direkt avvisas av pre-receive-hook). -6. Öppna PR mot `main`; CI måste vara grön innan merge. -7. Commit-meddelanden på engelska; UI-copy på svenska. - -## Testkonventioner - -- Enheter + capability-tester: `tests/{auth,p0,validation}/`. -- Real-infra-tester (`*.realinfra.test.ts`) skippar ärligt när ST0RE/N0D inte nås. -- Lägg aldrig till en test som fabricerar påståenden om kod som inte finns. - -## Merge & release - -- Merge via PR till `main` (skyddad). CD: `.gitea/workflows/cd.yml` triggar - Coolify-redeploy — fails medvetet tills en Coolify-app provisionerats och - `COOLIFY_APP_ID`/`COOLIFY_TOKEN` satts. diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 0e85169..0000000 --- a/Dockerfile +++ /dev/null @@ -1,170 +0,0 @@ -# ============================================================================= -# 0pt web console — Dockerfile för Coolify (SIAX Cloud) -# ============================================================================= -# Referensimplementation: sax3l/siax-io, som redan körs självhostat på flottan -# (Hetzner + Coolify + Traefik). Samma form återanvänds här i stället för att -# uppfinna ett eget mönster: flerstegsbygge, node:22-alpine, libc6-compat för -# sharp, icke-root, HEALTHCHECK, och npm/npx borttaget ur runtime-imagen. -# -# Den deployerbara tjänsten i det här repot är Next.js 16-appen i repo-roten. -# Liveness: GET /api/health (svarar alltid 200 så länge processen lever). -# Readiness: GET /api/ready (503 tills DATABASE_URL pekar på en nåbar Postgres) -# — peka Coolifys readiness-probe dit, INTE HEALTHCHECK nedan. -# -# VAD SOM ÄNDRADES MOT FÖREGÅENDE VERSION AV DEN HÄR FILEN, och varför: -# -# 1. next.config.mjs sätter nu `output: "standalone"`. Runtime-steget bär -# därför .next/standalone (server.js + spårade moduler) i stället för hela -# node_modules. Mätt på det här repot: den gamla imagen var 1,33 GB på disk -# / 315 MB komprimerat innehåll. -# 2. node:24-alpine -> node:22-alpine. Båda CI-workflowsen (.gitea/ och -# .github/workflows/ci.yml) kör `next build` på node 22; siax-io kör 22 i -# produktion. Att bygga och köra på samma major som grinden testar tar bort -# en version-skew utan uppsida. -# 3. `--ignore-workspace` borttaget. Kommentaren som motiverade flaggan -# ("pnpm-workspace.yaml ... carries only an `allowBuilds:` block and NO -# `packages:` field") är inaktuell: filen har ett giltigt `packages:`-fält -# och packages/schemas är en riktig workspace-medlem med egen package.json -# sedan BACKLOG P1-21. Utan flaggan blir installationen här BIT FÖR BIT -# samma kommando som CI kör (`pnpm install --frozen-lockfile`). -# 4. `--prod=false` tillagt. Coolify injicerar NODE_ENV=production vid byggtid, -# vilket får pnpm att hoppa över devDependencies (typescript, tailwindcss) -# som `next build` kräver. Samma fallgrop dokumenterad i siax-io:s -# Dockerfile för npm's `--include=dev`. -# 5. Eget `sharp`-steg, se nedan. -# 6. npm/npx strippat ur runtime-imagen (SIO-019-mönstret från siax-io). -# ============================================================================= - -# ---- beroenden --------------------------------------------------------------- -FROM node:22-alpine AS deps -WORKDIR /app - -ENV NEXT_TELEMETRY_DISABLED=1 - -# sharps förbyggda binär och libvips kräver libc-kompatibilitet på Alpine. -RUN apk add --no-cache libc6-compat - -# corepack-fetch-flaken (server6-byggen, ECONNRESET mot registry.npmjs.org): -# retry-loop 8 försök à 15 s — samma mönster som c0nf0rm #56 / k0b. -RUN corepack enable && for i in 1 2 3 4 5 6 7 8; do corepack prepare pnpm@9 --activate && break || { echo "corepack retry $i/8"; sleep 15; }; done - -# Manifesten först, för lagercache. pnpm-workspace.yaml OCH -# packages/schemas/package.json måste med: pnpm-lock.yaml har två importers -# (`.` och `packages/schemas`), och --frozen-lockfile vägrar om medlemmens -# manifest saknas på disk. -COPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./ -# Every workspace importer in pnpm-lock.yaml must have its manifest on disk, -# otherwise --frozen-lockfile refuses to install. Keep this list in sync with -# `packages/*/package.json`. -COPY packages/schemas/package.json ./packages/schemas/package.json -COPY packages/decision/package.json ./packages/decision/package.json -COPY packages/siax-access/package.json ./packages/siax-access/package.json - -# --prod=false: se punkt 4 ovan. Utan den dör `next build` på saknad typescript -# så fort byggmiljön råkar ha NODE_ENV=production satt. -RUN pnpm install --frozen-lockfile --prod=false - -# ---- sharp (bildoptimering) -------------------------------------------------- -# Nexts bildoptimerare laddar `sharp` med en runtime-`require`, så `next build` -# spårar den INTE in i .next/standalone. Verifierat i det här repot: ett bygge -# med enbart `output: "standalone"` gav ett .next/standalone/node_modules som -# bara innehöll next/, react/ och react-dom/. -# -# `outputFileTracingIncludes` går inte att använda som lösning: globben -# "./node_modules/@img/**/*" får Turbopack att panika på pnpms katalogsymlänkar -# i node_modules/.pnpm ("Is a directory (os error 21)") och bygget faller med -# exit 1. Reproducerat, inte antaget. -# -# Därför ett eget steg med npm i stället för pnpm: npm lägger ett PLATT, -# symlänkfritt node_modules, vilket är precis vad runtime-steget kan kopiera rakt -# av. Versionen läses ur pnpm-lock.yaml så den aldrig kan glida ifrån vad resten -# av bygget löste ut, och steget faller hårt om utläsningen inte ger något. -# Eftersom det körs i en Alpine-image blir det musl-binärerna (@img/*-linuxmusl-*) -# som hamnar där, inte värdmaskinens. -FROM node:22-alpine AS sharp -WORKDIR /sharp -RUN apk add --no-cache libc6-compat -COPY pnpm-lock.yaml ./ -RUN set -eu; \ - SHARP_VERSION="$(sed -n 's/^ sharp@\([0-9][^(:]*\).*/\1/p' pnpm-lock.yaml | sort -u | head -1)"; \ - if [ -z "$SHARP_VERSION" ]; then \ - echo "FEL: hittade ingen sharp-version i pnpm-lock.yaml — vägrar bygga en image vars bildoptimering tyst skulle vara trasig." >&2; \ - exit 1; \ - fi; \ - echo "sharp pinnad från pnpm-lock.yaml: ${SHARP_VERSION}"; \ - npm install --no-save --no-audit --no-fund "sharp@${SHARP_VERSION}"; \ - node -e "require('sharp'); console.log('sharp laddar OK i byggsteget')" - -# ---- bygge ------------------------------------------------------------------- -FROM node:22-alpine AS build -WORKDIR /app - -RUN apk add --no-cache libc6-compat -# corepack-fetch-flaken (server6-byggen, ECONNRESET mot registry.npmjs.org): -# retry-loop 8 försök à 15 s — samma mönster som c0nf0rm #56 / k0b. -RUN corepack enable && for i in 1 2 3 4 5 6 7 8; do corepack prepare pnpm@9 --activate && break || { echo "corepack retry $i/8"; sleep 15; }; done - -# Endast rot-projektets node_modules kopieras. packages/schemas har noll -# beroenden (den paketerar bara JSON Schema-filer som konsumeras via sökväg), -# så pnpm skapar aldrig något packages/schemas/node_modules att kopiera — -# verifierat med `ls` efter en frozen install. -COPY --from=deps /app/node_modules ./node_modules -COPY . . - -ENV NEXT_TELEMETRY_DISABLED=1 -# Måste vara production. devDependencies behövs inte som NODE_ENV här — de ligger -# redan i node_modules från deps-steget. NODE_ENV=development skulle dessutom -# dra in Reacts utvecklingsruntime under prerendering (se siax-io:s Dockerfile, -# där det är uppmätt åt båda hållen). -ENV NODE_ENV=production -# Bygget får inte kräva riktiga secrets: lib/db.ts och lib/stripe.ts är lata och -# env-skyddade, och middleware.ts:s startgrind (assertAuthConfigSafe) klagar bara -# om AUTH_ENFORCE uttryckligen sätts till "false" i produktion. Verifierat: hela -# bygget går igenom utan en enda env-variabel satt. -RUN pnpm run build - -# ---- runtime ----------------------------------------------------------------- -FROM node:22-alpine AS runtime -WORKDIR /app - -# `apk upgrade` hämtar Alpines senaste patchade paket (t.ex. libssl3/libcrypto3 — -# bastaggen släpar efter uppströms säkerhetsfixar) utan att röra den pinnade -# Node-versionen. wget behövs för HEALTHCHECK: bara node:*-alpine har varken -# curl eller wget. -RUN apk add --no-cache libc6-compat wget && apk upgrade --no-cache - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -# standalone-utdatan kopierar varken public/ eller .next/static — de måste med -# explicit, annars serveras appen utan statiska assets. -COPY --from=build --chown=node:node /app/public ./public -COPY --from=build --chown=node:node /app/.next/standalone ./ -COPY --from=build --chown=node:node /app/.next/static ./.next/static - -# sharp läggs BREDVID standalone-utdatans egna node_modules (next/react/react-dom). -# Docker slår ihop katalogerna, så inget skrivs över. `require('sharp')` från -# next/dist/... löses ut genom att vandra uppåt till /app/node_modules/sharp. -COPY --from=sharp --chown=node:node /sharp/node_modules ./node_modules - -# Bas-imagen node:22-alpine skeppar npms egen globala installation -# (/usr/local/lib/node_modules/npm) med ett eget beroendeträd som regelbundet -# bär kända HIGH/CRITICAL-CVE:er, helt utan koppling till den här appens kod. -# Runtime-steget kör `node server.js` — varken RUN, CMD eller HEALTHCHECK här -# anropar npm eller npx — så trädet kan tas bort i stället för att accepteras. -RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx /usr/local/share/man/man1/npm* - -USER node - -EXPOSE 3000 - -# LIVENESS. /api/health svarar 200 så länge processen kan svara — rätt signal för -# "starta om containern". Peka INTE hit en readiness-probe: /api/ready svarar 503 -# tills DATABASE_URL är satt och Postgres är nåbar, och en HEALTHCHECK mot den -# skulle få Coolify att döda en container som fungerar precis som avsett. -HEALTHCHECK --interval=30s --timeout=5s --start-period=25s --retries=3 \ - CMD wget -qO- http://127.0.0.1:3000/api/health >/dev/null 2>&1 || exit 1 - -CMD ["node", "server.js"] diff --git a/IMPLEMENTATION_REPORT.md b/IMPLEMENTATION_REPORT.md deleted file mode 100644 index 263286a..0000000 --- a/IMPLEMENTATION_REPORT.md +++ /dev/null @@ -1,315 +0,0 @@ -# IMPLEMENTATION_REPORT.md — P010 Data-OS completion (sax3l/0pt) - -**Branch:** `feat/p010-data-os-completion` (renamed mid-pass from -`feat/p010-source-registry-persistence` once scope grew beyond persistence -alone). -**Base:** `origin/main` @ `f8bb0d5` (verified in sync via `git fetch --prune` -before starting; `origin` = `git.siax.io/sax3l/0pt`, the canonical remote per -`CLAUDE.md` — the GitHub copy was not touched). -**Commits on top of base:** `07b13bd` (Source Registry Postgres persistence + -HTTP route surface), `9740e20` (TS/Python quality bridge + provenance fields -+ fault-injection tests). -**Report date:** 2026-09-01. Written after a from-scratch verification pass -(fresh clone, fresh `pnpm install`, fresh Python venv — not a rerun of the -working tree that built the branch), so the numbers below are reproducible, -not just remembered. - -This report follows `BEFORE_STATE.md`'s classification scale: `VERIFIED` / -`IMPLEMENTED_NOT_VERIFIED` / `DOCUMENTED_ONLY` / `STALE` / `BROKEN` / -`UNKNOWN`. Read `BEFORE_STATE.md` first — this report only describes the -delta from it. - ---- - -## 1. What actually changed, mapped to BEFORE_STATE.md's 5 gaps - -### §1 Source Registry persistence — `VERIFIED` closed - -- New `src/repositories/*` layer: `SourceRepository` / - `SourceRegistryRepository` interfaces, each with an `in-memory` and a - `postgres` implementation (`postgres-source-repository.ts`, - `postgres-source-registry-repository.ts`). `src/repositories/db.ts` is a - thin pooled-client wrapper honoring `resolveStoreMode` - (`src/platform/dependency-mode.ts`) — the same guard BEFORE_STATE.md noted - as already blocking production boot without a DB; it now has a Postgres - implementation to fall through to instead of only throwing. -- `sql/002_source_registry.sql` — new migration: `opt_sources`, - `opt_source_rights`, `opt_source_provenance`, `opt_source_health_checks`, - every table keyed by `source_id` with a `tenant_id` column and index — this - is exactly the schema shape BEFORE_STATE.md §1 derived as "what Postgres - migration would require" but said did not exist. It now exists and is - applied. -- `src/services/source-service.ts` and `source-registry-service.ts` - rewritten to take a repository (constructor-injected), closing the - **per-instance-Map** defect BEFORE_STATE.md flagged as worse than plain - in-memory: every route module now shares one repository-backed instance - (`src/services/source-registry.ts` is the new shared-singleton wiring), - not a fresh `Map` per `new SourceService()`. -- New route surface closing BEFORE_STATE.md's "effectively zero HTTP routes" - finding: `app/api/0pt/sources/route.ts` (create/list — did not exist - before), `[sourceId]/route.ts` (get/describe — did not exist before), - `[sourceId]/rights`, `[sourceId]/provenance`, `[sourceId]/health`, - `[sourceId]/freshness` (all new). `[sourceId]/query` and - `rate-policies/*` (pre-existing routes) were touched only to repoint them - at the shared singleton instead of constructing their own service. -- `tests/p0/source-registry-postgres.realinfra.test.ts` (340 lines, 7 - tests) — a **real-Postgres** suite, not a mock. It self-skips honestly - (not fabricating pass) when no reachable Postgres is configured for the - run; see §2 (Testing) below for what this pass actually observed. - -**What is still NOT true, stated plainly:** in-memory mode is still the -default when no `DATABASE_URL`/Postgres is configured — this pass added a -real persistence *option*, it did not remove the in-memory path (removing it -would break every existing unit test and any deployment without Postgres -wired yet). `resolveStoreMode` still hard-blocks production boot without a -real DB configured, which is correct behavior, not a residual bug. - -### §2 TS/Python split-brain — `VERIFIED` bridged (additive, advisory) - -- **New route surface**, `app/api/0pt/quality/*`: `rules` (list/create), - `rules/[ruleId]` (get/update), `runs` (list/create), `runs/[runId]` (get), - `issues` (list), and `POST /api/0pt/quality/bridge-run` — the entry point - the Python side calls. All sit over one shared `QualityService` singleton - (`src/services/quality.ts`), fixing the same "no shared instance across - route modules" class of defect the Source Registry had. -- `src/quality/quality-bridge.ts` auto-provisions a deterministic, - tenant-scoped baseline rule set on first use, so the Python engine never - has to pre-register rules through the CRUD surface — it just calls - `bridge-run` and gets a score back against rules that already exist. -- **Python side**, `services/api/workers/quality_bridge.py`: an HTTP client - to the route above. - - **Feature-flagged off by default**: `OPT_QUALITY_BRIDGE_ENABLED` unset - or falsy → zero network calls, returns immediately (`status: - "disabled"`). Verified in code (`_enabled()`, `services/api/workers/ - quality_bridge.py:59-60`) and in tests - (`test_disabled_by_default_makes_zero_network_calls`). - - **Fails open, unconditionally.** Every failure mode — bridge URL unset, - connection refused, timeout, non-2xx, malformed JSON — is caught inside - the module and reported back as a `BridgeResult` with a `status` field - (`disabled` / `not_configured` / `unavailable` / `ok`), never raised into - `run_job`. `pipeline.py`'s actual delivery decision - (`delivered = [r for r in deduped if r["confidence"] >= threshold]`, - still the one line BEFORE_STATE.md quoted) is **unchanged** by any - bridge outcome — success or failure. Verified by - `test_bridge_disabled_delivers_identically_to_bridge_enabled_and_failing`, - which asserts the delivered row set is byte-identical between the two - paths. - - **Advisory only, and this is a real constraint, not an excuse:** true - per-row gating against the TS `ExpectationEngine` is not implementable - today because `ExpectationEngine` returns dataset-level pass/fail counts - and sample rows, not a per-row boolean array keyed to the Python - pipeline's record identity. Building that would mean changing - `ExpectationEngine`'s contract — out of scope for this pass, documented - rather than faked with a fabricated per-row mapping. -- **Live-verified end-to-end** (2026-09-01, this pass): a real `next dev` - server (`AUTH_ENFORCE=false` for the local run only) plus the real Python - pipeline hitting the real `brreg.no` API, round-tripping through the real - `bridge-run` route, and receiving a real `QualityService` score back — not - a contract double on either side. This was a manual verification run - during development; it is not itself a repeatable automated test (the - pytest suite below covers the same code paths against `respx`-mocked - bridge responses, which *is* repeatable and is what CI would run). - -**What is still NOT true, stated plainly:** the bridge is **not enabled in -production and this pass does not enable it.** `OPT_QUALITY_BRIDGE_ENABLED` -stays unset/`false` as shipped. Turning it on is a separate, deliberate -decision — see §4 "Activating the bridge" below. Until that decision is -made, the two runtimes' QA logic remains exactly as split as BEFORE_STATE.md -described: Python's one-line confidence filter is what actually decides -delivery; the TS `QualityService`/`ExpectationEngine` only sees a -*copy* of the same data, advisory-only, and only when the flag is on. - -### §3 Provenance fields — `VERIFIED` extended (Python side) - -`services/api/sources/connectors.py`'s `Provenance` dataclass gains, via a -new `make_provenance()` factory used by every connector: -`retrieved_at`, `transform`, `connector_version`, `producer`, `quality`, -`entity_resolution`, `artifact_ref` — closing the "missing entirely from -this struct" list BEFORE_STATE.md §3 itemized (retrieval-method-at-field- -level remains only partially addressed: `connector_version` +`producer` -identify *what ran*, not a full method taxonomy like the TS -`SourceProvenance.method` enum). - -- `resolve.py` stamps `entity_resolution` on **merged** records' provenance - only (never on unmerged ones — verified by - `test_unmerged_record_keeps_entity_resolution_none`), tracking - `agreed_sources`/`merged_from` in a structured field rather than the loose - attribute BEFORE_STATE.md found. -- `pipeline.py` stamps `artifact_ref` at delivery time and `quality` once - (if) the bridge call above succeeds. - -**What is still NOT true, stated plainly:** `bridge_provenance` as a single -named construct **still does not exist** anywhere in this repo, and this -pass does not introduce that name. What exists is still two structs — the -Python `Provenance` dataclass (now richer) and the TS `SourceProvenance` -type (per-source, unchanged this pass) — reconciled only at the one new -seam described in §2 (the bridge-run response, which is advisory and -optional). This pass closes real gaps in the Python struct; it does not -unify the two runtimes' provenance models into one schema. Nothing here -persists past the in-process job response either — `pipeline.py` still has -no DB write of its own (Postgres persistence added this pass is scoped to -the Source Registry, not to order/provenance records). - -### §4 Failure-mode tests — `VERIFIED` extended, with an honest residual - -New pytest + respx suite, `services/api/tests/`: - -| File | Covers | -|---|---| -| `test_resolve_determinism.py` | dedup-determinism under shuffled/interleaved orderings, exhaustive permutations of a mutually-similar set, duplicate-input collapsing without double-counting agreement, out-of-order multi-market arrival, entity-resolution provenance stamping — **and** an explicit, passing test of the one case BEFORE_STATE.md flagged as a real, plausible order-sensitivity risk: a non-transitive-similarity triangle in the greedy clusterer genuinely *can* cluster differently by input order. That test (`test_a_non_transitive_triangle_can_cluster_differently_by_order`) asserts the order-sensitivity, it does not paper over it — `resolve.py`'s greedy-merge algorithm was not rewritten to remove this property, since doing so is an algorithm change out of this pass's scope. | -| `test_pipeline_fault_injection.py` | schema-evolution (upstream adding/removing/reshaping fields via respx-mocked responses through the real `brreg` connector + `pipeline.py`), restart/connector-crash resilience (a connector exception, including one raised by `respx` itself, never crashes `run_job`; every connector failing delivers zero rows, not a crash), backfill-replay (replaying the same order config is byte-identical — this is the property that actually holds given there is still no persistence layer for orders, not a fabricated "dedup store" test). | -| `test_quality_bridge.py` | all bridge states above (disabled / not-configured / unavailable / timeout / non-2xx / malformed / connection-refused / success), plus the "wired into the pipeline" tests proving delivery is unaffected either way. | -| `test_connectors_provenance.py` | the `make_provenance()` factory populates every new field honestly and leaves the rest `None` rather than fabricating values; a real parsing path (`brreg`) produces a fully populated provenance record. | -| `test_known_gaps.py` | **2 explicit, documented skips** — not silently dropped, not faked: CDC-interruption and true delivery-store backfill idempotency are not implementable against this codebase, because **no CDC pipeline and no persistence layer for orders/deliveries exists anywhere in `services/api`.** Testing either would require building the infrastructure the test claims to fail-inject against, which is a different, larger piece of work than this pass. | - -Net: of BEFORE_STATE.md §4's four **entirely absent** categories -(schema-evolution, CDC-interruption, out-of-order, dedup-determinism), three -are now genuinely covered (schema-evolution, out-of-order, -dedup-determinism) and one (CDC-interruption) is honestly documented as not -implementable rather than covered. Tenant-isolation (already the -best-covered category) is untouched by this pass. Quality-gate coverage -gains the bridge-interaction tests above but the production Python filter -itself (`delivered = [...]`) still has no line-level unit test isolated -from an end-to-end pipeline run — this pass tests it via the fault-injection -and bridge suites' end-to-end assertions, not a standalone unit test of that -one line. - -### §5 1P/ST0RE/N0D/AUD0/CL0UD integration — unchanged this pass - -Not touched. BEFORE_STATE.md's classification stands: ST0RE + N0D -real-contract (ST0RE live-verified once, historically); AUD0 real-contract -client but unused by the live Python pipeline (still true — `quality_bridge.py` -does not emit AUD0 events, and neither does the rest of `services/api`); -CL0UD real-contract but not platform-wide-enforced; 1P documented-only. - ---- - -## 2. Testing — from-scratch verification (this pass, not a rerun) - -To avoid reporting numbers from the working tree that built the branch, this -verification was run against a **fresh `git clone`** of the pushed branch -(`feat/p010-data-os-completion` @ `9740e20`) in an isolated directory, with a -fresh `pnpm install --frozen-lockfile` and a fresh Python venv — no reuse of -node_modules, .next cache, or an existing venv from development. - -| Check | Result | Note | -|---|---|---| -| `pnpm install --frozen-lockfile` | OK | lockfile v9, no resolution drift | -| `pnpm run typecheck` (`tsc -p tsconfig.check.json`, **CI-blocking**) | **0 errors** | Clean on the pristine clone. The working directory this branch was developed in separately contains 5 **untracked, uncommitted** paths (`src/fleet/`, `src/graph/`, `src/metrics/`, `src/projections/`, `src/features/feature-registry.ts`) that were already present and flagged, unaudited, in `BEFORE_STATE.md` before this pass started. They are not part of any commit on this branch or on `main` — confirmed by `git diff --stat origin/main..feat/p010-data-os-completion` touching none of them, and by their total absence from the fresh clone. Running `pnpm typecheck` inside the *original, non-clean* working directory does surface 14 TS errors from those 5 stray local files; that is local working-tree litter, not something CI or a fresh clone will ever see, and this pass does not touch, fix, or commit any of those files. | -| `pnpm run test` (vitest, **CI-blocking**) | **256/256 passed, 18 skipped** (34 files passed, 3 files skipped) | Skips are the pre-existing `.realinfra`/`.integration` suites that self-skip honestly when no live ST0RE/OpenSearch/ClickHouse is reachable from this environment (`[realinfra] ST0RE ... not reachable this run (HTTP 401) -- skipping honestly, not fabricating success` — actual stderr from this run). 249 of the 256 predate this branch; 7 are new (`tests/p0/quality-bridge.test.ts`). | -| `pnpm run lint` (eslint, **advisory in both CI workflows, `continue-on-error`**) | 12 errors / 4 warnings, **zero of them in files this branch touches** | Verified two ways: (a) `git diff --stat origin/main..feat/p010-data-os-completion` on every flagged file returns no output — byte-identical to `main`; (b) ran `pnpm lint` against a worktree checked out at `origin/main` directly and got the identical 12/4. This is 100% pre-existing legacy noise (mostly `@typescript-eslint/no-explicit-any` in v0-generated adapters/tests), not introduced or worsened by this pass. | -| `pnpm run build` (`next build`, **CI-blocking in `.gitea/workflows/ci.yml`**; not run by `.github/workflows/ci.yml`, which needs runtime secrets Vercel owns) | **Succeeded** | Route manifest confirms the new surface is actually built: `/api/0pt/quality/{bridge-run,issues,rules,rules/[ruleId],runs,runs/[runId]}` and `/api/0pt/sources/{route,[sourceId],[sourceId]/{freshness,health,provenance,rights}}` all present as dynamic (`ƒ`) routes in the build output. | -| `python -m pytest -v` (`services/api`, fresh venv from `requirements-dev.txt`) | **28 passed, 2 skipped** | The 2 skips are `test_known_gaps.py`'s documented CDC/backfill-idempotency gaps (§4 above) — `pytest.skip(reason=...)`, not silent omission. **Not currently wired into either `.gitea/workflows/ci.yml` or `.github/workflows/ci.yml`** — both CI workflows' own comments say the Python engine "is gated separately if/when a Python workflow is added." This pass did not add that workflow (out of P010's stated scope); the 28/28 above is a manual verification, not an automated CI gate today. Flagging this explicitly as a residual gap, not glossing over it. | - -**Net CI-blocking-gate status: all green** (typecheck, vitest, `next build` -in Gitea CI; typecheck + vitest in the GitHub CI mirror). Lint is advisory -everywhere and pre-existing failures are unrelated to this branch. Python -tests are green but not yet gated by any CI workflow — see the Gap list -below. - ---- - -## 3. Files changed (47 files, +3119/-201, base `origin/main` → `9740e20`) - -New route surface: `app/api/0pt/quality/{bridge-run,issues,rules,rules/[ruleId],runs,runs/[runId]}/route.ts`, -`app/api/0pt/sources/{route.ts,[sourceId]/route.ts,[sourceId]/{freshness,health,provenance,rights}/route.ts}`. -New Python: `services/api/workers/quality_bridge.py`, `services/api/pytest.ini`, -`services/api/requirements-dev.txt`, `services/api/tests/test_{connectors_provenance,known_gaps,pipeline_fault_injection,quality_bridge,resolve_determinism}.py`. -New TS repositories/migration: `src/repositories/*` (7 files), `sql/002_source_registry.sql`. -New TS quality bridge: `src/quality/quality-bridge.ts`, `src/services/quality.ts`, `src/services/source-registry.ts`. -Modified: `services/api/sources/connectors.py`, `services/api/workers/{pipeline,resolve}.py`, -`src/services/{source-registry-service,source-service,source-rate-policy-service}.ts`, -`src/validation/0pt-schemas.ts`, `src/http/siax-error-response.ts`, `.gitignore` -(added `.venv/`, `.pytest_cache/`), `sql/004_iceberg_write_boundary.sql` (minor). -Test files modified: `tests/p0/source-registry.test.ts` (repointed at shared -singleton), new `tests/p0/quality-bridge.test.ts`, -`tests/p0/source-registry-postgres.realinfra.test.ts`. - -No changes to: `services/scrapers/` (the still-unmerged WAF-bypass deletion -PR — untouched, not this pass's scope), the `app/`-root Next.js UI pages -(`bestall/`, `katalog/`, `portal/`, etc. — untouched), any of the 5 -untracked stray paths in the working tree. - ---- - -## 4. Activating the pipeline bridge — Simon's decision, not made here - -**`OPT_QUALITY_BRIDGE_ENABLED` ships unset (default `false`) and this pass -does not flip it, anywhere, including in any deploy config, `.env.example`, -or CI workflow.** This affects real customer orders once on — enabling it -in production is a separate decision, deliberately left to Simon. - -To turn it on later, the following must all be true first (this pass built -the mechanism, not the readiness): - -1. `OPT_QUALITY_BRIDGE_URL` (or `OPT_QUALITY_BRIDGE_EDGE_URL`, per - `quality_bridge.py`'s `_bridge_url()`) must point at a reachable, - authenticated `0pt.app` deployment — the Python engine and the Next.js - edge app are typically different deploy targets (VPS vs. Vercel/SIAX - Deploy Platform per the `siax-0pt` skill's two-plane model), so this is a - real cross-service network dependency, not a same-process flag flip. -2. Even fully enabled and reachable, the bridge remains **advisory-only by - design** — it stamps a `quality` field on delivered provenance and - surfaces a score; it does **not** and, per §2's `ExpectationEngine` - contract gap, currently **cannot** change which rows Python's - `delivered = [...]` line keeps or drops. Anyone enabling the flag - expecting stricter row-level gating will not get it from this code as - shipped — that would require the `ExpectationEngine` contract change - noted above, which is future work. -3. Recommended rollout, not yet executed: enable against one non-production - tenant/order first, confirm `bridge-run` round-trips correctly (the - manual live-verification in §1/§2 above did this once, ad hoc, during - development — it was not repeated against a staging environment as a - formal rollout step), then decide with Simon whether "advisory-only, - fails open" is the intended production posture or whether the per-row - gating gap needs to close first. - -**This pass leaves the flag off. No default, config file, or workflow in -this PR turns it on.** - ---- - -## 5. Residual gaps (honest, not exhaustive elsewhere in this doc) - -- Python engine tests (28/28) are not wired into either CI workflow yet — - a `.gitea/workflows/`/`.github/workflows/` Python job is future work, not - part of this pass. -- `bridge_provenance` as a single unified construct still does not exist; - this pass narrows but does not close the TS/Python provenance-schema gap - (§3 above). -- Per-row quality gating from the TS engine into the Python pipeline is not - implementable against the current `ExpectationEngine` contract (§2, §4 - above) — advisory-only is the ceiling of what this pass could honestly - build, not a corner cut. -- `resolve.py`'s greedy merge remains genuinely order-sensitive for - non-transitive-similarity triangles — tested and documented, not fixed - (fixing it is an algorithm change, out of scope here). -- CDC-interruption and true delivery-store backfill-idempotency tests remain - unimplementable because the underlying infrastructure (a CDC pipeline, a - delivery persistence store) does not exist anywhere in `services/api` — - documented via `pytest.skip` with reasons in `test_known_gaps.py`, not - silently dropped. -- 5 untracked/unaudited local paths (`src/fleet/`, `src/graph/`, - `src/metrics/`, `src/projections/`, `src/features/feature-registry.ts`) - remain exactly as found — not committed, not deleted, not audited by this - pass. They are not part of this PR's diff and a fresh clone never sees - them. -- `services/scrapers/` (WAF-bypass stack) remains on `main`, unmerged - deletion PR still open — unrelated to and untouched by this pass, flagged - again here only because `BEFORE_STATE.md` flagged it and it has not - changed. - ---- - -## 6. Process notes - -- No PR was open before this pass; one was opened as part of this pass (see - PR URL reported separately / in `PRODUCTION_EVIDENCE.json`). -- `BEFORE_STATE.md` and the 5 pre-existing untracked/unaudited paths were - left untouched and uncommitted throughout, exactly as found at the start. -- `.venv/` and `.pytest_cache/` (both created fresh under `services/api/` - while running the Python suite) were added to `.gitignore` so they are - never accidentally committed; no leftover Python processes. -- The `next dev` server and any background verification process used for - the live end-to-end bridge check (§1/§2) were both confirmed stopped - before this pass ended — no lingering listeners. diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7f3dc55..0000000 --- a/LICENSE +++ /dev/null @@ -1,15 +0,0 @@ -Copyright (c) 2026 SIAX Technology AB. All rights reserved. - -0pt (SIAX Core Platform: Data OS) is proprietary software and a confidential -product of SIAX Technology AB. It is provided for internal use within SIAX -Technology AB and its authorized affiliates only. - -No part of this software or its source code may be copied, modified, merged, -published, distributed, sublicensed, or sold outside of SIAX Technology AB -without the prior written permission of SIAX Technology AB. - -Unless required by applicable law or agreed to in writing, this software is -provided "AS IS", without warranties or conditions of any kind, either express -or implied. - -This is not an open-source license. diff --git a/Makefile b/Makefile deleted file mode 100644 index 2e77499..0000000 --- a/Makefile +++ /dev/null @@ -1,37 +0,0 @@ -# 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 deleted file mode 100644 index d255f06..0000000 --- a/ONBOARDING.md +++ /dev/null @@ -1,65 +0,0 @@ -# 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/PLAN/MASTERPLAN_INDEX.md b/PLAN/MASTERPLAN_INDEX.md deleted file mode 100644 index cdc8382..0000000 --- a/PLAN/MASTERPLAN_INDEX.md +++ /dev/null @@ -1,20 +0,0 @@ -# 0pt — Masterplan-index (SIAX Non-Platform Masterplan) - -> **AI-session i detta repo?** Börja i [siax-masterplan → EXECUTE.md](https://git.siax.io/sax3l/siax-masterplan/src/branch/main/EXECUTE.md) - -**Repo:** `0pt` · **Domän:** Data OS (data engine, sourcing/enrichment) · **Kategori:** SIAX Non-Platform masterplan - -**Statusvokabulär:** MISSING → SCAFFOLDED → IMPLEMENTED → WIRED → INTEGRATION_TESTED → LIVE_VERIFIED → PRODUCTION_READY. Legacy = `LEGACY`, bort först när ersättaren är live (ingen dubbel sanning). - -## Låsta arkitekturprinciper (gäller detta repo) - -1. **En canonical owner per domän och sanning** — detta repo äger sin - domän-sanning; andra ytor konsumerar via API/event/SDK, aldrig egen SQL. -2. Inga production-mock-fallbacks. Inga lokala fallback-sanningar. -3. `tenant_id`/`actor_id` server-resolved; ingen frontend-tenant-trust. -4. Allt API-first: kontrakt först (openapi.yaml), sedan surfaces. - -> Detta är en stub skapad av conformance-sweep mot `siax-standard` -> (kriterium: masterplan-lock + PLAN-index). Fyll på med repo-specifik -> planstatus — redigera inte låst principerna. Kanonisk källa: -> [sax3l/siax-masterplan](https://git.siax.io/sax3l/siax-masterplan). \ No newline at end of file diff --git a/PRODUCTION_EVIDENCE.json b/PRODUCTION_EVIDENCE.json deleted file mode 100644 index c92895c..0000000 --- a/PRODUCTION_EVIDENCE.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "report": "PRODUCTION_EVIDENCE.json", - "companion_doc": "IMPLEMENTATION_REPORT.md", - "generated_at": "2026-09-01T21:10:00+02:00", - "repo": { - "remote": "https://git.siax.io/sax3l/0pt.git", - "canonical_remote_per_claude_md": "git.siax.io/sax3l/0pt", - "note": "A second remote (github.com/sax3l/0pt) also claims to be this repo per CLAUDE.md; explicitly unresolved and left to Simon. This pass worked only against git.siax.io." - }, - "branch": { - "name": "feat/p010-data-os-completion", - "renamed_from": "feat/p010-source-registry-persistence", - "base_branch": "main", - "base_commit": "f8bb0d5ab9f35c652d09f7639d20458cfe849f56", - "head_commit": "9740e20818a8ce0a90498bdb12c501bc74395ac", - "commits_on_branch": [ - { - "sha": "07b13bd8331017462ead11b9b2ce8fa9188e520f", - "summary": "feat(data-os): Source Registry Postgres persistence + full HTTP route surface" - }, - { - "sha": "9740e20818a8ce0a90498bdb12c501bc74395ac", - "summary": "feat(data-os): TS/Python quality bridge + provenance fields + fault-injection tests" - } - ], - "pushed_to_origin_this_pass": true, - "diffstat_vs_base": { - "files_changed": 47, - "insertions": 3119, - "deletions": 201 - } - }, - "pull_request": { - "url": "https://git.siax.io/sax3l/0pt/pulls/28", - "number": 28, - "base": "main", - "head": "feat/p010-data-os-completion", - "state_at_report_time": "open", - "opened_this_pass": true - }, - "verification_methodology": { - "description": "All numbers below were produced by a from-scratch verification pass: a fresh `git clone` of the pushed branch into an isolated directory, a fresh `pnpm install --frozen-lockfile`, and a fresh Python venv built from requirements-dev.txt. This is deliberately NOT a rerun inside the original working tree that developed the branch, to avoid reporting stale or environment-contaminated results.", - "clean_clone_path_used_for_verification": "isolated scratch directory, discarded after this pass", - "original_working_tree": "/home/simon/siax-platforms-work/0pt" - }, - "checks": [ - { - "name": "pnpm install --frozen-lockfile", - "result": "ok", - "note": "lockfileVersion 9, no resolution drift from the committed pnpm-lock.yaml" - }, - { - "name": "typecheck", - "command": "pnpm run typecheck", - "underlying": "tsc -p tsconfig.check.json (src/**/*.ts + tests/**/*.ts)", - "ci_gate": "blocking", - "result": "pass", - "errors": 0, - "note": "Zero errors on the pristine clone. The original dev working tree separately contains 5 untracked/uncommitted stray paths (src/fleet/, src/graph/, src/metrics/, src/projections/, src/features/feature-registry.ts) that were already present before this pass (flagged in BEFORE_STATE.md as unaudited) and are NOT part of any commit on this branch or on main -- confirmed via `git diff --stat origin/main..feat/p010-data-os-completion` touching none of them, and via their total absence from the fresh clone. Running the same command inside that non-clean working tree surfaces 14 TS errors from those 5 files; that is local litter CI will never see, not a defect in this PR." - }, - { - "name": "unit/integration tests (TypeScript)", - "command": "pnpm run test", - "underlying": "vitest run", - "ci_gate": "blocking", - "result": "pass", - "test_files_passed": 34, - "test_files_skipped": 3, - "tests_passed": 256, - "tests_skipped": 18, - "tests_new_this_branch": 7, - "new_test_file": "tests/p0/quality-bridge.test.ts", - "skip_reason": "pre-existing .realinfra/.integration suites self-skip honestly when no live ST0RE/OpenSearch/ClickHouse is reachable from the verification environment", - "sample_skip_stderr": "[realinfra] ST0RE (https://api.st0re.siax.io) not reachable this run (ST0RE responded HTTP 401) -- skipping honestly, not fabricating success." - }, - { - "name": "lint", - "command": "pnpm run lint", - "underlying": "eslint src tests", - "ci_gate": "advisory (continue-on-error in both .gitea/workflows/ci.yml and .github/workflows/ci.yml)", - "result": "pre-existing failures only, zero introduced by this branch", - "errors": 12, - "warnings": 4, - "verification": [ - "git diff --stat origin/main..feat/p010-data-os-completion on every flagged file returns no output -- byte-identical to main", - "ran `pnpm lint` against a worktree checked out directly at origin/main and got the identical 12 errors / 4 warnings" - ] - }, - { - "name": "build", - "command": "pnpm run build", - "underlying": "next build", - "ci_gate": "blocking in .gitea/workflows/ci.yml; not run by .github/workflows/ci.yml (needs runtime secrets Vercel owns)", - "result": "pass", - "note": "Route manifest confirms new surface is built: /api/0pt/quality/{bridge-run,issues,rules,rules/[ruleId],runs,runs/[runId]} and /api/0pt/sources/{route,[sourceId],[sourceId]/{freshness,health,provenance,rights}} all present as dynamic routes." - }, - { - "name": "Python engine tests", - "command": "python -m pytest -v", - "cwd": "services/api", - "environment": "fresh venv, python3.12, requirements-dev.txt", - "ci_gate": "NOT WIRED INTO CI -- both .gitea/workflows/ci.yml and .github/workflows/ci.yml state the Python engine is gated separately if/when a Python workflow is added; this pass did not add that workflow", - "result": "pass", - "tests_passed": 28, - "tests_skipped": 2, - "skip_detail": [ - { - "test": "test_cdc_interruption_not_implementable_against_real_code", - "reason": "no CDC pipeline exists anywhere in services/api" - }, - { - "test": "test_true_backfill_idempotency_not_implementable_against_real_code", - "reason": "no delivery/order persistence layer exists anywhere in services/api" - } - ], - "new_test_files": [ - "services/api/tests/test_connectors_provenance.py", - "services/api/tests/test_known_gaps.py", - "services/api/tests/test_pipeline_fault_injection.py", - "services/api/tests/test_quality_bridge.py", - "services/api/tests/test_resolve_determinism.py" - ] - } - ], - "net_ci_status": { - "gitea_ci_blocking_gates": { - "typecheck": "green", - "test": "green", - "build": "green" - }, - "github_ci_blocking_gates": { - "typecheck": "green", - "test": "green" - }, - "advisory_only_not_blocking": ["lint (both workflows)", "typecheck:app (github only)"], - "unwired_gate_not_in_ci": ["python pytest (services/api) -- manually verified green, no automated workflow yet"] - }, - "production_feature_flags": { - "OPT_QUALITY_BRIDGE_ENABLED": { - "default_shipped_value": "unset (treated as false)", - "changed_by_this_pr": false, - "activated_in_production_by_this_pass": false, - "behavior_when_off": "quality_bridge.py makes zero network calls, returns status=disabled immediately; pipeline.py's delivery decision is completely unaffected", - "behavior_when_on_but_bridge_unreachable": "fails open -- status=unavailable, delivery decision unaffected, error never raised into run_job", - "behavior_when_on_and_reachable": "advisory only -- stamps a quality field on delivered provenance and records a score; does NOT and, per the current ExpectationEngine contract, currently CANNOT change which rows pipeline.py's confidence-threshold filter keeps or drops", - "decision_owner": "Simon", - "readiness_checklist_before_enabling": [ - "OPT_QUALITY_BRIDGE_URL / OPT_QUALITY_BRIDGE_EDGE_URL must point at a reachable, authenticated 0pt.app deployment (cross-service network dependency -- Python engine and Next.js edge are typically different deploy targets)", - "accept that this remains advisory-only (no per-row gating) unless the ExpectationEngine contract is separately extended to return per-row results", - "recommended: enable against one non-production tenant/order first and confirm bridge-run round-trips before any production enablement" - ], - "manual_live_verification_performed_this_pass": { - "what": "real `next dev` server (AUTH_ENFORCE=false, local only) + real Python pipeline hitting real brreg.no + real bridge-run round-trip + real QualityService score returned", - "when": "during development of commit 9740e20, 2026-09-01", - "repeatable_automated_equivalent": "services/api/tests/test_quality_bridge.py (respx-mocked bridge responses, run as part of the 28/28 pytest total above)", - "note": "the live run was a one-off manual verification during development, not a formal staging rollout step, and not itself part of the from-scratch CI-mirroring verification in this report" - } - } - }, - "residual_gaps": [ - "Python engine tests (28/28) not wired into any CI workflow yet", - "bridge_provenance as a single unified TS/Python construct still does not exist -- this pass narrows, does not close, the provenance-schema split", - "per-row quality gating from TS into Python is not implementable against the current ExpectationEngine contract -- advisory-only is the ceiling of this pass, not a corner cut", - "resolve.py's greedy merge remains genuinely order-sensitive for non-transitive-similarity triangles -- tested and documented, not fixed (algorithm change, out of scope)", - "CDC-interruption and true delivery-store backfill-idempotency tests remain unimplementable -- no CDC pipeline or delivery persistence layer exists anywhere in services/api", - "5 untracked/unaudited local paths (src/fleet/, src/graph/, src/metrics/, src/projections/, src/features/feature-registry.ts) left exactly as found -- not part of this diff, absent from a fresh clone", - "services/scrapers/ (WAF-bypass stack) remains on main with its deletion PR still open -- unrelated to and untouched by this pass" - ], - "process_notes": { - "pr_open_before_this_pass": false, - "before_state_md_left_untouched": true, - "gitignore_additions": [".venv/", ".pytest_cache/"], - "leftover_processes_confirmed_stopped": true - } -} diff --git a/README.md b/README.md deleted file mode 100644 index f4356eb..0000000 --- a/README.md +++ /dev/null @@ -1,117 +0,0 @@ -# 0pt — your data - -Data services on demand for the Nordics + Europe. Compose a data job, pay 50 % to start, -watch production live, validate with full provenance, deliver anywhere. Owned by **SIAX -Technology AB**. Honest accuracy is the moat: every field carries source, freshness and -confidence; nothing is guessed. - -**Live:** https://0pt.app · **Contract:** [`CLAUDE.md`](CLAUDE.md) + [`docs/`](docs/) - -> **Architecture (verified against code, not aspirational):** -> [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md). -> It documents the real API surface, the real persistence story, and the known -> security gaps — including several places where this README is optimistic. Read -> it before trusting any capability claim below. Repo identity + estate -> classification: [`siax.repo.json`](siax.repo.json) + -> [`siax.config.json`](siax.config.json). Compliance state: -> [`docs/compliance/`](docs/compliance/) (INVENTORY, RELATIONS, OSS-INVENTORY, -> GOLDEN-STANDARD-REPORT, BACKLOG, SLUTRAPPORT). - -## Two planes (see [docs/02-architecture.md](docs/02-architecture.md)) - -- **Edge (Vercel, this app)** — marketing, catalog, the order lifecycle, customer portal, - admin portal, and the API route handlers. Defers to the data engine when `ENGINE_URL` is set. -- **Engine (`services/api`, runs on a VPS)** — FastAPI orchestrator + worker pipeline doing - the **compliant** sourcing (official API → bulk → authorized web → licensed → else level 0). - Never on Vercel (long jobs). Bypass/stealth/proxy-rotation/fake-account paths are excluded - by design (CLAUDE.md §1). - -## Routes - -| Area | Routes | -|---|---| -| Marketing | `/` (hero, filosofi, katalog, motorn, visualiseringar, täckning, editorial, bevis, priser, footer) | -| Catalog | `/katalog`, `/katalog/[id]` | -| Sourcing map | `/kallor` | -| Order lifecycle | `/bestall` → `/bestall/forskott` → `/order/[id]` → `/order/[id]/validera` | -| Customer portal | `/portal`, `/portal/subscriptions`, `/portal/files`, `/portal/integrationer`, `/portal/settings` | -| Admin portal | `/admin`, `/admin/sources`, `/admin/jobs`, `/admin/customers` | -| API | `/api/{health,estimates,catalog,sources,orders,orders/[id],subscriptions,admin/metrics,uploads,auth/*,payments/*,webhooks/stripe,connections/db-grant}` | - -## Design system -Mono/editorial, light, radius 0, Inter + JetBrains Mono, single red `#e40014`, animated grain, -scroll-reveal motion. **No photography** — imagery is rendered data (`DataArtifact`). Every fact -shows a provenance square (`ProvenanceBadge`). Full spec: [docs/05-design-system.md](docs/05-design-system.md). - -## Run locally - -**SIAX toolchain-kontrakt (mise):** - -```bash -mise install # pinnar node 22.13.0 + pnpm 9.15.9 (se mise.toml [tools]) -mise run setup # pnpm install --frozen-lockfile -mise run check # typecheck + lint (snabbt, deterministiskt) -mise run verify # vitest + next build (full svit) -mise run build # deploybar artefakt -``` - -mise är task-entrypoint (`mise.toml`); gate-logiken bor kvar i -`.gitea/workflows/` och package.json — pinning: `.siax/template-pin.json`. - -```bash -pnpm dev # Next.js on http://localhost:3000 - -# data engine (separate, optional) -cd services/api && pip install -r requirements.txt && uvicorn orchestrator.main:app --reload -``` - -## Deploy -- **Config target (as of 2026-08-09):** `siax.config.json` declares `deploy.target: "coolify"`, - matching every other SIAX Cloud OS repo (n0d, st0re, aud0, ...) — a Dockerfile "for Coolify" - (already committed, see [`Dockerfile`](Dockerfile)) plus [`.gitea/workflows/cd.yml`](.gitea/workflows/cd.yml) - (renamed from `deploy.yml` 2026-08-10) that POSTs a restart to a Coolify app UUID on - `COOLIFY_TOKEN` push. -- ~~**Actually live right now:** the only running instance of `0pt.app` is still on **Vercel** - (project `0pt`, scope *Simon Axelsson's projects*) — no Coolify app has been provisioned for - this repo yet, so `deploy.yml`'s app UUID is a placeholder that fails the job on purpose until - someone fills it in. Flipping the config declares intent; it does not move traffic.~~ See - [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) §5/§8 for the full - "Vercel or fleet" state and what's still open (Coolify app provisioning + DNS cutover — a - separate decision from this config change, since the live Vercel deployment is not being - touched). -- **CORRECTED 2026-09-06 — the bullet above is no longer true. The Coolify deploy hook is LIVE.** - The repo secret `COOLIFY_TOKEN` exists (created 2026-08-31T11:34:34Z) and the repo Actions - variable `COOLIFY_APP_ID` holds a real 24-character UUID, **not** the - `TODO-no-coolify-app-provisioned-yet` literal. Both guard conditions in - [`.gitea/workflows/cd.yml`](.gitea/workflows/cd.yml) therefore pass. Measured on the push to - `main` at `1ff34410` (2026-09-06): job `Deploy to Coolify / deploy` (run 11695, job 26295) - finished **success** with `coolify deploy HTTP 200` in its log. **Every merge into `main` now - fires a real redeploy against `cloud.siax.io` — know what you are shipping before you merge.** - **Probed 2026-09-06 08:00 UTC:** `0pt.app` still answers from **Vercel** (`server: Vercel`, `x-vercel-id: arn1::…`) and returns **HTTP 402 `DEPLOYMENT_DISABLED` — "Payment required"** on both `/` and `/api/health`. So the public domain is *down*, DNS has **not** been cut over to Coolify, and the Coolify app — whatever state it is in — serves no `0pt.app` traffic. Two separate facts, both true at once: the deploy hook fires for real, and the product domain is dark behind an unpaid Vercel deployment. - Still not established: whether the Coolify app itself builds and starts — the deploy endpoint - returning 200 only means the request was accepted, not that a container came up healthy. -- **Engine + DB + embeds** → one VPS via [`infra/docker-compose.yml`](infra/docker-compose.yml) (Postgres+pgvector, Redis, api, worker, NocoDB, Metabase, Caddy). -- Env inventory: [`.env.example`](.env.example). -- **Auth on `/api/0pt/*` is fail-closed**: with `AUTH_ENFORCE` unset the middleware - REQUIRES a verified Zitadel Bearer token (401 without one). Only the literal - `AUTH_ENFORCE=false` disables it, and that is refused when `NODE_ENV=production` - (the process throws at startup rather than serving anonymously). - **Caveat (live-verified 2026-08-10):** the deployed instance still answers - `GET /api/0pt` with 200 anonymously — the middleware appears not to run in - production (gap S1, root cause unknown). Do not rely on this being enforced - until an anonymous request gets a real 401. -- **Fonts:** self-hosted via `@fontsource-variable/inter` + - `@fontsource-variable/jetbrains-mono` (no Google Fonts / `next/font/google`). -- **API contract (SSOT):** [`contracts/openapi.yaml`](contracts/openapi.yaml). - -## Status -Frontend, full order lifecycle, customer + admin portals, edge API, payment/auth/embed -infrastructure, the compliant data engine, schema and infra are all in place and building green. -Integrations are env-guarded and activate when their keys/DB are provided: - -1. **Database** — Vercel → Storage → Postgres (sets `POSTGRES_URL`) → durable persistence (schema in [`sql/001_init.sql`](sql/001_init.sql)). -2. **Stripe + Resend** keys → payments + email. -3. **VPS** for the engine → set `ENGINE_URL`. -4. **`AUTH_SECRET` + `DEV_LOGIN_KEY`** → login (full OAuth via Auth.js lands with the DB). - -© 2026 SIAX Technology AB. Not affiliated with Protosell. diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 6fa445c..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,98 +0,0 @@ -# Security Policy - -0pt (SIAX Core Platform: Data OS) is proprietary software operated by SIAX -Technology AB. This document explains how to report a suspected security -vulnerability. - -## Reporting a vulnerability - -Do not open a public GitHub issue for a suspected security problem. - -Email: security@siax.io (fallback: simon@siax.io) - -Include, if possible: -- A description of the issue and the impact you believe it has. -- Steps to reproduce, or a proof-of-concept request/response. -- The route, endpoint, or component affected. -- Whether the issue is already publicly known or exploited. - -## Response expectations - -- Acknowledgement: within 2 business days. -- Initial triage and severity assessment: within 5 business days. -- Remediation timeline: communicated after triage, based on severity. - Confirmed high/critical issues are prioritized ahead of other work. - -We will keep you informed of progress until the report is resolved and, where -appropriate, credit you in internal release notes (no public bug bounty -program exists at this time). - -## Scope - -In scope: -- The 0pt application and its API routes at 0pt.app. -- The 0pt engine/orchestrator services in this repository. -- Infrastructure-as-code and CI/CD configuration in this repository. - -Out of scope: -- Third-party services and dependencies not maintained in this repository - (report those to the respective vendor). -- Denial-of-service testing, spam, or social engineering against SIAX staff - or customers. -- Automated scanning that generates excessive load against production - systems without prior coordination. - -Note: `github.com/sax3l/0pt` (this checkout's `origin`) and -`git.siax.io/sax3l/0pt` (Gitea) are two diverged remotes that share part of -this codebase's history — see README "Known issues" for what's known and -what isn't about the relationship between them. Which one is deployed to -0pt.app is currently unconfirmed; if your finding is remote-specific, say -which remote/branch you tested against. - -## Known, already-tracked gaps - -The following are **not** news to us — they are open, named gaps as of this -pass, not secrets, and reporting them again mainly helps confirm priority -rather than surfacing something new: - -- **Zitadel bearer verification exists in code (fail-closed), but the deployed - instance still answers `/api/0pt/*` anonymously (gap S1).** `src/auth/zitadel.ts` - + `middleware.ts` implement real JWKS-based verification with a fail-closed - default (`assertAuthConfigSafe()` refuses `AUTH_ENFORCE=false` in production), - yet a live probe (2026-08-10) got HTTP 200 without any token. Root cause is - unconfirmed — do not treat the public instance as authenticated until an - anonymous request receives 401. Additionally, no handler reads the verified - tenant headers (`x-0pt-verified-tenant`): the P0 routes under `/api/0pt/*` - (`lakehouse/mappings`, `sources/rate-policies`, `sources/[sourceId]/query`, - `documents/capture`) accept `tenant_id`/`project_id` **from the client-supplied - request body or query string** rather than from a server-bound session — a - violation of this platform's own tenant-isolation rule (tenant_id must never be - trusted from client input). This is an open, prioritized gap (BACKLOG P0-1/P0-2), - not an intentional design. -- **The non-`/api/0pt` API surface has no authentication at all.** Orders, - admin metrics, payments (including the escrow `release` that flips an order to - `delivered`), subscriptions, uploads and DB-grant endpoints are unprotected in - code (live-verified 200 anonymously on `/api/orders` and `/api/admin/metrics`). -- **The WAF-bypass scraper stack is kept deliberately.** `services/scrapers/*` - (`waf_bypass.py`: FlareSolverr, Tor, cloudscraper, curl_cffi, proxy rotation, - fake identities) and `services/infra/docker-compose.data-stack.yml` - (flaresolverr/tor containers) contradict this repo's own compliance docs. - A cleanup PR that surgically removed this stack (PR #7, verified clean, 128/128 - tests) was closed unmerged by the account owner. **The account owner, Simon - Axelsson, was asked directly via an interactive decision prompt on 2026-08-11 - whether to keep or remove this code, and explicitly chose to keep it.** No - business or legal justification beyond "keep it" was recorded — none is - invented here. This is a narrow, scoped exception for the existing 0PT/0pt-motor - sourcing engine; it does not change `CLAUDE.md` §1's general anti-evasion policy, - which remains in force as written unless/until the account owner says otherwise. - Out of scope for deletion — see `docs/compliance/GOLDEN-STANDARD-REPORT.md` - control C6. -- Beyond those items, other authentication gaps on internal API routes may still - exist and are tracked internally pending an intentional, reviewed fix; reporting - any you find is still welcome and helps confirm priority. - -## Disclosure - -Please give us a reasonable opportunity to investigate and remediate an -issue before any public disclosure. We aim to coordinate disclosure timing -with reporters wherever possible. diff --git a/STATUS.md b/STATUS.md deleted file mode 100644 index 7d0939f..0000000 --- a/STATUS.md +++ /dev/null @@ -1,277 +0,0 @@ -# 0pt — capability status (Master-DoD §56 status honesty) - -> Classification scale (Master-DoD §2, exactly one per capability): -> `MISSING → DOCUMENTED_ONLY → SKELETON → MOCK_ONLY → IMPLEMENTED → INTEGRATION_TESTED → LIVE_VERIFIED → PRODUCTION_READY`. -> Only `PRODUCTION_READY` may be called DONE. Docs ≠ implementation. -> Last full audit + re-classification: **2026-08-31** (SIAX Platforms P0 wave 2). - -## How to read this - -- **INTEGRATION_TESTED** here means: verified against a REAL dependency - (live OpenSearch/ClickHouse container, or a recorded wire-contract double - pinned to the sibling repo's real handler code on its `main`) in CI. -- **LIVE_VERIFIED** requires a verification against the actual SIAX fleet - deployment. No new capability in this wave claims it. -- The unit/contract/integration suites are in `tests/` and run in - `.gitea/workflows/ci.yml` (jobs `quality` + `integration`). - -## Cross-product clients (Master-DoD §10 "Cross-product clients") - -| Capability | Status | Evidence | Honest gaps | -|---|---|---|---| -| CL0UD client (policy evaluate, `POST /api/policy/evaluate`) | **INTEGRATION_TESTED** (contract) | `src/clients/cl0ud-client.ts`; `tests/contracts/cl0ud-client.contract.test.ts` pins the real camelCase `{data}` envelope; fail-closed (missing decision ⇒ deny) | Not yet exercised against a live CL0UD deployment (no reachable CL0UD URL this session ⇒ not LIVE_VERIFIED) | -| CL0UD capability discovery (`GET /api/capabilities`) | **INTEGRATION_TESTED** (contract) | same suite | 0pt's own capabilities are not yet registered in CL0UD's registry (§3 gap) | -| ST0RE client (artifact register, camelCase `/v1/st0re/artifacts/register`) | **INTEGRATION_TESTED** (contract) | `src/clients/st0re-client.ts`; `tests/contracts/st0re-client.contract.test.ts` | Raw-bytes upload still goes via LocalBlobStore interim (pre-existing, unchanged); presign upload path not consumed yet | -| N0D client (create/get job, get result) | **INTEGRATION_TESTED** (contract) | `src/clients/n0d-client.ts`; `tests/contracts/n0d-client.contract.test.ts` pins `required_role`/`resource_requirements`/body `idempotency_key` | CloudEvents completion callback receiving endpoint not built in 0pt (poll path works) | -| AUD0 client (events + OpenLineage ingest) | **INTEGRATION_TESTED** (contract) | `src/clients/aud0-client.ts`; `tests/contracts/aud0-client.contract.test.ts`; legacy lineage shape converts to real OpenLineage | No durable local outbox for AUD0 outages (§70 "durable local/outbox path" — emission failure is surfaced, not queued) | -| **No production mock fallback (§3/§10)** | **INTEGRATION_TESTED** | `src/platform/dependency-mode.ts`; `tests/contracts/dependency-mode.contract.test.ts` drives NODE_ENV=production for all four factories ⇒ hard `DEPENDENCY_UNAVAILABLE` | — | - -**Correction of prior state (2026-08-31):** the pre-existing clients' HTTP -paths were verified against the sibling repos' actual `main` branches and -THREE of four were wrong: cl0ud (`/v1/cl0ud/policies/evaluate` — never -existed), st0re (snake_case body — would 400), aud0 lineage -(`/v1/aud0/lineage/events` — never existed). All fixed and pinned by -contract tests. The earlier MOCK_ONLY classification in the README was -accurate. - -## Data-OS capabilities (Master-DoD §10) - -| Capability | Status | Evidence | Honest gaps | -|---|---|---|---| -| Source Registry — register/query/describe/health, rights/license/purpose, freshness, provenance | **IMPLEMENTED** | `src/services/source-registry-service.ts`, `src/types/source-registry.ts`; `tests/p0/source-registry.test.ts` (fail-closed purpose gate, cross-tenant NOT_FOUND) | In-memory persistence (per-process); no Postgres store yet — production boot must therefore configure one before this counts further. No API route surface yet | -| Source rate-policy | **IMPLEMENTED** (pre-existing) | `src/services/source-rate-policy-service.ts` | unchanged this wave | -| Search — OpenSearch adapter, index defs, bulk, tenant + purpose/rights filter, rebuild, generation switch | **INTEGRATION_TESTED** | `src/search/*`; unit `tests/p0/search-service.test.ts`; **real OpenSearch 2.11** `tests/integration/search-opensearch.integration.test.ts` incl. projection-disaster rebuild (§68-style) — runs in CI service container | Hybrid (vector) query not built; no metrics emission; index definitions in-memory | -| Quality — versioned rules, runs, results, issues, score, trend, GE interchange | **IMPLEMENTED** (local engine) / **SKELETON** (GE delegation) | `src/quality/*`; `tests/p0/quality-service.test.ts`, `tests/p0/expectation-engine` cases | Local engine covers 7 GE core expectation types; everything else delegates via N0D job `0pt.quality.ge_run` whose EXECUTOR (0pt-motor GE runner) does not exist yet — delegated runs stay "delegated" until it does. Never fabricates results | -| Dataset — create (pre-existing) + version/query/export with `st0re:artifact:` refs | **IMPLEMENTED** | `src/services/dataset-version-service.ts`; `tests/p0/dataset-version-service.test.ts` (idempotency §53, canonical refs §5) | Export assumes bytes already written by a job; no route surface; in-memory persistence | -| Lineage — OpenLineage emission to AUD0's real ingest | **INTEGRATION_TESTED** (contract) | `src/lineage/*`; aud0 contract suite; emitted from dataset version/export, quality runs, search rebuilds | Emission is best-effort (never-throw); no outbox. AUD0-side ingestion verified against contract double, not live AUD0 | -| Metadata — OpenMetadata projection (never canonical) | **IMPLEMENTED** | `src/metadata/openmetadata-adapter.ts`; `tests/contracts/metadata-features.contract.test.ts`; `projectAll` rebuild path | No live OpenMetadata exercised (no local container this pass); lineage/glossary/ownership projection not built — only dataset entities | -| Analytics — ClickHouse, tenant-isolated, query limits, retention | **INTEGRATION_TESTED** | `src/analytics/clickhouse-analytics.ts`; **real ClickHouse 24.8** `tests/integration/analytics-clickhouse.integration.test.ts` (tenant isolation, closed DSL, TTL schema) — runs in CI service container | Closed metric-DSL only (no ad-hoc queries by design); no quota integration with CL0UD | -| Features — registry + Feast adapter | **IMPLEMENTED** (online path, registry) / **SKELETON** (materialize/get-historical) | `src/features/feast-adapter.ts`; contract suite pins Feast's `get-online-features` wire shape | No live Feast server exercised; offline/materialization are N0D delegation contracts whose executor (0pt-motor) does not exist yet; point-in-time correctness therefore NOT yet proven | -| Entity Resolution — candidate API, blocking, explanation, hard-rule hook, review handoff | **IMPLEMENTED** | `src/er/entity-resolution-service.ts`; `tests/p0/entity-resolution.test.ts` (hard rules override both directions, review band, blocking refusals) | Generic reference scorer (Jaccard+Levenshtein), NOT Splink probabilistic linkage — that is the `0pt.er.batch_resolution` N0D contract, executor pending in 0pt-motor. No persistence of review queues | -| Graph (§10 Graph) | **MISSING** | — | Not started this wave (explicitly out of the delivered P0 subset; next in line per §10) | - -## Canonical platform plumbing - -| Capability | Status | Evidence | -|---|---|---| -| SiaxRequestContext + propagation headers (mirror of cl0ud `@siax/contract`) | **INTEGRATION_TESTED** (contract-pinned) | `src/platform/siax-context.ts`; `tests/contracts/siax-context.contract.test.ts` | -| Canonical 12-code error model + typed `SiaxError` (§6) | **INTEGRATION_TESTED** (contract-pinned) | `src/platform/errors.ts`; same suite | -| Canonical resource refs (§5) | **IMPLEMENTED** | `src/platform/resource-ref.ts`; `tests/p0/dataset-version-service.test.ts` | - -## Pre-existing capabilities (unchanged this wave — prior classifications stand) - -| Capability | Status | Note | -|---|---|---| -| Iceberg write boundary via ST0RE lakehouse | LIVE_VERIFIED (2026-07-13) | `docs/ICEBERG_WRITE_BOUNDARY.md`; untouched | -| Document capture/extraction | IMPLEMENTED | LocalBlobStore interim persists bytes; ST0RE raw upload still pending | -| Edge order/payment app (Next.js) | see `docs/architecture/ARCHITECTURE.md` | outside this wave's scope | -| FastAPI engine (`services/api`) | IMPLEMENTED | outside this wave's scope | -| `services/scrapers/*` (WAF-bypass stack) | **quarantined, pending deletion PR** | `feature/0pt-remove-noncompliant-scrapers` — NOT touched by this wave; nothing new wires it | - -## Global gaps blocking PRODUCTION_READY (§3, tracked, not hidden) - -1. Persistent stores REMAINING: quality runs/rules and feature views are - still per-process in-memory (`resolveStoreMode` guarantees production - boot FAILS rather than serving Maps). CLOSED since the last revision: - dataset registry (`opt_datasets`) + immutable versions - (`opt_dataset_versions`, monotonic under transaction lock, idempotency - with payload-drift detection, cross-tenant ⇒ NOT_FOUND) and versioned - search index definitions (`opt_search_index_definitions`, sequential - schema versions, replay-idempotent, same-version-different-schema ⇒ - CONFLICT; OpenSearch remains a rebuildable projection, never canonical - truth) are Postgres-canonical with real-Postgres tests. -2. Capability registration in CL0UD + API0 discovery for the new - capabilities (§3 "Capability är registrerad i CL0UD Capability - Registry") — PARTIALLY CLOSED: the collaboration/meeting domain was - canonicalized in CL0UD's registry (cl0ud #73/#74); 0pt's own - capability set still needs the same registration. -3. CLOSED: AUD0 outbox for evidence durability under AUD0 outage (§70) — - durable Postgres outbox + dispatcher (at-least-once, bounded batch, - backoff, dead-letter, SKIP LOCKED leasing, tenant binding, idempotency) - + real-Postgres tests; dispatcher uses explicit `durable:false` so it - never re-enqueues its own delivery evidence. -4. CLOSED for datasets/versions/search/ER: `app/api/0pt/datasets*`, - `app/api/0pt/search/definitions` + tenant-safed query route, and - `app/api/0pt/er/{candidates,batch}` (+ internal ER route) are live; - clients cannot raise `rights_tag` via request body (fail-closed to - `internal`). Quality routes exist (`bridge-run/issues/rules/runs`). -5. 0pt-motor executors for the delegated jobs - (`0pt.quality.ge_run`, `0pt.features.*`, `0pt.er.batch_resolution`). -6. Observability: no metrics/traces emitted by the new services (§36). - ---- - -## P060-P068 ingestion plane (added 2026-09-04) - -Classification scale unchanged. Every row below states what was actually -verified, and by which command. - -| Requirement | BEFORE this pass | Status now | Evidence | -|---|---|---|---| -| **P060** Source Registry: durable canonical registry; restart preserves sources; an unclassified source cannot be activated | **PARTIAL / BROKEN.** The registry existed with Postgres persistence (`sql/002_source_registry.sql`, `src/repositories/postgres-source-registry-repository.ts`) and carried source_id/tenant/project/type/status/license/purpose/rights. It had NO authorization, NO schedule, NO checkpoint, NO schema, and no `owner` distinct from `created_by`. The activation DoD was inverted: `postgres-source-repository.ts:70` wrote `${input.status ?? "active"}`, so a source with no rights, no provenance and no authorization was ACTIVE the instant it was registered, and no activation operation existed at all | **INTEGRATION_TESTED** | `sql/007_ingestion_plane.sql` (authorization/schedule/schemas/checkpoints + `ALTER … SET DEFAULT 'pending'` + `owner_id`); `SourceRegistryService.classifySource`/`activateSource`; `tests/p0/source-activation-gate.test.ts` (11); `tests/api/0pt-p060-routes.test.ts` (9); `tests/p0/ingestion-plane-postgres.realinfra.test.ts` against a live Postgres 16 | -| **P061** Connector contract: discover/validate/connect/checkpoint/read/resume/backfill/health/disconnect; two heterogeneous connectors pass one conformance suite | **MISSING.** `services/api/sources/connectors.py` had per-source `fetch_*()` functions only — no lifecycle, no cursor, no resume, no health, no disconnect, no conformance suite | **INTEGRATION_TESTED** | `src/connectors/connector.ts` (all nine operations); `src/connectors/conformance.ts` (15 checks); `HttpJsonConnector` (real node:http server, opaque cursor) and `JsonlFileConnector` (real file, line-offset cursor, CDC deletes) both pass the identical suite — `tests/p0/connector-conformance.test.ts`, which also proves the suite FAILS a fail-open connector | -| **P062** CDC/batch/stream normalized to one envelope; restart resumes from a durable checkpoint without silent gaps | **MISSING**, and documented as such: `services/api/tests/test_known_gaps.py:10-19` skipped the CDC case because no CDC pipeline existed anywhere, and `workers/pipeline.py` had no persistence at all | **INTEGRATION_TESTED** | `src/types/ingestion.ts` (one envelope for all three modes); `src/ingestion/ingestion-runner.ts`; `tests/p0/ingestion-runner.test.ts` kills a run inside the sink and proves the resumed run covers all 10 records exactly once; same proof against real Postgres across fresh processes | -| **P063** Schema versioning + compatibility policy; a breaking schema is detected before consumer corruption | **MISSING.** `schema_version` existed as a free-form string on datasets/Iceberg writes; there was no comparison, no policy, no versioned source schema | **INTEGRATION_TESTED** | `src/schema/compatibility.ts`, `src/schema/schema-registry.ts`; `tests/p0/schema-evolution.test.ts` (12); the runner's schema gate runs before the first read — `tests/p0/ingestion-runner.test.ts` asserts `sinkCalls === 0` on a break | -| **P064** Quality rules behind the SIAX abstraction; failures block or quarantine per declared policy | **PARTIAL.** `src/quality/*` had rules, runs, results, issues, scores and a GE-compatible engine, but a failing rule only opened an ISSUE — the data flowed on regardless. `grep -ri quarantin src/` found one unrelated hit | **INTEGRATION_TESTED** | `src/quality/quality-policy.ts` composes the EXISTING `ExpectationEngine` (not a second engine) and adds the consequence; block / quarantine / warn each proved in `tests/p0/ingestion-runner.test.ts`; quarantine durability proved against real Postgres | -| **P065** Normalization/ER: only the canonical highway; no second simplified production engine | **CLOSED.** `services/api/workers/resolve.py` is now a thin adapter: Python no longer scores identity — it POSTs opaque records to `/api/0pt/internal/er/candidates` and applies ONLY explicit hard-rule `match` decisions (score/fuzzy can never merge records, even from a compromised bridge); canonical engine unreachable ⇒ records return UNMERGED, never fallback-scored. Company semantics `stable-identifiers.v3`: LEI global; org_nr hard identity only within the same registration_country; punctuation normalized; negative stable identity dominates every positive; fuzzy alone ⇒ human review, never auto-merge. Cluster-wide hard-negative constraints: A≈B ∧ B≈C never forces A≈C when canonical identity says A≠C | **INTEGRATION_TESTED** | `tests/p0/entity-resolution.test.ts` (v3 semantics incl. hard-negative transitivity), `tests/p0/entity-resolution-http.test.ts` (internal route fails closed: empty/wrong token ⇒ 401), `services/api/tests/test_resolve_determinism.py` (adapter contract: canonical edges only), shared request validation (`src/er/request-validation.ts`, records 2..5000), blocking pytest gate (`.gitea/workflows/python-engine.yml`) | -| **P066** Iceberg | out of scope — ST0RE is owned by another workflow | **NOT TOUCHED** | Contract read only (`src/services/iceberg-write-service.ts`, `src/clients/st0re-lakehouse-client.ts`) | -| **P067** Lineage: OpenLineage/AUD0 events; source → output lineage is queryable | **PARTIAL.** `src/lineage/lineage-emitter.ts` emitted OpenLineage to AUD0, but nothing in this repo could ANSWER a lineage question — no store, no query, no route | **INTEGRATION_TESTED** | `src/lineage/lineage-edge.ts`, `src/lineage/lineage-store.ts` (local queryable projection; AUD0 stays the system of record), `GET /api/0pt/lineage`; `tests/p0/ingestion-runner.test.ts` queries both directions and proves tenant scoping | -| **P068** Durable bounded backfill/replay; repeated backfill is idempotent | **MISSING**, documented as such: `services/api/tests/test_known_gaps.py:44-52` skipped it because "no delivery-store persistence layer exists to dedupe a backfill against" | **INTEGRATION_TESTED** | `opt_ingestion_ledger` + `IngestionRunner.backfill`; `tests/p0/ingestion-runner.test.ts` (repeat emits 0, live checkpoint untouched, a real content change still re-emits); same against real Postgres | - -### What was actually run (not "should pass") - -- `pnpm run typecheck` — clean. -- `pnpm run test` — **365 passed, 25 skipped** (was 311/18 before this pass). -- `pnpm run build` — clean; the four new routes appear in the route table. -- `pnpm run lint` — 12 errors / 5 warnings, **all pre-existing** (identical - count on the base commit; advisory in CI). This pass adds none. -- Real Postgres 16: `tests/p0/ingestion-plane-postgres.realinfra.test.ts` - with `INGESTION_TEST_DATABASE_URL` — 7 passed. -- **Real container restart**, run manually against that same database: - `docker restart` the Postgres container, then re-read through the real - repositories. Before and after were identical — 2 sources (one `active` - with its owner, one `pending`), 1 checkpoint (cursor 6, 6 committed), - 6 ledger rows, 1 lineage edge, 4 quarantined records, 1 schema version. - That is P060's "a restart preserves all sources", measured rather than - asserted. - -### A real bug this pass wrote and then caught - -The first draft of `IngestionRepository` combined the idempotency check and -its recording into one atomic call, made BEFORE the sink. The crash/resume -test then delivered **7 of 10 records**: the page in flight when the sink -died had already been marked delivered, so the resumed run skipped it as a -duplicate — permanent, silent data loss. The fix is the `peekDeliveries` / -`commitDeliveries` split, with both the ledger commit and the checkpoint -commit after the sink resolves. Recorded here because the failure mode is -the interesting artifact, not the fix. - -### Honest gaps in the new plane (not hidden) - -1. **Delivery is AT-LEAST-ONCE, not exactly-once.** Two concurrent runs on - the same (source, partition) can both peek "new" and both emit. - `envelope_id` is deterministic so a dedup-capable sink survives it. There - is no per-(source, partition) run lock. Do not describe this plane as - exactly-once. -2. **No CDC *source* exists.** The envelope, the operations and the runner - handle change events, and `JsonlFileConnector` emits real deletes from an - `_op` field — but this repo still has no Debezium/WAL/replication-slot - consumer. P062's contract is met; a CDC connector is still MISSING. -3. **0pt does not schedule itself.** `opt_source_schedule` is a declaration - ACT0 would read; there is no cron daemon here. -4. **The sink is caller-supplied.** The runner stops at the sink; the - lakehouse write boundary is ST0RE's (P066, another workflow's repo). -5. **Nothing here is deployed.** See the deployment note below. -6. **`QualityService`'s own rule/run/issue registry is still in-process - Maps** (`src/quality/quality-service.ts:47-50`). The new *enforcement* - path is durable; the older rule registry is not. Unchanged by this pass. -7. **Python tests still run nowhere.** `grep -rn "pytest" .gitea/workflows - .github/workflows` finds nothing — `services/api/tests/*` (9 files) has - never been executed by CI. Flagged, not fixed (it needs a workflow change - and a Python toolchain decision). - -### Deployment reality, measured 2026-09-04 - -`https://0pt.app/api/health` answers **200** from **Vercel** with -`{"db":"not_configured"}`, and `https://0pt.app/api/0pt/sources` answers -**404** — the deployed build predates the entire `app/api/0pt/*` capability -layer. `https://api.0pt.app/health` answers **404** (the FastAPI engine is -not reachable), and `.gitea/workflows/cd.yml` still guards on -`COOLIFY_APP_ID = TODO-no-coolify-app-provisioned-yet`. So: none of the -capability layer — neither this pass's work nor P010/P014's — is running in -production, and P060's durability guarantee cannot be exercised there, -because production has no database configured and does not serve the routes. -Verified with `curl`; not inferred. - -### P065 — closure record (2026-09-07) - -The removal plan below was executed in full and is verified in CI: - -1. ✅ Canonical ER exposed as `POST /api/0pt/er/candidates` (public, - tenant-safed) AND `POST /api/0pt/internal/er/candidates` (service-token, - fails closed without `X-Internal-Token`). -2. ✅ Python CI job: `.gitea/workflows/python-engine.yml` runs - `services/api/tests` as a BLOCKING pytest gate. -3. ✅ `workers/resolve.py` rewritten as the HTTP adapter — fail-closed to - UNMERGED records when the bridge is unreachable; no fallback scorer - remains. Cluster-wide hard-negative constraints prevent transitive - matches from overriding canonical identity decisions. -4. ✅ `services/api/tests/test_resolve_determinism.py` asserts the adapter - contract: canonical match edges merge; review/non-match edges do not; - hard negatives are respected transitively. - -Residual honesty: the PYTHON-side tests run in CI, but the full -adapter→Next.js route chain has not been exercised against a live deployed -edge in this branch (the route's own fail-closed behavior is unit-proven). -That keeps P065 at INTEGRATION_TESTED, not LIVE_VERIFIED. - ---- - -## SLUTPRODUKTIONS-VERIFIERING 2026-09-22 (live-mätt, ej påstått) - -**Prod: `https://0pt.app` på flottan (server6, Coolify-app `shxd6lepxuxfsjsp0ath3z1g`), -container-image på exakt merge-SHA `df6ab70` via den signerade release-kedjan -(cosign-attestation verifierad före redeploy). DNS A → 65.109.19.146, -NS chris/maxine.ns.cloudflare.com — Vercel är borta.** - -### Live-matris, mätt med curl (2026-09-22 04:09 UTC) - -| Yta | Resultat | -|---|---| -| Front `/` + 10 nyckelsidor (bestall, katalog, kallor, tjanster, portal, admin, order) | 200 ×11 | -| `/api/health` | 200 `{ok:true, db:"ok", engine:"ok"}` | -| `/api/ready` | 200 (fail-closed-grinden: var 503 före DB-provisioneringen) | -| `/api/0pt/*` (9 rutter) anonymt | **401 ×9** — S1-luckan (2026-08-07: anonym 200) är STÄNGD | -| `/api/orders`, `/api/admin/metrics` anonymt | 401 | -| `/api/subscriptions` | 200 `[]` (var 500: migration 014, se nedan) | -| `POST /api/estimates` | 200 (edge räknar + engine-wire) | -| `GET /api/catalog`, `/api/sources` | 200 (publika läsvyter, avsiktligt) | -| `GET /api/vehicles` utan param | 400 (kräver reg-nr — korrekt) | -| Security headers | HSTS(63072000+preload) · CSP · XFO DENY · nosniff · ingen X-Powered-By | - -### Denna pass fixat (3 PR, alla mergerade + deployade live) - -1. **PR #80** — G17 hook-pin re-bump `7725498 → a401880` (re-review: 0 diff under - `ci/v2/reference-hooks/`). Release chain + gated deploy gröna igen. -2. **PR #81 (fix för issue #53)**: 7 legacy money-routes (orders, orders/[id], - subscriptions, payments/deposit, payments/release, webhooks/stripe, - admin/metrics) mappar nu `SiaxError(DEPENDENCY_UNAVAILABLE, 503, retryable)` - via `app/api/siax-error-mapping.ts` i stället för att läcka Next opaka 500. - Stripe-webhooken svarar 503 `received:false reason:store_unavailable` på - store-outage — aldrig tyst `received:true` för ett betalt event som inte - processats. 3 nya tester pinar kontraktet. -3. **PR #86**: `sql/014_subscriptions_created_at.sql` — `lib/subscriptions-store.ts` - SELECT:ade `created_at` som `001_init.sql` aldrig definierat (42703 mätt live: - GET /api/subscriptions har ALWAYS varit 500 mot Postgres). Kolonnen ADD:ad - idempotent LIVE på 0pt-edge-postgres; route svarar nu 200 `[]`. -4. **PR #87**: cd-gatens not-ready-regex — cosign 3.1.3 säger - `no matching attestations:` (utan `found`); en transient not-ready misstolkades - som hårt verify-fel. Fail-closed för riktiga attestfel oförändrad. -5. **CI-infra reparerad**: repo-secrets `SIAX_CI_TOKEN` + `REGISTRY_TOKEN` var - döda efter token-svepet (2026-09-22 eng0-introspect-rotation revoade 107+ tokens; - fällan: 0pt:s repo-secret överrider org-secreten — org-PUT räcker inte). Nya - siax-ci-tokens (`write:package,read:package` + all) mintade och satta via - secrets-API (PUT 204, verifierade via lyckad release-kedja). - -### 0pt-motor (engine, satellite) - -- 6 containers Up på server6 (`motor-provisional`), core `/health` ok+pg+redis, - delivery `/version` = fb67870, core revision = 6cfd433 — båda IN main. -- CI grön på main (runs 55985/55987/56004) — healer-issue #38 (run 40562, - 2026-09-20) stängd som transient runner-flake (apt-GPG under last). -- Bara `main` kvar i 0pt och 0pt-motor (8 + 6 stale grenar raderade — alla - bevisade redan mergerade via `git merge-base --is-ancestor`). -- motor/-executor-koden (signals/NER/normalization, 3507 rader nya sedan 6cfd433) - konsumeras av `Dockerfile.motor`-imaget som ännu inte har ett driftsatt runtime - i stacken (core/ingest/resolve/delivery importerar ej motor/ — verifierat - import-graf). Byggs enligt `scripts/build-images.sh` + `verify-image.sh`-gaten - när executor-ytan aktiveras — dokumenterat i `docs/runbook/deploy.md`. - -### Kvarstår (ärligt, ej gömt) - -1. **Stripe-creds** (Simon): live-keys → edge-envs `STRIPE_SECRET_KEY`/`STRIPE_WEBHOOK_SECRET`. - Innan dess förblir payments 501 och issue #67:s staging-triage kan ej köras. -2. **queue: not_configured** — ingen Redis i edge-stacken (ärligt rapporterat). -3. **`0pt.siax.io`/`motor.siax.io`**: medvetet ej återupprättade (0pt.app + lokala - 127.0.0.1-portar är kanoniska ytor; issues #70/#35 stängda/avgjorda därefter). -4. **motor.siax.io**: motor:s publika yta kräver Simon-beslut (traefik-labels + DNS). diff --git a/acquisition.consumer.json b/acquisition.consumer.json deleted file mode 100644 index 0bca0ae..0000000 --- a/acquisition.consumer.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "https://git.siax.io/sax3l/siax-standard/raw/branch/main/schema/siax.acquisition-consumer.schema.json", - "schemaVersion": "siax.acquisition-consumer/v1", - "application": "0pt", - "owner": "0pt", - "tenantScoped": true, - "capabilities": [ - "siax.acquisition.plan", - "siax.acquisition.run", - "siax.acquisition.status", - "siax.acquisition.result", - "siax.acquisition.query_existing", - "siax.acquisition.discovery_search", - "siax.acquisition.extract", - "siax.acquisition.evidence" - ], - "allowedPurposes": [ - "data.acquisition", - "source.registration" - ], - "dataCategories": [ - "public", - "company", - "product", - "pricing", - "vehicle", - "property", - "document", - "event", - "other" - ], - "maxCostSEK": 25, - "reuseExistingFacts": true, - "mustNotOwnEngines": true, - "engines": [] -} diff --git a/analytics/dbt/README.md b/analytics/dbt/README.md deleted file mode 100644 index 1f7e9c8..0000000 --- a/analytics/dbt/README.md +++ /dev/null @@ -1,51 +0,0 @@ -# `analytics/dbt/` — BI-P022 - -The canonical SIAX transformation project. **Nothing in this repository runs -it.** Execution lives in N0D (`n0d#38`), against the contract in -`src/analytics/transform/dbt-contract.ts`. - -## Layers, and why they map onto BI-P003 - -| directory | tier (BI-P003) | may read | must not contain | -| ---------------------- | -------------- | --------------------- | --------------------- | -| `models/sources/` | `RAW` | — (declarations only) | any SQL | -| `models/staging/` | `NORMALIZED` | sources | joins, business logic | -| `models/intermediate/` | `CURATED` | staging | presentation shaping | -| `models/marts/` | `SERVING` | intermediate | direct source reads | - -`assertLayerDependenciesLegal` enforces the "may read" column against the -dependency graph in `manifest.json`. A mart reading a source directly is -refused — that is raw data reaching a dashboard, which is precisely what -BI-P003's tier rule exists to prevent, and a convention nobody checks would not -prevent it. - -## What a run must produce - -`manifest.json` and `run_results.json`, both registered as ST0RE artifacts. -`assertValidDbtJobResult` refuses a result missing either: without them the run -cannot be audited and its tests cannot be ingested by BI-P025's -`DbtQualityProvider`. - -It also refuses: - -- a result whose revision differs from the request — the run is not reproducible; -- a result whose observed dbt version differs from the pin — a runtime that is - not the pinned one silently changes what the models mean; -- `status: success` with **zero nodes executed** — a dbt selector that matches - nothing exits 0, which is the dbt-shaped false green. - -## Version posture - -`DBT_RUNTIME_PIN` records dbt Core 1.10.13 (stable line) with -`dbt-clickhouse` 1.9.2. **Both review flags are `false`.** No licence or -supply-chain review has been performed in this repo, and none is claimed. -`assertDbtRuntimeCertified` refuses a `production` run until they are true, so -BI-P022's "after current license/security verification" is a gate rather than a -sentence in a document. - -## Credentials - -A dbt run never receives a tenant's warehouse credential from this repo. N0D -resolves a credential whose write scope matches `target_layer` and -`expected_target_schema`, and the contract refuses a request whose expected -schema is not scoped to the job's tenant. diff --git a/analytics/dbt/dbt_project.yml b/analytics/dbt/dbt_project.yml deleted file mode 100644 index 59c8a95..0000000 --- a/analytics/dbt/dbt_project.yml +++ /dev/null @@ -1,43 +0,0 @@ -# BI-P022 — the canonical SIAX analytics transformation project. -# -# Execution happens in N0D (n0d#38), never here. This project is the MODELS and -# the CONVENTIONS; src/analytics/transform/dbt-contract.ts is the job contract -# N0D consumes, and it is what refuses a run that is not reproducible. -name: siax_analytics -version: 1.0.0 -config-version: 2 -profile: siax_analytics - -model-paths: ['models'] -test-paths: ['tests'] -snapshot-paths: ['snapshots'] -macro-paths: ['macros'] - -# The four layers map onto BI-P003's tiers rather than inventing a fifth -# vocabulary. `+tags` carries the tier so a manifest reader can assign one -# without parsing paths, and dbt-contract.ts asserts the mapping. -models: - siax_analytics: - +materialized: view - staging: - +schema: staging - +materialized: view - +tags: ['tier:NORMALIZED'] - intermediate: - +schema: intermediate - +materialized: table - +tags: ['tier:CURATED'] - marts: - +schema: marts - +materialized: table - +tags: ['tier:SERVING'] - -snapshots: - siax_analytics: - +target_schema: snapshots - -# `on_schema_change: fail` rather than `sync_all_columns`: a silently widened -# table is how a downstream metric starts averaging a column that did not exist -# yesterday. BI-P004's contract gate is the same rule one layer up. -vars: - siax_tenant_id: '__set_by_n0d__' diff --git a/analytics/dbt/models/intermediate/int_sales_enriched.sql b/analytics/dbt/models/intermediate/int_sales_enriched.sql deleted file mode 100644 index 22efc0b..0000000 --- a/analytics/dbt/models/intermediate/int_sales_enriched.sql +++ /dev/null @@ -1,16 +0,0 @@ --- CURATED (BI-P003). Joins and conformed entities. Reads STAGING only. -{{ config(materialized='table', tags=['tier:CURATED']) }} - -select - e.event_id, - e.tenant_id, - e.customer_id, - e.amount_sek, - e.channel, - coalesce(c.country_code, e.country_code) as country_code, - c.segment, - e.invoiced_at -from {{ ref('stg_sales_events') }} e -left join {{ ref('stg_customers') }} c - on e.customer_id = c.customer_id - and e.tenant_id = c.tenant_id -- the join itself stays tenant-scoped diff --git a/analytics/dbt/models/marts/mart_sales_daily.sql b/analytics/dbt/models/marts/mart_sales_daily.sql deleted file mode 100644 index 229caf7..0000000 --- a/analytics/dbt/models/marts/mart_sales_daily.sql +++ /dev/null @@ -1,17 +0,0 @@ --- SERVING (BI-P003). What a dashboard or a BI-P028 metric reads. --- --- Reads INTERMEDIATE only. Reading a source here would skip two layers, and --- assertLayerDependenciesLegal refuses that against the manifest graph. -{{ config(materialized='table', tags=['tier:SERVING']) }} - -select - tenant_id, - date_trunc('day', invoiced_at) as day, - country_code, - channel, - segment, - count(*) as order_count, - sum(amount_sek) as net_revenue_sek -from {{ ref('int_sales_enriched') }} -where channel != 'internal' -- matches the net_revenue metric's baked filter -group by 1, 2, 3, 4, 5 diff --git a/analytics/dbt/models/marts/schema.yml b/analytics/dbt/models/marts/schema.yml deleted file mode 100644 index a84817d..0000000 --- a/analytics/dbt/models/marts/schema.yml +++ /dev/null @@ -1,19 +0,0 @@ -version: 2 - -models: - - name: mart_sales_daily - description: > - SERVING projection behind the `net_revenue` and `orders` metrics. - A blocking test failure here quarantines the serving projection - (BI-P025); the quarantine is enforced on the READ side. - columns: - - name: tenant_id - tests: [not_null] - - name: day - tests: [not_null] - - name: net_revenue_sek - tests: [not_null] - - name: order_count - tests: - - not_null - - dbt_utils.accepted_range: { min_value: 1, inclusive: true } diff --git a/analytics/dbt/models/sources/sources.yml b/analytics/dbt/models/sources/sources.yml deleted file mode 100644 index 34c152c..0000000 --- a/analytics/dbt/models/sources/sources.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: 2 - -# RAW. Declared, never modelled. A source has no SQL of its own — anything that -# transforms belongs in staging, where BI-P003 assigns it NORMALIZED. -sources: - - name: opt_raw - description: > - Landed, untransformed data as delivered by the 0pt sourcing engine. - RAW tier: no BI surface may read this directly without a recorded - BI-P003 exception. - schema: "{{ var('siax_tenant_id') }}_raw" - loaded_at_field: ingested_at - freshness: - # BI-P026's DELAYED/STALE thresholds, expressed where dbt can act on them. - warn_after: { count: 90, period: minute } - error_after: { count: 180, period: minute } - tables: - - name: sales_events - description: One row per invoiced sales line, as landed. - columns: - - name: event_id - tests: [not_null, unique] - - name: tenant_id - description: Present on every raw row; the tier boundary depends on it. - tests: [not_null] - - name: ingested_at - tests: [not_null] - - name: customers - description: Customer master as landed. - columns: - - name: customer_id - tests: [not_null, unique] - - name: tenant_id - tests: [not_null] diff --git a/analytics/dbt/models/staging/schema.yml b/analytics/dbt/models/staging/schema.yml deleted file mode 100644 index dd4216c..0000000 --- a/analytics/dbt/models/staging/schema.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 - -models: - - name: stg_sales_events - description: Normalized sales events — one row per event_id, latest landing wins. - columns: - - name: event_id - tests: [not_null, unique] - - name: tenant_id - tests: [not_null] - - name: amount_sek - tests: - - not_null - - dbt_utils.accepted_range: - min_value: 0 - inclusive: true - - name: channel - tests: - - accepted_values: - values: ['web', 'retail', 'partner', 'internal'] - - name: stg_customers - columns: - - name: customer_id - tests: [not_null, unique] - - name: tenant_id - tests: [not_null] diff --git a/analytics/dbt/models/staging/stg_customers.sql b/analytics/dbt/models/staging/stg_customers.sql deleted file mode 100644 index 2ef66df..0000000 --- a/analytics/dbt/models/staging/stg_customers.sql +++ /dev/null @@ -1,11 +0,0 @@ --- NORMALIZED (BI-P003). -{{ config(materialized='view', tags=['tier:NORMALIZED']) }} - -select - cast(customer_id as varchar) as customer_id, - cast(tenant_id as varchar) as tenant_id, - cast(name as varchar) as customer_name, - cast(segment as varchar) as segment, - cast(country as varchar) as country_code -from {{ source('opt_raw', 'customers') }} -where tenant_id = '{{ var("siax_tenant_id") }}' diff --git a/analytics/dbt/models/staging/stg_sales_events.sql b/analytics/dbt/models/staging/stg_sales_events.sql deleted file mode 100644 index e1147e7..0000000 --- a/analytics/dbt/models/staging/stg_sales_events.sql +++ /dev/null @@ -1,30 +0,0 @@ --- NORMALIZED (BI-P003). Rename, cast, deduplicate. No joins, no business logic. --- --- The tenant predicate is applied HERE, at the first modelled layer, so every --- downstream model inherits it and no later model has to remember. It is the --- same forced-predicate rule as the query compiler's, one layer up. -{{ config(materialized='view', tags=['tier:NORMALIZED']) }} - -with source as ( - select * from {{ source('opt_raw', 'sales_events') }} - where tenant_id = '{{ var("siax_tenant_id") }}' -), - -deduplicated as ( - select - *, - row_number() over (partition by event_id order by ingested_at desc) as _rn - from source -) - -select - cast(event_id as varchar) as event_id, - cast(tenant_id as varchar) as tenant_id, - cast(customer_id as varchar) as customer_id, - cast(amount as double) as amount_sek, - cast(channel as varchar) as channel, - cast(country as varchar) as country_code, - cast(invoiced_at as timestamp) as invoiced_at, - cast(ingested_at as timestamp) as ingested_at -from deduplicated -where _rn = 1 diff --git a/analytics/dbt/snapshots/snap_customers.sql b/analytics/dbt/snapshots/snap_customers.sql deleted file mode 100644 index d712410..0000000 --- a/analytics/dbt/snapshots/snap_customers.sql +++ /dev/null @@ -1,15 +0,0 @@ --- Slowly-changing customer attributes, so a historical metric can be --- recomputed against what a customer's segment WAS, not what it is now. -{% snapshot snap_customers %} -{{ - config( - target_schema='snapshots', - unique_key='customer_id', - strategy='check', - check_cols=['segment', 'country_code'], - invalidate_hard_deletes=True - ) -}} -select customer_id, tenant_id, customer_name, segment, country_code -from {{ ref('stg_customers') }} -{% endsnapshot %} diff --git a/analytics/dbt/tests/assert_mart_is_not_empty.sql b/analytics/dbt/tests/assert_mart_is_not_empty.sql deleted file mode 100644 index ead1f98..0000000 --- a/analytics/dbt/tests/assert_mart_is_not_empty.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Fails when the serving mart is EMPTY. --- --- Without this, every column test passes trivially over zero rows and the --- whole suite reports green while the pipeline is dead — the same false green --- BI-P025's NativeQualityProvider refuses. -select 1 as empty_mart -having (select count(*) from {{ ref('mart_sales_daily') }}) = 0 diff --git a/analytics/dbt/tests/assert_no_cross_tenant_rows.sql b/analytics/dbt/tests/assert_no_cross_tenant_rows.sql deleted file mode 100644 index 5e5c679..0000000 --- a/analytics/dbt/tests/assert_no_cross_tenant_rows.sql +++ /dev/null @@ -1,11 +0,0 @@ --- A singular test that FAILS if the serving mart ever contains a row belonging --- to a tenant other than the one the run was scoped to. --- --- The tenant predicate is applied in staging and inherited downstream, so this --- test should be impossible to fail. That is precisely why it exists: an --- isolation guarantee nobody tests is the one that turns out to have four --- holes rather than the one reported. -select tenant_id, count(*) as rows_from_another_tenant -from {{ ref('mart_sales_daily') }} -where tenant_id != '{{ var("siax_tenant_id") }}' -group by tenant_id diff --git a/app/admin/customers/page.tsx b/app/admin/customers/page.tsx deleted file mode 100644 index 7dbeaa3..0000000 --- a/app/admin/customers/page.tsx +++ /dev/null @@ -1,142 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { useT } from "@/components/locale-provider"; -import { PortalShell } from "@/components/portal-shell"; -import { LoadingSkeleton, EmptyState, ErrorState } from "@/components/states"; -import { formatSEK } from "@/lib/data"; - -// Admin · Kunder. No auth/DB customer table yet, so we derive a customer list -// from GET /api/orders, grouping by flow as a stand-in for organisation. -// Each group: name, antal ordrar, summa (sum of total_ore → kr). - -interface Order { - order_id: string; - flow: string; - total_ore: number; - created_at: string; -} - -interface CustomerRow { - name: string; - orders: number; - totalOre: number; - last: string; -} - -function groupByFlow(orders: Order[]): CustomerRow[] { - const map = new Map(); - for (const o of orders) { - const name = o.flow || "unknown"; - const row = map.get(name) ?? { name, orders: 0, totalOre: 0, last: "" }; - row.orders += 1; - row.totalOre += o.total_ore ?? 0; - if (o.created_at > row.last) row.last = o.created_at; - map.set(name, row); - } - return [...map.values()].sort((a, b) => b.totalOre - a.totalOre); -} - -function fmtDate(iso: string): string { - if (!iso) return "—"; - const d = new Date(iso); - return Number.isNaN(d.getTime()) ? "—" : d.toISOString().slice(0, 10); -} - -export default function AdminCustomersPage() { - const t = useT(); - const [rows, setRows] = useState(null); - const [error, setError] = useState(false); - - const load = useCallback(async () => { - setError(false); - setRows(null); - try { - const r = await fetch("/api/orders", { cache: "no-store" }); - if (!r.ok) throw new Error(String(r.status)); - const data: Order[] = await r.json(); - setRows(groupByFlow(Array.isArray(data) ? data : [])); - } catch { - setError(true); - } - }, []); - useEffect(() => { - void load(); - }, [load]); - - const totalOre = rows?.reduce((s, r) => s + r.totalOre, 0) ?? 0; - - return ( - -

- {t({ en: "Derived from orders. Grouped by flow. Organisation linking at auth.", sv: "Härlett ur ordrar. Grupperat per flöde. Org-koppling vid auth." })} -

- - {error ? ( -
- -
- ) : !rows ? ( -
- -
- ) : rows.length === 0 ? ( -
- -
- ) : ( - <> -
- - {rows.length} {t({ en: "customers", sv: "kunder" })} - - - {rows.reduce((s, r) => s + r.orders, 0)} {t({ en: "orders", sv: "ordrar" })} - - - {formatSEK(totalOre / 100)} - -
- -
-
- {t({ en: "Customer", sv: "Kund" })} - {t({ en: "Orders", sv: "Ordrar" })} - {t({ en: "Latest", sv: "Senast" })} - {t({ en: "Total", sv: "Summa" })} -
- {rows.map((c) => ( -
- - - {c.name === "unknown" ? t({ en: "Unknown", sv: "Okänd" }) : c.name} - - - {c.orders} - - - {fmtDate(c.last)} - - - {formatSEK(c.totalOre / 100)} - -
- ))} -
- - )} -
- ); -} diff --git a/app/admin/import/page.tsx b/app/admin/import/page.tsx deleted file mode 100644 index f89363c..0000000 --- a/app/admin/import/page.tsx +++ /dev/null @@ -1,223 +0,0 @@ -"use client"; - -// C8: "Importera ordrar" — the first real product host of the @siax/ui -// ImportWizard + @siax/import-export runtime. The wizard renders the frozen -// store snapshot; THIS host owns every pipeline step's execution and all -// server I/O. The server (POST /api/orders/import) is the only authority: -// it re-parses and re-validates the raw CSV, so the client-side parse below -// exists purely to give the wizard rows to render. - -import { ImportWizard } from "@siax/ui"; -import { createImportRunStore, type ImportRunStore, type ImportRunSnapshot } from "@siax/import-export"; -import { createImportJob } from "@siax/contracts/import-export"; -import { useCallback, useState } from "react"; - -const IMPORT_COLUMNS = ["flow", "service_type", "type", "volume", "frequency", "markets"] as const; - -interface ServerReport { - imported?: number; - totalRows?: number; - validRows?: number; - errorCount?: number; - errors?: { row: number; field: string; code: string; message?: string }[]; - order_ids?: string[]; -} - -/** Client-side RFC 4180 parse — presentation only; the server re-parses the raw CSV. */ -function clientParse(text: string): Record[] { - const rows: string[][] = []; - let field = ""; - let row: string[] = []; - let inQuotes = false; - for (let i = 0; i < text.length; i++) { - const ch = text[i]; - if (inQuotes) { - if (ch === '"') { - if (text[i + 1] === '"') { - field += '"'; - i++; - } else inQuotes = false; - } else field += ch; - } else if (ch === '"') inQuotes = true; - else if (ch === ",") { - row.push(field); - field = ""; - } else if (ch === "\n") { - row.push(field); - field = ""; - rows.push(row); - row = []; - } else if (ch !== "\r") field += ch; - } - if (field.length > 0 || row.length > 0) { - row.push(field); - rows.push(row); - } - const nonEmpty = rows.filter((r) => r.length > 1 || (r[0] ?? "").trim() !== ""); - if (nonEmpty.length === 0) return []; - const header = nonEmpty[0].map((h) => h.trim().toLowerCase()); - return nonEmpty.slice(1).map((cells) => { - const obj: Record = {}; - header.forEach((h, idx) => { - if (h) obj[h] = (cells[idx] ?? "").trim(); - }); - return obj; - }); -} - -async function postImport(mode: "validate" | "dry_run" | "execute", csv: string): Promise<{ ok: boolean; status: number; data: ServerReport & { error?: { message?: string } } }> { - const res = await fetch("/api/orders/import", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ mode, csv }), - }); - let data: ServerReport & { error?: { message?: string } } = {}; - try { - data = await res.json(); - } catch { - data = { error: { message: `HTTP ${res.status}` } }; - } - return { ok: res.ok, status: res.status, data }; -} - -export default function AdminImportPage() { - const [rawCsv, setRawCsv] = useState(null); - const [store, setStore] = useState(null); - const [serverReport, setServerReport] = useState(null); - const [serverError, setServerError] = useState(null); - const [recentOrders, setRecentOrders] = useState<{ order_id: string; flow: string; status: string }[] | null>(null); - - const onFile = useCallback(async (file: File) => { - const text = await file.text(); - setRawCsv(text); - setServerReport(null); - setServerError(null); - setRecentOrders(null); - const rows = clientParse(text); - const s = createImportRunStore({ - job: createImportJob({ - id: globalThis.crypto?.randomUUID?.() ?? `imp-${Date.now()}`, - tenantId: "current", - entity: "orders", - format: "csv", - source: { system: "0pt-admin", channel: "web" }, - createdAt: new Date().toISOString(), - }), - rows, - }); - s.detect(); - setStore(s); - }, []); - - const onValidate = useCallback(async () => { - if (!store || !rawCsv) return; - setServerError(null); - const { ok, status, data } = await postImport("validate", rawCsv); - setServerReport(data); - store.validate(); // local snapshot for the wizard report (same frozen kernel) - if (!ok) setServerError(`Servervalidering avvisade filen (HTTP ${status}): ${data.error?.message ?? "okänt fel"}`); - }, [store, rawCsv]); - - const onDryRun = useCallback(async () => { - if (!store || !rawCsv) return; - setServerError(null); - const { ok, status, data } = await postImport("dry_run", rawCsv); - setServerReport(data); - store.dryRun(); - if (!ok) setServerError(`Servern nekade generalrepet (HTTP ${status}): ${data.error?.message ?? "okänt fel"}`); - }, [store, rawCsv]); - - const onExecute = useCallback(async () => { - if (!store || !rawCsv) return; - setServerError(null); - const { ok, status, data } = await postImport("execute", rawCsv); - setServerReport(data); - if (!ok) { - // Server is the authority — a failed execute is terminal for this run. - store.fail(`Servern avvisade exekveringen (HTTP ${status}): ${data.error?.message ?? "okänt fel"}`); - return; - } - // Host executor proxies the server result into the store's hard gates. - const snapshot: ImportRunSnapshot = await store.execute(async () => ({ imported: data.imported ?? 0 })); - // Reload-persistence proof: re-read the REAL order list after import. - try { - const res = await fetch("/api/orders", { cache: "no-store" }); - if (res.ok) { - const orders = (await res.json()) as { order_id: string; flow: string; status: string }[]; - const importedIds = new Set(data.order_ids ?? []); - setRecentOrders(orders.filter((o) => importedIds.has(o.order_id))); - } - } catch { - /* panel stays null — server report above still shows the truth */ - } - void snapshot; - }, [store, rawCsv]); - - return ( -
-

Importera ordrar

-

- CSV med kolumnerna: {IMPORT_COLUMNS.join(", ")}. Endast flow är obligatorisk - (catalog | custom | subscription). Parsering och validering sker på servern — - filen laddas inte upp förrän ett steg körs. -

- { - const f = e.target.files?.[0]; - if (f) void onFile(f); - }} - /> - {store && ( -
- ({ source: c, target: c }))} - onMap={() => { - store.map({ columns: IMPORT_COLUMNS.map((c) => ({ source: c, target: c, required: c === "flow" })) }); - }} - onValidate={onValidate} - onDryRun={onDryRun} - onExecute={onExecute} - /> -
- )} - {serverError && ( -

- {serverError} -

- )} - {serverReport && (serverReport.errorCount ?? 0) > 0 && ( -
-

Felrader (serverns rapport)

-
    - {(serverReport.errors ?? []).map((e, i) => ( -
  • - Rad {e.row} · {e.field} · {e.code} - {e.message ? ` — ${e.message}` : ""} -
  • - ))} -
-
- )} - {recentOrders && ( -
-

Importerade ordrar (lästa från /api/orders efter import)

- {recentOrders.length === 0 ? ( -

Inga importerade ordrar hittades vid reload.

- ) : ( -
    - {recentOrders.map((o) => ( -
  • - {o.order_id} — {o.flow} — {o.status} -
  • - ))} -
- )} -
- )} -
- ); -} diff --git a/app/admin/jobs/page.tsx b/app/admin/jobs/page.tsx deleted file mode 100644 index 3a641e9..0000000 --- a/app/admin/jobs/page.tsx +++ /dev/null @@ -1,216 +0,0 @@ -"use client"; - -import { useCallback, useEffect, useState } from "react"; -import { PortalShell, StatusPill } from "@/components/portal-shell"; -import { LoadingSkeleton, EmptyState, ErrorState } from "@/components/states"; -import { formatSEK } from "@/lib/data"; -import { useT } from "@/components/locale-provider"; - -// Admin pipeline monitor. Reads GET /api/orders, keeps the rows whose status is -// in_production/review, and renders each as a live pipeline row: the five engine -// stages as hairline progress chips + a StatusPill. No fetching of stage data -// exists yet, so per-stage progress is derived deterministically from the order -// id — a stable placeholder until the orchestrator streams real stage events. - -interface OrderConfig { - markets?: string[]; - type?: string; - volume?: number; -} -interface Order { - order_id: string; - flow: string; - status: string; - config: OrderConfig; - total_ore: number; - est_confidence: number; - created_at: string; -} - -const STAGES = [ - "Källracing", - "Extraktion & OCR", - "Strukturering", - "Berikning & fusion", - "Kvalitetskontroll", -] as const; - -const RUNNING = new Set(["in_production", "review"]); - -// Deterministic placeholder progress per order, 0..1 across the 5 stages. -// "review" orders sit at the QA stage; in_production progress fans out. -function stageProgress(order: Order): number[] { - let h = 0; - for (const ch of order.order_id) h = (h * 31 + ch.charCodeAt(0)) >>> 0; - const front = order.status === "review" ? 4 : h % STAGES.length; - return STAGES.map((_, i) => { - if (i < front) return 1; - if (i > front) return 0; - return order.status === "review" ? 0.62 : 0.25 + ((h >> (i + 3)) % 60) / 100; - }); -} - -const STAGE_LABELS_EN: Record<(typeof STAGES)[number], string> = { - "Källracing": "Source racing", - "Extraktion & OCR": "Extraction & OCR", - "Strukturering": "Structuring", - "Berikning & fusion": "Enrichment & fusion", - "Kvalitetskontroll": "Quality control", -}; - -function PipelineRow({ order }: { order: Order }) { - const t = useT(); - const progress = stageProgress(order); - const markets = order.config.markets ?? []; - const done = progress.filter((p) => p >= 1).length; - - return ( -
-
-
- - {order.order_id.slice(0, 8)} - - {order.config.type ?? t({ en: "Company", sv: "Bolag" })} - - {markets.length ? markets.join(" · ") : "—"} - -
-
- - {(order.config.volume ?? 0).toLocaleString("sv-SE")} {t({ en: "rows", sv: "rader" })} - - - {formatSEK(order.total_ore / 100)} - - -
-
- -
- {STAGES.map((stage, i) => { - const p = progress[i]; - const active = p > 0 && p < 1; - const complete = p >= 1; - return ( -
-
- - {t({ en: STAGE_LABELS_EN[stage], sv: stage })} - - -
-
- -
-
- ); - })} -
- -

- {done}/{STAGES.length} {t({ en: "steps · started", sv: "steg · startad" })} {new Date(order.created_at).toLocaleDateString("sv-SE")} -

-
- ); -} - -export default function AdminJobsPage() { - const t = useT(); - const [orders, setOrders] = useState(null); - const [error, setError] = useState(false); - - const load = useCallback(async () => { - setError(false); - setOrders(null); - try { - const r = await fetch("/api/orders", { cache: "no-store" }); - if (!r.ok) throw new Error(String(r.status)); - const data = (await r.json()) as Order[]; - setOrders(Array.isArray(data) ? data : []); - } catch { - setError(true); - } - }, []); - useEffect(() => { - void load(); - }, [load]); - - const running = (orders ?? []).filter((o) => RUNNING.has(o.status)); - - return ( - -

- {t({ - en: "Active production. Five stages per job. Escrow releases on approval.", - sv: "Aktiv produktion. Fem steg per jobb. Escrow släpps vid godkänt.", - })} -

- - {error ? ( -
- -
- ) : !orders ? ( -
- -
- ) : running.length === 0 ? ( -
- -
- ) : ( -
-
- - {running.length} {t({ en: "active jobs", sv: "aktiva jobb" })} - - -
-
- {running.map((o) => ( - - ))} -
-
- )} -
- ); -} diff --git a/app/admin/notes/page.tsx b/app/admin/notes/page.tsx deleted file mode 100644 index 97a0de4..0000000 --- a/app/admin/notes/page.tsx +++ /dev/null @@ -1,238 +0,0 @@ -"use client"; - -// Admin-yta: anteckningar på order — @siax/collaboration-kernelns första -// produktkonsument (spår C). Allt nätverk går via lib/order-notes-client.ts -// (kernel-transport över host-API): offline-läge = medvetet no-op med varning -// (utkastet bevaras, ingen tyst förlust), stale skrivning = 409 -> banner + -// reload av serverns aktuella version. -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; - -import { PortalShell } from "@/components/portal-shell"; -import { EmptyState, ErrorState, LoadingSkeleton } from "@/components/states"; -import { - ConflictError, - OfflineError, - OrderNotesClient, - type OrderNoteDto, -} from "@/lib/order-notes-client"; - -interface Order { - order_id: string; - flow: string; - status: string; - created_at: string; -} - -export default function OrderNotesPage() { - const [orders, setOrders] = useState(null); - const [orderId, setOrderId] = useState(""); - const [notes, setNotes] = useState(null); - const [loadError, setLoadError] = useState(false); - const [offlineMode, setOfflineMode] = useState(false); - const [draft, setDraft] = useState(""); - const [editing, setEditing] = useState<{ note: OrderNoteDto; body: string } | null>(null); - - const client = useMemo(() => new OrderNotesClient("admin-simon"), []); - - useEffect(() => { - client.transport.setOffline(offlineMode); - }, [client, offlineMode]); - - useEffect(() => { - (async () => { - try { - const res = await fetch("/api/orders", { cache: "no-store" }); - if (!res.ok) throw new Error("orders fetch failed"); - const body = await res.json(); - setOrders(Array.isArray(body) ? body : []); - } catch { - setLoadError(true); - } - })(); - }, []); - - const loadNotes = useCallback( - async (id: string) => { - setNotes(null); - setLoadError(false); - try { - const { notes: loaded } = await client.load(id); - setNotes(loaded); - } catch { - setLoadError(true); - } - }, - [client], - ); - - const addNote = useCallback(async () => { - if (!orderId || !draft.trim()) return; - if (offlineMode) { - // Offline = klient-varning + no-op. Utkastet ligger kvar i rutan — - // INGET skickas, inget försvinner tyst. - toast.warning("Offline-läge: anteckningen SKICKAS EJ. Utkastet är bevarat i rutan."); - return; - } - try { - const note = await client.add(orderId, draft); - setDraft(""); - setNotes((prev) => [...(prev ?? []), note]); - toast.success("Anteckning sparad"); - } catch (err) { - if (err instanceof OfflineError) { - toast.warning("Offline: inget skickades — utkastet är bevarat."); - return; - } - setLoadError(true); - } - }, [client, draft, offlineMode, orderId]); - - const saveEdit = useCallback(async () => { - if (!editing) return; - if (offlineMode) { - toast.warning("Offline-läge: ändringen SKICKAS EJ. Texten är bevarad i redigeringsrutan."); - return; - } - try { - const updated = await client.update(editing.note, editing.body, editing.note.version); - setNotes((prev) => (prev ?? []).map((n) => (n.note_id === updated.note_id ? updated : n))); - setEditing(null); - toast.success("Anteckning uppdaterad"); - } catch (err) { - if (err instanceof ConflictError) { - // Någon annan skrev samtidigt (stale version -> 409). Ladda om - // serverns aktuella version — förlora ALDRIG den lokala texten tyst. - toast.error(`Konflikt: anteckningen ändrades av någon annan (serverversion ${err.currentVersion}). Laddar om — din text finns kvar nedan.`); - setDraft((prev) => (prev ? prev : editing.body)); - await loadNotes(orderId); - setEditing(null); - return; - } - if (err instanceof OfflineError) { - toast.warning("Offline: inget skickades — ändringen är bevarad."); - return; - } - setLoadError(true); - } - }, [client, editing, loadNotes, offlineMode, orderId]); - - return ( - - - {offlineMode && ( -
- Offline-läge aktivt: inga anteckningar skickas till servern. Utkast bevaras lokalt — - ingen tyst förlust. -
- )} - - {loadError ? ( - orderId && loadNotes(orderId)} /> - ) : orders === null ? ( - - ) : orders.length === 0 ? ( - - ) : ( -
- - - {orderId === "" ? ( - - ) : notes === null ? ( - - ) : notes.length === 0 ? ( - - ) : ( -
    - {notes.map((n) => ( -
  • -
    - - {n.author_id} · v{n.version} · klocka {n.clock} - - {new Date(n.updated_at).toLocaleString("sv-SE")} -
    - {editing?.note.note_id === n.note_id ? ( -
    -