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

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

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

Importera ordrar

+

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

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

+ {serverError} +

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

Felrader (serverns rapport)

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

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

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

Inga importerade ordrar hittades vid reload.

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

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

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

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

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