feat(contract): OpenAPI 3.1 SSOT (orders/estimates/enrich/resolve/provenance) + AR-4 fixes (drop @vercel/analytics + next/font/google -> self-hosted fontsource) + repo hygiene
Deploy to Coolify / deploy (push) Failing after 2s
CI (SIAX Cloud) / security (push) Successful in 19s
CI (SIAX Cloud) / quality (push) Successful in 40s

This commit is contained in:
2026-08-10 04:10:30 +02:00
parent d2b5ec8e4f
commit 9888c0078c
10 changed files with 635 additions and 35 deletions
+3
View File
@@ -97,6 +97,9 @@ ENGINE_URL=http://localhost:8000
CL0UD_URL=
N0D_URL=
AUD0_URL=
# 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
+37
View File
@@ -0,0 +1,37 @@
# 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
+5
View File
@@ -1,3 +1,8 @@
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/
+4
View File
@@ -1,9 +1,13 @@
@import '@fontsource-variable/inter';
@import '@fontsource-variable/jetbrains-mono';
@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
--font-inter: 'Inter Variable', 'Inter', system-ui, sans-serif;
--font-jetbrains-mono: 'JetBrains Mono Variable', 'JetBrains Mono', ui-monospace, monospace;
--background: #FFFFFF;
--foreground: #0A0A0A;
--card: #FFFFFF;
+1 -7
View File
@@ -1,16 +1,11 @@
import React from "react";
import type { Metadata } from "next";
import { Inter, JetBrains_Mono } from "next/font/google";
import { Analytics } from "@vercel/analytics/next";
import { Toaster } from "sonner";
import { LocaleProvider } from "@/components/locale-provider";
import { ThemeProvider } from "@/components/theme-provider";
import { RouteReveal } from "@/components/route-reveal";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const jetbrainsMono = JetBrains_Mono({ subsets: ["latin"], variable: "--font-jetbrains-mono" });
export const metadata: Metadata = {
title: "0pt — your data",
description:
@@ -47,7 +42,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body className={`${inter.variable} ${jetbrainsMono.variable} font-sans antialiased`}>
<body className="font-sans antialiased">
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:left-4 focus:top-4 focus:z-[100] focus:bg-foreground focus:px-4 focus:py-2 focus:font-mono focus:text-[11px] focus:uppercase focus:tracking-[0.12em] focus:text-background"
@@ -61,7 +56,6 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
</LocaleProvider>
</ThemeProvider>
<Toaster position="bottom-right" toastOptions={{ classNames: { toast: "font-mono text-xs tracking-[0.06em] rounded-none border border-border bg-background text-foreground", title: "font-mono", description: "font-mono text-muted-foreground" } }} />
<Analytics />
</body>
</html>
);
+548
View File
@@ -0,0 +1,548 @@
openapi: 3.1.0
info:
title: 0pt Data OS API
version: 1.0.0
description: >
SIAX Cloud Data OS — order-, estimat- och capability-API (0pt).
Single Source of Truth för HTTP-kontraktet (SSOT). Routehandlers i
app/api/* realiserar dessa kontrakt; se x-siax-status för vad som är
live vs kontrakt-first (target).
x-siax:
appId: 0PT
capabilityPrefix: 0pt
tenantBinding: header X-Tenant-Id (live), session (target — se BACKLOG P0-2)
servers:
- url: https://0pt.app
description: Production (edge plane)
- url: https://api.0pt.app
description: Target API-host (Caddy, EJ verifierad live)
security:
- bearerAuth: []
paths:
/api/health:
get:
operationId: 0pt.health
summary: Liveness + dependency state
x-siax-status: live
responses:
"200":
description: OK — db/queue rapporterar "configured"/"not_configured"
content:
application/json:
schema:
$ref: "#/components/schemas/HealthResponse"
/api/estimates:
post:
operationId: 0pt.estimate_order
summary: Prisestimat för en datajob-konfiguration
x-siax-status: live
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EstimateRequest"
responses:
"200":
description: Estimat (öre)
content:
application/json:
schema:
$ref: "#/components/schemas/EstimateResponse"
"400":
$ref: "#/components/responses/BadRequest"
/api/orders:
post:
operationId: 0pt.create_order
summary: Skapa draft-order (50 % deposition startar jobbet)
x-siax-status: live
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrderRequest"
responses:
"201":
description: Order skapad
content:
application/json:
schema:
$ref: "#/components/schemas/OrderSummary"
"400":
$ref: "#/components/responses/BadRequest"
get:
operationId: 0pt.list_orders
summary: Lista ordrar (org-scoping saknas — se BACKLOG P0-3)
x-siax-status: live
x-siax-gap: S3 — ingen auth/org-scoping
security: []
responses:
"200":
description: Orderlista
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/OrderSummary"
/api/orders/{id}:
get:
operationId: 0pt.get_order
summary: Hämta order
x-siax-status: live
security: []
parameters:
- $ref: "#/components/parameters/OrderId"
responses:
"200":
description: Order
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
$ref: "#/components/responses/NotFound"
patch:
operationId: 0pt.update_order
summary: Uppdatera order
x-siax-status: live
security: []
parameters:
- $ref: "#/components/parameters/OrderId"
requestBody:
required: true
content:
application/json:
schema:
type: object
responses:
"200":
description: Uppdaterad order
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
$ref: "#/components/responses/NotFound"
/api/0pt:
get:
operationId: 0pt.capability_index
summary: Capability-index
x-siax-status: live
responses:
"200":
description: Index med endpoints
content:
application/json:
schema:
type: object
additionalProperties: true
/api/0pt/sources/{sourceId}/query:
post:
operationId: 0pt.query_source
summary: Bunden källfråga (enrich-primitiv)
x-siax-status: live
x-siax-gap: S10 — connection_config ur body (SSRF-primitiv, BACKLOG P0-7)
parameters:
- name: sourceId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/SourceQueryRequest"
responses:
"200":
description: Källdata med provenance per fält
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/EnrichedRecord"
"400":
$ref: "#/components/responses/BadRequest"
/api/0pt/documents/capture:
post:
operationId: 0pt.capture_document
summary: Ta emot dokument (PII) för extraktion
x-siax-status: live
x-siax-gap: S2 — tenant_id från body (BACKLOG P0-2)
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
"201":
description: Dokument registrerat
"400":
$ref: "#/components/responses/BadRequest"
get:
operationId: 0pt.list_documents
summary: Lista dokument
x-siax-status: live
x-siax-gap: S2 — tenant_id från query (BACKLOG P0-2)
responses:
"200":
description: Dokumentlista
/api/0pt/documents/{documentId}/extraction:
post:
operationId: 0pt.extract_document
summary: Extrahera dokument (OCR/AI — env-guardad, 501 utan konfig)
x-siax-status: live
parameters:
- name: documentId
in: path
required: true
schema:
type: string
responses:
"200":
description: Extraktion — aldrig fabricerade värden
"404":
$ref: "#/components/responses/NotFound"
"501":
$ref: "#/components/responses/NotConfigured"
/api/0pt/lakehouse/mappings:
post:
operationId: 0pt.map_dataset_to_iceberg
summary: Registrera dataset↔Iceberg-mappning
x-siax-status: live
x-siax-gap: S2 — tenant_id från body (BACKLOG P0-2)
responses:
"201":
description: Mappning registrerad
"400":
$ref: "#/components/responses/BadRequest"
get:
operationId: 0pt.list_iceberg_mappings
summary: Lista mappningar
x-siax-status: live
x-siax-gap: S2 — tenant_id från query (BACKLOG P0-2)
responses:
"200":
description: Mappningslista
/api/0pt/lakehouse/mappings/{mappingId}/writes:
post:
operationId: 0pt.request_iceberg_write
summary: Iceberg-write (L4) — tung variant delegeras till N0D-jobb
x-siax-status: live
x-siax-gap: S5 — CL0UD fail-open (BACKLOG P0-5)
parameters:
- name: mappingId
in: path
required: true
schema:
type: string
responses:
"202":
description: Skrivning begärd (job_id eller synkront snapshot)
"400":
$ref: "#/components/responses/BadRequest"
/api/0pt/sources/rate-policies:
post:
operationId: 0pt.create_rate_policy
summary: Registrera käll-rate-policy
x-siax-status: live
x-siax-gap: S2 — tenant_id från body (BACKLOG P0-2)
responses:
"201":
description: Rate-policy skapad
get:
operationId: 0pt.list_rate_policies
summary: Lista rate-policies
x-siax-status: live
x-siax-gap: S2 — tenant_id från query (BACKLOG P0-2)
responses:
"200":
description: Rate-policylista
/api/screen:
get:
operationId: 0pt.screen_entity
summary: Sanktions-/PEP-screening via OpenSanctions
x-siax-status: live
security: []
parameters:
- name: q
in: query
required: true
schema:
type: string
responses:
"200":
description: Screeningträffar
"400":
$ref: "#/components/responses/BadRequest"
/v1/0pt/enrichment/company-lists:
post:
operationId: 0pt.enrich_company_list
summary: >
(TARGET) Capability-formen av enrichment: lista → enrichade poster med
bridge_provenance. Kontrakt-first — implementeras av 0pt-motor/edge
via N0D-jobb (job_type 0pt.enrichment.company_list).
x-siax-status: target
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/EnrichCompanyListRequest"
responses:
"202":
description: Jobb begärt — poll via /v1/0pt/jobs/{id} (target)
"401":
$ref: "#/components/responses/Unauthorized"
/v1/0pt/resolve/entities:
post:
operationId: 0pt.resolve_entities
summary: (TARGET) Entity resolution/dedupe (rapidfuzz idag, spaCy planerat)
x-siax-status: target
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/ResolveEntitiesRequest"
responses:
"200":
description: Kluster med konfidens
"401":
$ref: "#/components/responses/Unauthorized"
/v1/0pt/provenance/records/{entityType}/{entityKey}:
get:
operationId: 0pt.get_provenance
summary: (TARGET) Provenance per fält — bridge_provenance (källa, färskhet, konfidens, level)
x-siax-status: target
security: []
parameters:
- name: entityType
in: path
required: true
schema:
type: string
enum: [company, contact, vehicle, ubo]
- name: entityKey
in: path
required: true
schema:
type: string
responses:
"200":
description: Provenance per fält
content:
application/json:
schema:
$ref: "#/components/schemas/ProvenanceRecord"
"401":
$ref: "#/components/responses/Unauthorized"
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: Zitadel-utfärdad token; verifieras i middleware mot ZITADEL_ISSUER
parameters:
OrderId:
name: id
in: path
required: true
schema:
type: string
responses:
BadRequest:
description: Ogiltig input
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
Unauthorized:
description: Saknad/ogiltig Bearer-token
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
NotFound:
description: Resursen finns inte
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
NotConfigured:
description: Integration ej konfigurerad (501)
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
schemas:
ErrorEnvelope:
type: object
required: [error]
properties:
error:
type: object
required: [code]
properties:
code:
type: string
description: error.code-envelope (t.ex. bad_json, storage_not_configured)
message:
type: string
HealthResponse:
type: object
required: [ok, service, db, queue, ts]
properties:
ok:
type: boolean
service:
type: string
db:
type: string
enum: [configured, not_configured]
queue:
type: string
enum: [configured, not_configured]
ts:
type: string
format: date-time
EstimateRequest:
type: object
properties:
flow:
type: string
config:
type: object
additionalProperties: true
EstimateResponse:
type: object
properties:
estimate:
type: object
additionalProperties: true
CreateOrderRequest:
type: object
properties:
flow:
type: string
default: custom
config:
type: object
additionalProperties: true
OrderSummary:
type: object
required: [order_id, status]
properties:
order_id:
type: string
status:
type: string
deposit_ore:
type: integer
description: Deposition i öre
total_ore:
type: integer
description: Totalsumma i öre
Order:
type: object
properties:
order_id:
type: string
status:
type: string
config:
type: object
additionalProperties: true
SourceQueryRequest:
type: object
required: [tenant_id, source_id, query]
properties:
tenant_id:
type: string
description: (GAP) ska komma från session — BACKLOG P0-2
project_id:
type: string
source_id:
type: string
query:
type: string
connection_config:
type: object
additionalProperties: true
description: (GAP) SSRF-primitiv — BACKLOG P0-7
EnrichedRecord:
type: object
properties:
fields:
type: object
additionalProperties: true
provenance:
type: object
additionalProperties:
$ref: "#/components/schemas/FieldProvenance"
FieldProvenance:
type: object
required: [source_id, freshness, confidence, level]
properties:
source_id:
type: string
freshness:
type: string
format: date-time
confidence:
type: number
minimum: 0
maximum: 1
level:
type: integer
minimum: 0
maximum: 3
description: 0=ej tillgänglig · 1=licensierad · 2=authorized web · 3=officiell API/bulk
EnrichCompanyListRequest:
type: object
required: [market, entities]
properties:
market:
type: string
entities:
type: array
items:
type: object
additionalProperties: true
fields:
type: array
items:
type: string
output_artifact_type:
type: string
default: st0re.artifact
ResolveEntitiesRequest:
type: object
required: [records]
properties:
records:
type: array
items:
type: object
additionalProperties: true
threshold:
type: number
default: 0.85
algorithm:
type: string
enum: [rapidfuzz, target_spacy]
default: rapidfuzz
ProvenanceRecord:
type: object
additionalProperties:
$ref: "#/components/schemas/FieldProvenance"
+3 -2
View File
@@ -1,5 +1,5 @@
{
"name": "my-v0-project",
"name": "@siax/0pt",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@9.15.9",
@@ -17,6 +17,8 @@
"typecheck:app": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.5",
"@fontsource-variable/jetbrains-mono": "^5.2.5",
"@hookform/resolvers": "^3.10.0",
"@radix-ui/react-accordion": "1.2.2",
"@radix-ui/react-alert-dialog": "1.1.4",
@@ -45,7 +47,6 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
"@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+16 -26
View File
@@ -8,6 +8,12 @@ importers:
.:
dependencies:
'@fontsource-variable/inter':
specifier: ^5.2.5
version: 5.3.0
'@fontsource-variable/jetbrains-mono':
specifier: ^5.2.5
version: 5.3.0
'@hookform/resolvers':
specifier: ^3.10.0
version: 3.10.0(react-hook-form@7.80.0(react@19.2.0))
@@ -92,9 +98,6 @@ importers:
'@radix-ui/react-tooltip':
specifier: 1.1.6
version: 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@vercel/analytics':
specifier: 1.3.1
version: 1.3.1(next@16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)
autoprefixer:
specifier: ^10.4.20
version: 10.5.0(postcss@8.5.15)
@@ -433,6 +436,12 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
'@fontsource-variable/inter@5.3.0':
resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
'@fontsource-variable/jetbrains-mono@5.3.0':
resolution: {integrity: sha512-F32xpS2NsGYoQi2ADSkKTgpJj7ozajsGgDJ8woTnqjmIB+dxDIqImjl4pXZVEExu8UFZ2ndhmX18EBS/hdz3Lw==}
'@hookform/resolvers@3.10.0':
resolution: {integrity: sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==}
peerDependencies:
@@ -1643,17 +1652,6 @@ packages:
resolution: {integrity: sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vercel/analytics@1.3.1':
resolution: {integrity: sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA==}
peerDependencies:
next: '>= 13'
react: ^18 || ^19
peerDependenciesMeta:
next:
optional: true
react:
optional: true
'@vitest/expect@3.2.7':
resolution: {integrity: sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==}
@@ -2423,9 +2421,6 @@ packages:
engines: {node: '>=10'}
hasBin: true
server-only@0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
sharp@0.34.5:
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
@@ -2852,6 +2847,10 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
'@fontsource-variable/inter@5.3.0': {}
'@fontsource-variable/jetbrains-mono@5.3.0': {}
'@hookform/resolvers@3.10.0(react-hook-form@7.80.0(react@19.2.0))':
dependencies:
react-hook-form: 7.80.0(react@19.2.0)
@@ -3992,13 +3991,6 @@ snapshots:
'@typescript-eslint/types': 8.65.0
eslint-visitor-keys: 5.0.1
'@vercel/analytics@1.3.1(next@16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0)':
dependencies:
server-only: 0.0.1
optionalDependencies:
next: 16.0.10(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react: 19.2.0
'@vitest/expect@3.2.7':
dependencies:
'@types/chai': 5.2.3
@@ -4749,8 +4741,6 @@ snapshots:
semver@7.8.5: {}
server-only@0.0.1: {}
sharp@0.34.5:
dependencies:
'@img/colour': 1.1.0
+17
View File
@@ -2,5 +2,22 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"platform": "gitea",
"semanticCommits": "enabled",
"dependencyDashboard": true,
"schedule": ["before 6am on tuesday"],
"packageRules": [
{
"description": "Batch minor/patch updates of the v0-generated UI stack",
"matchDepNames": ["@radix-ui/*", "react", "react-dom", "next", "tailwindcss", "typescript"],
"groupName": "web-stack",
"separateMajorMinor": true
},
{
"description": "Do not auto-update the scraper stack (kept intact per deliberate user decision)",
"matchFiles": ["services/scrapers/requirements.txt", "services/api/requirements.txt"],
"enabled": false
}
]
}
+1
View File
@@ -7,3 +7,4 @@ requests>=2.33.0
beautifulsoup4>=4.12.0
lxml>=5.3.0
boto3>=1.35.0 # MinIO S3 sync
duckdb>=1.1.0 # DuckDBWriter alternative (lazy-imported in clickhouse_pipeline.py)