Compare commits

..
Author SHA1 Message Date
admin 9e34c822b3 chore: adopt SIAX Golden Standard docs and configuration
CI / validate (pull_request) Failing after 10s
security / scan (pull_request) Failing after 41s
siax-scaffold genererade saknade standardartefakter (arkitektur, säkerhet,
ändringslogg, runbooks, CI, devcontainer, licens m.m.) ur repots faktiska
filer. DOC-07 lämnas avsiktligt röd till en verklig restore bevisats.
2026-11-10 07:20:01 +01:00
admin 39fc5e3ce3 fix(security): smala gitleaks-fingerprints för verifierade falska positiver
Varför: REPO-15 (BLOCK) fällde repot på fynd i historiken. Varje fynd öppnades
och lästes; samtliga är syntetiska testvärden, dokumentationsplatshållare eller
tredjepartsinnehåll — ingen verklig hemlighet. Se kommentarerna i filen.

Ändring: .gitleaksignore med en post per exakt förekomst i formatet
<commit>:<fil>:<regel>:<rad>. Inga regel- eller globundantag, så ett framtida
riktigt fynd fälls fortfarande.

Verifiering: gitleaks detect -s . -> no leaks found; doctor --repo . -> REPO-15 PASS.
2026-11-10 03:09:20 +01:00
admin 585bdd4b44 docs(production): standardiserad produktionsberedskap och gap-analys 2026-11-10 02:07:52 +00:00
admin a4189333af docs(production): standardiserad produktionsberedskap och gap-analys
Lägger docs/production-readiness.md och siax/production-gaps.json enligt SIAX Master Definition of Done. Underlag: git-, kod- och live-evidens 2026-11-11. Nivån är evidensbaserad, inte ett leveransgodkännande.
2026-11-10 03:07:36 +01:00
admin fb072d47c7 docs(status): spegla STATUS.md i docs/ för produktionsaudit
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / integration (push) Failing after 37s
CI (SIAX Cloud) / security (push) Successful in 1m1s
CI (SIAX Cloud) / sonar (push) Successful in 1m10s
masterplan-lock / check (push) Successful in 1m3s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m8s
Deploy to Coolify / deploy (push) Successful in 2m39s
CI (SIAX Cloud) / quality (push) Successful in 10m12s
2026-11-10 02:21:43 +01:00
admin e7cfdf47f1 Merge pull request #536 (manually-merged)
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 14s
masterplan-lock / check (push) Successful in 22s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / security (push) Successful in 51s
CI (SIAX Cloud) / integration (push) Successful in 1m57s
CI (SIAX Cloud) / sonar (push) Successful in 2m21s
Deploy to Coolify / deploy (push) Successful in 3m5s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m53s
CI (SIAX Cloud) / quality (push) Successful in 10m45s
2026-11-10 01:55:30 +01:00
siax-bot dccdfed9bd fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-925f5ca2fd
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 24s
masterplan-lock / check (push) Successful in 10s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m53s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m4s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m31s
2026-11-10 01:30:33 +01:00
admin 925f5ca2fd Merge pull request #535
CI (SIAX Cloud) / security (push) Successful in 18s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 11s
masterplan-lock / check (push) Successful in 11s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / sonar (push) Successful in 1m44s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m35s
Deploy to Coolify / deploy (push) Successful in 1m40s
CI (SIAX Cloud) / integration (push) Successful in 10m31s
CI (SIAX Cloud) / quality (push) Successful in 12m23s
2026-11-10 00:01:01 +00:00
admin 2ea4062d09 chore: adopt SIAX Golden Standard docs and configuration
masterplan-lock / check (push) Successful in 14s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 14s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 37s
CI (SIAX Cloud) / security (pull_request) Successful in 38s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m23s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m7s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m28s
siax-scaffold genererade saknade standardartefakter (arkitektur, säkerhet,
ändringslogg, runbooks, CI, devcontainer, licens m.m.) ur repots faktiska
filer. DOC-07 lämnas avsiktligt röd till en verklig restore bevisats.
2026-11-10 00:55:03 +01:00
admin 6ae98c7034 Merge pull request 'chore(doctor): undanta genererade artefakter och spegelprosa från REPO-01' (#534) from chore/doctor-exemptions-repo01 into main
masterplan-lock / check (push) Successful in 11s
CI (SIAX Cloud) / security (push) Successful in 48s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 58s
Security Scan (SIAX Cloud) / security (push) Successful in 25s
CI (SIAX Cloud) / sonar (push) Successful in 1m57s
CI (SIAX Cloud) / quality (push) Canceled after 2m26s
CI (SIAX Cloud) / integration (push) Canceled after 2m22s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m16s
Deploy to Coolify / deploy (push) Successful in 3m7s
2026-11-09 23:46:45 +00:00
admin 54ff8a3958 ci: re-trigger efter serveromstart (statusuppdatering avbröts) 2026-11-10 00:41:53 +01:00
admin 869218fb8b Merge pull request #533
Deploy to Coolify / deploy (push) Canceled after 0s
CI (SIAX Cloud) / quality (push) Canceled after 0s
CI (SIAX Cloud) / integration (push) Canceled after 0s
CI (SIAX Cloud) / security (push) Canceled after 0s
CI (SIAX Cloud) / sonar (push) Canceled after 0s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 0s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Canceled after 0s
Security Scan (SIAX Cloud) / security (push) Canceled after 0s
masterplan-lock / check (push) Successful in 9s
2026-11-09 23:25:44 +00:00
admin 1e79d0c4cf chore(doctor): exempt generated artifacts and mirror prose from REPO-01
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 30s
CI (SIAX Cloud) / security (pull_request) Successful in 52s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m46s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m54s
Varför: REPO-01 (inga github.com-remotes eller -referenser i json/md) stod som
BLOCK även efter att de felaktiga lokala github-remoterna tagits bort. De
kvarvarande träffarna var av två slag: genererade artefakter (SBOM,
release-artefakter, package-lock.json) som listar tredjepartsberoendens
metadata, och dokumentationsprosa som antingen beskriver GitHub som read-only
spegel av kanoniska git.siax.io, länkar tredjepartsprojekt, eller citerar
GitHubs egna produkt-/inställningssidor i historiska driftsanteckningar.

Ändring: ny .siax-doctor.json med två smala REPO-01-undantag — ett för de
genererade artefakterna, ett för de namngivna dokumentationsfilerna — med
motivering per kategori. Där en dokumentationslänk pekade på en SIAX-repo via
GitHub-spegeln och raden inte handlade om spegeln i sig, är länken omskriven
till kanoniska https://git.siax.io/. Inga genererade filer är redigerade.

Verifiering: node packages/doctor/src/doctor.mjs --repo . ger REPO-01 PASS
("N träff(ar) undantagna via .siax-doctor.json").
2026-11-10 00:22:32 +01:00
admin 72d5876d23 docs(policy): SIAX Agent Execution Policy i AGENTS.md + CLAUDE.md
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 10s
masterplan-lock / check (pull_request) Successful in 11s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m17s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m52s
masterplan-lock / check (push) Successful in 5s
2026-11-10 00:18:20 +01:00
admin 596ca3f90e docs(production): standardiserad produktionsberedskap och gap-analys
CI (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 17s
Security Scan (SIAX Cloud) / security (push) Successful in 31s
CI (SIAX Cloud) / integration (push) Successful in 1m5s
CI (SIAX Cloud) / sonar (push) Successful in 1m10s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m29s
Deploy to Coolify / deploy (push) Successful in 1m35s
CI (SIAX Cloud) / quality (push) Successful in 12m2s
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 10s
masterplan-lock / check (pull_request) Successful in 10s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m1s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m35s
2026-11-09 22:57:08 +00:00
admin 66a7e253eb docs(production): standardiserad produktionsberedskap och gap-analys
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
masterplan-lock / check (push) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m59s
CI (SIAX Cloud) / quality (pull_request) Successful in 11m30s
Lägger docs/production-readiness.md och siax/production-gaps.json enligt SIAX Master Definition of Done. Underlag: git-, kod- och live-evidens 2026-11-09. Nivån är evidensbaserad, inte ett leveransgodkännande.
2026-11-09 23:56:19 +01:00
admin a4e6d7d8e2 Merge pull request 'chore(manifest): registrera saknade surface-poster (surfaces-fasen)' (#530) from chore/surface-posts-20261110 into main
CI (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 23s
masterplan-lock / check (push) Successful in 11s
Security Scan (SIAX Cloud) / security (push) Successful in 37s
CI (SIAX Cloud) / sonar (push) Successful in 1m23s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m45s
Deploy to Coolify / deploy (push) Successful in 3m0s
CI (SIAX Cloud) / quality (push) Canceled after 6m41s
CI (SIAX Cloud) / integration (push) Canceled after 6m39s
2026-11-09 22:49:57 +00:00
admin 416d00fc46 chore(manifest): registrera saknade surface-poster (surfaces-fasen)
masterplan-lock / check (push) Successful in 10s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
masterplan-lock / check (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 35s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m56s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m18s
Varje capability deklarerar i `surfaces[]` vilka ytor den ska ha (api, cli,
mcp, ui, job, sdk). Fram till nu saknades `siax.surface`-poster för många av
dem, vilket gjorde `siax-doctor surfaces` röd över estaten.

Denna commit lägger posterna. ÄRLIGHETSREGEL: status sätts till `live` ENDAST
där exponeringen kan påvisas i repot — operationId finns i committad OpenAPI,
i genererad SDK, eller i ett CLI-/MCP-paket. Övriga poster skrivs som
`planned`: deklarationen finns kvar men posten påstår inte att ytan är byggd.
Ingen yta har påhittats och ingen deklaration har tagits bort.
2026-11-09 23:40:39 +01:00
admin d3b1964d3a feat(capabilities): registrera capability-ägare med verifierat kontrakt (#529)
masterplan-lock / check (push) Successful in 14s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 18s
CI (SIAX Cloud) / security (push) Successful in 26s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / sonar (push) Successful in 1m31s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m44s
Deploy to Coolify / deploy (push) Successful in 2m9s
CI (SIAX Cloud) / integration (push) Successful in 10m12s
CI (SIAX Cloud) / quality (push) Successful in 11m26s
2026-11-09 21:25:08 +00:00
Claude Code 59c0e008f5 Merge pull request #528
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / sonar (push) Successful in 1m39s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m58s
Deploy to Coolify / deploy (push) Successful in 2m23s
CI (SIAX Cloud) / integration (push) Successful in 9m55s
CI (SIAX Cloud) / quality (push) Successful in 10m12s
masterplan-lock / check (push) Successful in 7s
2026-11-09 22:01:44 +01:00
admin 20d66ea40d feat(capabilities): bind operationId for API-surface capability records
masterplan-lock / check (push) Successful in 8s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
masterplan-lock / check (pull_request) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 26s
CI (SIAX Cloud) / integration (pull_request) Successful in 37s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m26s
Records with an api surface but no operationId cannot be attested against the
implementation (doctor CAP-OPID-MISSING). Each operationId here is taken verbatim
from this repo's own contract file and matches the capability one-to-one; no
operationId was guessed.
2026-11-09 21:48:55 +01:00
Claude Code 7ce87ffa01 Merge pull request #527
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 13s
CI (SIAX Cloud) / security (push) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / integration (push) Successful in 45s
CI (SIAX Cloud) / sonar (push) Successful in 1m28s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m35s
Deploy to Coolify / deploy (push) Successful in 1m44s
CI (SIAX Cloud) / quality (push) Successful in 10m24s
2026-11-09 21:33:40 +01:00
admin d81325f27c Merge pull request 'feat(auth): dual-accept CL0UD workload identity on the machine routes' (#525) from feat/workload-identity-dual-accept-20261108 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 9s
CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 31s
CI (SIAX Cloud) / sonar (push) Successful in 1m38s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m58s
Deploy to Coolify / deploy (push) Successful in 2m2s
CI (SIAX Cloud) / quality (push) Successful in 10m14s
CI (SIAX Cloud) / integration (push) Successful in 10m18s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 8s
masterplan-lock / check (push) Successful in 11s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 27s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 33s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m58s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m43s
2026-11-09 17:40:31 +00:00
admin d40360aabc feat(auth): dual-accept CL0UD workload identity on the machine routes
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m5s
The internal machine routes (`quality/bridge-run`, `canonical/facts`,
`internal/evidence-outbox/dispatch`, `internal/er/candidates`,
`search/definitions`) trusted a shared `X-Internal-Token` with NO tenant
authority: middleware let it through without setting any verified tenant, so
`assertTenantMatchesVerified` was a no-op and a caller-chosen `tenant_id` in
the body was authoritative.

This closes that gap while keeping the engine working during the transition
to CL0UD workload tokens:

- `src/auth/internal-service-tenant.ts` — the server-configured
  `INTERNAL_SERVICE_TENANTS` scope, a scope header that only middleware may
  set, and a parser that strips/ignores client values.
- `src/auth/cl0ud-workload.ts` — a real `jose`/JWKS verifier for CL0UD
  workload JWTs (`iss`/`aud`/`exp`), deriving `tenant_id` and `capabilities`
  from verified claims only. Fail-closed when a token is presented but the
  issuer/JWKS/audience/tenant claim does not verify.
- `middleware.ts` — strips every `x-0pt-verified-*` header on all forwarded
  paths. Legacy internal tokens get the single configured tenant as scope
  (an ambiguous multi-tenant list is refused with 401 instead of widened);
  a Bearer CL0UD workload token is verified and its claims become the
  verified tenant/subject/capabilities.
- `src/http/tenant-guard.ts` — enforces the service scope on tenant-scoped
  writes (deny when the tenant is outside the scope) and adds
  `assertWorkloadCapabilities` for capability-scoped principals.
- `quality/bridge-run` now calls `assertTenantMatchesVerified` like
  `canonical/facts` already did.

Tests: `tests/auth/workload-identity-dual-accept.test.ts` (10 cases) covers
forged-header stripping, ambiguous-scope refusal, wrong audience, missing
tenant claim, cross-tenant denial and missing-capability denial.

Real CL0UD provisioning (service accounts + published JWKS) is still
required before a workload token exists in production; until then the
legacy token path is the only one that authenticates, now tenant-scoped.
2026-11-09 18:34:34 +01:00
siax-bot c6a5102a2a fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-62517aca49
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
masterplan-lock / check (pull_request) Successful in 29s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 38s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m27s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m46s
2026-11-09 17:30:20 +01:00
admin 62517aca49 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#523) from fix/prod-restore-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (push) Failing after 10s
CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / quality (push) Successful in 42s
CI (SIAX Cloud) / sonar (push) Successful in 53s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 59s
Deploy to Coolify / deploy (push) Successful in 49s
CI (SIAX Cloud) / integration (push) Successful in 9m56s
2026-11-09 16:12:42 +00:00
admin db26daa979 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 20s
CI (SIAX Cloud) / security (pull_request) Successful in 31s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 31s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m7s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m27s
2026-11-09 16:12:31 +00:00
admin c532cedb5e Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (38ae3ddf0b)' (#522) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 15s
CI (SIAX Cloud) / sonar (push) Canceled after 15s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 59s
Deploy to Coolify / deploy (push) Successful in 1m12s
2026-11-09 16:12:27 +00:00
siax-bot 40202178a3 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-38ae3ddf0b
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Failing after 18s
masterplan-lock / check (pull_request) Failing after 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m19s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m27s
2026-11-09 13:30:20 +01:00
admin 38ae3ddf0b Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#521) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / quality (push) Failing after 9s
CI (SIAX Cloud) / security (push) Failing after 9s
CI (SIAX Cloud) / sonar (push) Failing after 9s
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / integration (push) Successful in 1m6s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 59s
Deploy to Coolify / deploy (push) Successful in 1m23s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m12s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m27s
2026-11-09 12:14:54 +00:00
admin a7a4295043 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Failing after 20s
CI (SIAX Cloud) / quality (pull_request) Failing after 20s
CI (SIAX Cloud) / security (pull_request) Failing after 20s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m18s
2026-11-09 12:14:42 +00:00
admin f1d47ffb2e Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (b1ae910d64)' (#520) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / quality (push) Canceled after 15s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 14s
CI (SIAX Cloud) / sonar (push) Canceled after 14s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m5s
Deploy to Coolify / deploy (push) Successful in 1m20s
2026-11-09 12:14:39 +00:00
siax-bot 8e7c228bd2 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-b1ae910d64
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 42s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m18s
2026-11-09 09:32:10 +01:00
admin b1ae910d64 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#519) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / security (push) Failing after 13s
CI (SIAX Cloud) / sonar (push) Failing after 13s
CI (SIAX Cloud) / Component conformance (blocking) (push) Failing after 12s
CI (SIAX Cloud) / integration (push) Failing after 20s
Security Scan (SIAX Cloud) / security (push) Failing after 21s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Failing after 1m32s
Deploy to Coolify / deploy (push) Failing after 21s
CI (SIAX Cloud) / quality (push) Successful in 10m31s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
masterplan-lock / check (push) Failing after 20s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 37s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m8s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m34s
2026-11-09 07:50:38 +00:00
admin 91f270ea19 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / integration (pull_request) Failing after 1s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Failing after 1s
CI (SIAX Cloud) / security (pull_request) Failing after 1s
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 10s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 20s
CI (SIAX Cloud) / quality (pull_request) Successful in 57s
2026-11-09 07:50:27 +00:00
admin ac3a95f320 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (419c2d3f18)' (#518) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Failing after 8s
CI (SIAX Cloud) / Component conformance (blocking) (push) Failing after 8s
Security Scan (SIAX Cloud) / security (push) Failing after 8s
CI (SIAX Cloud) / quality (push) Failing after 9s
CI (SIAX Cloud) / security (push) Failing after 15s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 15s
Deploy to Coolify / deploy (push) Failing after 1m30s
2026-11-09 07:50:23 +00:00
siax-bot fab0a53573 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-419c2d3f18
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m3s
2026-11-09 05:30:19 +01:00
admin 419c2d3f18 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#517) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 11s
Security Scan (SIAX Cloud) / security (push) Successful in 23s
CI (SIAX Cloud) / security (push) Successful in 25s
CI (SIAX Cloud) / sonar (push) Successful in 1m11s
CI (SIAX Cloud) / integration (push) Successful in 1m11s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Failing after 2m47s
CI (SIAX Cloud) / quality (push) Successful in 10m9s
Deploy to Coolify / deploy (push) Failing after 10m31s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
masterplan-lock / check (pull_request) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 27s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 41s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m20s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m4s
2026-11-09 03:31:40 +00:00
admin e96e015722 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m11s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m18s
2026-11-09 03:31:28 +00:00
admin 344decd285 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (ebfa17028d)' (#516) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 10s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 14s
CI (SIAX Cloud) / sonar (push) Canceled after 13s
Security Scan (SIAX Cloud) / security (push) Canceled after 13s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m45s
Deploy to Coolify / deploy (push) Successful in 1m52s
2026-11-09 03:31:25 +00:00
siax-bot 32cd714126 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-ebfa17028d
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 40s
CI (SIAX Cloud) / quality (pull_request) Successful in 45s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m35s
2026-11-09 01:30:29 +01:00
admin ebfa17028d Merge pull request 'docs(identity): 0pt workload identity — målbild, design och blockerare' (#502) from docs/workload-identity-20261107 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 17s
CI (SIAX Cloud) / security (push) Successful in 23s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / sonar (push) Successful in 1m33s
CI (SIAX Cloud) / quality (push) Successful in 1m43s
Deploy to Coolify / deploy (push) Successful in 2m41s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m42s
CI (SIAX Cloud) / integration (push) Successful in 9m56s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m47s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m33s
2026-11-08 23:38:28 +00:00
admin 5a8d26eee6 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#515) from fix/prod-restore-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / sonar (push) Successful in 1m47s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m20s
Deploy to Coolify / deploy (push) Successful in 1m29s
CI (SIAX Cloud) / integration (push) Successful in 9m49s
CI (SIAX Cloud) / quality (push) Successful in 10m22s
2026-11-08 23:21:42 +00:00
admin d301f6e0cc Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / integration (pull_request) Successful in 43s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m8s
2026-11-08 23:21:31 +00:00
admin 85de12fdfe Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (6b57f7cb9d)' (#514) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / security (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 16s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 16s
Security Scan (SIAX Cloud) / security (push) Canceled after 17s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m24s
Deploy to Coolify / deploy (push) Successful in 1m29s
2026-11-08 23:21:27 +00:00
siax-bot 966a36f898 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-6b57f7cb9d
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 39s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m49s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
2026-11-08 21:30:19 +01:00
admin 6b57f7cb9d Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#513) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / integration (push) Successful in 28s
CI (SIAX Cloud) / sonar (push) Successful in 1m1s
Security Scan (SIAX Cloud) / security (push) Successful in 1m17s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m26s
Deploy to Coolify / deploy (push) Successful in 1m11s
CI (SIAX Cloud) / quality (push) Successful in 10m3s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m25s
2026-11-08 19:36:36 +00:00
admin c15b4088c3 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
CI (SIAX Cloud) / security (pull_request) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m47s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m3s
2026-11-08 19:36:24 +00:00
admin fd73f24193 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (72d6d1e8d1)' (#512) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 9s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 14s
CI (SIAX Cloud) / sonar (push) Canceled after 14s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 57s
Deploy to Coolify / deploy (push) Successful in 1m17s
2026-11-08 19:36:21 +00:00
siax-bot 5795b3503f fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-72d6d1e8d1
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
masterplan-lock / check (push) Successful in 25s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 26s
CI (SIAX Cloud) / security (pull_request) Successful in 31s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m30s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m33s
2026-11-08 17:30:22 +01:00
admin 72d6d1e8d1 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#511) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 16s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 1m5s
CI (SIAX Cloud) / integration (push) Successful in 1m7s
CI (SIAX Cloud) / sonar (push) Successful in 1m18s
CI (SIAX Cloud) / quality (push) Successful in 1m39s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 50s
Deploy to Coolify / deploy (push) Successful in 43s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m5s
2026-11-08 15:42:04 +00:00
admin 1b94cd7c80 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 28s
CI (SIAX Cloud) / security (pull_request) Successful in 29s
CI (SIAX Cloud) / quality (pull_request) Successful in 56s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m55s
2026-11-08 15:41:53 +00:00
admin 9dcf3c24fa Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (5010a6853b)' (#510) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 9s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 13s
CI (SIAX Cloud) / sonar (push) Canceled after 13s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m1s
Deploy to Coolify / deploy (push) Successful in 2m16s
2026-11-08 15:41:49 +00:00
siax-bot a0eba858ab fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-5010a6853b
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m3s
2026-11-08 13:30:18 +01:00
admin 5010a6853b Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#509) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 16s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / sonar (push) Successful in 1m20s
CI (SIAX Cloud) / integration (push) Successful in 1m21s
CI (SIAX Cloud) / quality (push) Successful in 1m47s
Deploy to Coolify / deploy (push) Successful in 2m36s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m43s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / quality (pull_request) Successful in 53s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m13s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 1m13s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m50s
2026-11-08 11:57:40 +00:00
admin b25c164837 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / integration (pull_request) Failing after 0s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m36s
2026-11-08 11:57:29 +00:00
admin be0acfd12c Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (f5600a25b9)' (#508) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 14s
CI (SIAX Cloud) / sonar (push) Canceled after 13s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 13s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m45s
Deploy to Coolify / deploy (push) Successful in 1m49s
2026-11-08 11:57:26 +00:00
siax-bot 5a56cc9a6e fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-f5600a25b9
CI (SIAX Cloud) / integration (pull_request) Failing after 0s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 11s
masterplan-lock / check (push) Successful in 9s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / quality (pull_request) Successful in 40s
2026-11-08 05:30:28 +01:00
admin f5600a25b9 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#507) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 13s
CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 25s
CI (SIAX Cloud) / sonar (push) Successful in 1m3s
CI (SIAX Cloud) / integration (push) Successful in 1m14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 41s
Deploy to Coolify / deploy (push) Successful in 1m21s
CI (SIAX Cloud) / quality (push) Successful in 10m41s
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (pull_request) Failing after 40s
CI (SIAX Cloud) / security (pull_request) Failing after 1m43s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m46s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m2s
2026-11-08 03:58:42 +00:00
admin d85f5ae559 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 9s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 11s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / integration (pull_request) Successful in 46s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m18s
2026-11-08 03:58:30 +00:00
admin 246589bde8 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (9aef3cd97f)' (#506) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / security (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 16s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 16s
Security Scan (SIAX Cloud) / security (push) Canceled after 17s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m39s
Deploy to Coolify / deploy (push) Successful in 1m42s
2026-11-08 03:58:27 +00:00
siax-bot fdba72bd35 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-9aef3cd97f
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (pull_request) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m19s
2026-11-08 01:30:28 +01:00
admin 9aef3cd97f Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#505) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 10s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 18s
CI (SIAX Cloud) / integration (push) Successful in 27s
CI (SIAX Cloud) / sonar (push) Successful in 1m11s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m25s
Deploy to Coolify / deploy (push) Successful in 1m37s
CI (SIAX Cloud) / quality (push) Successful in 10m15s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 9s
masterplan-lock / check (push) Successful in 11s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 40s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m47s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
2026-11-07 23:21:36 +00:00
admin 34ef39e5f5 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 12s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 26s
masterplan-lock / check (pull_request) Successful in 30s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m52s
CI (SIAX Cloud) / quality (pull_request) Successful in 11m18s
2026-11-07 23:21:24 +00:00
admin 43ec05ade2 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (a0e701efc8)' (#504) from fix/prod-evidence-2026-09-29 into main
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 13s
CI (SIAX Cloud) / security (push) Canceled after 12s
CI (SIAX Cloud) / sonar (push) Canceled after 12s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 12s
Security Scan (SIAX Cloud) / security (push) Canceled after 13s
masterplan-lock / check (push) Successful in 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m45s
Deploy to Coolify / deploy (push) Successful in 1m50s
2026-11-07 23:21:21 +00:00
siax-bot be6b92fb82 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-a0e701efc8
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 32s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m28s
2026-11-07 21:30:19 +01:00
admin a0e701efc8 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#493) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 9s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / sonar (push) Successful in 1m13s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 3m44s
Deploy to Coolify / deploy (push) Successful in 3m34s
CI (SIAX Cloud) / integration (push) Successful in 10m0s
CI (SIAX Cloud) / quality (push) Successful in 10m1s
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 8s
CI (SIAX Cloud) / sonar (pull_request) Skipped
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 30s
CI (SIAX Cloud) / quality (pull_request) Successful in 42s
CI (SIAX Cloud) / integration (pull_request) Successful in 59s
2026-11-07 19:10:33 +00:00
admin 48fbe6b184 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 11s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 26s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m55s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m10s
2026-11-07 19:10:22 +00:00
admin b90cded2d6 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (b2499ace79)' (#500) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / security (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 16s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 16s
Security Scan (SIAX Cloud) / security (push) Canceled after 16s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m26s
Deploy to Coolify / deploy (push) Successful in 1m47s
2026-11-07 19:10:17 +00:00
admin aba909c29a Merge branch 'main' into fix/prod-evidence-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 10s
masterplan-lock / check (pull_request) Successful in 9s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 49s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m6s
2026-11-07 19:10:04 +00:00
admin e00c7b0fc5 Merge pull request 'docs(runbook): legacy-instansaudit server5 (0pt + L0RE)' (#501) from docs/legacy-instances-20261107 into main
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 8s
CI (SIAX Cloud) / security (push) Successful in 16s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / quality (push) Successful in 42s
CI (SIAX Cloud) / sonar (push) Successful in 56s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m25s
Deploy to Coolify / deploy (push) Successful in 2m17s
CI (SIAX Cloud) / integration (push) Successful in 10m2s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m45s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m26s
2026-11-07 17:34:12 +00:00
admin de76f3795b Merge pull request 'fix(canonical): RFC3339-serialisering av timestamptz' (#498) from fix/canonical-iso-timestamps-20261107 into main
CI (SIAX Cloud) / quality (push) Canceled after 5s
CI (SIAX Cloud) / integration (push) Canceled after 5s
CI (SIAX Cloud) / security (push) Canceled after 5s
CI (SIAX Cloud) / sonar (push) Canceled after 5s
CI (SIAX Cloud) / Component conformance (blocking) (push) Canceled after 4s
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Canceled after 5s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m1s
Deploy to Coolify / deploy (push) Successful in 1m12s
2026-11-07 17:34:07 +00:00
admin 3421748f07 Merge branch 'main' into docs/legacy-instances-20261107
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 13s
masterplan-lock / check (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 27s
masterplan-lock / check (push) Successful in 34s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m18s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m29s
2026-11-07 17:19:41 +00:00
admin 9ac681ee27 Merge branch 'main' into fix/canonical-iso-timestamps-20261107
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 10s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / security (pull_request) Successful in 22s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 23s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m0s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m27s
2026-11-07 17:19:39 +00:00
admin cdbb10b8e3 Merge pull request 'test(lint): typa idempotency-store (fixar quality-gaten som blockerar alla PR)' (#503) from fix/lint-siax-access-idempotency into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 23s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 26s
Security Scan (SIAX Cloud) / security (push) Successful in 37s
CI (SIAX Cloud) / sonar (push) Successful in 1m22s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m48s
Deploy to Coolify / deploy (push) Successful in 2m55s
CI (SIAX Cloud) / integration (push) Successful in 9m58s
CI (SIAX Cloud) / quality (push) Successful in 10m34s
2026-11-07 17:19:35 +00:00
SIAX Agent 6bbbcf75d4 test(lint): typa idempotency-store i stället för any (blockerar quality-gaten)
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m7s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m22s
tests/siax-access/idempotency.test.ts hade fyra @typescript-eslint/no-explicit-any
vilket fick quality-jobbet att exita 1 (133 testfiler gröna men lint röd) och
därmed blockerade alla PR:er. Använder exporterade IdempotencyStore<unknown,
{x:number}>; eslint rent, 7/7 tester gröna.
2026-11-07 18:15:04 +01:00
SIAX Agent 8741169259 docs(identity): 0pt workload identity — målbild, integrationsdesign och blockerare
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
CI (SIAX Cloud) / integration (pull_request) Successful in 30s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m9s
Nuläge (delad INTERNAL_TOKEN + serverkonfigurerad tenant-allowlist) mot CL0UD:s
redan existerande service-account/workload-token-maskineri (aud:0pt, 10 min,
capabilities). Design: verifiering före route-logik, tenant från claim,
capability per route, dual-accept under övergång, negativa tester i samma svit.

Provisionering är blockerad: /account/service-accounts kräver operator-session
(307 -> /login) och Zitadel admin-PAT är invalid (AUTH-7fs1e).
2026-11-07 18:14:23 +01:00
admin 58dca92d9f Merge pull request 'fix(sql): 023 grants för canonical-tabellerna till app-rollen' (#499) from fix/canonical-grants-20261107 into main
masterplan-lock / check (push) Successful in 9s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 18s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / sonar (push) Successful in 1m7s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m6s
Deploy to Coolify / deploy (push) Successful in 2m9s
CI (SIAX Cloud) / quality (push) Canceled after 7m32s
CI (SIAX Cloud) / integration (push) Canceled after 7m32s
2026-11-07 17:12:03 +00:00
admin 7ca7b98fb2 Merge branch 'main' into docs/legacy-instances-20261107
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
masterplan-lock / check (push) Successful in 26s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 29s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m30s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m52s
2026-11-07 17:07:45 +00:00
admin b269a05538 Merge branch 'main' into fix/canonical-grants-20261107
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 8s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 12s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m28s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m30s
CI (SIAX Cloud) / quality (pull_request) Successful in 2m15s
2026-11-07 17:07:44 +00:00
admin 1b7c6c2de9 Merge branch 'main' into fix/canonical-iso-timestamps-20261107
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 11s
masterplan-lock / check (pull_request) Successful in 9s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 19s
CI (SIAX Cloud) / security (pull_request) Successful in 1m37s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m38s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m10s
2026-11-07 17:07:42 +00:00
SIAX Agent b7627a3a31 docs(runbook): legacy-instansaudit server5 (0pt + L0RE) och disposition
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 3s
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m12s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m46s
0pt legacy: 32 kanoniska facts exporterade och återinförda via API (replay
duplicates=32), dump till server11, api/web stoppade med restart=no, pg kvar
för forensik. L0RE-orphan refererad till PR #485. Ingen databas/volymer raderade.
2026-11-07 17:37:33 +01:00
SIAX Agent afd0b4ec46 fix(canonical): serialisera timestamptz som RFC3339 (ISO-8601)
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m11s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m1s
queryFacts använde String(Date) -> 'Sat Nov 07 2026 15:00:00 GMT+0000 (...)',
vilket L0RE:s canonical-projection avvisar ('must be RFC3339 date-time').
Inför iso()-helper: Date -> toISOString(), annars String(). Gäller
observed_at, valid_from, valid_to, created_at.
2026-11-07 17:30:40 +01:00
SIAX Agent 4dbabd14ac fix(sql): 023 grants for canonical tables to app-role 0pt_app
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 45s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m7s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m7s
022 skapade tabellerna som 0pt_edge; appen kör som 0pt_app och fick
'permission denied'. Övriga 0pt-tabeller har explicita grants (inga default
ACL), så kanoniska tabeller behöver samma. Idempotent och rollskyddad; RLS
(FORCE) gäller fortfarande för 0pt_app.
2026-11-07 17:29:03 +01:00
admin b2499ace79 Merge pull request #497: idempotent 022 canonical facts
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 10s
CI (SIAX Cloud) / security (push) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m11s
Deploy to Coolify / deploy (push) Successful in 1m14s
CI (SIAX Cloud) / sonar (push) Successful in 1m15s
CI (SIAX Cloud) / quality (push) Successful in 10m1s
CI (SIAX Cloud) / integration (push) Successful in 10m5s
Retry-safe RLS policies (DROP POLICY IF EXISTS) + header fix.
2026-11-07 16:28:21 +00:00
SIAX Agent 0213459f33 fix(sql): 022 canonical facts blir idempotent (DROP POLICY IF EXISTS)
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 48s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m47s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m39s
CREATE POLICY saknade skydd mot omkörning, så en retry efter partiell körning
skulle fastna på 'policy already exists'. Tabellerna använder redan
CREATE TABLE/INDEX IF NOT EXISTS; policynivån görs nu likvärdig. Rubriken
rättad från 020 till 022.

Verifierat på scratch-databas: två körningar i rad OK, RLS ENABLE+FORCE,
3 policies, 11 index, cross-tenant INSERT nekad och tenant B ser 0 rader.
2026-11-07 17:24:56 +01:00
admin c91d2aaca6 Merge pull request #494: 0pt internal-service tenant isolation
masterplan-lock / check (push) Successful in 9s
Security Scan (SIAX Cloud) / security (push) Successful in 32s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 57s
CI (SIAX Cloud) / security (push) Successful in 58s
CI (SIAX Cloud) / sonar (push) Successful in 1m15s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m44s
Deploy to Coolify / deploy (push) Successful in 1m59s
CI (SIAX Cloud) / integration (push) Successful in 9m48s
CI (SIAX Cloud) / quality (push) Successful in 10m22s
Server-configured INTERNAL_SERVICE_TENANTS; forged verified headers stripped; bridge-run guarded; single-tenant rule until workload-identity binding exists.
2026-11-07 15:55:11 +00:00
SIAX Agent 1a581fde29 fix(auth): en (1) tenant per intern serviceprincipal — multi-tenant nekas
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 8s
CI (SIAX Cloud) / security (pull_request) Successful in 35s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 44s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m14s
Den delade X-Internal-Token identifierar EN tjänsteprocess, inte flera
oberoende principaler. En allowlist med flera tenants skulle låta varje
tokeninnehavare välja mellan dem — samma klientstyrda auktoritet som guarden
ska ta bort. Multi-tenant-tjänsteåtkomst kräver verifierad
workload-identity-binding (CL0UD-delegation); tills den finns failar en
ambig konfiguration stängt (401) och ingen identitet forwardas.

Tester: multi-tenant nekas utan side effect, en tenant fungerar, 75/75 i
auth+ cross-tenant-sviterna, tsc ren.
2026-11-07 16:39:48 +01:00
admin bfdc5d1326 Merge pull request 'ci: add blocking component conformance gate' (#496) from ci/design-gate-20261107 into main
Security Scan (SIAX Cloud) / security (push) Successful in 21s
masterplan-lock / check (push) Successful in 1m34s
CI (SIAX Cloud) / Component conformance (blocking) (push) Successful in 1m39s
CI (SIAX Cloud) / security (push) Successful in 1m55s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m8s
CI (SIAX Cloud) / sonar (push) Successful in 2m47s
Deploy to Coolify / deploy (push) Successful in 2m50s
CI (SIAX Cloud) / integration (push) Successful in 11m23s
CI (SIAX Cloud) / quality (push) Successful in 11m48s
2026-11-07 15:32:45 +00:00
admin 831781f19d ci: add blocking component conformance gate
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 20s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 30s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m51s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m21s
2026-11-07 15:21:31 +00:00
SIAX Agent b97ad0b2f4 fix(auth): bind internal-service tenant authority to server config
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 36s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m24s
The first revision of this fix copied a CLIENT header (X-0pt-Internal-Tenant)
or a single INTERNAL_TENANT_ID into x-0pt-verified-tenant. That is not an
authorization source: any holder of the shared internal token could still name
any tenant, leaving the original cross-tenant write reachable through a
different header.

- INTERNAL_SERVICE_TENANTS (server env) is now the only tenant authority for
  the X-Internal-Token principal; empty => no authority (fail closed, 403).
- Middleware strips inbound x-0pt-verified-{tenant,sub} and the service scope
  header on every path, including the dev escape hatch.
- x-0pt-verified-service-tenants carries the allowlist; the tenant guard checks
  membership and denies anything outside it.
- POST /api/0pt/quality/bridge-run now runs the same tenant guard as
  canonical/facts (it auto-provisions tenant-scoped rules).
- 16 new regression tests; auth + cross-tenant + canonical + bridge suites
  87/87; tsc --noEmit clean.
2026-11-07 15:32:51 +01:00
admin 019cc23fa2 feat(capability): descriptor-completion wave 1 (0pt) — verified openapi operationIds
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 31s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 1m2s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m8s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m3s
2026-11-07 15:23:41 +01:00
admin 849e7ae802 fix(auth): internal-token requests får verifierad tenant (P0 tenant-isolering)
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 43s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m3s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m15s
Bevisat: POST /api/0pt/canonical/facts med X-Internal-Token och tenant_id
999999999999999999 returnerade 201 och skrev data i annan tenant. Orsak:
internal-token-grenen returnerade next() utan x-0pt-verified-tenant, så
assertTenantMatchesVerified blev no-op.

Fix: INTERNAL_TENANT_ID (eller X-0pt-Internal-Tenant) sätts som verifierad
tenant, vilket gör guard-enforcement identisk med verifierad Bearer. Varnar
om variabeln saknas.
2026-11-07 14:55:59 +01:00
admin 04aeaa754b Merge pull request 'ci(sonar): hosten är sonar.siax.dev, inte .io' (#492) from fix/sonar-host-url into main
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / sonar (push) Successful in 1m1s
Deploy to Coolify / deploy (push) Successful in 1m36s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m34s
CI (SIAX Cloud) / quality (push) Successful in 1m37s
CI (SIAX Cloud) / integration (push) Successful in 9m54s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m46s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m39s
masterplan-lock / check (push) Successful in 4s
2026-11-06 11:33:13 +00:00
admin 98039d3d68 ci(sonar): self-hosted SonarQube lives at sonar.siax.dev, not .io
masterplan-lock / check (push) Successful in 10s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / integration (pull_request) Successful in 46s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m30s
2 of 3 recent main runs failed the sonar gate with 'Failed to query server
version: Call to URL https://sonar.siax.io/... failed: null' -- DNS for
sonar.siax.io does not resolve (000) while sonar.siax.dev answers 200 UP
(SonarQube 26.10). Internal tool + admin console = siax.dev per the 2026-11-03
domain policy; the .io hostname was never registered.
2026-11-06 12:17:53 +01:00
admin 8c91b90ae0 Merge pull request 'fix(portal): återanvänd kanonisk @siax/ui FileDropzone' (#490) from feat/ui-reuse-canonical-dropzone into main
CI (SIAX Cloud) / sonar (push) Failing after 8s
CI (SIAX Cloud) / security (push) Successful in 14s
Security Scan (SIAX Cloud) / security (push) Successful in 14s
CI (SIAX Cloud) / quality (push) Successful in 45s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m8s
Deploy to Coolify / deploy (push) Successful in 1m23s
CI (SIAX Cloud) / integration (push) Successful in 9m46s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 9s
masterplan-lock / check (pull_request) Successful in 9s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m55s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m20s
2026-11-06 10:19:28 +00:00
admin 4f92cdd102 fix(portal): reuse canonic @siax/ui FileDropzone instead of a local ad-hoc one
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 57s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m21s
0pt rendered its own dashed drop surface (role=button + hidden input). @siax/ui
already ships FileDropzone — the native labelled input with drag-over/drop
forwarding, aria wiring and no upload logic — so replace the local variant
(REUSE, CLASSIFICATION per UI standard). Removes the local inputRef.
2026-11-06 11:04:03 +01:00
admin 9ca16b7f6c Merge pull request 'feat(@siax/access): universell idempotenskontrakt' (#489) from feat/universal-idempotency-tuple into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (push) Failing after 7s
CI (SIAX Cloud) / security (push) Successful in 18s
Security Scan (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / integration (push) Successful in 27s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m13s
Deploy to Coolify / deploy (push) Successful in 1m28s
CI (SIAX Cloud) / quality (push) Successful in 10m22s
2026-11-06 10:01:59 +00:00
admin a424cf1581 fix(@siax/access): restore relationships.ts that disappeared when the package split into two files
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 13s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 9m59s
b323948 shipped the OpenFGA relationship client inside src/index.ts; when
src/idempotency.ts was added, index.ts was rewritten as the diff of both,
losing every relationship export and breaking the existing tests. Move the
original module to src/relationships.ts and let index.ts re-export both.

Restores 7 pre-existing relationship tests (both suites 14 passed).
2026-11-06 10:49:11 +01:00
admin f77a4fec96 feat(@siax/access): universal idempotency contract
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 3s
CI (SIAX Cloud) / security (pull_request) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / quality (pull_request) Failing after 30s
CI (SIAX Cloud) / integration (pull_request) Canceled after 1m12s
operationIdentity binds tenant/project/actor + every result-affecting input
into a stable fingerprint; a changed payload under the same key is a CONFLICT,
never a replay of someone else's receipt. runIdempotentOperation encodes the
0pt-proven order (durable claim -> downstream dispatch -> save receipt ->
replay) and the commit boundaries. Structural IdempotencyStore lets each
product ship its own Postgres adapter while sharing the contract.

Derived from 0pt's durable upload attempts; DOC0/N0D can adopt the same
shape instead of re-implementing it.
2026-11-06 10:47:59 +01:00
admin 58f81a10b7 Merge pull request 'feat(idempotens): operationsscope på uppladdningsidentitet' (#488) from feat/idempotency-operation-scope into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (push) Failing after 6s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / integration (push) Successful in 27s
CI (SIAX Cloud) / security (push) Successful in 38s
CI (SIAX Cloud) / quality (push) Successful in 41s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m48s
Deploy to Coolify / deploy (push) Successful in 2m0s
2026-11-06 09:44:37 +00:00
admin 6ca6e8e0a6 feat(idempotency): operation-scope binding for upload identity
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 7s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m7s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m17s
Per the reap-use mandate: 'identical file + different instruction' is a
DIFFERENT operation. bindOperationToUploadIdentity() folds instruction,
field contract, model, data class and export format into the DOC0-compatible
upload filename so the durable fingerprint covers every result-affecting
input. The bytes' SHA-256 stays stable; DOC0 still receives the caller's
x-idempotency-key (its own idempotency, its own contract).

Classification: STANDARDIZE/0pt-owned (upstream DOC0 untouched).
2026-11-06 10:29:59 +01:00
admin cb0b951c42 Merge pull request 'chore(ui): document domain/legacy UI exceptions' (#486) from feat/component-allowlist-20261105 into main
CI (SIAX Cloud) / sonar (push) Failing after 28s
CI (SIAX Cloud) / security (push) Successful in 39s
Security Scan (SIAX Cloud) / security (push) Successful in 40s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m48s
Deploy to Coolify / deploy (push) Successful in 2m52s
CI (SIAX Cloud) / integration (push) Successful in 10m27s
CI (SIAX Cloud) / quality (push) Successful in 10m50s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 13s
CI (SIAX Cloud) / security (pull_request) Successful in 21s
masterplan-lock / check (pull_request) Successful in 38s
CI (SIAX Cloud) / integration (pull_request) Successful in 45s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m13s
2026-11-05 23:36:47 +00:00
admin 415396f25d chore(ui): document domain/legacy UI exceptions
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 7s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / security (pull_request) Successful in 27s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m53s
CI (SIAX Cloud) / quality (pull_request) Successful in 11m31s
2026-11-06 00:36:16 +01:00
admin c46ebbf810 feat(0pt): canonical-fact machine route + renumber migration to 022 (#484)
CI (SIAX Cloud) / sonar (push) Failing after 13s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 20s
Deploy to Coolify / deploy (push) Successful in 2m0s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m54s
CI (SIAX Cloud) / quality (push) Successful in 10m4s
CI (SIAX Cloud) / integration (push) Successful in 9m49s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 7s
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 44s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m28s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m3s
2026-11-05 19:16:13 +00:00
admin 03cdb7815e feat(0pt): canonical structured observations — entities, facts, relations (#480)
CI (SIAX Cloud) / integration (push) Successful in 10m10s
CI (SIAX Cloud) / quality (push) Successful in 10m10s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m21s
Deploy to Coolify / deploy (push) Successful in 1m27s
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (push) Failing after 10s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / security (push) Successful in 32s
2026-11-05 17:53:34 +00:00
SIAX Agent 116304849b evidence(prod): bind source-bundle artifactId to real deployed image digest
CI (SIAX Cloud) / sonar (push) Failing after 12s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / security (push) Successful in 45s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m27s
Deploy to Coolify / deploy (push) Successful in 1m41s
CI (SIAX Cloud) / integration (push) Successful in 10m6s
CI (SIAX Cloud) / quality (push) Successful in 10m14s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 38s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m12s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m55s
2026-11-04 12:31:55 +01:00
SIAX Agent c7966eb586 evidence(prod): bind source-bundle artifactId to real deployed image digest
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (push) Failing after 7s
CI (SIAX Cloud) / security (push) Successful in 39s
Security Scan (SIAX Cloud) / security (push) Successful in 1m1s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m44s
Deploy to Coolify / deploy (push) Successful in 1m50s
CI (SIAX Cloud) / quality (push) Successful in 2m12s
CI (SIAX Cloud) / integration (push) Successful in 10m25s
2026-11-04 12:14:30 +01:00
SIAX Agent d5aea59482 evidence(prod): bind source-bundle artifactId to real deployed image digest
masterplan-lock / check (push) Successful in 20s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / sonar (push) Failing after 23s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m1s
Deploy to Coolify / deploy (push) Successful in 2m21s
CI (SIAX Cloud) / quality (push) Canceled after 6m16s
CI (SIAX Cloud) / integration (push) Canceled after 6m16s
2026-11-04 12:08:18 +01:00
SIAX Agent ce6be94e50 evidence(prod): bind source-bundle artifactId to real deployed image digest
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (push) Failing after 6s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / integration (push) Successful in 26s
CI (SIAX Cloud) / quality (push) Successful in 1m55s
Deploy to Coolify / deploy (push) Successful in 3m35s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 3m46s
2026-11-04 12:04:41 +01:00
SIAX Agent 4c20e5457f evidence(prod): real artifact-digest, running-digest and deploy-log for 13e156ec7e
CI (SIAX Cloud) / sonar (push) Failing after 6s
masterplan-lock / check (push) Successful in 10s
CI (SIAX Cloud) / security (push) Successful in 15s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / integration (push) Successful in 1m0s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m5s
CI (SIAX Cloud) / quality (push) Successful in 1m26s
Deploy to Coolify / deploy (push) Successful in 1m57s
2026-11-04 11:53:31 +01:00
siax-estate-bot b29e4848b6 chore(runtime-evidence): sbom — estate-kampanj issue #76 (app 0pt)
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (push) Failing after 12s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 39s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m13s
Deploy to Coolify / deploy (push) Successful in 1m32s
CI (SIAX Cloud) / integration (push) Successful in 9m57s
CI (SIAX Cloud) / quality (push) Successful in 10m33s
2026-11-04 10:32:04 +01:00
admin f53bcf8f50 Merge pull request #477
CI (SIAX Cloud) / security (push) Failing after 7s
CI (SIAX Cloud) / sonar (push) Failing after 14s
Security Scan (SIAX Cloud) / security (push) Failing after 9s
CI (SIAX Cloud) / quality (push) Successful in 1m11s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m21s
Deploy to Coolify / deploy (push) Successful in 1m35s
CI (SIAX Cloud) / integration (push) Successful in 9m52s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 5s
CI (SIAX Cloud) / security (pull_request) Failing after 12s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m17s
CI (SIAX Cloud) / integration (pull_request) Successful in 11m4s
Merged via automated design-conformance rollout after required checks and approval.
2026-11-04 01:45:28 +00:00
SIAX Agent 76e3c0dc2d chore(design): add design-conformance manifest
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / security (pull_request) Failing after 8s
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 9s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 9s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m9s
CI (SIAX Cloud) / quality (pull_request) Successful in 11m2s
2026-11-04 02:45:24 +01:00
SIAX Provenance bcdfc536b7 chore(provenance): record the 0pt deployment source bundle
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / security (pull_request) Failing after 5s
CI (SIAX Cloud) / sonar (push) Failing after 9s
masterplan-lock / check (pull_request) Successful in 13s
CI (SIAX Cloud) / security (push) Failing after 13s
Security Scan (SIAX Cloud) / security (push) Failing after 12s
masterplan-lock / check (push) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 22s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m33s
Deploy to Coolify / deploy (push) Successful in 1m47s
CI (SIAX Cloud) / quality (push) Successful in 2m3s
CI (SIAX Cloud) / quality (pull_request) Successful in 3m44s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m1s
CI (SIAX Cloud) / integration (push) Successful in 10m0s
Coolify application 0pt finished deployment of commit 13e156ec7e at 2026-11-04T01:26:48Z. This is the source-to-production evidence PROV-SOURCE-BUNDLE requires.
2026-11-04 02:34:08 +01:00
admin 77ddc0654d Merge pull request 'feat(auth): adopt @siax/auth-zitadel canonical claim normalizer' (#475) from feat/auth-zitadel-20261104 into main
masterplan-lock / check (push) Successful in 6s
Security Scan (SIAX Cloud) / security (push) Failing after 7s
CI (SIAX Cloud) / security (push) Failing after 9s
CI (SIAX Cloud) / sonar (push) Failing after 39s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m20s
Deploy to Coolify / deploy (push) Successful in 2m31s
CI (SIAX Cloud) / integration (push) Successful in 10m8s
CI (SIAX Cloud) / quality (push) Canceled after 10m19s
2026-11-04 01:23:52 +00:00
admin 063206d8b0 feat(auth): adopt @siax/auth-zitadel canonical claim normalizer
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
CI (SIAX Cloud) / security (pull_request) Failing after 4s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 11s
masterplan-lock / check (pull_request) Successful in 38s
CI (SIAX Cloud) / integration (pull_request) Successful in 2m5s
CI (SIAX Cloud) / quality (pull_request) Successful in 9m58s
2026-11-04 02:23:21 +01:00
admin 024f4fa8c3 Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#473) from fix/prod-restore-2026-09-29 into main
Security Scan (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / integration (push) Successful in 50s
CI (SIAX Cloud) / sonar (push) Successful in 1m0s
CI (SIAX Cloud) / quality (push) Successful in 1m50s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m13s
Deploy to Coolify / deploy (push) Successful in 2m15s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 4s
CI (SIAX Cloud) / security (pull_request) Failing after 6s
masterplan-lock / check (push) Successful in 23s
CI (SIAX Cloud) / integration (pull_request) Successful in 39s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
2026-11-03 19:31:38 +00:00
admin 19df2b6a20 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / security (pull_request) Successful in 24s
masterplan-lock / check (push) Successful in 29s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 50s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m27s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
2026-11-03 19:31:27 +00:00
admin 13e156ec7e Merge pull request 'feat(ui): upgrade to DES1GN @siax/ui 0.16.13' (#472) from feat/des1gn-ui-upgrade into main
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / security (push) Successful in 13s
Security Scan (SIAX Cloud) / security (push) Successful in 22s
CI (SIAX Cloud) / integration (push) Successful in 46s
CI (SIAX Cloud) / quality (push) Successful in 1m0s
CI (SIAX Cloud) / sonar (push) Successful in 1m12s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Failing after 2m58s
Deploy to Coolify / deploy (push) Failing after 11m42s
2026-11-03 19:08:07 +00:00
admin 57616d84b7 feat(ui): upgrade to DES1GN @siax/ui 0.16.13
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / integration (pull_request) Successful in 36s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m19s
2026-11-03 19:57:16 +01:00
admin b547046963 feat(acquisition): X-800 consumer contract (#470)
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (push) Successful in 1m1s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m8s
Deploy to Coolify / deploy (push) Successful in 1m14s
CI (SIAX Cloud) / sonar (push) Successful in 1m15s
CI (SIAX Cloud) / quality (push) Successful in 10m34s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 3s
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 14s
CI (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m11s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
2026-11-03 16:46:24 +00:00
siax-bot 70444a369e fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-ef84832cdb
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 20s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 43s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m42s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m54s
2026-11-03 17:30:19 +01:00
admin ef84832cdb Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#469) from fix/prod-restore-2026-09-29 into main
CI (SIAX Cloud) / sonar (push) Successful in 1m4s
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / security (push) Successful in 59s
CI (SIAX Cloud) / quality (push) Successful in 1m29s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 54s
Deploy to Coolify / deploy (push) Successful in 1m19s
CI (SIAX Cloud) / integration (push) Successful in 9m47s
2026-11-03 15:28:31 +00:00
admin 10b989351a Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / integration (pull_request) Failing after 16s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / quality (pull_request) Successful in 53s
2026-11-03 15:28:19 +00:00
admin 6265734a34 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (8b5e23aa9b)' (#467) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 13s
CI (SIAX Cloud) / sonar (push) Canceled after 13s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Deploy to Coolify / deploy (push) Successful in 4m34s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 4m38s
2026-11-03 15:28:16 +00:00
admin f135ab2ac0 Merge branch 'main' into fix/prod-evidence-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m0s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m46s
2026-11-03 15:28:03 +00:00
admin cc72e5d434 Merge pull request 'feat(analytics): add 0pt analytics HTTP surface + AUD0 ingestion' (#468) from feat/analytics-api into main
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / quality (push) Successful in 40s
CI (SIAX Cloud) / sonar (push) Successful in 1m27s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m36s
Deploy to Coolify / deploy (push) Successful in 1m44s
CI (SIAX Cloud) / integration (push) Successful in 10m14s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 7s
CI (SIAX Cloud) / security (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (pull_request) Successful in 44s
CI (SIAX Cloud) / integration (pull_request) Successful in 56s
2026-11-03 13:12:52 +00:00
admin 766db9e9bb fix(analytics): bump PyJWT to 2.14.0 (CVE-2026-102268)
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
masterplan-lock / check (push) Successful in 23s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m11s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m49s
2026-11-03 14:02:39 +01:00
admin 5f5d31f86d feat(analytics): add 0pt analytics HTTP surface + AUD0 ingestion
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Failing after 14s
Security Scan (SIAX Cloud) / security (pull_request) Failing after 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 58s
CI (SIAX Cloud) / quality (pull_request) Canceled after 1m59s
2026-11-03 14:00:34 +01:00
siax-bot 4d88157cc1 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-8b5e23aa9b
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m27s
2026-11-03 13:30:22 +01:00
admin 8b5e23aa9b Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#466) from fix/prod-restore-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / sonar (push) Successful in 1m5s
Deploy to Coolify / deploy (push) Successful in 3m2s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 3m13s
CI (SIAX Cloud) / quality (push) Successful in 10m0s
CI (SIAX Cloud) / integration (push) Successful in 10m30s
2026-11-03 11:24:46 +00:00
admin 068d00febb Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 22s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m7s
2026-11-03 11:24:35 +00:00
admin 9f74b53a4d Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (5772b4043a)' (#465) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 13s
Security Scan (SIAX Cloud) / security (push) Successful in 15s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 15s
Deploy to Coolify / deploy (push) Successful in 2m31s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m31s
2026-11-03 11:24:31 +00:00
siax-bot ea16eabf79 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-5772b4043a
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m3s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m25s
2026-11-03 09:30:18 +01:00
admin 5772b4043a Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#464) from fix/prod-restore-2026-09-29 into main
Security Scan (SIAX Cloud) / security (push) Successful in 16s
CI (SIAX Cloud) / security (push) Successful in 34s
CI (SIAX Cloud) / quality (push) Successful in 1m0s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 56s
Deploy to Coolify / deploy (push) Successful in 32s
CI (SIAX Cloud) / sonar (push) Successful in 2m30s
CI (SIAX Cloud) / integration (push) Successful in 9m50s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 18s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 27s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m2s
2026-11-03 07:28:15 +00:00
admin afa3a888bc Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
masterplan-lock / check (push) Successful in 13s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / integration (pull_request) Successful in 51s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m14s
2026-11-03 07:28:03 +00:00
admin 9c99c2c633 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (b70ae2bbfe)' (#463) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / quality (push) Canceled after 16s
CI (SIAX Cloud) / integration (push) Canceled after 16s
CI (SIAX Cloud) / security (push) Canceled after 16s
CI (SIAX Cloud) / sonar (push) Canceled after 16s
Security Scan (SIAX Cloud) / security (push) Canceled after 17s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m22s
Deploy to Coolify / deploy (push) Successful in 1m55s
2026-11-03 07:28:00 +00:00
siax-bot 2ec3fe9b64 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-b70ae2bbfe
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 4s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 25s
CI (SIAX Cloud) / security (pull_request) Successful in 38s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m20s
2026-11-03 05:30:19 +01:00
admin b70ae2bbfe Merge pull request 'fix(manifest): provenRestore=true (restore-drill 2026-09-29)' (#462) from fix/prod-restore-2026-09-29 into main
Security Scan (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / sonar (push) Successful in 1m20s
CI (SIAX Cloud) / security (push) Successful in 1m43s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m57s
Deploy to Coolify / deploy (push) Successful in 2m26s
CI (SIAX Cloud) / integration (push) Successful in 10m13s
CI (SIAX Cloud) / quality (push) Successful in 10m41s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 3s
masterplan-lock / check (pull_request) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / quality (pull_request) Successful in 42s
CI (SIAX Cloud) / integration (pull_request) Successful in 58s
2026-11-03 03:00:45 +00:00
admin c33d3f4d76 Merge branch 'main' into fix/prod-restore-2026-09-29
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 9s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 26s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m2s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m33s
2026-11-03 03:00:33 +00:00
admin 03e065c34d Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (140a02a797)' (#461) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 6s
CI (SIAX Cloud) / quality (push) Canceled after 14s
CI (SIAX Cloud) / integration (push) Canceled after 14s
CI (SIAX Cloud) / security (push) Canceled after 13s
CI (SIAX Cloud) / sonar (push) Canceled after 13s
Security Scan (SIAX Cloud) / security (push) Canceled after 14s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 3m22s
Deploy to Coolify / deploy (push) Successful in 3m25s
2026-11-03 03:00:30 +00:00
siax-bot 35719a6c68 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-140a02a797
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 5s
masterplan-lock / check (push) Successful in 8s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m38s
2026-11-03 01:30:31 +01:00
admin 140a02a797 Merge pull request 'fix(portal): en idempotensnyckel per vald fil' (#460) from fix/ui-upload-key into main
Security Scan (SIAX Cloud) / security (push) Successful in 34s
CI (SIAX Cloud) / security (push) Successful in 42s
CI (SIAX Cloud) / sonar (push) Successful in 1m13s
CI (SIAX Cloud) / quality (push) Successful in 1m32s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m35s
Deploy to Coolify / deploy (push) Successful in 1m38s
CI (SIAX Cloud) / integration (push) Successful in 10m10s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 7s
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 59s
CI (SIAX Cloud) / quality (pull_request) Successful in 11m57s
2026-11-03 00:04:54 +00:00
admin 446b9aa299 fix(portal): one idempotency key per selected file
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 6s
masterplan-lock / check (pull_request) Successful in 6s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 34s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m37s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m40s
A double click or a retry after a network error generated a new key each time
and therefore a duplicate document. The key is now stable until the file
selection changes, so the same upload returns the same verified receipt.
2026-11-03 00:51:42 +01:00
admin 6652fde6df Merge pull request 'fix(upload): idempotent retry ger samma verifierade kvitto' (#459) from fix/upload-retry-receipt into main
masterplan-lock / check (push) Successful in 3s
Security Scan (SIAX Cloud) / security (push) Successful in 17s
CI (SIAX Cloud) / security (push) Successful in 18s
CI (SIAX Cloud) / integration (push) Successful in 59s
CI (SIAX Cloud) / sonar (push) Successful in 59s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m7s
Deploy to Coolify / deploy (push) Successful in 1m27s
CI (SIAX Cloud) / quality (push) Successful in 10m3s
2026-11-02 23:36:57 +00:00
admin 92eb78ba0a fix(upload): an idempotent retry returns the same verified receipt
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 4s
masterplan-lock / check (pull_request) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 35s
CI (SIAX Cloud) / integration (pull_request) Successful in 37s
CI (SIAX Cloud) / quality (pull_request) Successful in 43s
A client retry with the same idempotency key replayed the journal and got
'outcome_unknown' even when the first upload had succeeded, because the
verified DOC0 receipt was never persisted. Store document/version/checksum/
stage on the attempt (sql/021) and return it on replay; only a missing receipt
falls back to observation. No second POST, no duplicate document.
2026-11-03 00:35:32 +01:00
admin 96d61d1308 Merge pull request 'fix(credits): importera trial-helpern i balance-routen' (#457) from fix/balance-trial-import into main
CI (SIAX Cloud) / security (push) Successful in 24s
Deploy to Coolify / deploy (push) Successful in 1m35s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m33s
CI (SIAX Cloud) / sonar (push) Successful in 1m41s
Security Scan (SIAX Cloud) / security (push) Successful in 1m54s
CI (SIAX Cloud) / quality (push) Successful in 3m21s
CI (SIAX Cloud) / integration (push) Successful in 9m57s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 11s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / security (pull_request) Successful in 37s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m11s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m36s
2026-11-02 21:39:17 +00:00
admin 4ea55c74bb fix(credits): import the trial-credit helper in the balance route
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 3s
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 46s
CI (SIAX Cloud) / security (pull_request) Successful in 46s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m17s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m47s
The call was added without its import, so GET /api/0pt/credits/balance
returned CREDITS_UNAVAILABLE (readTrialCredits is not defined). Caught live
right after enabling trial credits.
2026-11-02 22:27:19 +01:00
admin ffd7e3341e Merge pull request 'feat(credits): engångs trial-grant så ledgern körs skarpt' (#456) from feat/credit-trial-grant into main
masterplan-lock / check (push) Successful in 4s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / security (push) Successful in 32s
CI (SIAX Cloud) / integration (push) Successful in 49s
CI (SIAX Cloud) / sonar (push) Successful in 1m8s
CI (SIAX Cloud) / quality (push) Successful in 1m13s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m21s
Deploy to Coolify / deploy (push) Successful in 1m38s
2026-11-02 21:17:39 +00:00
admin dade37ee05 feat(credits): one-time trial grant so the ledger runs in production
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m58s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m25s
- Ledger accepts kind='grant' (a server-owned balance grant, not a payment).
- OPT_TRIAL_CREDITS=N grants N credits once per tenant (idempotent key
  trial:<tenant>) on balance/structure, so reserve -> settle is exercised
  before any customer purchase.
- sql/020 extends the kind constraint (re-runnable).
- Tests for the grant helper; full suite 1345 passed.
2026-11-02 22:05:06 +01:00
admin 456e331d80 Merge pull request 'fix(signing): cosign-pubkey + SBOM + deployment/env-poster (6a4a45a480)' (#455) from fix/prod-evidence-2026-09-29 into main
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / security (push) Successful in 19s
Security Scan (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / quality (push) Successful in 1m13s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m20s
Deploy to Coolify / deploy (push) Successful in 1m32s
CI (SIAX Cloud) / sonar (push) Successful in 1m50s
CI (SIAX Cloud) / integration (push) Successful in 10m58s
2026-11-02 20:50:29 +00:00
admin 6a4a45a480 Merge pull request 'ci(integration): rensa OpenSearch read-only-block (inte bara vattenmärket)' (#454) from fix/ci-opensearch-clear-readonly into main
masterplan-lock / check (push) Successful in 7s
CI (SIAX Cloud) / security (push) Successful in 21s
Security Scan (SIAX Cloud) / security (push) Successful in 27s
CI (SIAX Cloud) / quality (push) Successful in 40s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 49s
Deploy to Coolify / deploy (push) Successful in 56s
CI (SIAX Cloud) / sonar (push) Successful in 58s
CI (SIAX Cloud) / integration (push) Successful in 10m5s
2026-11-02 19:41:48 +00:00
admin f29d2876a1 ci(integration): clear the OpenSearch read-only block, not just the watermark
masterplan-lock / check (push) Successful in 4s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / integration (pull_request) Successful in 59s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m24s
Disabling cluster.routing.allocation.disk.threshold_enabled does not clear a
cluster already in flood stage: it keeps cluster.blocks.read_only_allow_delete
and every write returns 403 index_create_block_exception (run 389046, still red
after the previous fix). Clear that block explicitly on every attempt, set the
threshold off at boot, and print the probe/settings bodies for future triage.

Verified locally against opensearch:2.11.1: read-only block -> 403, then
PUT {... read_only_allow_delete: null} -> index creation 200.
2026-11-02 20:29:30 +01:00
admin 24f4a407c8 Merge pull request 'fix(live): betalningsspärr i pilot + tydligt OCR-fel' (#453) from fix/live-honesty-and-billing-guard into main
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 24s
CI (SIAX Cloud) / security (push) Successful in 26s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m1s
CI (SIAX Cloud) / quality (push) Successful in 1m13s
Deploy to Coolify / deploy (push) Successful in 1m14s
CI (SIAX Cloud) / sonar (push) Successful in 2m17s
CI (SIAX Cloud) / integration (push) Failing after 5m39s
2026-11-02 19:14:57 +00:00
admin a0dd6b7653 fix(live): refuse billing in pilot mode and explain unparsed documents
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 3s
masterplan-lock / check (push) Successful in 14s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / quality (pull_request) Successful in 54s
CI (SIAX Cloud) / integration (pull_request) Successful in 1m2s
- Checkout returns 503 CREDITS_DISABLED while OPT_CREDITS_MODE=off, so a
  customer can never pay for credits that the pipeline will not consume.
- Packs expose purchases_enabled; the portal hides Buy and says billing is off.
- Structuring an unparsed document (e.g. a scanned PDF with no text layer) now
  answers 422 DOCUMENT_NOT_PARSED instead of a generic INVALID_REQUEST.
2026-11-02 20:14:12 +01:00
admin 8822b2adaa Merge pull request 'ci(integration): åtgärda OpenSearch disk-vattenmärke (index_create_block)' (#451) from fix/ci-opensearch-watermark into main
masterplan-lock / check (push) Successful in 9s
CI (SIAX Cloud) / security (push) Successful in 26s
Security Scan (SIAX Cloud) / security (push) Successful in 33s
CI (SIAX Cloud) / sonar (push) Successful in 2m2s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 2m26s
Deploy to Coolify / deploy (push) Successful in 2m28s
CI (SIAX Cloud) / integration (push) Successful in 10m1s
CI (SIAX Cloud) / quality (push) Successful in 10m22s
2026-11-02 18:40:50 +00:00
admin 153c9b0cfc ci(integration): disable the CI-only OpenSearch disk watermark and prove index creation
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 15s
CI (SIAX Cloud) / security (pull_request) Successful in 17s
CI (SIAX Cloud) / integration (pull_request) Successful in 9m48s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m0s
The throwaway single-node cluster inherits the runner's disk watermark; on a
busy runner the flood stage turns every new index read-only and the blocking
suite fails with 403 index_create_block_exception (run 388536). Disable the
threshold for this container and create+delete a probe index before the tests.
2026-11-02 19:18:03 +01:00
admin 32182b50dd Merge pull request 'docs(deploy): runbook + workflow-rubriker matchar live-pipelinen' (#450) from docs/deploy-runbook into main
CI (SIAX Cloud) / security (push) Successful in 18s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m9s
CI (SIAX Cloud) / sonar (push) Successful in 1m17s
Deploy to Coolify / deploy (push) Successful in 1m23s
CI (SIAX Cloud) / quality (push) Successful in 1m28s
CI (SIAX Cloud) / integration (push) Failing after 5m23s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (push) Successful in 5s
masterplan-lock / check (pull_request) Successful in 7s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / security (pull_request) Successful in 16s
CI (SIAX Cloud) / quality (pull_request) Successful in 10m7s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m30s
2026-11-02 17:58:38 +00:00
siax-bot 6e2d7b3068 Merge pull request 'feat(auth): central Keycloak portal-trust' (#449) from fix/keycloak-portal-trust-20261102 into main
masterplan-lock / check (push) Successful in 8s
Security Scan (SIAX Cloud) / security (push) Successful in 21s
CI (SIAX Cloud) / security (push) Successful in 43s
CI (SIAX Cloud) / sonar (push) Successful in 1m6s
Release chain (G17, ADR-W16-003) / Build, push, G17 (SBOM/scan/attest/verify/evidence) (push) Successful in 1m17s
Deploy to Coolify / deploy (push) Successful in 1m20s
CI (SIAX Cloud) / integration (push) Failing after 5m18s
CI (SIAX Cloud) / quality (push) Successful in 10m15s
2026-11-02 17:47:25 +00:00
siax-bot 0b50f342a3 fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-c386004f2e
CI (SIAX Cloud) / integration (push) Skipped
CI (SIAX Cloud) / sonar (push) Skipped
masterplan-lock / check (push) Successful in 5s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / security (push) Successful in 23s
CI (SIAX Cloud) / quality (push) Successful in 1m35s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 6s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 18s
CI (SIAX Cloud) / security (pull_request) Successful in 19s
CI (SIAX Cloud) / integration (pull_request) Successful in 28s
CI (SIAX Cloud) / quality (pull_request) Successful in 45s
2026-11-02 17:30:19 +01:00
892 changed files with 723 additions and 110788 deletions
-12
View File
@@ -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
-50
View File
@@ -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 },
},
},
};
+1 -12
View File
@@ -1,15 +1,4 @@
{
"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"
}
"image": "mcr.microsoft.com/devcontainers/base:ubuntu"
}
-35
View File
@@ -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
-25
View File
@@ -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
+6 -285
View File
@@ -1,286 +1,7 @@
# =============================================================================
# 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.
# Miljövariabler för 0pt
# Genererad 2026-11-10 ur källkodens läsningar (endast NAMN — inga värden).
# Fyll i lokalt i .env (som är git-ignorerad). Riktiga hemligheter ligger i
# Infisical och injiceras vid körning; kopiera aldrig in dem här.
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
# 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
# <issuer>/.well-known/openid-configuration, falling back to <issuer>/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://<accountid>.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://<resource>.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://<your-odoo-host>
# ---------------------------------------------------------------------------
# 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=
# Central Keycloak (SIAX Portal)
KEYCLOAK_ISSUER=https://auth.portal.siax.dev/realms/portal
KEYCLOAK_EXPECTED_AUDIENCE=account
KEYCLOAK_AUTHORIZED_PARTY=siax-portal
# Inga miljövariabler detekterades i källkoden vid genereringen.
# Om repot inför konfiguration via miljö, lägg till namnen här.
-37
View File
@@ -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
-206
View File
@@ -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 <predicate-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'
+13 -268
View File
@@ -1,277 +1,22 @@
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).
name: CI
# Genererad av siax-scaffold (CI-01) för stack: none.
# Grinden körs på push och pull request. Anpassa stegen när repot får egna
# test-/lintkommandon — scaffolden kör bara det som faktiskt finns.
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.
push: { branches: [main] }
pull_request: {}
permissions: { contents: read }
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
validate:
runs-on: ubuntu-latest
timeout-minutes: 30
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Enable corepack + pin pnpm@9 (pnpm-lock.yaml is lockfileVersion 9.0)
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Kontrollera att repot är konsekvent
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 }}
# `--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"
- 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:-<none>}"
if [ "$NAME" = "$WANT" ]; then FOUND="$ip"; fi
done
[ -n "$FOUND" ] || { echo "FATAL: no OpenSearch behind the alias identifies as $WANT"; exit 1; }
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:-<denied>}"
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.io)
env:
SONAR_HOST_URL: https://sonar.siax.io
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"
test -f README.md
echo "Ingen körtid upptäckt — ingen build/test-stege genererad."
-18
View File
@@ -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 .
-22
View File
@@ -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
-54
View File
@@ -1,54 +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"
- 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' .
-77
View File
@@ -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:-<no response>}"
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
-184
View File
@@ -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.
+40 -30
View File
@@ -1,43 +1,53 @@
name: Security Scan (SIAX Cloud)
# Security scanning on every push/PR + weekly re-run.
name: security
# CI-10 — gitleaks, Semgrep, OSV-Scanner, Trivy. Körs på PR, på push mot main
# och nattligt. Scannrarna körs som containrar för att inte kräva att runnern
# har verktygen installerade (samma mönster som siax-standards egen security.yml).
#
# 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.
# FÄLLA (verifierad i siax-standards egen CI-utredning): ett stegnamn som
# innehåller ": " (kolon + mellanslag) får Giteas YAML-parser att TYST kasta
# bort hela workflowen — den dispatcheras aldrig och syns inte som fel.
# Håll därför alla `name:`-rader på steg i ren ASCII utan kolon-mellanslag.
#
# 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.
# TODO (CI-10, ej wired av scaffolden): licenskontroll kräver ett policybeslut
# om vilka licenser som är tillåtna — lägg till steget när listan finns.
on:
push:
branches: ["main"]
pull_request:
workflow_dispatch:
push: { branches: [main] }
pull_request: {}
schedule:
- cron: "0 5 * * 1" # weekly Monday re-scan
- cron: "17 2 * * *"
permissions: { contents: read }
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true
jobs:
security:
scan:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install Trivy
with: { fetch-depth: 0 }
# gitleaks over worktree and history (REPO-15, SEC-01)
- name: gitleaks secret scan
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)
docker run --rm -v "$PWD:/repo" -w /repo \
zricethezav/gitleaks:latest detect --source /repo --no-banner --exit-code 1
# Trivy filsystem, blockerar på HIGH och CRITICAL (SUP-*)
- name: Trivy filesystem scan
run: |
trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 --skip-dirs 'siax/sbom' .
docker run --rm -v "$PWD:/repo" -w /repo \
aquasec/trivy:latest fs --scanners vuln,secret,misconfig \
--severity HIGH,CRITICAL --exit-code 1 --no-progress /repo
# Kända sårbarheter i lockfilen
- name: OSV Scanner
run: |
docker run --rm -v "$PWD:/repo" -w /repo \
ghcr.io/google/osv-scanner:latest scan source --recursive /repo
# Statisk kodanalys, felallvarlighet bryter bygget
- name: Semgrep code scan
run: |
docker run --rm -v "$PWD:/src" -w /src \
semgrep/semgrep:latest semgrep scan --config=p/ci --error --quiet
-44
View File
@@ -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
-59
View File
@@ -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
-57
View File
@@ -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/
+11
View File
@@ -0,0 +1,11 @@
# gitleaks-fingerprints för verifierade falska positiver (REPO-15).
# En post per exakt förekomst: <commit>:<fil>:<regel>:<rad>.
# Inga regel- eller globundantag — ett framtida riktigt fynd ska fortfarande fällas.
#
# Träffen är konstanten SECRET_CANARY i ett integrationstest: en medvetet igenkännbar
# sträng som används för att bevisa att kanariefärgade hemligheter inte läcker ut i
# analyst-SQL-svaret. Att maskera den skulle göra testet oförmöget att mäta läckage.
#
# generic-api-key tests/integration/analyst-sql-clickhouse.integration.test.ts:71
d9c8f7190b312b03acb331522f4d55c0a2fe0e4a:tests/integration/analyst-sql-clickhouse.integration.test.ts:generic-api-key:71
-8
View File
@@ -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/
-11
View File
@@ -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
-6
View File
@@ -1,6 +0,0 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 110
}
-14
View File
@@ -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"
}
-8
View File
@@ -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.
-12
View File
@@ -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.
-24
View File
@@ -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
-9
View File
@@ -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).
-20
View File
@@ -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 <clone> 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."
}
-141
View File
@@ -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"
-117
View File
@@ -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.
-14
View File
@@ -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.
-9
View File
@@ -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.
-7
View File
@@ -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."
}
-9
View File
@@ -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.
-15
View File
@@ -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."
}
+29 -76
View File
@@ -1,85 +1,38 @@
# AGENTS.md — 0pt
# 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 repot gör:** TODO: en mening om repots syfte. Ska stämma ordagrant med
`README.md` och `docs/architecture/ARCHITECTURE.md` §1 (DOC-19).
## 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)
**Starta/testa:**
```bash
pnpm run typecheck && pnpm run test && pnpm run build # blocking
pnpm run lint # advisory
TODO: kommandot som startar repot lokalt (DX-01)
TODO: kommandot som kör testsviten (TEST-01)
TODO: byggkommandot
```
## Commit & push
**Kodkonventioner:** TODO: språk, ramverk, formatterare, namngivning. Håll
kontroll-ID:n (REPO-01 …) exakta som i
`siax-standard/standard/SIAX-F0KUSREPO-PROMPT-v2.0.md`.
- 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).
**Ändras ALDRIG:**
## Vid osäkerhet
- `contracts/` — kontraktet är källan till sanning (API-01). Kod genereras
från kontraktet, aldrig tvärtom.
- Migrationer som redan körts i drift (DATA-*) — lägg till en ny i stället.
- TODO: repospecifika frysta ytor (publika endpoints, filformat, nycklar …).
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).
**Ägare:** SIAX Technology AB (@sax3l).
**Kontrakt:** `contracts/openapi.yaml` (API-01). TODO: bekräfta att kontraktet
beskriver hela HTTP-ytan (API-02) — scaffolden kan inte veta det.
**Självdiagnos (AGENT-04):**
```bash
node <sökväg-till-siax-standard>/packages/doctor/src/doctor.mjs --repo . --fail-on=block
```
---
_Genererad av `siax-scaffold` (siax-standard). Varje `TODO:` i filen måste
fyllas i av en människa. Samlad lista: `docs/compliance/SCAFFOLD-TODO.md`._
+66 -108
View File
@@ -1,128 +1,86 @@
# Changelog
# Ändringslogg — 0pt
> 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).
Alla väsentliga ändringar dokumenteras här. Filen är genererad ur repots
git-historik av `siax-scaffold` (DOC-11) och ska uppdateras när historiken
ändras; den ska aldrig handredigeras med påståenden som inte finns i git.
Formatet följer [Keep a Changelog](https://keepachangelog.com/sv/1.1.0/).
## [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)
Nästa version. Nya poster läggs överst när de mergas.
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.
## 2026-11-10
- 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)
- fix(security): smala gitleaks-fingerprints för verifierade falska positiver (`39fc5e3`)
- docs(production): standardiserad produktionsberedskap och gap-analys (`a418933`)
- docs(status): spegla STATUS.md i docs/ för produktionsaudit (`fb072d4`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-925f5ca2fd (`dccdfed`)
- chore: adopt SIAX Golden Standard docs and configuration (`2ea4062`)
- ci: re-trigger efter serveromstart (statusuppdatering avbröts) (`54ff8a3`)
- chore(doctor): exempt generated artifacts and mirror prose from REPO-01 (`1e79d0c`)
- docs(policy): SIAX Agent Execution Policy i AGENTS.md + CLAUDE.md (`72d5876`)
### Claimed here before 2026-09-07 but NOT implemented — status 2026-09-15
## 2026-11-09
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):
- docs(production): standardiserad produktionsberedskap och gap-analys (`66a7e25`)
- chore(manifest): registrera saknade surface-poster (surfaces-fasen) (`416d00f`)
- feat(capabilities): registrera capability-ägare med verifierat kontrakt (#529) (`d3b1964`)
- feat(capabilities): bind operationId for API-surface capability records (`20d66ea`)
- feat(auth): dual-accept CL0UD workload identity on the machine routes (`d40360a`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-62517aca49 (`c6a5102`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-38ae3ddf0b (`4020217`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-b1ae910d64 (`8e7c228`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-419c2d3f18 (`fab0a53`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-ebfa17028d (`32cd714`)
- 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-11-08
## 2026-08-11 — Evasion-stack decision provenance
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-6b57f7cb9d (`966a36f`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-72d6d1e8d1 (`5795b35`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-5010a6853b (`a0eba85`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-f5600a25b9 (`5a56cc9`)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-9aef3cd97f (`fdba72b`)
- 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-11-07
## 2026-08-09/10 — Golden Standard compliance pass (FAS 0–10)
- fix(signing): cosign-pubkey + SBOM + deployment/env-poster för prod-imagen prod-a0e701efc8 (`be6b92f`)
- test(lint): typa idempotency-store i stället för any (blockerar quality-gaten) (`6bbbcf7`)
- docs(identity): 0pt workload identity — målbild, integrationsdesign och blockerare (`8741169`)
- docs(runbook): legacy-instansaudit server5 (0pt + L0RE) och disposition (`b7627a3`)
- fix(canonical): serialisera timestamptz som RFC3339 (ISO-8601) (`afd0b4e`)
- fix(sql): 023 grants for canonical tables to app-role 0pt_app (`4dbabd1`)
- fix(sql): 022 canonical facts blir idempotent (DROP POLICY IF EXISTS) (`0213459`)
- fix(auth): en (1) tenant per intern serviceprincipal — multi-tenant nekas (`1a581fd`)
- ci: add blocking component conformance gate (`831781f`)
- fix(auth): bind internal-service tenant authority to server config (`b97ad0b`)
- feat(capability): descriptor-completion wave 1 (0pt) — verified openapi operationIds (`019cc23`)
- fix(auth): internal-token requests får verifierad tenant (P0 tenant-isolering) (`849e7ae`)
- `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-11-06
## 2026-08 — Auth + capability hardening ("Wave 2/3")
- ci(sonar): self-hosted SonarQube lives at sonar.siax.dev, not .io (`98039d3`)
- fix(portal): reuse canonic @siax/ui FileDropzone instead of a local ad-hoc one (`4f92cdd`)
- fix(@siax/access): restore relationships.ts that disappeared when the package split into two files (`a424cf1`)
- feat(@siax/access): universal idempotency contract (`f77a4fe`)
- feat(idempotency): operation-scope binding for upload identity (`6ca6e8e`)
- chore(ui): document domain/legacy UI exceptions (`415396f`)
- `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-11-05
## 2026-07-13 — Scraper stack (kept, see BACKLOG P3-3 / SECURITY.md)
- feat(0pt): canonical-fact machine route + renumber migration to 022 (#484) (`c46ebbf`)
- feat(0pt): canonical structured observations — entities, facts, relations (#480) (`03cdb78`)
- `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.
## 2026-11-04
## Earlier
- evidence(prod): bind source-bundle artifactId to real deployed image digest (`1163048`)
- evidence(prod): bind source-bundle artifactId to real deployed image digest (`c7966eb`)
- evidence(prod): bind source-bundle artifactId to real deployed image digest (`d5aea59`)
- evidence(prod): bind source-bundle artifactId to real deployed image digest (`ce6be94`)
- evidence(prod): real artifact-digest, running-digest and deploy-log for 13e156ec7e6b (`4c20e54`)
- chore(runtime-evidence): sbom — estate-kampanj issue #76 (app 0pt) (`b29e484`)
- chore(design): add design-conformance manifest (`76e3c0d`)
- 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
Genererad: 2026-11-10. Underlag: `git log --no-merges` (senaste 50 commits).
-67
View File
@@ -1,67 +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.
-8
View File
@@ -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
-58
View File
@@ -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.
-170
View File
@@ -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"]
-315
View File
@@ -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.
+13 -10
View File
@@ -1,15 +1,18 @@
SIAX Proprietary License
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.
SPDX-License-Identifier: LicenseRef-SIAX-Proprietary
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.
This software and its source code are proprietary and confidential.
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.
1. No license, express or implied, is granted except by a separate written
agreement signed by SIAX Technology AB.
2. Copying, modifying, distributing, sublicensing or deploying this software
outside SIAX Technology AB's own systems requires such an agreement.
3. Third-party components included in or fetched by this repository remain
under their own licenses; see the dependency lockfile and NOTICE files.
4. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
This is not an open-source license.
Questions about licensing: legal@siax.io
-37
View File
@@ -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
-65
View File
@@ -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`.
-20
View File
@@ -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).
-175
View File
@@ -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
}
}
-117
View File
@@ -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.
+36 -82
View File
@@ -1,98 +1,52 @@
# Security Policy
# Säkerhetspolicy — 0pt
0pt (SIAX Core Platform: Data OS) is proprietary software operated by SIAX
Technology AB. This document explains how to report a suspected security
vulnerability.
## Rapportera en sårbarhet
## Reporting a vulnerability
Mejla **security@siax.io** med repo, version/commit, reproduktion och påverkan.
Skicka inte en publik issue för säkerhetsproblem. Vi bekräftar mottagande och
återkommer med bedömning; koordinera offentliggörande med oss.
Do not open a public GitHub issue for a suspected security problem.
## Omfattning
Email: security@siax.io (fallback: simon@siax.io)
Detta repo (`0pt`) ingår i SIAX-plattformen. Policyn omfattar källkod,
CI-kedjor, containers och de hemligheter som används vid bygge och drift.
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.
## Hemligheter
## Response expectations
- Inga hemligheter i git — varken i kod, `.env`, fixturer eller historik.
- Hemligheter lagras i Infisical (`vault.siax.io`) och injiceras vid körning
(`siax-secrets run --env <miljö> -- <kommando>`). Värden skrivs aldrig ut i
loggar eller dokumentation.
- `.env.example` innehåller namn och beskrivningar, aldrig värden.
- `gitleaks` körs i CI (REPO-15). Ett fynd är ett stopp, inte en varning.
- 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.
## Beroenden och kedjan
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).
- Låsta versioner och lockfil i varje repo (SUP-01). Renovate hanterar
uppdateringar (SUP-05); säkerhetsuppdateringar prioriteras.
- CI-actioner pinnas till commit-SHA när så krävs av standarden (SUP-06).
- Containrar byggs från egna baser med minimal yta; ingen `tsx`/`ts-node`
i produktion (CD-03).
## Scope
## Åtkomst och identitet
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.
- Tjänst-till-tjänst sker med workload-identitet och tenant-scope, inte med
delade statiska nycklar (se `siax.repo.json` → `identity`).
- Minsta behörighet gäller; ingen app får bredare åtkomst än sin deklarerade
kapacitet.
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.
## Personuppgifter
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.
- Klassificering och PII-krav deklareras i `siax.repo.json` → `discovery`.
- Personuppgifter får inte loggas, cachelagras eller kopieras till miljöer som
inte är godkända för ändamålet.
## Known, already-tracked gaps
## Incidenter
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:
Följ `docs/runbook/incident.md`. Bevara evidens, begränsa spridning, återställ
och skriv en efteranalys. Kontakta ansvarig owner enligt `.gitea/CODEOWNERS`.
- **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.
Denna policy är genererad 2026-11-10 och gäller tills en mänskligt granskad
version ersätter den. Den påstår ingen genomförd granskning eller certifiering.
-277
View File
@@ -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).
-51
View File
@@ -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.
-43
View File
@@ -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__'
@@ -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
@@ -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
-19
View File
@@ -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 }
-34
View File
@@ -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]
-26
View File
@@ -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]
@@ -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") }}'
@@ -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
@@ -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 %}
@@ -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
@@ -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
-142
View File
@@ -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<string, CustomerRow>();
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<CustomerRow[] | null>(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 (
<PortalShell variant="admin" title={t({ en: "Customers", sv: "Kunder" })}>
<p className="font-mono text-[11px] uppercase tracking-[0.1em] text-muted-foreground">
{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." })}
</p>
{error ? (
<div className="mt-6">
<ErrorState onRetry={load} />
</div>
) : !rows ? (
<div className="mt-6">
<LoadingSkeleton rows={6} />
</div>
) : rows.length === 0 ? (
<div className="mt-6">
<EmptyState
title={t({ en: "No customers yet", sv: "Inga kunder ännu" })}
line={t({ en: "Customers appear here as soon as the first order is created.", sv: "Kunder visas här så fort första ordern skapas." })}
ctaHref="/bestall"
ctaLabel={t({ en: "New order", sv: "Ny beställning" })}
/>
</div>
) : (
<>
<div className="mt-6 flex flex-wrap items-baseline gap-x-8 gap-y-2 border border-border bg-secondary/40 px-4 py-3">
<span className="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
{rows.length} {t({ en: "customers", sv: "kunder" })}
</span>
<span className="font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground">
{rows.reduce((s, r) => s + r.orders, 0)} {t({ en: "orders", sv: "ordrar" })}
</span>
<span className="ml-auto font-mono text-sm tabular-nums">
{formatSEK(totalOre / 100)}
</span>
</div>
<div className="mt-px border border-border">
<div className="grid grid-cols-[2fr_0.8fr_1fr_1fr] border-b border-border bg-secondary/50 px-4 py-2.5 font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
<span>{t({ en: "Customer", sv: "Kund" })}</span>
<span className="text-right">{t({ en: "Orders", sv: "Ordrar" })}</span>
<span className="text-right">{t({ en: "Latest", sv: "Senast" })}</span>
<span className="text-right">{t({ en: "Total", sv: "Summa" })}</span>
</div>
{rows.map((c) => (
<div
key={c.name}
className="grid grid-cols-[2fr_0.8fr_1fr_1fr] items-center border-b border-border px-4 py-3.5 text-sm last:border-b-0"
>
<span className="flex items-center gap-2.5 font-medium">
<span
aria-hidden
className="inline-block h-2 w-2 shrink-0"
style={{ backgroundColor: "var(--prov-2)" }}
/>
<span className="truncate">{c.name === "unknown" ? t({ en: "Unknown", sv: "Okänd" }) : c.name}</span>
</span>
<span className="text-right font-mono text-xs tabular-nums text-muted-foreground">
{c.orders}
</span>
<span className="text-right font-mono text-xs tabular-nums text-muted-foreground">
{fmtDate(c.last)}
</span>
<span className="text-right font-mono text-sm tabular-nums">
{formatSEK(c.totalOre / 100)}
</span>
</div>
))}
</div>
</>
)}
</PortalShell>
);
}
-223
View File
@@ -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<string, string>[] {
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<string, string> = {};
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<string | null>(null);
const [store, setStore] = useState<ImportRunStore | null>(null);
const [serverReport, setServerReport] = useState<ServerReport | null>(null);
const [serverError, setServerError] = useState<string | null>(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 (
<main style={{ maxWidth: 720, margin: "0 auto", padding: "2rem 1rem" }}>
<h1 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>Importera ordrar</h1>
<p style={{ marginBottom: "1rem", opacity: 0.8 }}>
CSV med kolumnerna: {IMPORT_COLUMNS.join(", ")}. Endast <code>flow</code> är obligatorisk
(catalog | custom | subscription). Parsering och validering sker på servern —
filen laddas inte upp förrän ett steg körs.
</p>
<input
type="file"
accept=".csv,text/csv"
aria-label="Välj CSV-fil"
onChange={(e) => {
const f = e.target.files?.[0];
if (f) void onFile(f);
}}
/>
{store && (
<div style={{ marginTop: "1.5rem" }}>
<ImportWizard
store={store}
columns={IMPORT_COLUMNS.map((c) => ({ 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}
/>
</div>
)}
{serverError && (
<p role="alert" style={{ marginTop: "1rem", color: "crimson" }}>
{serverError}
</p>
)}
{serverReport && (serverReport.errorCount ?? 0) > 0 && (
<section style={{ marginTop: "1.5rem" }} aria-label="Felrapport från servern">
<h2 style={{ fontSize: "1.1rem" }}>Felrader (serverns rapport)</h2>
<ul>
{(serverReport.errors ?? []).map((e, i) => (
<li key={i}>
Rad {e.row} · {e.field} · {e.code}
{e.message ? ` — ${e.message}` : ""}
</li>
))}
</ul>
</section>
)}
{recentOrders && (
<section style={{ marginTop: "1.5rem" }} aria-label="Importerade ordrar efter reload">
<h2 style={{ fontSize: "1.1rem" }}>Importerade ordrar (lästa från /api/orders efter import)</h2>
{recentOrders.length === 0 ? (
<p>Inga importerade ordrar hittades vid reload.</p>
) : (
<ul>
{recentOrders.map((o) => (
<li key={o.order_id}>
<code>{o.order_id}</code> — {o.flow} — {o.status}
</li>
))}
</ul>
)}
</section>
)}
</main>
);
}
-216
View File
@@ -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 (
<div className="border-b border-border px-4 py-4 last:border-b-0">
<div className="flex flex-wrap items-baseline justify-between gap-3">
<div className="flex items-baseline gap-3">
<span className="font-mono text-xs text-muted-foreground">
{order.order_id.slice(0, 8)}
</span>
<span className="text-sm font-medium">{order.config.type ?? t({ en: "Company", sv: "Bolag" })}</span>
<span className="font-mono text-[11px] text-muted-foreground">
{markets.length ? markets.join(" · ") : "—"}
</span>
</div>
<div className="flex items-center gap-5">
<span className="font-mono text-[11px] text-muted-foreground">
{(order.config.volume ?? 0).toLocaleString("sv-SE")} {t({ en: "rows", sv: "rader" })}
</span>
<span className="font-mono text-[11px] text-muted-foreground">
{formatSEK(order.total_ore / 100)}
</span>
<StatusPill status={order.status} />
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
{STAGES.map((stage, i) => {
const p = progress[i];
const active = p > 0 && p < 1;
const complete = p >= 1;
return (
<div key={stage} className="min-w-0">
<div className="flex items-center justify-between gap-2">
<span className="truncate font-mono text-[9.5px] uppercase tracking-[0.1em] text-muted-foreground">
{t({ en: STAGE_LABELS_EN[stage], sv: stage })}
</span>
<span
aria-hidden
className="inline-block h-1.5 w-1.5 shrink-0"
style={{
backgroundColor: complete
? "var(--prov-3)"
: active
? "var(--accent-red)"
: "var(--border)",
}}
/>
</div>
<div
className="mt-1.5 h-1 w-full border border-border bg-background"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(p * 100)}
aria-label={`${t({ en: STAGE_LABELS_EN[stage], sv: stage })}: ${Math.round(p * 100)}%`}
>
<span
className="block h-full"
style={{
width: `${Math.round(p * 100)}%`,
backgroundColor: complete ? "var(--prov-3)" : "var(--accent-red)",
}}
/>
</div>
</div>
);
})}
</div>
<p className="mt-3 font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
{done}/{STAGES.length} {t({ en: "steps · started", sv: "steg · startad" })} {new Date(order.created_at).toLocaleDateString("sv-SE")}
</p>
</div>
);
}
export default function AdminJobsPage() {
const t = useT();
const [orders, setOrders] = useState<Order[] | null>(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 (
<PortalShell variant="admin" title={t({ en: "Jobs & pipeline", sv: "Jobb & pipeline" })}>
<p className="font-mono text-[11px] uppercase tracking-[0.1em] text-muted-foreground">
{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.",
})}
</p>
{error ? (
<div className="mt-6">
<ErrorState onRetry={load} />
</div>
) : !orders ? (
<div className="mt-6">
<LoadingSkeleton rows={6} />
</div>
) : running.length === 0 ? (
<div className="mt-6">
<EmptyState
title={t({ en: "No jobs in production", sv: "Inga jobb i produktion" })}
line={t({
en: "When an order is paid the pipeline starts here — source racing through quality control in real time.",
sv: "När en order betalats startar pipelinen här — källracing till kvalitetskontroll i realtid.",
})}
ctaHref="/admin"
ctaLabel={t({ en: "To overview", sv: "Till översikt" })}
/>
</div>
) : (
<div className="mt-6">
<div className="flex items-center justify-between border border-b-0 border-border bg-secondary/50 px-4 py-2.5">
<span className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">
{running.length} {t({ en: "active jobs", sv: "aktiva jobb" })}
</span>
<button
type="button"
onClick={load}
className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
{t({ en: "Refresh", sv: "Uppdatera" })}
</button>
</div>
<div className="border border-border">
{running.map((o) => (
<PipelineRow key={o.order_id} order={o} />
))}
</div>
</div>
)}
</PortalShell>
);
}
-238
View File
@@ -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<Order[] | null>(null);
const [orderId, setOrderId] = useState<string>("");
const [notes, setNotes] = useState<OrderNoteDto[] | null>(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 (
<PortalShell variant="admin" title="Orderanteckningar">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={offlineMode}
onChange={(e) => setOfflineMode(e.target.checked)}
/>
Offline-läge (blockera utskick — varning + no-op, utkast bevaras)
</label>
{offlineMode && (
<div role="alert" className="rounded border border-amber-400 bg-amber-50 p-2 text-sm">
Offline-läge aktivt: inga anteckningar skickas till servern. Utkast bevaras lokalt —
ingen tyst förlust.
</div>
)}
{loadError ? (
<ErrorState onRetry={() => orderId && loadNotes(orderId)} />
) : orders === null ? (
<LoadingSkeleton />
) : orders.length === 0 ? (
<EmptyState title="Inga ordrar" line="Inga ordrar att annotera ännu." />
) : (
<div className="space-y-4">
<label className="flex items-center gap-2 text-sm">
Order:{" "}
<select
aria-label="Välj order"
value={orderId}
onChange={(e) => {
setOrderId(e.target.value);
void loadNotes(e.target.value);
}}
className="rounded border px-2 py-1"
>
<option value="">— välj —</option>
{orders.map((o) => (
<option key={o.order_id} value={o.order_id}>
{o.order_id} ({o.flow}, {o.status})
</option>
))}
</select>
</label>
{orderId === "" ? (
<EmptyState title="Välj order" line="Välj en order för att se och skriva anteckningar." />
) : notes === null ? (
<LoadingSkeleton />
) : notes.length === 0 ? (
<EmptyState title="Tomt" line="Inga anteckningar på den här ordern ännu." />
) : (
<ul className="space-y-2">
{notes.map((n) => (
<li key={n.note_id} className="rounded border p-3 text-sm">
<div className="mb-1 flex justify-between text-xs opacity-70">
<span>
{n.author_id} · v{n.version} · klocka {n.clock}
</span>
<span>{new Date(n.updated_at).toLocaleString("sv-SE")}</span>
</div>
{editing?.note.note_id === n.note_id ? (
<div className="space-y-2">
<textarea
aria-label="Redigera anteckning"
value={editing.body}
onChange={(e) => setEditing({ ...editing, body: e.target.value })}
className="w-full rounded border p-2"
rows={3}
/>
<div className="flex gap-2">
<button className="rounded bg-black px-3 py-1 text-white" onClick={() => void saveEdit()}>
Spara (v{editing.note.version} → v{editing.note.version + 1})
</button>
<button className="rounded border px-3 py-1" onClick={() => setEditing(null)}>
Avbryt
</button>
</div>
</div>
) : (
<>
<p className="whitespace-pre-wrap">{n.body}</p>
<button
className="mt-2 text-xs underline"
onClick={() => setEditing({ note: n, body: n.body })}
>
Redigera
</button>
</>
)}
</li>
))}
</ul>
)}
{orderId !== "" && (
<div className="space-y-2">
<textarea
aria-label="Ny anteckning"
placeholder="Skriv en anteckning på ordern…"
value={draft}
onChange={(e) => setDraft(e.target.value)}
className="w-full rounded border p-2"
rows={3}
/>
<button
className="rounded bg-black px-4 py-2 text-sm text-white disabled:opacity-40"
disabled={!draft.trim() || notes === null}
onClick={() => void addNote()}
>
Lägg till anteckning
</button>
</div>
)}
</div>
)}
</PortalShell>
);
}
-155
View File
@@ -1,155 +0,0 @@
"use client";
import { ActionGate, InboxList } from "@siax/ui";
import { nextOrderStatus } from "@/lib/order-flow";
import { useCallback, useEffect, useState } from "react";
import { toast } from "sonner";
import { useT } from "@/components/locale-provider";
import { PortalShell, StatusPill } from "@/components/portal-shell";
import { LoadingSkeleton, ErrorState, EmptyState } from "@/components/states";
import { formatSEK } from "@/lib/data";
import { mergeOrders } from "@/lib/order-client";
interface Order {
order_id: string;
flow: string;
status: string;
total_ore: number;
deposit_ore: number;
created_at: string;
}
interface Metrics {
total: number;
in_production: number;
review: number;
delivered: number;
deposit_paid_ore: number;
pipeline_ore: number;
}
export default function AdminPage() {
const t = useT();
const [orders, setOrders] = useState<Order[] | null>(null);
const [metrics, setMetrics] = useState<Metrics | null>(null);
const [error, setError] = useState(false);
const load = useCallback(async () => {
setError(false);
try {
const [oRes, mRes] = await Promise.all([
fetch("/api/orders", { cache: "no-store" }),
fetch("/api/admin/metrics", { cache: "no-store" }),
]);
if (!oRes.ok || !mRes.ok) throw new Error("fetch failed");
const [o, m] = await Promise.all([oRes.json(), mRes.json()]);
if (!m || typeof m.total !== "number") throw new Error("bad metrics shape");
setOrders(mergeOrders(Array.isArray(o) ? o : []) as Order[]);
setMetrics(m);
} catch {
setError(true);
}
}, []);
useEffect(() => { void load(); }, [load]);
async function advance(id: string, status: string) {
const next = nextOrderStatus(status);
if (!next) return;
try {
const r = await fetch(`/api/orders/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ status: next }) });
if (r.ok) {
toast.success(`${id.slice(0, 8)}… → ${next}`);
void load();
} else {
toast.error(t({ en: "Could not advance order.", sv: "Kunde inte flytta fram ordern." }));
}
} catch {
toast.error(t({ en: "Network error.", sv: "Nätverksfel." }));
}
}
return (
<PortalShell variant="admin" title={t({ en: "Order queue & operations", sv: "Orderkö & drift" })}>
{error ? (
<ErrorState onRetry={load} />
) : !metrics || !orders ? (
<LoadingSkeleton rows={6} />
) : (
<>
<div className="grid grid-cols-2 gap-px border border-border bg-border md:grid-cols-5">
{[
[t({ en: "Orders", sv: "Ordrar" }), String(metrics.total)],
[t({ en: "In production", sv: "I produktion" }), String(metrics.in_production)],
[t({ en: "In review", sv: "Granskas" }), String(metrics.review)],
[t({ en: "Delivered", sv: "Levererade" }), String(metrics.delivered)],
[t({ en: "Deposits in", sv: "Förskott in" }), formatSEK((metrics.deposit_paid_ore || 0) / 100)],
].map(([l, v]) => (
<div key={l} className="bg-background p-5">
<p className="font-mono text-[10px] uppercase tracking-[0.12em] text-muted-foreground">{l}</p>
<p className="mt-2 text-2xl font-medium tabular-nums tracking-[-0.02em]">{v}</p>
</div>
))}
</div>
{orders.some((o) => o.inbox_item) ? (
<section className="mt-10">
<h2 className="font-mono text-[11px] uppercase tracking-[0.14em] text-muted-foreground">{t({ en: "Inbox", sv: "Inkorg" })}</h2>
<div className="mt-4">
<InboxList
items={orders.map((o) => o.inbox_item).filter(Boolean)}
labels={{
resolve: t({ en: "Advance", sv: "Flytta fram" }),
emptyTitle: t({ en: "Inbox is empty", sv: "Inkorgen är tom" }),
}}
onResolve={(item) => {
const order = orders.find((o) => `order:${o.order_id}` === item.id);
if (order?.advance_action?.state === "available" && nextOrderStatus(order.status)) {
advance(order.order_id, order.status);
}
}}
/>
</div>
</section>
) : null}
<section className="mt-10">
<div className="flex items-center justify-between">
<h2 className="font-mono text-[11px] uppercase tracking-[0.14em] text-muted-foreground">{t({ en: "Order queue", sv: "Orderkö" })}</h2>
{/* DoD §65: real anchor, not a JS-handler button — the download is a plain GET of the CSV endpoint. */}
<a href="/api/orders/export" download className="border border-border px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.08em] text-muted-foreground transition-colors hover:bg-foreground hover:text-background">
{t({ en: "Export CSV", sv: "Exportera CSV" })}
</a>
</div>
{orders.length === 0 ? (
<div className="mt-6"><EmptyState title={t({ en: "Empty queue.", sv: "Tom kö." })} line={t({ en: "No orders yet.", sv: "Inga ordrar än." })} /></div>
) : (
<div className="mt-4 border border-border">
<div className="grid grid-cols-[1.4fr_1.2fr_1fr_1fr_1.2fr] border-b border-border bg-secondary/50 px-4 py-2.5 font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
<span>{t({ en: "ID", sv: "ID" })}</span><span>{t({ en: "Flow", sv: "Flöde" })}</span><span>{t({ en: "Status", sv: "Status" })}</span><span className="text-right">{t({ en: "Total", sv: "Summa" })}</span><span className="text-right">{t({ en: "Action", sv: "Åtgärd" })}</span>
</div>
{orders.map((o) => (
<div key={o.order_id} className="grid grid-cols-[1.4fr_1.2fr_1fr_1fr_1.2fr] items-center border-b border-border px-4 py-3 last:border-b-0">
<span className="font-mono text-[12px] tabular-nums">{o.order_id.slice(0, 8)}…</span>
<span className="text-sm">{o.flow}</span>
<StatusPill status={o.status} />
<span className="text-right font-mono text-[12px] tabular-nums">{formatSEK(o.total_ore / 100)}</span>
<span className="text-right">
<ActionGate action={o.advance_action} className="inline-block text-right">
<button
onClick={() => advance(o.order_id, o.status)}
className="border border-foreground px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.08em] transition-colors hover:bg-foreground hover:text-background"
>
→ {nextOrderStatus(o.status) ?? t({ en: "done", sv: "klar" })}
</button>
</ActionGate>
</span>
</div>
))}
</div>
)}
</section>
</>
)}
</PortalShell>
);
}
-66
View File
@@ -1,66 +0,0 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useT } from "@/components/locale-provider";
import { PortalShell } from "@/components/portal-shell";
import { LoadingSkeleton, ErrorState } from "@/components/states";
interface Source {
source_id: string;
name: string;
market: string;
access: string;
licence: string;
cadence: string;
confidence: number;
}
const ACCESS_COLOR: Record<string, string> = {
api: "var(--prov-3)", bulk: "var(--prov-2)", authorized_web: "var(--prov-1)", licensed: "var(--prov-0)",
};
export default function AdminSourcesPage() {
const t = useT();
const [sources, setSources] = useState<Source[] | null>(null);
const [error, setError] = useState(false);
const load = useCallback(async () => {
setError(false);
try {
const r = await fetch("/api/sources", { cache: "no-store" });
setSources(await r.json());
} catch { setError(true); }
}, []);
useEffect(() => { void load(); }, [load]);
return (
<PortalShell variant="admin" title={t({ en: "Source registry & freshness", sv: "Källregister & färskhet" })}>
<p className="font-mono text-[11px] uppercase tracking-[0.1em] text-muted-foreground">
{t({ en: "Legal sourcing. API > bulk > authorized web > licence. Verify terms.", sv: "Laglig sourcing. API > bulk > auktoriserad webb > licens. Verifiera villkor." })}
</p>
{error ? (
<div className="mt-6"><ErrorState onRetry={load} /></div>
) : !sources ? (
<div className="mt-6"><LoadingSkeleton rows={8} /></div>
) : (
<div className="mt-6 border border-border">
<div className="grid grid-cols-[1.6fr_0.8fr_1fr_1.6fr_1fr] border-b border-border bg-secondary/50 px-4 py-2.5 font-mono text-[10px] uppercase tracking-[0.1em] text-muted-foreground">
<span>{t({ en: "Source", sv: "Källa" })}</span><span>{t({ en: "Market", sv: "Marknad" })}</span><span>{t({ en: "Access", sv: "Åtkomst" })}</span><span>{t({ en: "Licence", sv: "Licens" })}</span><span>{t({ en: "Freshness", sv: "Färskhet" })}</span>
</div>
{sources.map((s) => (
<div key={s.source_id} className="grid grid-cols-[1.6fr_0.8fr_1fr_1.6fr_1fr] items-center border-b border-border px-4 py-3 text-sm last:border-b-0">
<span className="font-medium">{s.name}</span>
<span className="font-mono text-xs">{s.market}</span>
<span className="flex items-center gap-2 font-mono text-[11px] uppercase tracking-[0.06em]">
<span className="inline-block h-2 w-2" style={{ backgroundColor: ACCESS_COLOR[s.access] ?? "var(--muted-foreground)" }} />
{s.access}
</span>
<span className="truncate pr-3 text-xs text-muted-foreground">{s.licence}</span>
<span className="font-mono text-xs text-muted-foreground">{s.cadence}</span>
</div>
))}
</div>
)}
</PortalShell>
);
}
-95
View File
@@ -1,95 +0,0 @@
"use client";
import { useState } from "react";
import { PortalShell } from "@/components/portal-shell";
import { useT } from "@/components/locale-provider";
export default function ToolsPage() {
const t = useT();
return (
<PortalShell variant="admin" title={t({ en: "Tools", sv: "Verktyg" })}>
<p className="font-mono text-[11px] uppercase tracking-[0.1em] text-muted-foreground">
{t({ en: "Quick lookups against legal sources. Provenance on every hit.", sv: "Snabboppslag mot lagliga källor. Härkomst på varje träff." })}
</p>
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
<VehicleTool />
<ScreenTool />
</div>
</PortalShell>
);
}
function VehicleTool() {
const t = useT();
const [plate, setPlate] = useState("");
const [res, setRes] = useState<Record<string, unknown> | null>(null);
const [busy, setBusy] = useState(false);
async function lookup() {
if (!plate.trim()) return;
setBusy(true); setRes(null);
try { setRes(await (await fetch(`/api/vehicles?plate=${encodeURIComponent(plate)}`)).json()); } catch { setRes({ error: true }); }
setBusy(false);
}
const v = (res?.vehicle ?? null) as { make?: string; model?: string; year?: string; reg_plate?: string } | null;
return (
<section className="border border-border p-6">
<div className="flex items-center justify-between border-b border-border pb-3">
<h2 className="font-mono text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{t({ en: "Vehicle — RDW (NL)", sv: "Fordon — RDW (NL)" })}</h2>
<span className="inline-block h-2 w-2" style={{ backgroundColor: "var(--prov-3)" }} />
</div>
<div className="mt-4 flex gap-2">
<input value={plate} onChange={(e) => setPlate(e.target.value)} placeholder={t({ en: "Registration no. (NL)", sv: "Registreringsnr (NL)" })} className="flex-1 border border-border bg-input px-3 py-2 font-mono text-xs uppercase outline-none focus:border-foreground" />
<button onClick={lookup} disabled={busy} className="bg-foreground px-4 py-2 font-mono text-[11px] uppercase tracking-[0.1em] text-background transition-colors hover:bg-[var(--accent-red)] disabled:opacity-50">{busy ? "…" : t({ en: "Look up", sv: "Slå upp" })}</button>
</div>
<div className="mt-4 min-h-[60px] font-mono text-xs">
{res === null ? <span className="text-muted-foreground">—</span>
: v ? (
<div className="space-y-1">
<div>{v.make} {v.model}</div>
<div className="text-muted-foreground">{v.reg_plate} · {v.year ?? "—"} · {t({ en: "source RDW", sv: "källa RDW" })}</div>
</div>
) : <span className="text-muted-foreground">{t({ en: "No match (not available).", sv: "Ingen träff (ej tillgänglig)." })}</span>}
</div>
</section>
);
}
function ScreenTool() {
const t = useT();
const [q, setQ] = useState("");
const [matches, setMatches] = useState<{ name: string; score: number; schema: string }[] | null>(null);
const [busy, setBusy] = useState(false);
async function screen() {
if (!q.trim()) return;
setBusy(true); setMatches(null);
try { const d = await (await fetch(`/api/screen?q=${encodeURIComponent(q)}`)).json(); setMatches(d.matches ?? []); } catch { setMatches([]); }
setBusy(false);
}
return (
<section className="border border-border p-6">
<div className="flex items-center justify-between border-b border-border pb-3">
<h2 className="font-mono text-[11px] uppercase tracking-[0.12em] text-muted-foreground">{t({ en: "Sanctions / PEP — OpenSanctions", sv: "Sanktioner / PEP — OpenSanctions" })}</h2>
<span className="inline-block h-2 w-2" style={{ backgroundColor: "var(--prov-2)" }} />
</div>
<div className="mt-4 flex gap-2">
<input value={q} onChange={(e) => setQ(e.target.value)} placeholder={t({ en: "Name or company", sv: "Namn eller bolag" })} className="flex-1 border border-border bg-input px-3 py-2 font-mono text-xs outline-none focus:border-foreground" />
<button onClick={screen} disabled={busy} className="bg-foreground px-4 py-2 font-mono text-[11px] uppercase tracking-[0.1em] text-background transition-colors hover:bg-[var(--accent-red)] disabled:opacity-50">{busy ? "…" : t({ en: "Screen", sv: "Skärma" })}</button>
</div>
<div className="mt-4 min-h-[60px] font-mono text-xs">
{matches === null ? <span className="text-muted-foreground">—</span>
: matches.length === 0 ? <span style={{ color: "var(--prov-3)" }}>{t({ en: "No matches.", sv: "Inga träffar." })}</span>
: (
<ul className="space-y-1">
{matches.map((m, i) => (
<li key={i} className="flex justify-between gap-3">
<span>{m.name}</span>
<span className="text-muted-foreground">{m.schema}{m.score ? ` · ${Math.round(m.score * 100)}%` : ""}</span>
</li>
))}
</ul>
)}
<p className="mt-3 text-[10.5px] text-muted-foreground">{t({ en: "Requires verified OpenSanctions terms/key for full matches.", sv: "Kräver verifierade OpenSanctions-villkor/nyckel för fullständiga träffar." })}</p>
</div>
</section>
);
}
-37
View File
@@ -1,37 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { PostgresCreditLedger } from '@/src/document-workbench/credit-ledger';
import { getRepositorySql, repositoryDatabaseUrl } from '@/src/repositories/db';
import { resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/** GET /api/0pt/credits/balance — the authenticated tenant's credit balance. */
export async function GET(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const url = repositoryDatabaseUrl();
if (!url) {
return NextResponse.json(
{
error: 'CREDITS_NOT_CONFIGURED',
message: 'Credits are not available until the database is configured',
},
{ status: 503 },
);
}
try {
const ledger = new PostgresCreditLedger(getRepositorySql(url));
const balance = await ledger.balance(verified.scope.tenantId);
return NextResponse.json({ tenant_id: verified.scope.tenantId, ...balance });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Credit balance unavailable';
const notConfigured = /RLS-enforced|isolation migration/i.test(message);
return NextResponse.json(
{ error: notConfigured ? 'CREDITS_NOT_CONFIGURED' : 'CREDITS_UNAVAILABLE', message },
{ status: 503 },
);
}
}
-146
View File
@@ -1,146 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { getStripe } from '@/lib/stripe';
import { PostgresCreditPurchaseStore } from '@/src/document-workbench/credit-purchase-store';
import { findPack, readCreditPriceBook } from '@/src/document-workbench/price-book';
import { getRepositorySql, repositoryDatabaseUrl } from '@/src/repositories/db';
import { isValidScopeId, resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
function appBaseUrl(request: NextRequest): string {
const configured = process.env.NEXT_PUBLIC_APP_URL;
if (configured && /^https:\/\//.test(configured)) return configured.replace(/\/+$/, '');
return request.nextUrl.origin;
}
/**
* POST /api/0pt/credits/checkout { pack_id }
*
* Creates a Stripe Checkout Session and stores the server-owned price snapshot
* BEFORE any credit exists. Credits are granted only when the Stripe webhook
* re-reads the session and its payment intent and the ledger accepts the
* idempotent verified-payment command.
*/
export async function POST(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const { tenantId } = verified.scope;
const book = readCreditPriceBook();
if (!book) {
return NextResponse.json(
{ error: 'PRICE_BOOK_NOT_CONFIGURED', message: 'Credit purchases are not available yet' },
{ status: 503 },
);
}
const stripe = getStripe();
if (!stripe) {
return NextResponse.json(
{ error: 'STRIPE_NOT_CONFIGURED', message: 'Payments are not available yet' },
{ status: 503 },
);
}
const databaseUrl = repositoryDatabaseUrl();
if (!databaseUrl) {
return NextResponse.json(
{
error: 'CREDITS_NOT_CONFIGURED',
message: 'Credits are not available until the database is configured',
},
{ status: 503 },
);
}
let body: unknown;
try {
body = await request.json();
} catch {
return NextResponse.json({ error: 'INVALID_BODY', message: 'JSON body required' }, { status: 400 });
}
const packId = (body as { pack_id?: unknown } | null)?.pack_id;
if (typeof packId !== 'string' || !isValidScopeId(packId)) {
return NextResponse.json(
{ error: 'INVALID_PACK', message: 'A valid pack_id is required' },
{ status: 400 },
);
}
const pack = findPack(book, packId);
if (!pack) {
return NextResponse.json({ error: 'UNKNOWN_PACK', message: 'Unknown credit pack' }, { status: 404 });
}
const purchaseId = crypto.randomUUID();
const base = appBaseUrl(request);
const metadata = {
kind: '0pt_credit_purchase',
tenant_id: tenantId,
purchase_id: purchaseId,
price_book_version: book.version,
};
let session;
try {
session = await stripe.checkout.sessions.create({
mode: 'payment',
client_reference_id: tenantId,
line_items: [
{
quantity: 1,
price_data: {
currency: book.currency.toLowerCase(),
unit_amount: pack.amountMinor,
product_data: { name: `${pack.credits} 0pt credits` },
},
},
],
metadata,
payment_intent_data: { metadata },
success_url: `${base}/portal/krediter?status=success&purchase=${encodeURIComponent(purchaseId)}`,
cancel_url: `${base}/portal/krediter?status=cancelled`,
});
} catch {
return NextResponse.json(
{ error: 'CHECKOUT_UNAVAILABLE', message: 'The payment provider could not start a checkout' },
{ status: 502 },
);
}
try {
await new PostgresCreditPurchaseStore(getRepositorySql(databaseUrl)).create({
tenantId,
purchaseId,
checkoutSessionId: session.id,
amountMinor: pack.amountMinor,
currency: book.currency,
credits: pack.credits,
});
} catch (error: unknown) {
// Never leave a payable session with no server-side snapshot behind it.
try {
await stripe.checkout.sessions.expire(session.id);
} catch {
/* best effort; the webhook will refuse the unmatched session */
}
const message = error instanceof Error ? error.message : 'Credit purchase could not be stored';
return NextResponse.json({ error: 'CREDITS_NOT_CONFIGURED', message }, { status: 503 });
}
if (!session.url) {
return NextResponse.json(
{ error: 'CHECKOUT_UNAVAILABLE', message: 'The payment provider returned no checkout URL' },
{ status: 502 },
);
}
return NextResponse.json(
{
purchase_id: purchaseId,
checkout_url: session.url,
credits: pack.credits,
amount_minor: pack.amountMinor,
currency: book.currency,
},
{ status: 201 },
);
}
-36
View File
@@ -1,36 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { readCreditPriceBook } from '@/src/document-workbench/price-book';
import { resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
/**
* GET /api/0pt/credits/packs — sellable credit packs from the server-owned
* price book. No price is ever accepted from the client.
*/
export async function GET(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const book = readCreditPriceBook();
if (!book) {
return NextResponse.json(
{
error: 'PRICE_BOOK_NOT_CONFIGURED',
message: 'Credit packs are not available until the server price book is configured',
},
{ status: 503 },
);
}
return NextResponse.json({
version: book.version,
currency: book.currency,
packs: book.packs.map((pack) => ({
id: pack.id,
credits: pack.credits,
amount_minor: pack.amountMinor,
})),
});
}
-27
View File
@@ -1,27 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { datasetService } from "@/src/services/datasets";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ datasetId: string }> },
) {
try {
const { datasetId } = await params;
const tenantId = new URL(request.url).searchParams.get("tenant_id")?.trim();
if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "tenant_id is required");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const dataset = await datasetService.getDataset(datasetId);
if (dataset.tenant_id !== tenantId) {
throw new SiaxError("NOT_FOUND", "0pt.dataset", `dataset ${datasetId} not found`);
}
return NextResponse.json(dataset);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,27 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { datasetVersionService } from "@/src/services/datasets";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ datasetId: string; version: string }> },
) {
try {
const { datasetId, version: rawVersion } = await params;
const tenantId = new URL(request.url).searchParams.get("tenant_id")?.trim();
if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "tenant_id is required");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const version = Number(rawVersion);
if (!Number.isSafeInteger(version) || version < 1) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "version must be a positive integer");
}
return NextResponse.json(await datasetVersionService.getVersion(datasetId, tenantId, version));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,75 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { datasetVersionService } from "@/src/services/datasets";
function required(body: Record<string, unknown>, field: string): string {
const value = body[field];
if (typeof value !== "string" || !value.trim()) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", `${field} is required`);
}
return value.trim();
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ datasetId: string }> },
) {
try {
const { datasetId } = await params;
const raw = await request.json();
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "JSON object body is required");
}
const body = raw as Record<string, unknown>;
const tenantId = required(body, "tenant_id");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
if (!Array.isArray(body.artifact_refs) || !body.artifact_refs.every((value) => typeof value === "string")) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "artifact_refs must be a string array");
}
const recordCount = body.record_count === undefined ? undefined : Number(body.record_count);
const idempotencyKey = request.headers.get("idempotency-key")?.trim()
|| (typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "")
|| undefined;
const version = await datasetVersionService.createVersion({
dataset_id: datasetId,
tenant_id: tenantId,
project_id: required(body, "project_id"),
artifact_refs: body.artifact_refs,
schema_version: required(body, "schema_version"),
...(recordCount === undefined ? {} : { record_count: recordCount }),
...(typeof body.description === "string" && body.description.trim()
? { description: body.description.trim() }
: {}),
created_by: request.headers.get("x-0pt-verified-sub") ?? "local-dev",
...(typeof body.correlation_id === "string" && body.correlation_id.trim()
? { correlation_id: body.correlation_id.trim() }
: {}),
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
});
return NextResponse.json(version, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ datasetId: string }> },
) {
try {
const { datasetId } = await params;
const tenantId = new URL(request.url).searchParams.get("tenant_id")?.trim();
if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "tenant_id is required");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
return NextResponse.json(await datasetVersionService.listVersions(datasetId, tenantId));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-91
View File
@@ -1,91 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { datasetService } from "@/src/services/datasets";
import type { DatasetStatus, DatasetType } from "@/src/types/dataset";
const DATASET_TYPES = new Set<DatasetType>(["raw", "structured", "enriched", "curated", "feature", "derived"]);
const DATASET_STATUSES = new Set<DatasetStatus>(["active", "archived", "deprecated"]);
function actorId(request: NextRequest): string {
return request.headers.get("x-0pt-verified-sub") ?? "local-dev";
}
function stringField(body: Record<string, unknown>, field: string): string {
const value = body[field];
if (typeof value !== "string" || !value.trim()) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", `${field} is required`);
}
return value.trim();
}
export async function POST(request: NextRequest) {
try {
const raw = await request.json();
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "JSON object body is required");
}
const body = raw as Record<string, unknown>;
const tenantId = stringField(body, "tenant_id");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const datasetType = stringField(body, "dataset_type") as DatasetType;
if (!DATASET_TYPES.has(datasetType)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported dataset_type");
}
const status = body.status === undefined ? undefined : String(body.status) as DatasetStatus;
if (status !== undefined && !DATASET_STATUSES.has(status)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported status");
}
const dataset = await datasetService.registerDataset({
dataset_id: stringField(body, "dataset_id"),
tenant_id: tenantId,
project_id: stringField(body, "project_id"),
name: stringField(body, "name"),
dataset_type: datasetType,
...(status ? { status } : {}),
...(typeof body.description === "string" && body.description.trim()
? { description: body.description.trim() }
: {}),
...(typeof body.correlation_id === "string" && body.correlation_id.trim()
? { correlation_id: body.correlation_id.trim() }
: {}),
created_by: actorId(request),
});
return NextResponse.json(dataset, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id")?.trim();
if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "tenant_id is required");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const typeRaw = searchParams.get("dataset_type") ?? undefined;
const statusRaw = searchParams.get("status") ?? undefined;
if (typeRaw && !DATASET_TYPES.has(typeRaw as DatasetType)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported dataset_type");
}
if (statusRaw && !DATASET_STATUSES.has(statusRaw as DatasetStatus)) {
throw new SiaxError("INVALID_REQUEST", "0pt.dataset", "unsupported status");
}
return NextResponse.json(await datasetService.listDatasets({
tenant_id: tenantId,
project_id: searchParams.get("project_id") ?? undefined,
dataset_type: typeRaw as DatasetType | undefined,
status: statusRaw as DatasetStatus | undefined,
}));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,62 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { parseBody, documentExtractionSchema } from "@/src/validation/0pt-schemas";
import { documentExtractionService } from "@/src/services/document-extraction-service";
import { assertTenantMatchesVerified, resolveDocumentActor } from "@/src/http/tenant-guard";
export async function POST(request: NextRequest, { params }: { params: Promise<{ documentId: string }> }) {
try {
const { documentId } = await params;
const __raw = await request.json();
const __parsed = parseBody(documentExtractionSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const body = __parsed.data;
const tenantGuard = assertTenantMatchesVerified(request, body.tenant_id);
if (tenantGuard) return tenantGuard;
const actor = resolveDocumentActor(request, body.actor_id);
if (actor.response) return actor.response;
if (!actor.actorId || !body.model_id) {
return NextResponse.json({ error: "actor and model_id are required" }, { status: 400 });
}
const result = await documentExtractionService.extractDocument(documentId, {
tenant_id: body.tenant_id,
project_id: body.project_id,
actor_id: actor.actorId,
correlation_id: body.correlation_id ?? crypto.randomUUID(),
model_id: body.model_id,
});
return NextResponse.json(result, { status: 201 });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
if (/not configured/i.test(message)) {
return NextResponse.json({ error: message }, { status: 501 });
}
if (/^Policy denied/i.test(message)) {
return NextResponse.json({ error: message }, { status: 403 });
}
if (/^Policy requires approval/i.test(message)) {
return NextResponse.json({ error: message }, { status: 409 });
}
const status = /not found/i.test(message) ? 404 : 400;
return NextResponse.json({ error: message }, { status });
}
}
export async function GET(request: NextRequest, { params }: { params: Promise<{ documentId: string }> }) {
try {
const { documentId } = await params;
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const extractions = await documentExtractionService.listExtractionsForDocument(documentId, tenantId);
return NextResponse.json(extractions);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 404 });
}
}
-64
View File
@@ -1,64 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { parseBody, documentCaptureSchema } from "@/src/validation/0pt-schemas";
import { documentCaptureService } from "@/src/services/document-capture-service";
import { assertTenantMatchesVerified, resolveDocumentActor } from "@/src/http/tenant-guard";
export async function POST(request: NextRequest) {
try {
const __raw = await request.json();
const __parsed = parseBody(documentCaptureSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const body = __parsed.data;
const tenantGuard = assertTenantMatchesVerified(request, body.tenant_id);
if (tenantGuard) return tenantGuard;
const actor = resolveDocumentActor(request, body.actor_id);
if (actor.response) return actor.response;
const result = await documentCaptureService.captureFromMailbox(
{
mailbox: {
tenantId: body.mailbox?.tenant_id,
clientId: body.mailbox?.client_id,
clientSecret: body.mailbox?.client_secret,
mailboxAddress: body.mailbox?.mailbox_address,
folder: body.mailbox?.folder,
},
since_iso: body.since_iso,
top: body.top,
max_pages: body.max_pages,
},
{
tenant_id: body.tenant_id,
project_id: body.project_id,
// In enforced mode middleware supplies the verified subject; the
// body can only agree with it. Development retains the old fallback.
actor_id: actor.actorId ?? "",
correlation_id: body.correlation_id ?? "",
},
);
return NextResponse.json(result, { status: 201 });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 400 });
}
}
// P014: `tenant_id` used to be optional here, and
// DocumentCaptureService.listDocuments treats an absent tenant_id filter as
// "no restriction" — so calling this with no query params at all returned
// EVERY tenant's captured documents to any authenticated caller. Every
// sibling list/get route in this file already requires tenant_id; this was
// the one outlier. Now required, same as the rest of the surface.
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const projectId = searchParams.get("project_id") ?? undefined;
const documents = await documentCaptureService.listDocuments({ tenant_id: tenantId, project_id: projectId });
return NextResponse.json(documents);
}
-56
View File
@@ -1,56 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { readCreditPriceBook } from '@/src/document-workbench/price-book';
import { resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
function csv(value: string | undefined): string[] {
return (value ?? '')
.split(',')
.map((part) => part.trim())
.filter(Boolean);
}
/**
* GET /api/0pt/documents/models — the server-approved model routes and limits
* the customer may choose from. Never a free-form model list.
*/
export async function GET(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const models = csv(process.env.OPT_ALLOWED_MODELS);
const dataClasses = csv(process.env.OPT_ALLOWED_DATA_CLASSES);
const destination = process.env.OPT_MODEL_DESTINATION;
const maxInputTokens = Number(process.env.OPT_MODEL_MAX_INPUT_TOKENS ?? '0');
const maxOutputTokens = Number(process.env.OPT_MODEL_MAX_OUTPUT_TOKENS ?? '0');
if (
models.length === 0 ||
dataClasses.length === 0 ||
(destination !== 'internal' && destination !== 'external') ||
!Number.isSafeInteger(maxInputTokens) ||
maxInputTokens <= 0 ||
!Number.isSafeInteger(maxOutputTokens) ||
maxOutputTokens <= 0
) {
return NextResponse.json(
{
error: 'MODEL_POLICY_NOT_CONFIGURED',
message: 'Document structuring is not available until the model policy is configured',
},
{ status: 503 },
);
}
const book = readCreditPriceBook();
return NextResponse.json({
models,
data_classes: dataClasses,
destination,
external_transfer_allowed: process.env.OPT_EXTERNAL_TRANSFER_ALLOWED === 'true',
max_input_tokens: maxInputTokens,
max_output_tokens: maxOutputTokens,
price_book_version: book?.version ?? null,
});
}
-443
View File
@@ -1,443 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { Doc0SourceClient, Doc0SourceUnavailableError } from '@/src/document-workbench/doc0-source-client';
import { Inf0OutcomeUnknownError } from '@/src/document-workbench/inf0-gateway-client';
import { createInf0Gateway } from '@/src/document-workbench/inf0-gateway';
import { PostgresCreditLedger } from '@/src/document-workbench/credit-ledger';
import { creditsForUsage, readCreditPriceBook } from '@/src/document-workbench/price-book';
import { structureDocument } from '@/src/document-workbench/structure-document';
import { exportValidatedOutput } from '@/src/document-workbench/export-validated-output';
import type {
OutputFieldContract,
OutputFieldType,
} from '@/src/document-workbench/structured-output-validation';
import { readDocumentWorkbenchRuntime } from '@/src/document-workbench/runtime-config';
import { createProjectAuthorizer } from '@/src/document-workbench/project-access';
import { getRepositorySql, repositoryDatabaseUrl } from '@/src/repositories/db';
import { isValidScopeId, resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const FIELD_TYPES: readonly OutputFieldType[] = ['string', 'integer', 'boolean', 'decimal', 'date'];
const DATA_CLASSES = ['none', 'low', 'medium', 'high', 'restricted'] as const;
function csv(value: string | undefined): string[] {
return (value ?? '')
.split(',')
.map((part) => part.trim())
.filter(Boolean);
}
function modelPolicy() {
const allowedModelIds = csv(process.env.OPT_ALLOWED_MODELS);
const allowedDataClasses = csv(process.env.OPT_ALLOWED_DATA_CLASSES);
const destination = process.env.OPT_MODEL_DESTINATION;
const maxInputTokens = Number(process.env.OPT_MODEL_MAX_INPUT_TOKENS ?? '0');
const maxOutputTokens = Number(process.env.OPT_MODEL_MAX_OUTPUT_TOKENS ?? '0');
if (
allowedModelIds.length === 0 ||
allowedDataClasses.length === 0 ||
(destination !== 'internal' && destination !== 'external') ||
!Number.isSafeInteger(maxInputTokens) ||
maxInputTokens <= 0 ||
!Number.isSafeInteger(maxOutputTokens) ||
maxOutputTokens <= 0
) {
return undefined;
}
return {
allowedModelIds,
allowedDataClasses,
destination: destination as 'internal' | 'external',
externalTransferAllowed: process.env.OPT_EXTERNAL_TRANSFER_ALLOWED === 'true',
maxInputTokens,
maxOutputTokens,
};
}
function parseFields(raw: unknown): OutputFieldContract[] | undefined {
if (!Array.isArray(raw) || raw.length === 0 || raw.length > 100) return undefined;
const fields: OutputFieldContract[] = [];
const seen = new Set<string>();
for (const item of raw) {
if (!item || typeof item !== 'object' || Array.isArray(item)) return undefined;
const field = item as Record<string, unknown>;
if (
typeof field.name !== 'string' ||
!/^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(field.name) ||
seen.has(field.name) ||
typeof field.type !== 'string' ||
!FIELD_TYPES.includes(field.type as OutputFieldType)
) {
return undefined;
}
seen.add(field.name);
fields.push({ name: field.name, type: field.type as OutputFieldType, required: true });
}
return fields;
}
interface StructureBody {
project_id?: unknown;
document_id?: unknown;
version_id?: unknown;
checksum_sha256?: unknown;
pipeline_stage?: unknown;
instruction?: unknown;
fields?: unknown;
model_id?: unknown;
data_class?: unknown;
format?: unknown;
}
/**
* POST /api/0pt/documents/structure
*
* Upload → DOC0 segments → INF0 model → mechanically validated, evidence-bound
* output (optionally exported). Credits are reserved before the model runs and
* settled to the measured usage afterwards.
*
* Nothing is trusted from the client except the requested target fields and
* instruction. Source text is re-read from DOC0 and every returned value must
* point at an exact span in that text.
*/
export async function POST(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const { tenantId, actorId } = verified.scope;
const runtimeConfig = readDocumentWorkbenchRuntime();
const gateway = createInf0Gateway();
if (!runtimeConfig.doc0 || !gateway) {
return NextResponse.json(
{
error: 'WORKBENCH_NOT_CONFIGURED',
message: 'Document structuring is not available until DOC0 and INF0 are configured',
},
{ status: 503 },
);
}
const policy = modelPolicy();
if (!policy) {
return NextResponse.json(
{
error: 'MODEL_POLICY_NOT_CONFIGURED',
message: 'Document structuring is not available until the model policy is configured',
},
{ status: 503 },
);
}
// Explicit pilot switch: `OPT_CREDITS_MODE=off` runs structuring without a
// wallet. It is deliberately NOT a silent fallback — the response reports
// `credits: null` and the operator has named the mode.
const creditsEnabled =
process.env.OPT_CREDITS_MODE !== 'off' &&
Boolean(readCreditPriceBook()) &&
Boolean(repositoryDatabaseUrl());
const priceBook = creditsEnabled ? readCreditPriceBook() : undefined;
const databaseUrl = creditsEnabled ? repositoryDatabaseUrl() : undefined;
if (!creditsEnabled && process.env.OPT_CREDITS_MODE !== 'off') {
return NextResponse.json(
{
error: 'CREDITS_NOT_CONFIGURED',
message: 'Document structuring is not available until credits are configured',
},
{ status: 503 },
);
}
let body: StructureBody;
try {
body = (await request.json()) as StructureBody;
} catch {
return NextResponse.json({ error: 'INVALID_BODY', message: 'JSON body required' }, { status: 400 });
}
const projectId = body.project_id;
const documentId = body.document_id;
const versionId = body.version_id;
const checksum = body.checksum_sha256;
const pipelineStage = body.pipeline_stage;
const instruction = body.instruction;
const modelId = body.model_id;
const dataClass = body.data_class ?? 'low';
const format = body.format ?? 'json';
if (
!isValidScopeId(projectId) ||
!isValidScopeId(documentId) ||
!isValidScopeId(versionId) ||
typeof checksum !== 'string' ||
!/^[0-9a-f]{64}$/.test(checksum) ||
pipelineStage !== 'parsed' ||
typeof instruction !== 'string' ||
!instruction.trim() ||
instruction.length > 4000 ||
typeof modelId !== 'string' ||
!policy.allowedModelIds.includes(modelId) ||
typeof dataClass !== 'string' ||
!(DATA_CLASSES as readonly string[]).includes(dataClass) ||
(format !== 'json' && format !== 'csv')
) {
return NextResponse.json(
{ error: 'INVALID_REQUEST', message: 'Invalid document structure request' },
{ status: 400 },
);
}
const fields = parseFields(body.fields);
if (!fields) {
return NextResponse.json(
{ error: 'INVALID_FIELDS', message: 'A valid field contract is required' },
{ status: 400 },
);
}
const authorize = createProjectAuthorizer();
if (!(await authorize({ tenantId, projectId, actorId }))) {
return NextResponse.json(
{ error: 'OPT_ACCESS_DENIED', message: 'Project access is not authorized' },
{ status: 403 },
);
}
const correlationId = request.headers.get('x-request-id') ?? crypto.randomUUID();
const context = {
tenantId,
projectId,
actorId,
capability: '0pt.document.structure.v1',
correlationId,
idempotencyKey: correlationId,
};
let sources;
try {
sources = await new Doc0SourceClient({
baseUrl: runtimeConfig.doc0.baseUrl,
serviceToken: runtimeConfig.doc0.serviceToken,
}).read({
tenantId,
projectId,
actorId: runtimeConfig.doc0.actorId ?? actorId,
requestId: correlationId,
receipt: {
documentId,
versionId,
checksumSha256: checksum,
pipelineStage: 'parsed',
},
});
} catch (error: unknown) {
if (error instanceof Doc0SourceUnavailableError) {
return NextResponse.json(
{
error: 'DOC0_SOURCE_UNAVAILABLE',
message:
'The extracted source could not be verified (version, checksum, page cap or tenant). Review or reconciliation is required.',
},
{ status: 422 },
);
}
return NextResponse.json(
{ error: 'DOC0_SOURCE_UNAVAILABLE', message: 'The extracted source is unavailable' },
{ status: 502 },
);
}
console.log('[0pt-workbench] doc0 source read:', sources.sources.length, 'segments');
const sourceChars = sources.sources.reduce((total, source) => total + source.text.length, 0);
const estimatedInputTokens = Math.ceil(JSON.stringify(sources.sources).length / 4) + 1024;
if (estimatedInputTokens > policy.maxInputTokens) {
return NextResponse.json(
{
error: 'SOURCE_TOO_LARGE',
message: 'The document is larger than the configured model input limit',
},
{ status: 413 },
);
}
const maximumCredits =
creditsEnabled && priceBook
? creditsForUsage(priceBook, {
pages: Math.max(1, Math.ceil(sourceChars / 1800)),
inputTokens: estimatedInputTokens,
outputTokens: policy.maxOutputTokens,
})
: 0;
const reservationId = crypto.randomUUID();
const ledger =
creditsEnabled && databaseUrl ? new PostgresCreditLedger(getRepositorySql(databaseUrl)) : undefined;
if (ledger) {
try {
await ledger.apply({
tenantId,
idempotencyKey: `reserve:${reservationId}`,
kind: 'reserve',
reference: reservationId,
amount: maximumCredits,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Credit reservation failed';
if (/Insufficient available credits/i.test(message)) {
return NextResponse.json(
{
error: 'INSUFFICIENT_CREDITS',
message: 'Not enough credits for this job',
required_credits: maximumCredits,
},
{ status: 402 },
);
}
return NextResponse.json({ error: 'CREDITS_UNAVAILABLE', message }, { status: 503 });
}
}
try {
const result = await structureDocument({
gateway,
request: {
context,
admission: {
tenantId,
projectId,
jobId: `doc:${documentId}:${correlationId}`,
modelId,
dataClass,
maxInputTokens: policy.maxInputTokens,
maxOutputTokens: policy.maxOutputTokens,
maximumCredits,
},
policy: {
tenantId,
projectId,
allowedModelIds: policy.allowedModelIds,
allowedDataClasses: policy.allowedDataClasses,
destination: policy.destination,
externalTransferAllowed: policy.externalTransferAllowed,
maxInputTokens: policy.maxInputTokens,
maxOutputTokens: policy.maxOutputTokens,
},
reservation: ledger
? {
tenantId,
projectId,
jobId: `doc:${documentId}:${correlationId}`,
status: 'reserved',
credits: maximumCredits,
}
: undefined,
},
countTokens: async (messages) => Math.ceil(JSON.stringify(messages).length / 4),
instructions: instruction,
fields,
sources: sources.sources,
});
const charged =
ledger && priceBook
? Math.min(
maximumCredits,
creditsForUsage(priceBook, {
pages: Math.max(1, Math.ceil(sourceChars / 1800)),
inputTokens: result.model.promptTokens,
outputTokens: result.model.completionTokens,
}),
)
: 0;
let balance;
if (ledger) {
try {
balance = (
await ledger.apply({
tenantId,
idempotencyKey: `settle:${reservationId}`,
kind: 'settle',
reference: reservationId,
amount: charged,
})
).balance;
} catch {
// The model result is real; keep the reservation for reconciliation
// rather than reporting a balance we could not write.
balance = undefined;
}
}
if (result.status !== 'mechanically_validated') {
return NextResponse.json(
{
status: 'needs_review',
issues: result.issues,
model: result.model,
credits: ledger ? { reserved: maximumCredits, charged, balance } : null,
},
{ status: 200 },
);
}
const exported =
format === 'json'
? exportValidatedOutput({
format: 'json',
fields,
sources: sources.sources,
value: result.value,
evidence: result.evidence,
})
: exportValidatedOutput({
format: 'csv',
fields,
sources: sources.sources,
value: result.value,
evidence: result.evidence,
});
return NextResponse.json(
{
status: 'mechanically_validated',
value: result.value,
evidence: result.evidence,
model: result.model,
credits: ledger ? { reserved: maximumCredits, charged, balance } : null,
export:
exported.status === 'ready'
? { media_type: exported.mediaType, content: exported.content }
: undefined,
},
{ status: 200 },
);
} catch (error: unknown) {
if (error instanceof Inf0OutcomeUnknownError) {
// Never release a reservation whose model call may have completed.
return NextResponse.json(
{
status: 'outcome_unknown',
message:
'The model call may have completed. The credit reservation is kept until its usage is reconciled.',
reservation_id: reservationId,
},
{ status: 202 },
);
}
if (ledger) {
try {
await ledger.apply({
tenantId,
idempotencyKey: `release:${reservationId}`,
kind: 'release',
reference: reservationId,
amount: 0,
});
} catch {
/* the reservation stays for reconciliation */
}
}
const message = error instanceof Error ? error.message : 'Structuring failed';
console.error('[0pt-workbench] structure failed:', message);
return NextResponse.json(
{ error: 'STRUCTURE_FAILED', message, reservation_id: reservationId },
{ status: 502 },
);
}
}
-241
View File
@@ -1,241 +0,0 @@
import { NextResponse, type NextRequest } from 'next/server';
import { Doc0UploadClient } from '@/src/document-workbench/doc0-upload-client';
import {
DocumentUploadCoordinator,
type CoordinatedUpload,
} from '@/src/document-workbench/upload-coordinator';
import { PostgresUploadAttemptStore } from '@/src/document-workbench/upload-attempt-store';
import {
createProjectAuthorizer,
projectAuthorizationConfigured,
} from '@/src/document-workbench/project-access';
import { readDocumentWorkbenchRuntime } from '@/src/document-workbench/runtime-config';
import { getRepositorySql, repositoryDatabaseUrl } from '@/src/repositories/db';
import { isValidScopeId, resolveVerifiedScope } from '@/src/http/verified-scope';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const CLASSIFICATIONS = [
'public',
'internal',
'confidential',
'restricted',
'personal-data',
'financial',
'legal',
] as const;
function isClassification(value: string): value is (typeof CLASSIFICATIONS)[number] {
return (CLASSIFICATIONS as readonly string[]).includes(value);
}
function idempotencyKey(request: NextRequest, form: FormData): string | undefined {
const fromHeader = request.headers.get('idempotency-key');
const fromBody = form.get('idempotency_key');
const value = fromHeader ?? (typeof fromBody === 'string' ? fromBody : undefined);
return value && /^[\x21-\x7e]{1,256}$/.test(value) ? value : undefined;
}
/**
* POST /api/0pt/documents/upload — customer document upload.
*
* multipart/form-data:
* file (required) the original bytes; never trusted metadata
* project_id (required) verified project the file belongs to
* idempotency_key (required) or `Idempotency-Key` header
* classification optional, defaults to `restricted` (conservative)
*
* The tenant and actor come ONLY from middleware's verified headers. The route
* fails closed when project authorization or DOC0 is not configured.
*/
export async function POST(request: NextRequest) {
const verified = resolveVerifiedScope(request);
if ('response' in verified) return verified.response;
const { tenantId, actorId } = verified.scope;
const runtimeConfig = readDocumentWorkbenchRuntime();
if (!runtimeConfig.doc0) {
return NextResponse.json(
{
error: 'DOC0_NOT_CONFIGURED',
message: 'Document upload is not available until DOC0 is configured',
},
{ status: 503 },
);
}
if (!projectAuthorizationConfigured()) {
return NextResponse.json(
{
error: 'PROJECT_AUTHORIZATION_NOT_CONFIGURED',
message: 'Document upload is not available until project authorization is configured',
},
{ status: 503 },
);
}
const databaseUrl = repositoryDatabaseUrl();
if (!databaseUrl) {
return NextResponse.json(
{
error: 'UPLOAD_JOURNAL_NOT_CONFIGURED',
message: 'Document upload is not available until its durable journal is configured',
},
{ status: 503 },
);
}
const declaredLength = Number(request.headers.get('content-length') ?? '0');
if (Number.isFinite(declaredLength) && declaredLength > runtimeConfig.doc0.maxUploadBytes + 128 * 1024) {
return NextResponse.json(
{ error: 'PAYLOAD_TOO_LARGE', message: 'The uploaded file is too large' },
{ status: 413 },
);
}
let form: FormData;
try {
form = await request.formData();
} catch {
return NextResponse.json(
{ error: 'INVALID_UPLOAD', message: 'A multipart form with a file is required' },
{ status: 400 },
);
}
const file = form.get('file');
const projectId = form.get('project_id');
const key = idempotencyKey(request, form);
const classificationRaw = form.get('classification');
const classification =
typeof classificationRaw === 'string' && classificationRaw.trim()
? classificationRaw.trim()
: 'restricted';
if (!(file instanceof File) || file.size <= 0) {
return NextResponse.json(
{ error: 'INVALID_UPLOAD', message: 'A non-empty file is required' },
{ status: 400 },
);
}
if (!isValidScopeId(projectId)) {
return NextResponse.json(
{ error: 'INVALID_PROJECT', message: 'A valid project_id is required' },
{ status: 400 },
);
}
if (!key) {
return NextResponse.json(
{
error: 'IDEMPOTENCY_KEY_REQUIRED',
message: 'An idempotency key is required for document uploads',
},
{ status: 400 },
);
}
if (!isClassification(classification)) {
return NextResponse.json(
{ error: 'INVALID_CLASSIFICATION', message: 'Unknown classification' },
{ status: 400 },
);
}
if (file.size > runtimeConfig.doc0.maxUploadBytes) {
return NextResponse.json(
{ error: 'PAYLOAD_TOO_LARGE', message: 'The uploaded file is too large' },
{ status: 413 },
);
}
let bytes: Uint8Array;
try {
bytes = new Uint8Array(await file.arrayBuffer());
} catch {
return NextResponse.json(
{ error: 'INVALID_UPLOAD', message: 'The uploaded file could not be read' },
{ status: 400 },
);
}
if (bytes.byteLength === 0 || bytes.byteLength !== file.size) {
return NextResponse.json(
{ error: 'INVALID_UPLOAD', message: 'The uploaded file is incomplete' },
{ status: 400 },
);
}
const authorize = createProjectAuthorizer();
const sql = getRepositorySql(databaseUrl);
// DOC0 authorizes the SERVICE identity and requires x-actor-id to be an
// actor that identity holds a grant for. The end-user actor is still the
// journal/authorization scope on our side; only the DOC0 call acts as the
// service.
const doc0ActorId = runtimeConfig.doc0.actorId ?? actorId;
const doc0Client = new Doc0UploadClient({
baseUrl: runtimeConfig.doc0.baseUrl,
serviceToken: runtimeConfig.doc0.serviceToken,
maxBytes: runtimeConfig.doc0.maxUploadBytes,
});
const coordinator = new DocumentUploadCoordinator({
attempts: new PostgresUploadAttemptStore(sql),
doc0: {
upload: (input) => doc0Client.upload({ ...input, actorId: doc0ActorId }),
inspect: (input) => doc0Client.inspect({ ...input, actorId: doc0ActorId }),
},
authorize: async (scope) => authorize(scope),
maxBytes: runtimeConfig.doc0.maxUploadBytes,
});
let outcome: CoordinatedUpload;
try {
outcome = await coordinator.execute({
tenantId,
projectId,
actorId,
requestId: request.headers.get('x-request-id') ?? crypto.randomUUID(),
idempotencyKey: key,
filename: file.name || 'source',
mimeType: file.type || 'application/octet-stream',
classification,
bytes,
});
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Upload failed';
if (/not authorized/i.test(message)) {
return NextResponse.json(
{ error: 'OPT_ACCESS_DENIED', message: 'Project access is not authorized' },
{ status: 403 },
);
}
if (/cannot be reused/i.test(message)) {
return NextResponse.json(
{
error: 'IDEMPOTENCY_CONFLICT',
message: 'The idempotency key was already used for different content',
},
{ status: 409 },
);
}
return NextResponse.json({ error: 'UPLOAD_FAILED', message }, { status: 400 });
}
if (outcome.status === 'receipt_verified') {
return NextResponse.json(
{
status: 'receipt_verified',
document_id: outcome.receipt.documentId,
version_id: outcome.receipt.versionId,
checksum_sha256: outcome.receipt.checksumSha256,
pipeline_stage: outcome.receipt.pipelineStage,
},
{ status: 201 },
);
}
return NextResponse.json(
{
status: 'outcome_unknown',
message:
'The upload may have been accepted. Its journal record is reported below; it does not prove storage or extraction.',
observation: outcome.observation ?? { status: 'reconciliation_required' },
},
{ status: 202 },
);
}
-44
View File
@@ -1,44 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { getBatchEntityResolutionService } from "@/src/services/entity-resolution";
function required(body: Record<string, unknown>, field: string): string {
const value = body[field];
if (typeof value !== "string" || !value.trim()) {
throw new SiaxError("INVALID_REQUEST", "0pt.er", `${field} is required`);
}
return value.trim();
}
export async function POST(request: NextRequest) {
try {
const raw = await request.json();
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new SiaxError("INVALID_REQUEST", "0pt.er", "JSON object body is required");
}
const body = raw as Record<string, unknown>;
const tenantId = required(body, "tenant_id");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const idempotencyKey = request.headers.get("idempotency-key")?.trim()
|| (typeof body.idempotency_key === "string" ? body.idempotency_key.trim() : "")
|| undefined;
const job = await getBatchEntityResolutionService().submitBatchResolution({
tenant_id: tenantId,
project_id: required(body, "project_id"),
records_artifact_ref: required(body, "records_artifact_ref"),
actor_id: request.headers.get("x-0pt-verified-sub") ?? "local-dev",
...(typeof body.correlation_id === "string" && body.correlation_id.trim()
? { correlation_id: body.correlation_id.trim() }
: {}),
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
});
return NextResponse.json(job, { status: 202 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-17
View File
@@ -1,17 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { parseErCandidateRequest } from "@/src/er/request-validation";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { entityResolutionService } from "@/src/services/entity-resolution";
export async function POST(request: NextRequest) {
try {
const input = parseErCandidateRequest(await request.json());
const guard = assertTenantMatchesVerified(request, input.tenant_id);
if (guard) return guard;
return NextResponse.json(entityResolutionService.generateCandidates(input));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-101
View File
@@ -1,101 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { verifyInternalServiceToken } from "@/src/auth/internal-token";
import {
EntityResolutionService,
type ErCandidateResult,
type ErRecord,
} from "@/src/er/entity-resolution-service";
import { parseErCandidateRequest } from "@/src/er/request-validation";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
const er = new EntityResolutionService();
function rawStringField(record: ErRecord, field: string): string | undefined {
const value = record.fields[field];
if (value === null || value === undefined) return undefined;
const normalized = String(value).trim();
return normalized || undefined;
}
function stableIdentifier(record: ErRecord, field: string): string | undefined {
const value = rawStringField(record, field);
if (!value) return undefined;
const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "");
return normalized || undefined;
}
function countryIdentifier(record: ErRecord): string | undefined {
const value = rawStringField(record, "registration_country") ?? rawStringField(record, "country");
if (!value) return undefined;
const normalized = value.toUpperCase().replace(/[^A-Z0-9]/g, "");
return normalized || undefined;
}
/**
* Company identity hard rule v3.
* - LEI is globally scoped.
* - National registration numbers are comparable only within the same country.
* - Any comparable stable-ID conflict dominates every positive match.
*/
er.registerHardRule({
rule_id: "0pt.company.stable-identifiers.v3",
decide(a, b) {
let sharedMatch = false;
const aLei = stableIdentifier(a, "lei");
const bLei = stableIdentifier(b, "lei");
if (aLei && bLei) {
if (aLei !== bLei) return "force_non_match";
sharedMatch = true;
}
const aCountry = countryIdentifier(a);
const bCountry = countryIdentifier(b);
const aOrg = stableIdentifier(a, "org_nr");
const bOrg = stableIdentifier(b, "org_nr");
if (aOrg && bOrg && aCountry && bCountry && aCountry === bCountry) {
if (aOrg !== bOrg) return "force_non_match";
sharedMatch = true;
}
return sharedMatch ? "force_match" : "abstain";
},
});
function assertInternal(request: NextRequest): NextResponse | undefined {
if (verifyInternalServiceToken(request.headers)) return undefined;
return NextResponse.json(
{ error: "UNAUTHENTICATED", message: "valid X-Internal-Token is required" },
{ status: 401 },
);
}
/**
* Company/Work boundary: fuzzy similarity is candidate evidence, never identity truth.
* Only product hard rules backed by stable identifiers may emit `match` here.
*/
export function enforceCompanyReviewBoundary(result: ErCandidateResult): ErCandidateResult {
const pairs = result.pairs.map((pair) =>
pair.decided_by === "score" && pair.status === "match"
? { ...pair, status: "pending_review" as const }
: pair,
);
return {
...result,
pairs,
review_queue: pairs.filter((pair) => pair.status === "pending_review"),
};
}
export async function POST(request: NextRequest) {
const denied = assertInternal(request);
if (denied) return denied;
try {
const result = await er.generateCandidates(parseErCandidateRequest(await request.json()));
return NextResponse.json(enforceCompanyReviewBoundary(result));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,69 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { verifyInternalServiceToken } from "@/src/auth/internal-token";
import { createAud0Client } from "@/src/clients/aud0-client";
import {
Aud0OutboxDispatcher,
createAud0OutboxRepositoryFromEnv,
} from "@/src/evidence/aud0-outbox";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
function assertInternal(request: NextRequest): NextResponse | undefined {
if (verifyInternalServiceToken(request.headers)) return undefined;
return NextResponse.json(
{ error: "UNAUTHENTICATED", message: "valid X-Internal-Token is required" },
{ status: 401 },
);
}
function repository() {
const repo = createAud0OutboxRepositoryFromEnv();
if (!repo) {
throw new SiaxError(
"DEPENDENCY_UNAVAILABLE",
"0pt.aud0_outbox",
"DATABASE_URL is required for durable evidence dispatch",
);
}
return repo;
}
export async function POST(request: NextRequest) {
const denied = assertInternal(request);
if (denied) return denied;
try {
const raw = await request.json().catch(() => ({})) as { limit?: unknown };
const limit = raw.limit === undefined ? 100 : Number(raw.limit);
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
return NextResponse.json(
{ error: "INVALID_REQUEST", message: "limit must be an integer between 1 and 500" },
{ status: 400 },
);
}
const repo = repository();
// CRITICAL: the dispatcher must bypass the durable wrapper. Using the
// default client here would enqueue the event again and could create an
// infinite outbox loop instead of delivering to AUD0.
const rawAud0 = createAud0Client(undefined, { durable: false });
const dispatcher = new Aud0OutboxDispatcher(repo, rawAud0);
const result = await dispatcher.sweep(limit);
const stats = await repo.stats();
return NextResponse.json({ result, stats });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET(request: NextRequest) {
const denied = assertInternal(request);
if (denied) return denied;
try {
return NextResponse.json(await repository().stats());
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,28 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { mappingService } from "@/src/services/lakehouse-mapping-registry";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
// P014: this lookup used to accept only `mappingId`, with NO tenant check
// anywhere in the request path -- any authenticated caller could read
// another tenant's dataset<->Iceberg-table mapping (namespace, table name,
// storage URI) by guessing or enumerating IDs. Now requires `?tenant_id=`
// and both checks it against the verified token and threads it into the
// service so a foreign mapping_id 404s instead of leaking -- see
// DatasetIcebergMappingService.getMapping's doc comment.
export async function GET(request: NextRequest, { params }: { params: Promise<{ mappingId: string }> }) {
try {
const { mappingId } = await params;
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const mapping = await mappingService.getMapping(mappingId, tenantId);
return NextResponse.json(mapping);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 404 });
}
}
@@ -1,45 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { parseBody, requestIcebergWriteSchema } from "@/src/validation/0pt-schemas";
import { writeService } from "@/src/services/lakehouse-mapping-registry";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
// Registers a new Iceberg snapshot (a write) through ST0RE's table registry
// for an already-mapped dataset. See docs/ICEBERG_WRITE_BOUNDARY.md.
//
// `writeService` is the SHARED instance from
// src/services/lakehouse-mapping-registry.ts -- this route used to construct
// its own `new IcebergWriteService()`, which held a private in-memory mapping
// registry, so a mapping created via POST /lakehouse/mappings was never
// visible here and every write failed with "mapping not found".
//
// The body is validated against requestIcebergWriteSchema before it reaches
// the service (Wave 2 hardening).
export async function POST(request: NextRequest, { params }: { params: Promise<{ mappingId: string }> }) {
try {
const { mappingId } = await params;
const __raw = await request.json();
const __parsed = parseBody(requestIcebergWriteSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const body = __parsed.data;
const tenantGuard = assertTenantMatchesVerified(request, body.tenant_id);
if (tenantGuard) return tenantGuard;
const result = await writeService.requestIcebergWrite({
dataset_iceberg_mapping_id: mappingId,
tenant_id: body.tenant_id,
actor_id: body.actor_id,
correlation_id: body.correlation_id,
snapshot_type: body.snapshot_type,
schema_version: body.schema_version,
manifest_uri: body.manifest_uri,
record_count: body.record_count,
size_bytes: body.size_bytes,
heavy: body.heavy,
});
return NextResponse.json(result, { status: 202 });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 400 });
}
}
-84
View File
@@ -1,84 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { parseBody, mapDatasetToIcebergTableSchema } from "@/src/validation/0pt-schemas";
import { mappingService, writeService } from "@/src/services/lakehouse-mapping-registry";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import type { IcebergTableFormat } from "@/src/clients/st0re-lakehouse-client";
// Shared instances come from src/services/lakehouse-mapping-registry.ts so a
// mapping created via POST is visible to a later GET, AND to the sibling
// /{mappingId}/writes route, within the same server process. (Each route file
// previously constructed its own DatasetIcebergMappingService/
// IcebergWriteService, so a mapping created here was invisible to the writes
// route -- every write failed with "mapping not found".) This still matches the
// in-memory-per-module convention app/api/0pt/datasets and app/api/0pt/quality
// use; Postgres persistence exists in sql/ but is not wired into route handlers
// in this pass -- same limitation those routes already have.
// POST registers a NEW Iceberg table in ST0RE's table registry (via
// IcebergWriteService.mapDatasetToIcebergTable) and records the
// dataset_iceberg_mapping row that points at it -- this is 0pt's write-path
// entrypoint, see docs/ICEBERG_WRITE_BOUNDARY.md.
//
// The body is validated against mapDatasetToIcebergTableSchema BEFORE it
// reaches the service (Wave 2 hardening): an unvalidated request body used to
// be passed straight into the capability call.
export async function POST(request: NextRequest) {
try {
const __raw = await request.json();
const __parsed = parseBody(mapDatasetToIcebergTableSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const body = __parsed.data;
const tenantGuard = assertTenantMatchesVerified(request, body.tenant_id);
if (tenantGuard) return tenantGuard;
const result = await writeService.mapDatasetToIcebergTable({
tenant_id: body.tenant_id,
project_id: body.project_id,
dataset_id: body.dataset_id,
dataset_iceberg_mapping_id: body.dataset_iceberg_mapping_id,
namespace: body.namespace,
table_name: body.table_name,
storage_uri: body.storage_uri,
catalog_ref: body.catalog_ref,
// mapDatasetToIcebergTableSchema types this as a plain string
// (pre-existing, listed among package.json's "9 pre-existing errors
// ... Wave 2 Zod schemas type fields as plain string while the
// services want narrower unions" -- deliberately left to its own
// change, not widened here). This cast changes no runtime behavior:
// an unrecognized value flows through exactly as it did before
// (ST0RE's own table-registration route is the actual validator) --
// it only satisfies the type checker, which previously never saw this
// file (nothing under tests/ imported it before P014's cross-tenant
// regression tests).
table_format: body.table_format as IcebergTableFormat | undefined,
actor_id: body.actor_id,
correlation_id: body.correlation_id,
});
return NextResponse.json(result, { status: 201 });
} catch (error: unknown) {
const message = error instanceof Error ? error.message : "Unknown error";
return NextResponse.json({ error: message }, { status: 400 });
}
}
// P014: tenant_id used to be optional here, and
// DatasetIcebergMappingService.listMappings treats an absent tenant_id
// filter as "no restriction" -- omitting the query param returned every
// tenant's dataset<->Iceberg-table mappings (namespace, table name, storage
// URI). Now required and cross-checked, matching the rest of the surface.
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const mappings = await mappingService.listMappings({
tenant_id: tenantId,
project_id: searchParams.get("project_id") ?? undefined,
dataset_id: searchParams.get("dataset_id") ?? undefined,
});
return NextResponse.json(mappings);
}
-47
View File
@@ -1,47 +0,0 @@
/**
* app/api/0pt/lineage/route.ts
* =============================
* P067 DoD: "source -> output lineage is QUERYABLE". This is the query.
*
* GET /api/0pt/lineage?tenant_id=…&output_name=… -> what fed this output
* GET /api/0pt/lineage?tenant_id=…&input_name=… -> what this input produced
*
* The answer comes from 0pt's local projection of the OpenLineage events it
* emits to AUD0 (src/lineage/lineage-store.ts). AUD0 remains the system of
* record; this endpoint exists because "emitted to AUD0" was never the same
* thing as "answerable from 0pt", and before this route nothing in this repo
* could answer either question.
*/
import { NextRequest, NextResponse } from "next/server";
import { lineageStore } from "@/src/services/source-registry";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
const outputName = searchParams.get("output_name") ?? undefined;
const inputName = searchParams.get("input_name") ?? undefined;
if (!tenantId) return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
if (!outputName && !inputName) {
return NextResponse.json(
{ error: "one of output_name or input_name is required — an unfiltered lineage dump is not a query" },
{ status: 400 },
);
}
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const limitRaw = searchParams.get("limit");
const limit = limitRaw ? Math.min(Math.max(Number(limitRaw), 1), 500) : undefined;
const edges = await lineageStore.query({
tenant_id: tenantId,
output_name: outputName,
input_name: inputName,
limit,
});
return NextResponse.json({ edges, count: edges.length });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-64
View File
@@ -1,64 +0,0 @@
/**
* app/api/0pt/quality/bridge-run/route.ts
* ==========================================
* The P010 TS/Python split-brain bridge (BEFORE_STATE.md §2). This is the
* ONE route the production Python engine
* (services/api/workers/quality_bridge.py, called from
* services/api/workers/pipeline.py) talks to. It:
*
* 1. auto-provisions a deterministic, tenant-scoped baseline rule set for
* (tenant_id, dataset_id) the first time it's seen (src/quality/quality-bridge.ts) --
* the Python engine never has to pre-register rules through the CRUD
* surface just to get a quality read on an order's already-produced rows;
* 2. runs QualityService.runQuality synchronously against the rows the
* pipeline already produced (delivered, post the Python filter --
* this is an ADVISORY second opinion, not a gate, until Simon decides
* to wire per-row gating -- see quality_bridge.py's docstring for why
* that isn't done in this pass);
* 3. returns the run + any open issues.
*
* This does not replace, call, or block on the Python filter in any way --
* it is purely additive. If this route is unreachable, times out, or 500s,
* the Python side is required to fail open (never raise into the pipeline).
*/
import { NextRequest, NextResponse } from "next/server";
import { parseBody, qualityBridgeRunSchema } from "@/src/validation/0pt-schemas";
import { qualityService } from "@/src/services/quality";
import { ensureBaselineRules } from "@/src/quality/quality-bridge";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
export async function POST(request: NextRequest) {
try {
const __raw = await request.json();
const __parsed = parseBody(qualityBridgeRunSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const body = __parsed.data;
await ensureBaselineRules(qualityService, {
tenant_id: body.tenant_id,
project_id: body.project_id,
dataset_id: body.dataset_id,
});
const run = await qualityService.runQuality({
tenant_id: body.tenant_id,
project_id: body.project_id,
dataset_id: body.dataset_id,
dataset_version: body.dataset_version,
rows: body.rows,
correlation_id: body.correlation_id,
});
const issues = qualityService.listIssues({
tenant_id: body.tenant_id,
dataset_id: body.dataset_id,
status: "open",
});
return NextResponse.json({ run, issues }, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-31
View File
@@ -1,31 +0,0 @@
/**
* app/api/0pt/quality/issues/route.ts
* ======================================
* quality.list_issues (GET) -- issues opened by finished quality runs
* (human-review handoff, Master-DoD §10).
*/
import { NextRequest, NextResponse } from "next/server";
import { qualityService } from "@/src/services/quality";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import type { DataQualityIssue } from "@/src/quality/quality-types";
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const issues = qualityService.listIssues({
tenant_id: tenantId,
dataset_id: searchParams.get("dataset_id") ?? undefined,
status: (searchParams.get("status") as DataQualityIssue["status"]) ?? undefined,
});
return NextResponse.json(issues);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,61 +0,0 @@
/**
* app/api/0pt/quality/rules/[ruleId]/route.ts
* ===============================================
* quality.get_rule (GET) + quality.update_rule (PUT, versioned -- bumps
* `version`, records the superseded row). Cross-tenant reads are NOT_FOUND
* (no existence oracle), matching QualityService.getRule's own contract.
*/
import { NextRequest, NextResponse } from "next/server";
import { qualityService } from "@/src/services/quality";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { z } from "zod";
const updateQualityRuleSchema = z.object({
tenant_id: z.string().min(1),
name: z.string().min(1).optional(),
expectation: z.object({ expectation_type: z.string().min(1), kwargs: z.record(z.string(), z.unknown()) }).optional(),
severity: z.enum(["info", "warning", "error", "critical"]).optional(),
status: z.enum(["active", "disabled", "superseded"]).optional(),
});
export async function GET(request: NextRequest, { params }: { params: Promise<{ ruleId: string }> }) {
try {
const { ruleId } = await params;
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const versions = searchParams.get("versions") === "true";
if (versions) {
return NextResponse.json(qualityService.getRuleVersions(ruleId, tenantId));
}
return NextResponse.json(qualityService.getRule(ruleId, tenantId));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ ruleId: string }> }) {
try {
const { ruleId } = await params;
const raw = await request.json();
const parsed = updateQualityRuleSchema.safeParse(raw);
if (!parsed.success) {
return NextResponse.json(
{ error: "validation_error", issues: parsed.error.issues.map((i) => ({ path: i.path.join("."), message: i.message })) },
{ status: 400 }
);
}
const { tenant_id, ...patch } = parsed.data;
const tenantGuard = assertTenantMatchesVerified(request, tenant_id);
if (tenantGuard) return tenantGuard;
const rule = await qualityService.updateRule(ruleId, tenant_id, patch);
return NextResponse.json(rule);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-52
View File
@@ -1,52 +0,0 @@
/**
* app/api/0pt/quality/rules/route.ts
* ====================================
* quality.create_rule (POST) + quality.list_rules (GET) -- the CRUD surface
* over QualityService's versioned rule registry (src/quality/quality-service.ts,
* Master-DoD §10). Uses the shared singleton from src/services/quality.ts,
* not a fresh `new QualityService()` per route module -- see that file's
* doc comment (and BEFORE_STATE.md §1, which found and fixed the same
* per-module-instance defect for the Source Registry).
*/
import { NextRequest, NextResponse } from "next/server";
import { parseBody, createQualityRuleSchema } from "@/src/validation/0pt-schemas";
import { qualityService } from "@/src/services/quality";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import type { DataQualityRule } from "@/src/quality/quality-types";
export async function POST(request: NextRequest) {
try {
const __raw = await request.json();
const __parsed = parseBody(createQualityRuleSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const tenantGuard = assertTenantMatchesVerified(request, __parsed.data.tenant_id);
if (tenantGuard) return tenantGuard;
const rule = await qualityService.createRule(__parsed.data);
return NextResponse.json(rule, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const rules = qualityService.listRules({
tenant_id: tenantId,
dataset_id: searchParams.get("dataset_id") ?? undefined,
status: (searchParams.get("status") as DataQualityRule["status"]) ?? undefined,
});
return NextResponse.json(rules);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-25
View File
@@ -1,25 +0,0 @@
/**
* app/api/0pt/quality/runs/[runId]/route.ts
* =============================================
* quality.get_run (GET). Cross-tenant reads are NOT_FOUND.
*/
import { NextRequest, NextResponse } from "next/server";
import { qualityService } from "@/src/services/quality";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
export async function GET(request: NextRequest, { params }: { params: Promise<{ runId: string }> }) {
try {
const { runId } = await params;
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
return NextResponse.json(qualityService.getRun(runId, tenantId));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-49
View File
@@ -1,49 +0,0 @@
/**
* app/api/0pt/quality/runs/route.ts
* ====================================
* quality.run (POST) + quality.list_runs (GET). POST executes all active
* rules for a dataset against caller-supplied rows -- runs synchronously
* when every active rule's expectation is locally executable, otherwise
* delegates to the GE backend via N0D (QualityService.runQuality never
* fabricates a result for a check it couldn't run).
*/
import { NextRequest, NextResponse } from "next/server";
import { parseBody, createQualityRunSchema } from "@/src/validation/0pt-schemas";
import { qualityService } from "@/src/services/quality";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
export async function POST(request: NextRequest) {
try {
const __raw = await request.json();
const __parsed = parseBody(createQualityRunSchema, __raw);
if (!__parsed.ok) {
return NextResponse.json(__parsed.body, { status: __parsed.status });
}
const tenantGuard = assertTenantMatchesVerified(request, __parsed.data.tenant_id);
if (tenantGuard) return tenantGuard;
const run = await qualityService.runQuality(__parsed.data);
return NextResponse.json(run, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const tenantId = searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const tenantGuard = assertTenantMatchesVerified(request, tenantId);
if (tenantGuard) return tenantGuard;
const runs = qualityService.listRuns({
tenant_id: tenantId,
dataset_id: searchParams.get("dataset_id") ?? undefined,
});
return NextResponse.json(runs);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-19
View File
@@ -1,19 +0,0 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
status: "ok",
product: "0pt",
version: "1.0.0",
endpoints: [
"/api/0pt/sources",
"/api/0pt/datasets",
"/api/0pt/data-contracts",
"/api/0pt/quality/checks",
"/api/0pt/quality/reports",
"/api/0pt/features/views",
"/api/0pt/documents/capture",
"/api/0pt/documents/{documentId}/extraction",
],
});
}
@@ -1,65 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { getSearchService } from "@/src/services/search";
function primitiveFilters(value: unknown): Record<string, string | number | boolean> | undefined {
if (value === undefined) return undefined;
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", "filters must be an object");
}
const result: Record<string, string | number | boolean> = {};
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
throw new SiaxError("INVALID_REQUEST", "0pt.search", `filter ${key} must be string, number or boolean`);
}
result[key] = item;
}
return result;
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ indexName: string }> },
) {
try {
const { indexName } = await params;
const raw = await request.json();
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", "JSON object body is required");
}
const body = raw as Record<string, unknown>;
const tenantId = typeof body.tenant_id === "string" ? body.tenant_id.trim() : "";
const purpose = typeof body.purpose === "string" ? body.purpose.trim() : "";
if (!tenantId) throw new SiaxError("INVALID_REQUEST", "0pt.search", "tenant_id is required");
if (!purpose) throw new SiaxError("INVALID_REQUEST", "0pt.search", "purpose is required");
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
// Public/authenticated traffic is deliberately fixed to internal rights.
// A client-supplied higher rights_tag would be privilege escalation by
// request body; broader tags require a future CL0UD-derived entitlement.
if (body.rights_tag !== undefined && body.rights_tag !== "internal") {
throw new SiaxError(
"AUTHORIZATION_DENIED",
"0pt.search",
"rights_tag cannot be elevated by the caller",
);
}
const result = await getSearchService().query(indexName, {
tenant_id: tenantId,
purpose,
rights_tag: "internal",
...(typeof body.text === "string" ? { text: body.text } : {}),
...(body.from === undefined ? {} : { from: Number(body.from) }),
...(body.size === undefined ? {} : { size: Number(body.size) }),
...(body.filters === undefined ? {} : { filters: primitiveFilters(body.filters) }),
});
return NextResponse.json(result);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
-58
View File
@@ -1,58 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { verifyInternalServiceToken } from "@/src/auth/internal-token";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { SiaxError } from "@/src/platform/errors";
import { getSearchService } from "@/src/services/search";
import type { SearchFieldType } from "@/src/search/search-types";
const FIELD_TYPES = new Set<SearchFieldType>(["keyword", "text", "long", "double", "date", "boolean"]);
export async function POST(request: NextRequest) {
if (!verifyInternalServiceToken(request.headers)) {
return NextResponse.json({ error: "UNAUTHENTICATED", message: "valid X-Internal-Token is required" }, { status: 401 });
}
try {
const raw = await request.json();
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", "JSON object body is required");
}
const body = raw as Record<string, unknown>;
const indexName = typeof body.index_name === "string" ? body.index_name.trim() : "";
const version = Number(body.version);
if (!body.fields || typeof body.fields !== "object" || Array.isArray(body.fields)) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", "fields must be an object");
}
const fields: Record<string, SearchFieldType> = {};
for (const [name, rawType] of Object.entries(body.fields as Record<string, unknown>)) {
if (!name.trim() || typeof rawType !== "string" || !FIELD_TYPES.has(rawType as SearchFieldType)) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", `invalid field definition for ${name || "<empty>"}`);
}
fields[name.trim()] = rawType as SearchFieldType;
}
if (!Array.isArray(body.default_search_fields) || !body.default_search_fields.every((v) => typeof v === "string")) {
throw new SiaxError("INVALID_REQUEST", "0pt.search", "default_search_fields must be a string array");
}
const definition = await getSearchService().registerIndexDefinitionDurable(
{
index_name: indexName,
version,
fields,
default_search_fields: body.default_search_fields.map((v) => String(v).trim()),
},
request.headers.get("x-0pt-verified-sub") ?? "svc_0pt",
);
return NextResponse.json(definition, { status: 201 });
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function GET() {
try {
return NextResponse.json(await getSearchService().listIndexDefinitionsDurable());
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,47 +0,0 @@
/**
* app/api/0pt/sources/[sourceId]/activate/route.ts
* ==================================================
* P060 activation gate over HTTP. POST activates a source ONLY when it is
* classified (owner + unexpired authorization + rights + provenance);
* otherwise it returns 403 AUTHORIZATION_DENIED naming every missing fact.
* GET returns the classification without changing anything, so an operator
* can see what is missing before trying.
*/
import { NextRequest, NextResponse } from "next/server";
import { activateSourceSchema, parseBody } from "@/src/validation/0pt-schemas";
import { sourceRegistryService } from "@/src/services/source-registry";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
export async function GET(request: NextRequest, { params }: { params: Promise<{ sourceId: string }> }) {
try {
const { sourceId } = await params;
const tenantId = new URL(request.url).searchParams.get("tenant_id");
if (!tenantId) {
return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
}
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
return NextResponse.json(await sourceRegistryService.classifySource(sourceId, tenantId));
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function POST(request: NextRequest, { params }: { params: Promise<{ sourceId: string }> }) {
try {
const { sourceId } = await params;
const parsed = parseBody(activateSourceSchema, await request.json());
if (!parsed.ok) return NextResponse.json(parsed.body, { status: parsed.status });
const guard = assertTenantMatchesVerified(request, parsed.data.tenant_id);
if (guard) return guard;
const source = await sourceRegistryService.activateSource(
sourceId,
parsed.data.tenant_id,
parsed.data.actor_id,
);
return NextResponse.json(source);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
@@ -1,45 +0,0 @@
/**
* app/api/0pt/sources/[sourceId]/authorization/route.ts
* ======================================================
* P060 "authorization": the recorded decision that 0pt may connect to this
* source at all — separate from rights (what the DATA may be used for). PUT
* is an upsert; a service identity as `granted_by` is rejected (400), because
* a source may not authorize itself.
*/
import { NextRequest, NextResponse } from "next/server";
import { parseBody, setSourceAuthorizationSchema } from "@/src/validation/0pt-schemas";
import { sourceRegistryService } from "@/src/services/source-registry";
import { siaxErrorResponse } from "@/src/http/siax-error-response";
import { assertTenantMatchesVerified } from "@/src/http/tenant-guard";
export async function GET(request: NextRequest, { params }: { params: Promise<{ sourceId: string }> }) {
try {
const { sourceId } = await params;
const tenantId = new URL(request.url).searchParams.get("tenant_id");
if (!tenantId) return NextResponse.json({ error: "tenant_id is required" }, { status: 400 });
const guard = assertTenantMatchesVerified(request, tenantId);
if (guard) return guard;
const auth = await sourceRegistryService.getAuthorization(sourceId, tenantId);
if (!auth) {
return NextResponse.json({ error: `source ${sourceId} has no authorization record` }, { status: 404 });
}
return NextResponse.json(auth);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}
export async function PUT(request: NextRequest, { params }: { params: Promise<{ sourceId: string }> }) {
try {
const { sourceId } = await params;
const parsed = parseBody(setSourceAuthorizationSchema, await request.json());
if (!parsed.ok) return NextResponse.json(parsed.body, { status: parsed.status });
const guard = assertTenantMatchesVerified(request, parsed.data.tenant_id);
if (guard) return guard;
return NextResponse.json(
await sourceRegistryService.setAuthorization({ ...parsed.data, source_id: sourceId }),
);
} catch (error: unknown) {
return siaxErrorResponse(error);
}
}

Some files were not shown because too many files have changed in this diff Show More