diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..1014ba78
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
index cda27fc6..639ded72 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,6 +1,7 @@
* text=auto
.gitattributes text eol=lf
.gitignore text eol=lf
+.prettierignore text eol=lf
*.sha256 text eol=lf
*.md text eol=lf
*.json text eol=lf
@@ -10,6 +11,7 @@
*.toml text eol=lf
*.ts text eol=lf
*.tsx text eol=lf
+*.mjs text eol=lf
*.kt text eol=lf
*.kts text eol=lf
*.py text eol=lf
@@ -17,6 +19,7 @@
*.bat text eol=crlf
*.cmd text eol=crlf
*.png binary
+*.ico binary
*.jpg binary
*.jpeg binary
*.gif binary
diff --git a/.gitignore b/.gitignore
index d827e7f5..f90e6541 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,6 +30,7 @@ htmlcov/
# Gradle and Android
.gradle/
+.kotlin/
**/build/
local.properties
*.apk
diff --git a/.node-version b/.node-version
new file mode 100644
index 00000000..1dd37d53
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+24.17.0
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 00000000..cfd45e6d
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,9 @@
+.github/workflows/README.md
+.superpowers/
+AGENTS.md
+README.md
+docs/
+**/README.md
+pnpm-lock.yaml
+packages/contracts/generated/
+packages/design-tokens/brand/derivatives.json
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 00000000..bd70f7ae
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1,6 @@
+nodejs 24.17.0
+pnpm 11.18.0
+python 3.13.0
+java temurin-21
+postgres 17
+redis 7.4
diff --git a/docs/plans/000-platform-program.md b/docs/plans/000-platform-program.md
new file mode 100644
index 00000000..273d9f69
--- /dev/null
+++ b/docs/plans/000-platform-program.md
@@ -0,0 +1,61 @@
+# DataBreeze Platform Implementation Program
+
+**Status:** Approved
+**Implementation branch:** `dev` through short-lived `feat/*` and `fix/*` branches
+**Primary specifications:** `docs/product/`, `docs/architecture/`, `docs/specs/`, and accepted ADRs
+
+## Goal
+
+Implement DataBreeze as one Vietnamese-first, local-first business data platform across Web, Windows Desktop, and Android. The platform turns user-controlled files, documents, captures, and governed datasets into traceable jobs, evidence, reviews, approvals, reports, and safe actions without depending on restricted marketplace APIs.
+
+The program covers all 611 normative requirements. P0 requirements are release gates, P1 requirements complete the generally available capability, and P2 requirements are preserved as extension seams but are not scheduled for the first release.
+
+## Locked decisions
+
+- One clean monorepo with independently releasable deployables.
+- TypeScript for Web, Desktop, shared packages, and the NestJS/Fastify control plane.
+- Native Kotlin/Compose for Android and Python for the shared processing engine.
+- PostgreSQL is authoritative; S3-compatible storage holds cloud bytes; Redis is non-authoritative.
+- Local, Hybrid, and Cloud data modes remain visible and enforceable throughout every workflow.
+- First usable release is a private dogfood alpha built on the full multi-tenant architecture.
+- The first cross-platform workflow is Folder Autopilot plus Spreadsheet Auditor.
+- Core value does not require Shopee, TikTok Shop, accounting, advertising, or ERP partnerships.
+- AWS Singapore is the first hosted target through portable containers and OpenTofu.
+- The existing DataBreeze name and canonical logo files are retained without redrawing.
+
+## Delivery program
+
+| Phase | Child plan | Release gate |
+|---|---|---|
+| 0 | `010-engineering-foundation.md` | Toolchains, contracts, brand, deployable shells, local dependencies, and CI build reproducibly. |
+| 1A | `020-identity-audit-entitlements.md` | IAM, AUD, and provider-independent BUA foundations pass tenant and security gates. |
+| 1B | `030-artifacts-datasets-evidence.md` | IAE and DSM provide immutable artifacts, evidence, datasets, schemas, rules, and mappings. |
+| 1C | `040-jobs-processing-approvals.md` | JRA, admission coordination, cloud workers, and the local engine execute signed typed jobs. |
+| 1D | `050-devices-sync-offline.md` | Desktop and Android enroll, sync, recover, conflict, and revoke safely. |
+| 1E | `060-collaboration-integrations.md` | NCO and INT provide governed collaboration, notifications, API keys, and webhooks. |
+| 2 | `070-dogfood-folder-spreadsheet.md` | One spreadsheet-folder workflow crosses all three applications and preserves the original. |
+| 3 | `1xx-wave-1-*.md` | Folder Autopilot, Spreadsheet Auditor, Quote Intelligence, and Operations Capture. |
+| 4 | `2xx-wave-2-*.md` | Invoice Leak Detector, Client Report Factory, and Private Data Analyst. |
+| 5 | `3xx-wave-3-*.md` | Migration Ready, Data Quality Guard, and Embedded Importer. |
+| 6 | `400-production-readiness.md` | Signing, restoration, scaling, security, support, and progressive releases pass. |
+
+Child plans are written and approved before their product slice begins. Each names exact requirement IDs, paths, contract changes, migrations, tests, telemetry, failure behavior, rollback, and intentionally deferred requirements.
+
+## Branch, commit, and review policy
+
+- `main` contains stable releases. `dev` is the integration branch.
+- New capabilities use `feat/`; corrections use `fix/`; operational and documentation work use conventional prefixes when more accurate.
+- Commit one coherent tested unit at a time. Do not combine unrelated applications or domains merely to reduce commit count.
+- Pull requests target `dev`, normally contain 30–50 commits, and must not exceed 70 commits.
+- Invoke CodeRabbit once per pull request after the branch is ready for review. Validate every comment against the specifications and tests; fix valid findings and document why invalid findings are not applied.
+- Promote `dev` to `main` only through a separate release pull request after the relevant production gates pass.
+
+## Cross-cutting definition of done
+
+- Requirement-to-task-to-test traceability is complete.
+- Generated TypeScript, Kotlin, and Python contracts agree.
+- Tenant scope, authorization, data mode, evidence, approval, audit, and retention rules cannot be bypassed.
+- Vietnamese and English user-facing copy are complete for the delivered slice.
+- Relevant unit, integration, contract, end-to-end, security, accessibility, recovery, and performance tests pass.
+- Migrations, observability, operations, rollback, and release evidence are present.
+- No critical- or high-severity security finding remains unresolved for a production release.
diff --git a/docs/plans/010-engineering-foundation.md b/docs/plans/010-engineering-foundation.md
new file mode 100644
index 00000000..7cfa17bb
--- /dev/null
+++ b/docs/plans/010-engineering-foundation.md
@@ -0,0 +1,129 @@
+# Engineering Foundation Implementation Plan
+
+**Status:** Approved
+**Parent:** `000-platform-program.md`
+**Branch:** `feat/platform-foundation`
+
+## Outcome
+
+Create the reproducible monorepo foundation required by Stage 0 of the product roadmap. This plan introduces no customer workflow or production data migration. It establishes tested build, contract, brand, application-shell, infrastructure, observability, security, and delivery boundaries on which every normative requirement will depend.
+
+## Global constraints
+
+- Node.js 24 LTS, pnpm/Corepack, Turborepo, strict TypeScript, Python 3.13 through `uv`, JDK 21, PostgreSQL 17, and Redis 7.4 are pinned by repository-controlled configuration.
+- Web and Desktop may share React packages. Android consumes generated contracts and tokens but remains native Kotlin/Compose.
+- Clients never import service implementation packages.
+- No client or processing worker receives database credentials.
+- Generated artifacts must be reproducible and checked for drift in CI.
+- New behavior follows test-first red/green/refactor development.
+- Canonical legacy brand sources retain their exact bytes and documented SHA-256 values.
+- No secret, credential, runtime database, customer file, generated report, signing key, APK, or installer is committed.
+
+## Tasks
+
+### Task 1: Root workspace and runtime pins
+
+Create the root pnpm/Turborepo workspace, package scripts, TypeScript base configurations, editor-neutral formatting/linting configuration, runtime-version files, and package-manager pin. Add a smoke test that validates workspace package discovery and runtime policy. Commit as `chore(repo): bootstrap the monorepo toolchain`.
+
+### Task 2: Repository dependency-boundary enforcement
+
+Add executable checks that prevent clients from importing service implementations, prevent feature-to-feature persistence imports, and require public package exports. Cover allowed and rejected fixture graphs before enabling the check in root `lint`. Commit as `test(architecture): enforce repository dependency boundaries`.
+
+### Task 3: Requirement traceability tooling
+
+Implement a read-only parser that discovers all stable requirement IDs, rejects duplicates or malformed priorities, and produces a deterministic traceability index. Add fixtures for duplicates, gaps, and valid documents, then generate the initial index for all 611 requirements. Commit as `feat(traceability): index normative requirements`.
+
+### Task 4: Contract source layout and base envelopes
+
+Create versioned JSON Schemas for UUID identifiers, UTC timestamps, revisions, tenant scope, correlation metadata, RFC 7807-compatible problems, idempotent commands, cursor pages, and the canonical event envelope. Test valid and invalid examples with a standards-compliant validator. Commit as `feat(contracts): define shared protocol envelopes`.
+
+### Task 5: Cross-language contract generation
+
+Create deterministic generators and generated-package layouts for TypeScript, Kotlin, and Python. Add a drift command that regenerates into a temporary directory and byte-compares outputs. Commit as `feat(contracts): generate typescript kotlin and python models`.
+
+### Task 6: Contract compatibility and fixture package
+
+Add schema compatibility policy, shared valid/invalid protocol fixtures, and consumer tests proving all three generated model sets accept and reject equivalent payloads. Commit as `test(contracts): enforce cross-language parity`.
+
+### Task 7: Permission and tenant-scope primitives
+
+Create versioned permission constants, the six initial role bundles, tenant-scope value objects, and deny-by-default helpers without implementing IAM persistence. Test narrowing and cross-scope rejection. Link IAM-001 through IAM-004, IAM-009, and IAM-019 as partial foundation coverage. Commit as `feat(permissions): add scoped authorization primitives`.
+
+### Task 8: Configuration and provider ports
+
+Create typed configuration loading with explicit development/test/preview/staging/production profiles and ports for object storage, email, push, OCR, AI, payments, telemetry, and secrets. Reject missing production configuration and unknown keys. Commit as `feat(config): define portable provider boundaries`.
+
+### Task 9: Vietnamese and English terminology package
+
+Create the canonical `vi-VN` and `en` message catalogs, locale negotiation, formatting helpers, and completeness tests. Vietnamese is the default and missing keys fail CI. Commit as `feat(i18n): establish complete bilingual catalogs`.
+
+### Task 10: Immutable legacy brand sources
+
+Copy the three canonical named logo files into the design-system source directory. Add a manifest containing dimensions, intended use, and the approved SHA-256 hashes, plus a checksum test that fails on byte changes. Commit as `feat(brand): preserve canonical databreeze assets`.
+
+### Task 11: Reproducible brand derivatives
+
+Build a deterministic image pipeline for Web favicons/social assets, Desktop icons, and Android launcher/notification sources. Preserve aspect ratio, colors, and safe zones; prohibit wordmark duplication. Add dimension, checksum, and visual-regression fixtures. Commit as `feat(brand): generate platform logo derivatives`.
+
+### Task 12: Design tokens and accessible UI primitives
+
+Create shared color, typography, spacing, motion, focus, status, and logo-usage tokens. Export TypeScript/CSS and generated Android resources. Add contrast, reduced-motion, and generation-drift tests. Commit as `feat(design-system): add shared accessible tokens`.
+
+### Task 13: Web application shell
+
+Create the React/Vite shell with React Router, TanStack Query, Tailwind, accessible primitives, bilingual routing/layout, error boundaries, and placeholder authenticated navigation. Add Vitest/Testing Library and Playwright smoke coverage. Commit as `feat(web): create the governed workspace shell`.
+
+### Task 14: Control-plane API shell
+
+Create the NestJS/Fastify modular-monolith shell, health/readiness endpoints, request correlation, RFC 7807 errors, structured validation, OpenAPI generation, Prisma multi-schema layout, and domain boundary structure. Test boot, validation, and error behavior. Commit as `feat(api): create the modular control plane shell`.
+
+### Task 15: Windows Desktop security shell
+
+Create the Electron/React/Vite shell with sandboxing, context isolation, disabled Node integration, restrictive navigation/CSP, a versioned allowlisted preload API, local-state abstraction, and sidecar lifecycle port. Add security preference and IPC rejection tests. Link DSK-001, DSK-002, and DSK-008 as partial coverage. Commit as `feat(desktop): create the secure local agent shell`.
+
+### Task 16: Python engine shell
+
+Create the `uv` project, Pydantic protocol models, versioned action-manifest registry, deterministic handler interface, framed JSON-RPC entry point, cloud-worker entry point, Ruff/type/pytest configuration, and a test processor. Test malformed frames, unsupported actions, deterministic output, and resource metadata. Commit as `feat(engine): create the typed processing runtime`.
+
+### Task 17: Native Android shell
+
+Create the Gradle wrapper/version catalog and Kotlin/Compose application with bilingual resources, navigation, Room/WorkManager boundaries, Keystore and sync ports, network security configuration, backup exclusions, and baseline unit/instrumentation tests. Commit as `feat(android): create the offline companion shell`.
+
+### Task 18: Local development infrastructure
+
+Create Docker Compose definitions for PostgreSQL 17, Redis 7.4, MinIO, Mailpit, and an OpenTelemetry collector. Add health checks, named development volumes, `.env.example`, initialization scripts without credentials, and a smoke script that validates readiness. Commit as `feat(infra): add portable local dependencies`.
+
+### Task 19: AWS OpenTofu foundation
+
+Create reusable OpenTofu modules and environment compositions for AWS Singapore networking, S3/CloudFront Web hosting, ECS API/worker services, RDS, ElastiCache, KMS, Secrets Manager, logs, and GitHub OIDC. Use safe alpha defaults and explicit production scaling/PITR variables. Validate and lint without applying. Commit as `feat(infra): define the portable aws baseline`.
+
+### Task 20: Shared observability and safe diagnostics
+
+Create structured logging, correlation propagation, OpenTelemetry conventions, safe attribute allowlists, and content-redaction tests shared by API, Web, Desktop, Android, and engine adapters. Commit as `feat(observability): establish content-safe telemetry`.
+
+### Task 21: Continuous integration and supply-chain gates
+
+Create path-aware GitHub Actions for format, lint, typecheck, contract drift, unit/integration tests, builds, SBOM, dependency/license/secret scanning, container scanning, and release provenance. Workflows use least-privilege permissions and no long-lived AWS keys. Commit as `ci: add monorepo quality and security gates`.
+
+### Task 22: Developer workflow and operational foundations
+
+Document clean-checkout setup, branch/commit/PR policy, local services, contract changes, troubleshooting, provider adapters, release channels, and initial deployment/rollback/secret-rotation runbooks. Commit as `docs: document foundation development and operations`.
+
+### Task 23: Clean-checkout verification and release evidence
+
+Run the complete root verification from a clean worktree, build each deployable, regenerate contracts/assets, validate Compose and OpenTofu, and record requirement/test/build evidence without committing runtime artifacts. Fix only failures within this plan. Commit any necessary corrections in narrowly scoped `fix(...)` commits, then prepare the pull request to `dev`.
+
+## Acceptance and rollback
+
+- One documented bootstrap path prepares every available toolchain.
+- Root format, lint, typecheck, contract, unit, integration, and build commands exit successfully.
+- Web, API, Desktop, engine, and Android empty deployables build independently.
+- Local dependencies reach healthy state and can be torn down without deleting user-owned files.
+- Contract and brand regeneration is reproducible and drift-free.
+- CI uses synthetic fixtures only and emits no secrets or customer content.
+- Every task is a separate rollback unit. Reverting an application shell must not remove shared contracts used by another completed shell.
+- AWS resources are not applied by this plan; rollback is therefore repository reversion plus removal of local disposable containers/volumes when explicitly requested.
+
+## Deferred requirements
+
+All business workflows and persistent IAM/IAE/DSM/JRA/DSO/NCO/INT/BUA/AUD behavior beyond the explicitly named primitives remain deferred to the subsequent child plans. A passing engineering-foundation build does not mark those requirements implemented.
diff --git a/docs/specs/requirement-index.json b/docs/specs/requirement-index.json
new file mode 100644
index 00000000..ba41f2a4
--- /dev/null
+++ b/docs/specs/requirement-index.json
@@ -0,0 +1,5504 @@
+{
+ "requirements": [
+ {
+ "id": "AND-001",
+ "priority": "P0",
+ "requirement": "Android shall be implemented natively in Kotlin with Jetpack Compose and shall use Room, WorkManager, CameraX, scoped storage, Android Keystore, and Android share intents for their defined responsibilities.",
+ "source": {
+ "line": 92,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-002",
+ "priority": "P0",
+ "requirement": "Each organization enrollment shall use an IAM-defined DeviceIdentity with a distinct Keystore-backed signing key, device-bound short sessions, and rotating refresh credentials; DSO shall consume that identity only for capabilities, grants, operational health, synchronization, routing, and transfer.",
+ "source": {
+ "line": 93,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-003",
+ "priority": "P0",
+ "requirement": "Sensitive tokens and local encryption-key envelopes shall be non-exportable where Android permits and never stored in plaintext preferences, logs, backups, intents, or Compose state restoration.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-004",
+ "priority": "P0",
+ "requirement": "Camera capture shall preserve each original byte stream immutably; crop, rotate, perspective correction, enhancement, OCR, redaction, and compression shall create derived versions.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-005",
+ "priority": "P0",
+ "requirement": "`ACTION_SEND` and `ACTION_SEND_MULTIPLE` intake shall accept `content://` streams through scoped grants, validate actual content, copy to app-private staging, and never request or infer an unrestricted filesystem path.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-006",
+ "priority": "P0",
+ "requirement": "The application shall not request `MANAGE_EXTERNAL_STORAGE`; exported components shall be minimal, permission-protected where possible, and validate every intent/deep link.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-007",
+ "priority": "P0",
+ "requirement": "WorkManager jobs shall be unique and idempotent by capture/operation ID, resumable, constraint-aware, and safe across process death, reboot, duplicate scheduling, and lost acknowledgement.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-008",
+ "priority": "P0",
+ "requirement": "`LOCAL` mode shall prevent original bytes, previews, OCR text, thumbnails, voice content, source snippets, and reconstructable chunks from uploading; UI shall state that local-only content may be unavailable on other devices.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-009",
+ "priority": "P0",
+ "requirement": "Offline queues shall be encrypted, account/workspace-scoped, dependency-aware, and reconciled through `DSO` with explicit conflicts and no silent last-write-wins for protected fields.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-010",
+ "priority": "P0",
+ "requirement": "Approval decisions shall require online server authorization, the current bound subject hash/policy, and MFA when required; notification actions and cached roles shall never finalize approval.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-011",
+ "priority": "P0",
+ "requirement": "Push and lock-screen notifications shall comply with `NCO` and contain no file/client name, extracted value, amount, evidence, comment text, voice transcript, or other sensitive content.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-012",
+ "priority": "P0",
+ "requirement": "Evidence review shall preserve the exact ArtifactVersion and coordinate, request the minimum authorized representation, and report `SOURCE_OFFLINE` or another explicit reason rather than substituting newer content; `LOCAL` Desktop evidence opens on that Desktop and is not streamed to Android without explicit derived publication.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-013",
+ "priority": "P1",
+ "requirement": "Camera capture shall support multi-page ordering, retake/removal before finalization, orientation, flash, focus, blur/glare hints, and a user-confirmed quality override.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-014",
+ "priority": "P1",
+ "requirement": "Voice capture shall be user-initiated and visibly foregrounded, enforce workspace duration/size policy, preserve the original recording, and version transcript/audio enhancements separately.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-015",
+ "priority": "P1",
+ "requirement": "Users shall be able to select Wi-Fi-only, charging, battery, roaming, and cellular-size behavior within stricter organization policy; the app shall display queued bytes and reasons.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-016",
+ "priority": "P1",
+ "requirement": "The app shall provide focused Inbox, capture, review, approval, comment, notification, report, sync, conflict, device, and account screens and shall direct full administration to Web.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-017",
+ "priority": "P1",
+ "requirement": "Vietnamese shall be the default complete locale with English fallback, and all critical workflows shall support TalkBack, switch access, font scaling to 200%, high contrast, and non-color cues.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-018",
+ "priority": "P1",
+ "requirement": "Cached source previews and staged captures shall have visible storage usage, policy retention, explicit cleanup, and safeguards preventing cleanup of unfinalized or unsynchronized user data without confirmation.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-019",
+ "priority": "P1",
+ "requirement": "Account switch/sign-out shall stop work, close account databases, clear session material, and prevent one account or workspace from observing another's cached metadata.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-020",
+ "priority": "P1",
+ "requirement": "The app shall use verified Android App Links for DataBreeze web origins, reject unrecognized schemes/hosts/actions, and re-authorize every resolved resource.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-021",
+ "priority": "P1",
+ "requirement": "Background execution shall comply with Android limits, use foreground services only for user-visible capture or policy-compliant long transfer, and never run hidden continuous polling.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-022",
+ "priority": "P1",
+ "requirement": "The app shall expose content-redacted diagnostics, sync status, app/protocol version, device revocation state, and safe recovery/export guidance.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AND-023",
+ "priority": "P0",
+ "requirement": "Android shall implement the DSO user-mediated offline-package exporter for strict-Local handoff: explicit item/destination/purpose consent, source Device signature, authenticated encryption and destination key/passphrase envelope, exact manifest/hash/expiry, OS-selected user transfer, content-safe receipt state, and zero cloud upload, live relay, background peer discovery, or unregistered destination.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/platforms/android.md"
+ }
+ },
+ {
+ "id": "AUD-001",
+ "priority": "P0",
+ "requirement": "AUD shall be the sole authoritative audit ledger; application logs, analytics, outboxes, provider dashboards, and module-specific timelines shall not substitute for it.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-002",
+ "priority": "P0",
+ "requirement": "Every mandatory audited mutation shall persist its domain change and canonical AuditEvent in one PostgreSQL transaction; failure to append the audit event shall abort the mutation.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-003",
+ "priority": "P0",
+ "requirement": "Audit events shall be insert-only to application roles; corrections shall append a linked correction event, and no public or internal application API shall update or delete an existing event.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-004",
+ "priority": "P0",
+ "requirement": "Every AuditEvent shall carry full applicable TenantScope, action key/version, category, risk class, outcome, server time, principal snapshot, subject references, source subsystem, correlation ID, idempotency identity, schema version, and content hash.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-005",
+ "priority": "P0",
+ "requirement": "Human, service-account, Device, system, and provider actors shall use explicit actor types and immutable identifiers; display labels shall be snapshots for interpretation and shall never become authorization evidence.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-006",
+ "priority": "P0",
+ "requirement": "Every subject reference shall include resource type and tenant-scoped ID plus version/revision/hash when the action depends on exact content; cross-scope subjects shall be rejected before append.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-007",
+ "priority": "P0",
+ "requirement": "AUD shall deduplicate a repeated producer operation/action/subject identity and return the original event ID; a replay with the same identity and different canonical content shall be quarantined and alerted.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-008",
+ "priority": "P0",
+ "requirement": "Each workspace shall have a server-assigned monotonic AuditEvent sequence; organization-only events shall use a separate organization sequence, and neither sequence shall depend on client clocks.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-009",
+ "priority": "P0",
+ "requirement": "Audit action definitions shall be versioned, immutable after publication, and declare mandatory context, permitted outcomes, reason policy, safe-change fields, retention class, and whether failure must block the owning action.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-010",
+ "priority": "P0",
+ "requirement": "Safe before/after summaries shall use allowlisted typed fields or salted hashes and shall exclude secrets, credentials, raw source values, evidence excerpts, unrestricted paths, full external payloads, and payment credentials.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-011",
+ "priority": "P0",
+ "requirement": "Login/recovery, authorization denial where safe, membership/role/policy change, privileged read/export, Device lifecycle, data-mode/content movement, retention/deletion, definition publication, job effect, review/approval, connector/credential, billing, and support action classes shall be registered and audited.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-012",
+ "priority": "P0",
+ "requirement": "Audit query and export shall require explicit IAM permissions and TenantScope/resource checks; audit access shall never imply access to linked source content, evidence, billing secrets, or another workspace.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-013",
+ "priority": "P0",
+ "requirement": "Reading privileged audit categories, creating or downloading an export, changing retention, applying a legal hold, verifying a seal, or using support tooling shall itself create a bounded non-recursive AuditEvent.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-014",
+ "priority": "P0",
+ "requirement": "Periodic seals shall cover contiguous closed sequence ranges using deterministic event hashes and a Merkle root, be signed by a rotating control-plane key, and be copied to storage unavailable to the event-table write role.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-015",
+ "priority": "P0",
+ "requirement": "Integrity verification shall detect missing, reordered, duplicated, or altered events and invalid or missing seals; a failure shall raise a security alert and mark the affected range and exports unverified without rewriting history.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-016",
+ "priority": "P0",
+ "requirement": "Retention expiry shall follow the action's published retention class, active legal holds, tenant policy, and applicable deployment policy; it shall be auditable and preserve content-safe tombstones plus seal continuity.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-017",
+ "priority": "P0",
+ "requirement": "Query APIs shall use stable cursor pagination, bounded time ranges, deterministic ordering, field allowlists, and safe filters; callers shall not supply arbitrary SQL, expressions, or export templates.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-018",
+ "priority": "P0",
+ "requirement": "Audit exports shall pin TenantScope, filters, upper sequence watermark, event/action schema versions, redaction policy, event count, checksums, signer/key version, creation actor/time, purpose, and expiry in an immutable manifest.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-019",
+ "priority": "P0",
+ "requirement": "Local and Hybrid policy shall permit only content-safe AuditEvent metadata as `CONTROL_METADATA`; Local source content, paths, values, previews, and evidence snippets shall never enter the canonical ledger.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-020",
+ "priority": "P0",
+ "requirement": "An offline action shall create a Device-signed LocalAuditFragment linked to its operation and authorization snapshot; it becomes a canonical AuditEvent only after server verification and acceptance. A rejected, tampered, wrong-scope, or revoked-Device fragment shall remain quarantined and exportable under policy, and the server shall append its own content-safe canonical rejection AuditEvent without treating the fragment's claimed action as accepted.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-021",
+ "priority": "P0",
+ "requirement": "Restored deployments shall verify sequence continuity and the latest independent seals before privileged mutations resume; audit partitions, action definitions, holds, exports, keys, and seal records shall be included in disaster recovery.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-022",
+ "priority": "P1",
+ "requirement": "Authorized administrators shall create scoped legal holds and retention exceptions with reason, authority reference, effective period, and immutable release history; a hold shall not broaden event visibility.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-023",
+ "priority": "P1",
+ "requirement": "AUD shall support signed JSON Lines and CSV exports with a canonical JSON manifest and independently documented verification procedure; presentation PDFs may be derived but shall not be the verification source.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "AUD-024",
+ "priority": "P1",
+ "requirement": "New action definitions and actor/subject types shall pass schema, privacy, retention, authorization, idempotency, and golden-fixture review before registration; extensions shall not emit arbitrary untyped payloads.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/foundation/audit-ledger.md"
+ }
+ },
+ {
+ "id": "BUA-001",
+ "priority": "P0",
+ "requirement": "The control plane shall enforce effective entitlements and limits server-side before every billable upload, processing job, paid-module action, seat addition, export class, and API operation.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-002",
+ "priority": "P0",
+ "requirement": "Client entitlement displays and offline caches shall be advisory; a modified Web, Desktop, or Android client shall not bypass server admission control.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-003",
+ "priority": "P0",
+ "requirement": "Plan versions and optional commercial price mappings shall be immutable, effective-dated, and referenced by subscriptions and entitlement snapshots.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-004",
+ "priority": "P0",
+ "requirement": "Usage shall be recorded in an append-only PostgreSQL ledger with stable idempotency keys; corrections shall be new adjustment records, never updates or deletes.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-005",
+ "priority": "P0",
+ "requirement": "Redis, analytics stores, provider dashboards, and client counters shall not be authoritative for subscription state, entitlements, or usage.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-006",
+ "priority": "P1",
+ "requirement": "When a commercial billing provider is enabled, its webhooks shall be signature-verified, stored idempotently, ordered per provider object, and reconciled with provider APIs before ambiguous state changes.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-007",
+ "priority": "P0",
+ "requirement": "The `ExecutionAdmissionCoordinator` shall persist a BUA quota reservation and JRA job/admission creation in one modular-monolith transaction so concurrent requests cannot oversubscribe a hard limit; BUA and JRA shall expose contracts to the coordinator and shall not import each other's services or persistence.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-008",
+ "priority": "P0",
+ "requirement": "Infrastructure retries and failed attempts that produce no customer result shall not create duplicate or unjustified billable usage.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-009",
+ "priority": "P0",
+ "requirement": "`PAST_DUE`, `SUSPENDED`, cancellation, downgrade, or quota excess shall never delete or overwrite artifacts, versions, evidence, results, comments, audit history, or local data.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-010",
+ "priority": "P0",
+ "requirement": "Suspended and cancelled organizations shall retain authenticated read, authorized download/export, billing remediation, and explicit deletion-request access while data remains under retention policy.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-011",
+ "priority": "P0",
+ "requirement": "Billing changes, provider-link changes, organization deletion, and manual credits/debits shall require Owner authority, recent MFA where sensitive, idempotency, and immutable audit records.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-012",
+ "priority": "P0",
+ "requirement": "`LOCAL` original bytes shall not count toward cloud storage usage and shall never upload for metering; only verified synchronized classes may contribute to cloud usage.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-013",
+ "priority": "P1",
+ "requirement": "A 14-day grace period shall follow verified payment failure before suspension, unless fraud, abuse, or legal restrictions require a separately audited immediate safety suspension.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-014",
+ "priority": "P1",
+ "requirement": "Downgrades shall normally take effect at period end; over-limit dimensions shall block new growth while preserving read, export, and user-directed cleanup.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-015",
+ "priority": "P1",
+ "requirement": "Entitlement responses shall include stable reason codes, effective/expiry timestamps, limit, used, reserved, and reset time without exposing provider secrets.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-016",
+ "priority": "P1",
+ "requirement": "Usage aggregation shall reconcile ledger totals, object-storage inventory, successful job results, membership counts, and provider-reported quantities on a scheduled basis.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-017",
+ "priority": "P1",
+ "requirement": "Manual adjustments shall require a reason, actor, related organization, unit, quantity, effective period, and optional prior usage-event reference.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-018",
+ "priority": "P1",
+ "requirement": "Billing communications shall follow `NCO`, use content-minimized templates, and never include source names, values, or payment credentials.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-019",
+ "priority": "P1",
+ "requirement": "Reactivation shall rebuild entitlements and re-authorize eligible nonterminal jobs whose dispatch was blocked by entitlement policy; it shall not automatically execute stale, destructive, external, or approval-gated work.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-020",
+ "priority": "P1",
+ "requirement": "The platform shall provide machine-readable usage export and invoice metadata in organization currency while preserving raw quantities in canonical units.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-021",
+ "priority": "P0",
+ "requirement": "The foundation shall issue signed, Device- and workspace-bound offline entitlement leases that expire within 24 hours, bind plan/entitlement and authorization revisions plus allowed action limits, cannot authorize cloud or external effects, and fail closed after expiry or revocation is observed.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "BUA-022",
+ "priority": "P0",
+ "requirement": "Every deployment shall support provider-independent `BUILT_IN_FREE`, `DEVELOPMENT`, or `ADMIN_GRANTED` subscription sources; provider absence or outage shall not block creating or enforcing one of those sources, and all sources shall use the same immutable PlanVersion, EntitlementSnapshot, reservation, usage, and authorization contracts.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/foundation/billing-usage-administration.md"
+ }
+ },
+ {
+ "id": "CRF-001",
+ "priority": "P0",
+ "requirement": "Every report definition, template, run, version, output, and release shall be scoped to a workspace and client/project.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-002",
+ "priority": "P0",
+ "requirement": "Published template versions shall be immutable and shall declare supported output formats, exact `DSM` dataset/schema contract references, parameters, blocks, and renderer requirements.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-003",
+ "priority": "P0",
+ "requirement": "Report definitions shall pin a template version or an explicit version-selection policy and shall never switch a released report implicitly.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-004",
+ "priority": "P0",
+ "requirement": "Each report run shall freeze exact `DSM` dataset, metric, and rule versions, parameters, template version, renderer versions, locale, timezone, effective `DSO` policy, `jraJobId`, and pinned `resultManifestId` in a manifest.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-005",
+ "priority": "P0",
+ "requirement": "Preflight shall enforce client scope, permissions, schema compatibility, data-quality gates, freshness, required evidence, and output capability.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-006",
+ "priority": "P0",
+ "requirement": "A blocked preflight shall enumerate every blocking and warning condition and shall not produce an approvable report.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-007",
+ "priority": "P0",
+ "requirement": "Metric values, table records, and chart series shall be generated by deterministic implementations bound to exact immutable `DSM` metric/rule versions.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-008",
+ "priority": "P0",
+ "requirement": "Every consequential metric and derived table/chart value shall have evidence lineage to governed dataset fields and source evidence.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-009",
+ "priority": "P0",
+ "requirement": "Report blocks shall support stable IDs, conditional inclusion, page/section behavior, localization, accessibility labels, and per-format fallbacks.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-010",
+ "priority": "P0",
+ "requirement": "Narrative content shall distinguish authored text, generated draft text, parameter substitution, and deterministic fact insertion.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-011",
+ "priority": "P0",
+ "requirement": "AI-generated narrative shall use a bounded approved fact manifest, be provider-neutral, be labeled during review, and require human acceptance before approval.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-012",
+ "priority": "P0",
+ "requirement": "AI or free text shall not alter deterministic metrics, datasets, chart series, or evidence references.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-013",
+ "priority": "P0",
+ "requirement": "The system shall create report versions rather than mutating a submitted, approved, released, or withdrawn version.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-014",
+ "priority": "P0",
+ "requirement": "Comments shall attach to a report version and stable block or evidence anchor and shall preserve resolution history.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-015",
+ "priority": "P0",
+ "requirement": "A material change to data, parameters, bound `DSM` metric/rule versions, template, narrative, or output shall change the subject version/hash and shall invalidate the bound `JRA` `ApprovalRequest`; the module shall not carry a prior decision forward.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-016",
+ "priority": "P0",
+ "requirement": "Release shall require a valid `JRA` `ApprovalDecision` for the exact requested action and report subject type/ID/version/hash plus an explicit audience, format set, evidence policy, and expiry/retention constraint.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-017",
+ "priority": "P0",
+ "requirement": "Generated DOCX, PPTX, XLSX, PDF, and web outputs shall identify report/version, generation time, client, period, and confidentiality classification where configured.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-018",
+ "priority": "P0",
+ "requirement": "Format-specific generation failure shall be visible and shall not mark that output ready or silently substitute another format.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-019",
+ "priority": "P0",
+ "requirement": "A released report shall never grant access to source datasets or evidence beyond the release policy and viewer permissions.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-020",
+ "priority": "P0",
+ "requirement": "Repeated run, `JRA` approval-facade, release, or export requests shall be idempotent and shall not create duplicate requests, versions, or notifications.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-021",
+ "priority": "P1",
+ "requirement": "Templates shall support reusable blocks, nested sections, client brand tokens, headers/footers, tables, charts, images, appendices, and references.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-022",
+ "priority": "P1",
+ "requirement": "Report authors shall preview with fixture or authorized data and compare visual/content changes between versions.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-023",
+ "priority": "P1",
+ "requirement": "Schedules shall support calendar periods, timezone, client sets, parameter derivation, dataset selection rules, and failure policy.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-024",
+ "priority": "P1",
+ "requirement": "Batch runs shall isolate client data and expose per-run status, retry, and audit history.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-025",
+ "priority": "P1",
+ "requirement": "Users shall be able to clone a definition or template while preserving attribution and creating independent future versions.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-026",
+ "priority": "P1",
+ "requirement": "Evidence manifests shall be exportable in a machine-readable format with stable block/value identifiers.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-027",
+ "priority": "P1",
+ "requirement": "Released web reports shall support revocable links, expiry, optional authentication, download policy, and view audit subject to privacy policy.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "CRF-028",
+ "priority": "P2",
+ "requirement": "Provider-neutral AI may draft summaries, explanations, and transitions, but generated material shall remain reviewable and removable without changing deterministic report content.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/features/client-report-factory.md"
+ }
+ },
+ {
+ "id": "DQG-001",
+ "priority": "P0",
+ "requirement": "The system shall create a quality-dataset binding to an existing `DSM` `Dataset` and exact immutable `DatasetVersion` records, then record module-specific criticality, intended use, cadence, locale, `dataModeConstraint`, `effectiveDataModePolicyRef`, `retentionConstraint`, and `effectiveRetentionPolicyRef` without registering a parallel dataset identity or broadening workspace policy.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-002",
+ "priority": "P0",
+ "requirement": "A quality contract shall bind immutable `DSM` dataset, schema, semantic-definition, key, rule-set, and reference-dataset versions plus module-owned ownership, fitness, incident, and waiver policy.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-003",
+ "priority": "P0",
+ "requirement": "Published quality-contract binding versions shall be immutable; edits shall create a draft with a named parent and machine-readable diff, while referenced rule-suite publication and versioning remain canonical in `DSM`.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-004",
+ "priority": "P0",
+ "requirement": "Quality contracts shall select schema, type, requiredness, completeness, uniqueness, format, range, allowed-set, reference, cross-field, and referential rules from the canonical `DSM` rule catalog.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-005",
+ "priority": "P0",
+ "requirement": "Every bound `DSM` `RuleDefinitionVersion` or `RuleSetVersion` shall expose the scope, severity, typed parameters, null behavior, evidence fields, cost class, version, and stable failure reason code required by the quality engine.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-006",
+ "priority": "P0",
+ "requirement": "Quality-contract activation shall reject arbitrary code, unknown functions, type mismatches, cycles, missing or incompatible `DSM` references, ambiguous locale/rounding, and resource-unbounded definitions without republishing the referenced rules.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-007",
+ "priority": "P0",
+ "requirement": "The system shall support exact count, distinct-count, sum, signed-balance, and grouped control-total reconciliation between named dataset versions.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-008",
+ "priority": "P0",
+ "requirement": "Reconciliation definitions shall state join/group keys, units or currencies, decimal precision, rounding mode, tolerance, missing-key behavior, and severity.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-009",
+ "priority": "P0",
+ "requirement": "The system shall support volume, schema, category-frequency, numeric-distribution, null-rate, and freshness drift against a versioned baseline.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-010",
+ "priority": "P0",
+ "requirement": "Every drift rule shall record metric, baseline population/window, minimum sample, comparison method, threshold, direction, and multiple-comparison policy where applicable.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-011",
+ "priority": "P0",
+ "requirement": "A monitor shall bind an immutable contract version, source selector, execution location, trigger/schedule, late-arrival policy, and escalation policy.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-012",
+ "priority": "P0",
+ "requirement": "Monitors shall support manual, cron-like scheduled, governed dataset-version arrival, and pre-release invocation without unrestricted event subscriptions.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-013",
+ "priority": "P0",
+ "requirement": "A run shall bind exact dataset, contract, rule, reference, baseline, engine, and parsing versions plus `jraJobId` and a pinned `resultManifestId` before its result is accepted.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-014",
+ "priority": "P0",
+ "requirement": "Deterministic rule results shall be `PASS`, `FAIL`, `NOT_EVALUATED`, or `ERROR`; statistical drift results shall additionally expose statistic, threshold, sample size, and significance.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-015",
+ "priority": "P0",
+ "requirement": "Every failed or errored result shall expose stable reason codes, affected counts, denominators, and evidence or an explicit reason evidence could not be produced.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-016",
+ "priority": "P0",
+ "requirement": "Every record-level `QualityFindingDetail` shall be immutable and retain page/sheet/cell/row/column or dataset-row evidence, the exact observed value subject to masking policy, its stable fingerprint, and `sharedFindingId` when linked to actionable work.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-017",
+ "priority": "P0",
+ "requirement": "Repeated failures with the same diagnostic fingerprint shall retain immutable occurrence detail and shall link to the same canonical `JRA` `Finding` envelope when policy considers them one actionable issue rather than creating duplicate workflow records.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-018",
+ "priority": "P0",
+ "requirement": "Incident projections shall group immutable diagnostic-detail and `sharedFindingId` references without copying or overriding their `JRA` workflow state, assignment, disposition, evidence references, or history.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-019",
+ "priority": "P0",
+ "requirement": "The canonical `JRA` `Finding` and `ReviewTask` envelopes shall own severity, status, owner, acknowledgement and resolution targets, comments, timeline, disposition, and escalation state; module incident views shall be permission-filtered projections only.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-020",
+ "priority": "P0",
+ "requirement": "Every module finding or incident transition facade shall delegate to `JRA`, enforce its permission and reason requirements, and return the canonical revision without persisting an independent transition or decision.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-021",
+ "priority": "P0",
+ "requirement": "A repair proposal shall use allowlisted typed transformations and bind exact diagnostic-detail IDs, linked `sharedFindingId` values, source dataset version, contract version, expected outcome, and plan hash.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-022",
+ "priority": "P0",
+ "requirement": "Repair preview shall show exact affected count, bounded before/after examples, rule impacts, control-total changes, collisions, and unrepairable findings.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-023",
+ "priority": "P0",
+ "requirement": "Applying a repair shall create a derived artifact/dataset version and shall never mutate an original source artifact or dataset version.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-024",
+ "priority": "P0",
+ "requirement": "A consequential repair shall create or reuse one `JRA` `ApprovalRequest` bound to the requested action, exact repair-plan subject type/ID/version/hash, and source fingerprint; the module shall store only `jraApprovalRequestId` plus those subject bindings, and any change shall invalidate the request through `JRA`.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-025",
+ "priority": "P0",
+ "requirement": "A verification run shall evaluate the same or explicitly superseding contract version and link before/after results to the repair.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-026",
+ "priority": "P0",
+ "requirement": "A module incident projection shall not close solely because a repair job completed; verified results shall be required before requesting an authorized canonical `JRA` transition.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-027",
+ "priority": "P0",
+ "requirement": "Waivers shall require scope, reason, risk owner, compensating control, start, expiry, affected rule/dataset versions, requested action, exact subject type/ID/version/hash, and `jraApprovalRequestId`; approver eligibility and decision shall remain owned by `JRA`.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-028",
+ "priority": "P0",
+ "requirement": "Waived failures shall remain visible and excluded from pass-rate numerators unless a report explicitly presents a separate policy-compliant metric.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-029",
+ "priority": "P0",
+ "requirement": "Monitor and incident notifications shall use canonical `JRA` finding/review state and honor permissions, severity, quiet hours, escalation paths, and deduplication windows.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-030",
+ "priority": "P1",
+ "requirement": "Users shall compare runs by rule result, affected rate, distribution, finding set, incident impact, contract diff, and dataset version.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-031",
+ "priority": "P1",
+ "requirement": "`DSM` rule templates may be reused across compatible semantic field types; a quality-contract binding shall pin the immutable template or rule version and expose only declared module-local parameter overrides.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-032",
+ "priority": "P1",
+ "requirement": "Data owners shall publish a fit-for-use scorecard showing critical-rule status, freshness, reconciliation, open incidents, waivers, and trend without hiding raw results.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-033",
+ "priority": "P1",
+ "requirement": "Desktop and cloud execution of the same deterministic fixture shall produce equivalent rule states, reason codes, counts, and exact reconciliation totals.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-034",
+ "priority": "P1",
+ "requirement": "The system shall support backfill evaluation of a published contract over a bounded set of historical dataset versions without altering their original monitoring history.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-035",
+ "priority": "P1",
+ "requirement": "Users shall export a signed quality report and machine-readable result manifest with checksums and evidence references.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DQG-036",
+ "priority": "P2",
+ "requirement": "The system may suggest new rules from recurring data patterns, but every suggestion shall remain an unpublished draft until a steward reviews parameters and estimated impact and publishes it through `DSM`.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/features/data-quality-guard.md"
+ }
+ },
+ {
+ "id": "DSK-001",
+ "priority": "P0",
+ "requirement": "Electron renderer windows shall use `contextIsolation: true`, `nodeIntegration: false`, sandboxing, navigation restrictions, and a restrictive Content Security Policy.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-002",
+ "priority": "P0",
+ "requirement": "The preload shall expose only a versioned allowlist of schema-validated capabilities; renderer code shall have no raw IPC, filesystem, process, keychain, updater, or shell access.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-003",
+ "priority": "P0",
+ "requirement": "IPC handlers shall verify sender frame/origin, workspace context, permission, argument size/schema, and current window capability before invoking the main process.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-004",
+ "priority": "P0",
+ "requirement": "Folder access shall require a local OS picker and explicit workspace/action grant; the cloud shall receive only opaque grant IDs and shall never specify arbitrary paths.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-005",
+ "priority": "P0",
+ "requirement": "File access shall be read-only by default, originals shall remain immutable, and every correction or transformation shall produce a new version or staged copy.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-006",
+ "priority": "P0",
+ "requirement": "Desktop shall execute a canonical `JRA` Job only from a signed, unexpired, nonce-protected envelope whose schemas, handler digests, data mode, capabilities, device, workspace, and resources verify locally.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-007",
+ "priority": "P0",
+ "requirement": "Desktop shall not expose or implement cloud-triggered shell commands, arbitrary scripts, arbitrary URL navigation, raw keyboard/mouse control, or unrestricted file operations.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-008",
+ "priority": "P0",
+ "requirement": "The Python engine shall be bundled, versioned, started without a shell, receive a scrubbed environment and attempt-specific handles, and communicate only through bounded framed JSON-RPC.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-009",
+ "priority": "P0",
+ "requirement": "Sidecar requests/responses, progress, errors, and result manifests shall be runtime-validated; malformed, oversized, timed-out, or wrong-attempt messages shall terminate or quarantine the attempt.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-010",
+ "priority": "P0",
+ "requirement": "`LOCAL` mode shall prevent original bytes and reconstructable derived content, including previews, OCR/transcripts, row/cell values, thumbnails, source snippets, paths, and chunks, from reaching cloud endpoints or telemetry.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-011",
+ "priority": "P0",
+ "requirement": "Local queues, metadata, capability paths, keys, and staged sensitive outputs shall be encrypted with a device-protected key and separated by Windows user profile.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-012",
+ "priority": "P0",
+ "requirement": "File-watcher intake shall be debounced, stable-file checked, content-hashed, idempotent, and resistant to partial writes, rename storms, junction loops, and duplicate events.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-013",
+ "priority": "P0",
+ "requirement": "Write actions shall use proposed effect manifests, policy approval, atomic operations where supported, effect idempotency, receipts, and an undo path; they shall never overwrite an original silently.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-014",
+ "priority": "P0",
+ "requirement": "Installers, executables, update manifests, and update packages shall be signed and verified; an invalid or downgraded update shall not execute.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-015",
+ "priority": "P1",
+ "requirement": "Desktop shall provide local Inbox, job/review/approval status, evidence navigation, conflict resolution, device health, queue state, and data-location indicators without duplicating full Web administration.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-016",
+ "priority": "P1",
+ "requirement": "Offline-capable actions shall use expiring authorization and entitlement leases, durable operation IDs, append-only local events, and `DSO` conflict rules.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-017",
+ "priority": "P1",
+ "requirement": "A user shall be able to pause all watchers and local execution immediately; pause state shall persist across restart and be visible to authorized Web users as content-free device state.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-018",
+ "priority": "P1",
+ "requirement": "Sidecar supervision shall enforce time, process-tree, temporary-storage, and configurable memory/CPU limits, and shall kill the full child process tree on cancellation or timeout.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-019",
+ "priority": "P1",
+ "requirement": "Diagnostics export shall be user-initiated, previewable, content-redacted, and exclude file names, paths, source values, keys, tokens, comments, and evidence snippets.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-020",
+ "priority": "P1",
+ "requirement": "The application shall recover from process crash, Windows restart, sleep, network change, and update without duplicating intake, jobs, file effects, or sync operations.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-021",
+ "priority": "P1",
+ "requirement": "Vietnamese shall be the default complete locale with English fallback, and the application shall support keyboard, screen reader, high-contrast, reduced-motion, and Windows scaling settings.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-022",
+ "priority": "P1",
+ "requirement": "Device revocation shall block new sync, dispatch, blob transfer, and session refresh immediately; local content shall remain encrypted and the UI shall provide sign-out/export guidance without claiming remote wipe.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-023",
+ "priority": "P0",
+ "requirement": "A control-plane request to open `LOCAL` evidence shall contain only an opaque EvidenceReference and content-free context; Desktop shall re-authorize it and render locally, and shall never upload or stream the rendition unless the user separately publishes a governed derivative.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-024",
+ "priority": "P0",
+ "requirement": "Offline recipe work shall use `JRA` ProvisionalExecution records with client execution IDs, signed cached definitions, valid offline leases, immutable manifests, and no canonical Job/approval claim; server acceptance shall create at most one canonical Job and rejection shall quarantine the local result.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-025",
+ "priority": "P0",
+ "requirement": "Before offline recipe execution, Desktop shall verify the complete JRA RecipePublicationEnvelope, workspace/recipe version and hash, referenced action handler/input/output schema hashes, DSM definition hashes, policy references, supported envelope schema, signer/key version, signature, and offline-validity time; encrypted cache storage alone shall never satisfy authenticity.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSK-026",
+ "priority": "P0",
+ "requirement": "Desktop shall implement DSO offline-package import through explicit user selection, isolated staging, full manifest/signature/recipient/workspace/purpose/policy/expiry/classification/hash verification, idempotent IAE placement/provisional-intake creation, content-safe receipt reconciliation, and quarantine on any mismatch; it shall not use cloud staging, live relay, or automatic peer discovery.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/platforms/desktop.md"
+ }
+ },
+ {
+ "id": "DSM-001",
+ "priority": "P0",
+ "requirement": "Every Dataset, definition, mapping, rule set, run, and lineage record shall have a stable UUID and an owning workspace; project scope shall be recorded when applicable.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-002",
+ "priority": "P0",
+ "requirement": "A DatasetVersion shall be immutable and shall reference exact input versions, schema version, mapping version, rule-set version, engine build, content fingerprint, row counts, quality state, and lineage manifest.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-003",
+ "priority": "P0",
+ "requirement": "Dataset bytes and snapshots shall remain owned by `IAE`; this foundation shall store governed metadata and opaque storage references rather than create an alternate artifact or storage authority.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-004",
+ "priority": "P0",
+ "requirement": "Each published SchemaVersion shall use stable field IDs and declare field type, nullability, constraints, unit, semantic role, aliases, localized labels, sensitivity, and default behavior.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-005",
+ "priority": "P0",
+ "requirement": "Schema publication shall classify compatibility as additive-compatible, validation-tightening, migration-required, or breaking and shall reject a claim contradicted by structural comparison.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-006",
+ "priority": "P0",
+ "requirement": "Published schema, semantic, metric, rule-set, and mapping versions shall be immutable, canonical-hashed, and retained as historical readers while referenced by an active result.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-007",
+ "priority": "P0",
+ "requirement": "A MetricDefinitionVersion shall declare grain, typed inputs, filters, aggregation, unit, null/zero behavior, rounding, deterministic implementation, evidence policy, and executable fixtures.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-008",
+ "priority": "P0",
+ "requirement": "A RuleSetVersion shall contain only published deterministic rules and allowlisted typed functions and shall declare scope, severity, parameters, missing-input behavior, and blocking behavior.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-009",
+ "priority": "P0",
+ "requirement": "A MappingVersion shall bind a source schema or fingerprint range to stable target field IDs and record every transform, default, exclusion, reviewer, compatibility decision, and parent version.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-010",
+ "priority": "P0",
+ "requirement": "Saved mappings shall not apply automatically after material source drift, ambiguous header matching, incompatible type change, or target breaking change; the system shall require review.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-011",
+ "priority": "P0",
+ "requirement": "Profiling shall disclose whether it is complete or sampled, the deterministic sample method and seed where applicable, excluded scopes, scanned counts, and resource limits.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-012",
+ "priority": "P0",
+ "requirement": "Validation and transformation runs shall pin immutable inputs and definition versions and execute through registered `JRA` typed actions with idempotent result acceptance.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-013",
+ "priority": "P0",
+ "requirement": "Every validation finding shall include a stable fingerprint, rule/version, severity, subject, actual/expected typed values where safe, evidence references, and run/input versions.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-014",
+ "priority": "P0",
+ "requirement": "Every material derived field, metric, aggregate, or release-gating conclusion shall carry reproducible lineage and `IAE` evidence or be explicitly marked `UNSUPPORTED_BY_SOURCE`.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-015",
+ "priority": "P0",
+ "requirement": "Missing, null, blank, invalid, zero, not-applicable, and redacted states shall remain distinct through mapping, rules, metrics, APIs, and exports.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-016",
+ "priority": "P0",
+ "requirement": "AI-assisted labels, mappings, semantic definitions, or rule suggestions shall remain drafts, identify provider/configuration provenance, and require deterministic validation plus authorized confirmation before publication.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-017",
+ "priority": "P0",
+ "requirement": "Local, Hybrid, and Cloud processing shall follow `DSO`; `LOCAL` originals, source values, reconstructable previews, and evidence excerpts shall never synchronize, while content-free metadata and explicitly approved derived outputs may synchronize only as `DSO` permits.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-018",
+ "priority": "P0",
+ "requirement": "Authorization shall be enforced through `IAM` for catalog discovery, definition management, execution, row/field access, evidence resolution, export, certification, and deprecation.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-019",
+ "priority": "P1",
+ "requirement": "Reprocessing after input, schema, mapping, rule, metric, or engine change shall create a new run and DatasetVersion and shall never revise a historical result in place.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-020",
+ "priority": "P1",
+ "requirement": "Data-quality gates shall bind an exact schema, rule-set, and policy version and shall expose `PASS`, `PASS_WITH_WARNINGS`, `BLOCKED`, or `INCOMPLETE` with contributing findings.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-021",
+ "priority": "P1",
+ "requirement": "Dataset and definition APIs shall use idempotency keys for creation, revision preconditions for mutable drafts, stable cursor pagination, and machine-readable compatibility errors.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-022",
+ "priority": "P1",
+ "requirement": "Governed-data exports shall include data permitted by policy plus schema, semantic, metric, mapping, rule-set, quality, lineage, evidence, and checksum manifests sufficient for independent verification.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-023",
+ "priority": "P1",
+ "requirement": "Rule and transform extensions shall use a versioned typed registry, deterministic contract, declared resource limits, security review, and golden fixtures and shall not execute arbitrary customer code.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-024",
+ "priority": "P2",
+ "requirement": "The system shall allow authorized administrators to promote compatible definitions or templates across workspaces only as sanitized unsigned drafts with no source values, evidence, secrets, access policy, certification, or automatic activation.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-025",
+ "priority": "P0",
+ "requirement": "A shared ReferenceEntity shall have a stable workspace-scoped identity and immutable versions; a `BUSINESS_PARTY` version shall declare supplier/customer roles, canonical display name, localized aliases, typed external identifiers, status, default business attributes, visibility policy, provenance, and canonical hash.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-026",
+ "priority": "P0",
+ "requirement": "Feature modules shall keep extracted party text separate and bind an exact authorized ReferenceEntityVersion; no feature shall own a second canonical supplier/customer identity, alias registry, identifier authority, project-visibility rule, or merge history.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSM-027",
+ "priority": "P1",
+ "requirement": "Reference-entity merge, split, redirect, and correction shall append immutable resolution history and new versions, preserve every historical binding, require actor/reason/evidence, reject cross-workspace targets, and never silently retarget a prior result.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/foundation/datasets-schemas-rules-mappings.md"
+ }
+ },
+ {
+ "id": "DSO-001",
+ "priority": "P0",
+ "requirement": "DSO shall use the IAM DeviceIdentity ID as its only Device identity key and shall not store a second public key, organization/user ownership record, enrollment state, activation state, or authoritative revocation status.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-002",
+ "priority": "P0",
+ "requirement": "DSO capabilities and grants shall require an `ACTIVE` IAM DeviceIdentity with a matching organization and current security epoch; IAM alone shall own the enrollment challenge, proof-of-possession registration, explicit activation, and identity limits.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-003",
+ "priority": "P0",
+ "requirement": "IAM revocation or security-epoch change shall make DSO block new sync, blob, stream, route, transfer, and job-dispatch operations immediately and terminate connected-client grants within 60 seconds; cached offline grants shall expire within 24 hours and fail closed on reconnect, without claiming remote wipe.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-004",
+ "priority": "P0",
+ "requirement": "Synchronization shall use an append-only workspace change log and opaque cursor; it shall not rely on timestamp polling, client clocks, or Redis history.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-005",
+ "priority": "P0",
+ "requirement": "Pull batches and offline pushes shall be idempotent and safely repeatable after timeout, crash, lost acknowledgement, or cursor replay.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-006",
+ "priority": "P0",
+ "requirement": "All synchronized commands shall be re-authorized server-side for principal, device, workspace, project, resource, action, data mode, and entitlement.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-007",
+ "priority": "P0",
+ "requirement": "`LOCAL` mode shall technically prevent upload of original bytes and reconstructable derived content, including chunks, previews, OCR/transcripts, thumbnails, row/cell values, and source snippets, regardless of client request; only a separately confirmed approved derived result may synchronize.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-008",
+ "priority": "P0",
+ "requirement": "Hybrid mode shall be the default and shall synchronize only the explicit data classifications and synchronization payload classes enabled by the workspace policy manifest.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-009",
+ "priority": "P0",
+ "requirement": "Blob transfer shall be resumable, chunk-hashed, content-hash verified, encrypted in transit and at rest, and published only after complete verification.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-010",
+ "priority": "P0",
+ "requirement": "Offline queues shall be encrypted, append-only until acknowledged, dependency-aware, and keyed by stable operation IDs generated before first execution.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-011",
+ "priority": "P0",
+ "requirement": "Conflict handling shall follow the explicit per-entity rules in this specification and shall never silently use last-write-wins for assignments, workflow state, approvals, security, billing, or overlapping corrections.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-012",
+ "priority": "P0",
+ "requirement": "A sync cursor shall advance on a client only after the entire corresponding local transaction commits; partial application shall replay the same batch.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-013",
+ "priority": "P0",
+ "requirement": "Folder capabilities shall be created only through a local OS picker, represented to the cloud by opaque IDs and policy metadata, and limited to approved typed actions.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-014",
+ "priority": "P1",
+ "requirement": "The server shall retain tombstones for at least 90 days and longer than the maximum supported offline interval, with administrative export before a stale device is forced to resnapshot.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-015",
+ "priority": "P1",
+ "requirement": "Initial sync shall use a bounded consistent snapshot plus change-log watermark so concurrent mutations are neither missed nor duplicated.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-016",
+ "priority": "P1",
+ "requirement": "Clients shall support schema-version negotiation and preserve unknown forward-compatible fields; an unsupported breaking version shall require upgrade without corrupting the queue.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-017",
+ "priority": "P1",
+ "requirement": "A device shall report capability versions, local engine version, last sync, queue depth, and coarse health without sending local paths, file names, or source values.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-018",
+ "priority": "P1",
+ "requirement": "Data-mode transitions shall be audited, require Admin authority and recent MFA, and use explicit migration or verified purge workflows for existing replicas.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-019",
+ "priority": "P1",
+ "requirement": "Offline operations rejected after authorization or policy change shall be quarantined with a stable reason and export option; the client shall not repeatedly resubmit them.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-020",
+ "priority": "P1",
+ "requirement": "The system shall expose whether each artifact version is `LOCAL_ONLY`, `CLOUD_ONLY`, or `REPLICATED` and identify available devices without revealing filesystem paths.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-021",
+ "priority": "P0",
+ "requirement": "Every sync cursor shall be bound to principal, device, workspace, effective authorization scope, authorization epoch, data-mode policy, audience, and schema version; any scope change shall invalidate it, force an authorized resnapshot, backfill newly visible history, and lock then purge managed cache that is no longer authorized.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-022",
+ "priority": "P0",
+ "requirement": "Synchronizing an `APPROVED_DERIVED_RESULT` from Local mode shall require an immutable confirmation bound to resource/version, content hash, schema, data classification, policy revision, actor, source Device, destination, and time; a changed subject or policy shall require a new confirmation.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-023",
+ "priority": "P0",
+ "requirement": "DSO shall never reactivate an IAM-revoked DeviceIdentity; recovery shall reference a newly enrolled IAM identity and use an authorized import/reconciliation workflow for preserved local records.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-024",
+ "priority": "P0",
+ "requirement": "An execution route decision shall bind workspace, input placement/version hashes, action type/version, required capabilities, selected target/device when local, data-mode policy revision, authorization epoch, decision subject hash, and expiry; JRA creation shall reject a stale or mismatched decision, and DSO shall not create Jobs directly.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-025",
+ "priority": "P0",
+ "requirement": "A Local `ORIGINAL_CONTENT` item may leave its source Device only through an explicit user-mediated offline package whose manifest binds workspace, source/destination Devices or approved passphrase mode, exact content IDs/hashes/sizes/classifications, purpose, policy and authorization revisions, expiry, encryption/key-envelope profile, source signature, and package hash; import shall verify every binding, create IAE placement/lineage plus an auditable receipt, and shall never use cloud storage, a live relay, background peer transfer, or an unregistered destination.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-026",
+ "priority": "P0",
+ "requirement": "Workspace DataMode shall be the maximum authority; project, resource, module, recipe, and job constraints may only narrow it, and every placement, route, execution, transfer, resume, or sync admission shall enforce the intersection with current Workspace policy and fail closed when a cached effective-policy reference is stale.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "DSO-027",
+ "priority": "P0",
+ "requirement": "DSO shall own immutable WorkspaceDataModePolicyVersions and signed DataModePolicyManifests binding workspace, mode, classification-by-payload matrix, allowed placements/executors/destinations, confirmation/offline-package rules, canonical hash, authorization epoch, audience/Device, schema version, issue/expiry no later than the associated IAM offline snapshot or 24 hours, and signer/key version; clients shall reject tampered, stale, wrong-audience, or unsupported manifests, and cache encryption shall not replace signature verification.",
+ "source": {
+ "line": 160,
+ "path": "docs/specs/foundation/devices-sync-offline.md"
+ }
+ },
+ {
+ "id": "EI-001",
+ "priority": "P0",
+ "requirement": "Every resource shall be scoped to an owning organization, workspace, `ImporterCustomerPartition`, environment, and schema as applicable; a customer partition shall never replace the IAM workspace authorization boundary.",
+ "source": {
+ "line": 90,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-002",
+ "priority": "P0",
+ "requirement": "Every importer schema binding shall reference an immutable published `DSM` SchemaVersion whose stable field identifiers are independent of importer display labels.",
+ "source": {
+ "line": 91,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-003",
+ "priority": "P0",
+ "requirement": "Importer schema publication shall use `DSM` validation and compatibility classification for field types, requiredness, constraints, transforms, rule references, and version changes rather than create an importer-specific schema authority.",
+ "source": {
+ "line": 92,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-004",
+ "priority": "P0",
+ "requirement": "The module shall support CSV, TSV, XLSX, JSON arrays, JSON Lines, and configured delimited text within declared limits.",
+ "source": {
+ "line": 93,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-005",
+ "priority": "P0",
+ "requirement": "Hosted sessions shall use short-lived, single-purpose tokens; browser code shall never receive a long-lived API key or gateway credential.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-006",
+ "priority": "P0",
+ "requirement": "Session tokens shall bind `customerPartitionId`, environment, schema version or allowed version range, permissions, expiry, and optional external user reference.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-007",
+ "priority": "P0",
+ "requirement": "The hosted component shall enforce configured origins, frame policy, and secure cross-window message validation.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-008",
+ "priority": "P0",
+ "requirement": "Uploads shall support checksums, resumable parts, size/type limits, malware-scan state where configured, and idempotent completion.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-009",
+ "priority": "P0",
+ "requirement": "Source files shall be immutable; re-upload or replacement shall create a new artifact version.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-010",
+ "priority": "P0",
+ "requirement": "Importer mappings shall use `DSM` MappingVersions and preserve source column identity, target stable field ID, transform chain, suggestion provenance, and reviewer decision.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-011",
+ "priority": "P0",
+ "requirement": "Required or incompatible mappings shall block full validation and commit.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-012",
+ "priority": "P0",
+ "requirement": "Importer validation shall execute published `DSM` schema/rule versions and project their results as structured file-, column-, row-, field-, and cross-row errors with stable rule codes.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-013",
+ "priority": "P0",
+ "requirement": "Row corrections shall create an overlay or new dataset version and shall not modify the uploaded file.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-014",
+ "priority": "P0",
+ "requirement": "Commit shall be idempotent and shall create at most one result for a session version and idempotency key. When policy requires consequential approval, commit release shall bind to an accepted `JRA` ApprovalDecision whose `subjectRef` contains the exact session subject type/ID/version/hash; Embedded Importer shall not create an independent approval decision.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-015",
+ "priority": "P0",
+ "requirement": "Import results shall identify accepted, rejected, and skipped row counts and shall never silently drop a row.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-016",
+ "priority": "P0",
+ "requirement": "Saved importer mapping bindings shall reference customer-partition-isolated `DSM` mapping drafts/versions and compatible schema lineage unless explicitly promoted as sanitized drafts by an authorized admin.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-017",
+ "priority": "P0",
+ "requirement": "Importer API credentials shall use `IAM` service-account identity and `INT` credential conventions, including hashed-at-rest or signed secrets, one-time display, rotation, revocation, environment scope, and capability scope.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-018",
+ "priority": "P0",
+ "requirement": "Importer webhook bindings shall use authoritative `INT` subscriptions and deliveries so signing, timestamps, replay protection, retry, stable event/delivery identifiers, SSRF policy, and secret rotation are not reimplemented by this module.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-019",
+ "priority": "P0",
+ "requirement": "Support tooling and logs shall enforce IAM workspace access plus customer-partition scope and redact row values and secrets by default.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-020",
+ "priority": "P0",
+ "requirement": "A local gateway shall accept only signed JRA import jobs whose `jraJobId`, pinned `resultManifestId`, active IAM `iamDeviceId` and security epoch, DSO DeviceGrant/capability IDs, allowed importer schema/environment, `effectiveDataModePolicyRef`, and signature validate; no arbitrary path, key, command, feature-owned heartbeat, or copied IAM identity/DSO operational lifecycle state is allowed.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-021",
+ "priority": "P0",
+ "requirement": "Android shall expose administrative alerts and safe status metadata only, with no end-user import actions.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-022",
+ "priority": "P1",
+ "requirement": "Branding shall support logo, colors, typography tokens, help text, and custom domain where configured without permitting arbitrary executable content.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-023",
+ "priority": "P1",
+ "requirement": "The hosted UI shall support Vietnamese and English labels, locale-aware dates/numbers, keyboard navigation, and screen readers.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-024",
+ "priority": "P1",
+ "requirement": "Developers shall have sandbox and production environments with separate `IAM`/`INT` credentials, `DSM` schema bindings, webhook subscriptions, quotas, and data.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-025",
+ "priority": "P1",
+ "requirement": "Importer webhook delivery history and authorized manual replay shall use `INT` delivery resources, preserve the original event ID and import result, and apply importer-specific filters without creating another delivery record authority.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-026",
+ "priority": "P1",
+ "requirement": "The API shall support asynchronous status polling, event cursors, and downloadable structured error reports. Every asynchronous session/run shall store `jraJobId` and pinned `resultManifestId`; JRA owns dispatch, progress, cancel, retry, and terminal state, while importer status is an idempotent business projection.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-027",
+ "priority": "P1",
+ "requirement": "Administrators shall configure file, row, column, size, concurrency, execution-location, `dataModeConstraint`, and `retentionConstraint` limits within plan ceilings. Data mode shall resolve to `effectiveDataModePolicyRef` and only narrow the DSO workspace maximum; retention shall resolve to IAE `effectiveRetentionPolicyRef` and never authorize Embedded Importer to delete bytes.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "EI-028",
+ "priority": "P2",
+ "requirement": "A provider-neutral AI adapter may suggest mappings and transformations from bounded samples, but its suggestions shall require deterministic validation and configured human confirmation.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/features/embedded-importer.md"
+ }
+ },
+ {
+ "id": "FA-001",
+ "priority": "P0",
+ "requirement": "Desktop shall create its encrypted local folder authorization only after an authorized user selects the folder through a native picker and confirms scope plus effective DSO data-mode behavior; Folder Autopilot shall never receive or persist that path.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-002",
+ "priority": "P0",
+ "requirement": "Desktop alone shall store canonical root, volume identity, descendant policy, read/write capabilities, reparse-point policy, and local grant metadata. `DSO` alone shall own the content-free DeviceCapability/DeviceGrant, workspace/action authorization, status, expiry, and revocation; Folder Autopilot shall store none of those fields independently.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-003",
+ "priority": "P0",
+ "requirement": "Cloud APIs and jobs shall reference `deviceGrantId`, `AutopilotFolderBinding`, and `expectedCapabilityDigest` and shall never contain an unrestricted local path, local handle, independent grant copy, or instruction for Desktop to discover a new path.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-004",
+ "priority": "P0",
+ "requirement": "Every observed and destination path shall be canonicalized and verified by Desktop to remain inside its encrypted local authorization, while DSO DeviceGrant status/action scope and the expected capability digest shall be revalidated before access.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-005",
+ "priority": "P0",
+ "requirement": "The canonical `JRA` RecipeVersion shall contain the versioned typed triggers, conditions, and actions from a workspace-allowed catalog; Folder Autopilot shall not persist a second trigger or graph authority.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-006",
+ "priority": "P0",
+ "requirement": "JRA recipe validation shall reject arbitrary code, unknown action types, type mismatches, cycles, unreachable steps, and unbounded traversal; Folder Autopilot profile validation shall additionally reject missing/invalid DSO bindings, capability-digest mismatch, output recursion, and incompatible product-specific settings.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-007",
+ "priority": "P0",
+ "requirement": "`JRA` RecipeVersions shall be immutable after publication; edits, including a changed pinned Folder Autopilot profile payload/hash, create a JRA draft derived from a named parent and shall not create a separate feature-owned recipe version lineage.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-008",
+ "priority": "P0",
+ "requirement": "A designer shall preview a JRA draft plus its Folder Autopilot profile and DSO bindings against selected samples or a bounded scan before publication, with affected count and per-file action plans.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-009",
+ "priority": "P0",
+ "requirement": "Preview shall identify destination collisions, source/destination permission errors, insufficient disk space, unsupported files, recursive re-entry, and approval gates.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-010",
+ "priority": "P0",
+ "requirement": "Desktop shall wait for configurable file stability and retry transient locks before fingerprinting or processing a file.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-011",
+ "priority": "P0",
+ "requirement": "Each input shall receive a content hash, size, modified-time observation, stable execution key, and immutable artifact-version reference before an action.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-012",
+ "priority": "P0",
+ "requirement": "Recipe matching shall be deterministic for path, type, metadata, and validation conditions; classifier suggestions shall include confidence and evidence.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-013",
+ "priority": "P0",
+ "requirement": "An uncertain classification shall route to review rather than execute a class-dependent file mutation when it falls below the published threshold.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-014",
+ "priority": "P0",
+ "requirement": "Rename, copy, and move steps shall use computed relative destinations constrained to `OUTPUT` AutopilotFolderBindings backed by active DSO DeviceGrants and matching Desktop-local authorizations.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-015",
+ "priority": "P0",
+ "requirement": "The system shall never silently overwrite a destination; collision policy shall be `REVIEW`, `SKIP`, or deterministic unique-name generation.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-016",
+ "priority": "P0",
+ "requirement": "Content conversion and normalization shall create a new derivative file and shall not modify source file bytes in place.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-017",
+ "priority": "P0",
+ "requirement": "Permanent deletion shall not be available in the P0/P1 action catalog; removal workflows may move a file to a configured recovery folder with undo.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-018",
+ "priority": "P0",
+ "requirement": "A sensitive action plan shall require an authoritative `JRA` ApprovalRequest according to the applicable ApprovalPolicy, including moves across DSO DeviceGrants, externally synchronized outputs, or low-confidence classification.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-019",
+ "priority": "P0",
+ "requirement": "The authoritative `JRA` ApprovalRequest and ApprovalDecision shall bind approver, an exact subject type/ID/version/hash, plan hash, JRA RecipeVersion, source fingerprint, destinations, expiry, and decision reason; Folder Autopilot shall not create an independent approval decision.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-020",
+ "priority": "P0",
+ "requirement": "Desktop shall revalidate its local authorization, DSO DeviceGrant action scope/status, expected capability digest, `effectiveDataModePolicyRef`, source fingerprint, destination state, and the applicable `JRA` ApprovalDecision immediately before committing actions.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-021",
+ "priority": "P0",
+ "requirement": "Multi-step file operations shall use a staged plan and compensating actions so a failure cannot present a partially completed execution as successful.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-022",
+ "priority": "P0",
+ "requirement": "Every asynchronous execution shall store `jraJobId` and a pinned JRA `resultManifestId`. `JRA` owns dispatch, progress, cancellation, retry, steps, and terminal execution state; Folder Autopilot stores only an idempotent business projection with input/output fingerprints, evidence, reason codes, and actor/device attribution.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-023",
+ "priority": "P0",
+ "requirement": "Repeated file-system events or job delivery shall not create duplicate derivatives, moves, notifications, review items, or module submissions.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-024",
+ "priority": "P0",
+ "requirement": "Eligible executions shall expose an inverse plan until the configured undo expiry; ineligible steps shall be labeled before approval.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-025",
+ "priority": "P0",
+ "requirement": "Undo shall refuse to overwrite or discard a file changed after the execution and shall create a guided conflict item.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-026",
+ "priority": "P0",
+ "requirement": "Users with permission shall pause a RecipeAssignment immediately; retiring or replacing its canonical recipe/version shall use the JRA facade. In-flight jobs may finish only through the next safe checkpoint defined by JRA and the recipe policy.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-027",
+ "priority": "P0",
+ "requirement": "Revoking a `DSO` DeviceGrant shall invalidate its AutopilotFolderBindings, stop new access, cancel undispatched JRA work, and require revalidation of in-flight work without erasing audit history; Folder Autopilot shall not own or rewrite revocation state.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-028",
+ "priority": "P1",
+ "requirement": "A canonical JRA recipe step shall be permitted to submit an immutable artifact version to another DataBreeze module only through a typed module-intake action with an idempotency key.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-029",
+ "priority": "P1",
+ "requirement": "The system shall support scheduled reconciliation scans to recover file events missed during device sleep or watcher overflow.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-030",
+ "priority": "P1",
+ "requirement": "Operators shall filter, assign, and bulk-retry only failures they are authorized to retry. An actor eligible under the current `JRA` ApprovalPolicy may bulk-approve homogeneous plans only through authoritative JRA decisions, with each subject independently bound to its exact type/ID/version/hash and plan hash, separation-of-duties rules passing, required MFA current, and an explicit expiry; every bulk action shall preview exact affected items and policy boundaries.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-031",
+ "priority": "P1",
+ "requirement": "Workspace admins shall set profile/assignment-level concurrency, throughput, file-size, extension, schedule, confidence, approval, retention, and undo constraints within JRA, DSO, BUA, and IAE ceilings. `dataModeConstraint` may only narrow the DSO workspace maximum and shall resolve to `effectiveDataModePolicyRef`; `retentionConstraint` shall resolve to canonical IAE `effectiveRetentionPolicyRef` and shall never authorize Folder Autopilot to delete artifact bytes.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-032",
+ "priority": "P1",
+ "requirement": "Desktop shall maintain an output-lineage marker outside user file contents so recipe outputs do not recursively trigger the same lineage unless explicitly permitted.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-033",
+ "priority": "P1",
+ "requirement": "Web shall report the current JRA RecipeVersion assignment, last DSO device heartbeat and DeviceGrant status/revocation projection, watcher health, queue age, recent outcomes, and assignment pause state without making Folder Autopilot authoritative for those external states.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-034",
+ "priority": "P1",
+ "requirement": "An authorized user shall export a redacted execution ledger and evidence manifest without exporting local file contents.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "FA-035",
+ "priority": "P2",
+ "requirement": "JRA recipe templates plus Folder Autopilot profile payloads shall be shareable across workspaces only as unsigned drafts with all DSO grant/binding IDs, secrets, and policies removed.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/features/folder-autopilot.md"
+ }
+ },
+ {
+ "id": "IAE-001",
+ "priority": "P0",
+ "requirement": "Every intake shall create at most one InboxItem and one initial ArtifactVersion for a given workspace and idempotency key.",
+ "source": {
+ "line": 92,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-002",
+ "priority": "P0",
+ "requirement": "Original ArtifactVersion bytes shall be immutable, stored under non-overwritable keys or device handles, and verified with SHA-256 plus byte length.",
+ "source": {
+ "line": 93,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-003",
+ "priority": "P0",
+ "requirement": "Corrections, conversions, redactions, OCR text, thumbnails, and exports shall create new versioned records and shall never mutate or replace an original.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-004",
+ "priority": "P0",
+ "requirement": "`LOCAL` mode shall never upload original bytes or reconstructable derived content, including previews, OCR/transcripts, row/cell values, thumbnails, source snippets, or chunks; only policy-approved metadata and separately confirmed approved derived outputs may synchronize.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-005",
+ "priority": "P0",
+ "requirement": "Every extracted material value, finding, and report assertion shall carry one or more EvidenceReferences or be explicitly marked `UNSUPPORTED_BY_SOURCE`.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-006",
+ "priority": "P0",
+ "requirement": "EvidenceReference coordinates shall be typed, version-bound, validated against media geometry, and resolvable to the exact source version used for processing.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-007",
+ "priority": "P0",
+ "requirement": "Derived outputs shall store source version IDs, processor/recipe versions, and coordinate lineage so evidence survives conversion, normalization, filtering, and aggregation.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-008",
+ "priority": "P0",
+ "requirement": "Object downloads, previews, evidence tiles, and local-render requests shall re-evaluate `IAM` resource authorization at access time and use short-lived single-resource grants.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-009",
+ "priority": "P0",
+ "requirement": "Upload finalization shall verify content digest, actual media signature, size policy, scan state, and tenant ownership before publishing the artifact-created event.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-010",
+ "priority": "P0",
+ "requirement": "Suspected malicious content shall be quarantined, excluded from processing and preview, and visible only to permitted administrators through content-free metadata.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-011",
+ "priority": "P0",
+ "requirement": "Content hashes shall not enable cross-workspace existence queries or cross-tenant deduplication side channels.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-012",
+ "priority": "P0",
+ "requirement": "Source changes detected during processing shall create a new version and mark affected outputs stale; running work shall finish against its pinned source or stop according to recipe policy.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-013",
+ "priority": "P1",
+ "requirement": "Inbox items shall support assignment, labels, priority, due date, and states `NEW`, `ROUTED`, `NEEDS_REVIEW`, `PROCESSING`, `RESOLVED`, `QUARANTINED`, and `ARCHIVED`.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-014",
+ "priority": "P1",
+ "requirement": "Resumable cloud uploads shall resume at verified part boundaries and reject a final digest mismatch without exposing partial objects.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-015",
+ "priority": "P1",
+ "requirement": "Password-protected documents shall retain the original and request credentials locally or through a secret input that is never persisted in logs or artifact metadata.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-016",
+ "priority": "P1",
+ "requirement": "Retention deletion shall use explicit authorization, recent MFA for destructive organization-wide operations, legal-hold checks, tombstones, and verified object erasure.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-017",
+ "priority": "P1",
+ "requirement": "Same-workspace duplicate detection shall preserve separate intake context and shall not merge artifacts with distinct project, supplier, period, or approval history automatically.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-018",
+ "priority": "P1",
+ "requirement": "Export packages shall include a machine-readable manifest of artifact/version hashes, lineage, evidence references, processor versions, and approval state.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-019",
+ "priority": "P0",
+ "requirement": "Resolving evidence whose source is `LOCAL` shall return an open-on-source-device descriptor or `SOURCE_OFFLINE`; another Device receives content only through a verified DSO-025 user-mediated offline package that creates its own `DEVICE_LOCAL` placement or through explicit publication as a governed derived artifact, never through an implicit live-render relay.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-020",
+ "priority": "P0",
+ "requirement": "An ArtifactVersion or DatasetSnapshot shall support zero or more typed content placements across authorized devices and cloud objects; availability shall be derived from verified placements rather than one mutable storage class or locator.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAE-021",
+ "priority": "P0",
+ "requirement": "IAE shall alone determine authoritative deletion eligibility from the Workspace retention minimum, resource/module retention constraints, evidence/report lineage, active approvals, legal holds, AUD retention class, and recovery window; features shall never delete IAE bytes directly, and local cache cleanup shall not represent authoritative retention or deletion.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/foundation/inbox-artifacts-evidence.md"
+ }
+ },
+ {
+ "id": "IAM-001",
+ "priority": "P0",
+ "requirement": "Every user, organization, workspace, project, membership, service account, session, and device shall use a non-guessable stable UUID and store timestamps in UTC.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-002",
+ "priority": "P0",
+ "requirement": "The server shall enforce organization, workspace, project, resource, action, and channel authorization on every request, stream subscription, job execution, sync mutation, download, export, and shared-link access.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-003",
+ "priority": "P0",
+ "requirement": "Authorization shall deny by default and shall not trust role, tenant, or resource claims supplied outside a verified credential and server-side lookup.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-004",
+ "priority": "P0",
+ "requirement": "The initial roles shall be Owner, Admin, Analyst, Operator, Approver, and Viewer; permission constants shall be versioned independently so bundles can expand without renaming roles.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-005",
+ "priority": "P0",
+ "requirement": "Access tokens shall expire within 15 minutes; refresh tokens shall be rotating and single-use, and detected reuse shall revoke the token family.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-006",
+ "priority": "P0",
+ "requirement": "Organization policy shall support required MFA for all members, privileged roles, or privileged actions, with WebAuthn or TOTP and one-time recovery codes.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-007",
+ "priority": "P0",
+ "requirement": "Each organization enrollment on a Desktop or Android installation shall create a distinct asymmetric Device-identity key pair; private keys shall remain in the OS credential store and server records shall be independently and permanently revocable.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-008",
+ "priority": "P0",
+ "requirement": "Membership removal, device revocation, account suspension, and ownership changes shall invalidate server-side and connected-client authorization within 60 seconds and reject newly authenticated operations immediately; an offline device receives no remote-wipe guarantee and its narrowly allowed cached authorization shall expire within 24 hours.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-009",
+ "priority": "P0",
+ "requirement": "Resource lookup shall prove that the resource belongs to the evaluated workspace before a handler reads metadata or object bytes, preventing identifier-based tenant probing.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-010",
+ "priority": "P0",
+ "requirement": "Invitations shall be single-use, hashed at rest, email-bound, scope-bound, role-bound, and expire in no more than seven days.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-011",
+ "priority": "P0",
+ "requirement": "An organization shall always have at least one active Owner; the last Owner cannot leave or be removed without a completed transfer or organization deletion workflow.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-012",
+ "priority": "P0",
+ "requirement": "Privileged actions shall require a step-up MFA assertion no older than 10 minutes and an immutable audit event containing actor, target, before/after summary, IP class, device, and correlation ID.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-013",
+ "priority": "P0",
+ "requirement": "Service accounts shall be organization-owned, workspace-scoped, action-scoped, non-interactive, and authenticated with hashed rotating secrets or signed keys that show their last-use time.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-014",
+ "priority": "P1",
+ "requirement": "Project membership may only narrow workspace access unless an explicit project guest policy grants access to that project alone; it shall never imply access to sibling projects.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-015",
+ "priority": "P1",
+ "requirement": "Account recovery shall revoke all refresh-token families and require MFA re-enrollment confirmation before privileged actions resume.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-016",
+ "priority": "P1",
+ "requirement": "User locale shall default to Vietnamese (`vi-VN`) while allowing English (`en`) per user without changing stored business values or audit semantics.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-017",
+ "priority": "P1",
+ "requirement": "Client applications may use permission hints to hide controls, but all authoritative enforcement shall remain server-side.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-018",
+ "priority": "P1",
+ "requirement": "Bulk membership and policy changes shall use idempotency keys, return per-item outcomes, and never partially apply an ownership transfer.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-019",
+ "priority": "P0",
+ "requirement": "Every tenant-owned record and repository operation shall declare either organization or workspace scope, validate the complete tenant ancestry for nested resources, and reject optional, missing, or mismatched tenant filters before data access.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-020",
+ "priority": "P0",
+ "requirement": "IAM shall issue versioned signed OfflineAuthorizationSnapshots bound to organization/workspace/project, principal, Device, security and authorization epochs, allowed action/resource scopes, policy revisions, issue time, expiry no later than 24 hours, and signer/key version; a snapshot shall not authorize approval, membership, security/data-mode/retention/billing policy change, deletion, cloud/external effects, or access broader than the last online decision, and every reconnect shall re-authorize current state.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "IAM-021",
+ "priority": "P0",
+ "requirement": "IAM shall be the sole authority for DeviceIdentity ID, organization/user ownership, public key, enrollment challenge, activation status, security epoch, and permanent revocation; a revoked identity shall never reactivate, recovery shall create a new identity, and DSO shall reference the IAM identity without maintaining a second identity, key, or authoritative status.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/foundation/identity-workspaces-permissions.md"
+ }
+ },
+ {
+ "id": "ILD-001",
+ "priority": "P0",
+ "requirement": "The system shall create each invoice record from an immutable `IAE` artifact version, retain its content hash, and bind every asynchronous audit to `jraJobId` and a pinned `resultManifestId`.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-002",
+ "priority": "P0",
+ "requirement": "The engine shall extract invoice identifiers, dates, supplier, currency, totals, tax, payment reference, service period, and line items with field evidence.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-003",
+ "priority": "P0",
+ "requirement": "Users shall manage versioned contracts, amendments, POs, receipts/service records, and rate cards without overwriting prior effective versions.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-004",
+ "priority": "P0",
+ "requirement": "Supplier identity shall be a binding to an exact `DSM` BusinessParty `ReferenceEntityVersion`, separate from extracted supplier text; aliases, identifiers, project visibility, and merge history shall remain canonical in `DSM` and shall not be persisted independently by ILD.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-005",
+ "priority": "P0",
+ "requirement": "Candidate matching shall prioritize explicit identifiers and expose feature contributions and disqualifying conflicts.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-006",
+ "priority": "P0",
+ "requirement": "Ambiguous or low-confidence matches shall require review before a consequential variance is confirmed.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-007",
+ "priority": "P0",
+ "requirement": "The system shall support one invoice to many POs/contracts and split invoice lines across governing records with explicit allocations.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-008",
+ "priority": "P0",
+ "requirement": "Expected-charge calculations shall be deterministic, versioned, reproducible, and show every intermediate component.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-009",
+ "priority": "P0",
+ "requirement": "The system shall distinguish missing, zero, not applicable, unknown, estimated, and confirmed values.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-010",
+ "priority": "P0",
+ "requirement": "The engine shall detect exact and near duplicate invoices using identifiers, supplier, dates, amounts, line fingerprints, and artifact hashes.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-011",
+ "priority": "P0",
+ "requirement": "Duplicate `LeakFindingDetail` records shall disclose which signals matched, shall not rely on file name alone, and shall link `sharedFindingId` when actionable.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-012",
+ "priority": "P0",
+ "requirement": "The engine shall support price, quantity, unit, tiered-rate, discount, fee, freight, tax, service-period, and cumulative-cap checks.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-013",
+ "priority": "P0",
+ "requirement": "Unit and currency conversions shall require compatible dimensions and versioned rate provenance.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-014",
+ "priority": "P0",
+ "requirement": "Missing governing terms shall produce an incomplete calculation and a canonical `JRA` `ReviewTask` reference, not an assumed entitlement or module-owned review workflow.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-015",
+ "priority": "P0",
+ "requirement": "Every immutable financial diagnostic detail shall link billed evidence, governing evidence, calculation version, tolerance, variance, stable fingerprint, and `sharedFindingId` when actionable.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-016",
+ "priority": "P0",
+ "requirement": "Estimated exposure, reviewer-validated amount, approved dispute amount, and user-entered recovered amount shall be separate fields.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-017",
+ "priority": "P0",
+ "requirement": "Manual corrections and relationship overrides shall require an actor, reason, and retained prior value.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-018",
+ "priority": "P0",
+ "requirement": "Feature-specific case status changes, package generation, and closure shall enforce configured permissions; materiality or package approval shall use `JRA` with requested action and exact subject type/ID/version/hash, and the module shall persist only `jraApprovalRequestId` plus that binding.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-019",
+ "priority": "P0",
+ "requirement": "Approved evidence packages and closed case versions shall be immutable.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-020",
+ "priority": "P0",
+ "requirement": "The module shall expose no payment-execution, payment-status-changing, banking, or autonomous supplier-contact action.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-021",
+ "priority": "P1",
+ "requirement": "Users shall configure tolerances by supplier, contract, charge type, currency, amount, and percentage with an effective period.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-022",
+ "priority": "P1",
+ "requirement": "The system shall compare invoice totals and quantities across configurable historical windows for duplicate and cap analysis.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-023",
+ "priority": "P1",
+ "requirement": "Users shall assign actionable findings, request information, comment, and set due dates through the canonical `JRA` finding/review facade, while supporting-artifact links remain subject details owned by this module.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-024",
+ "priority": "P1",
+ "requirement": "The system shall support redaction profiles and preview redactions before evidence-package generation.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-025",
+ "priority": "P1",
+ "requirement": "The system shall export PDF, web, XLSX, and JSON case packages with stable evidence identifiers.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-026",
+ "priority": "P1",
+ "requirement": "Dashboards shall separate gross flagged exposure, validated amount, approved dispute amount, and user-confirmed recovery.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-027",
+ "priority": "P1",
+ "requirement": "Recurring approved-folder intake shall deduplicate identical artifacts and link supplier revisions.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "ILD-028",
+ "priority": "P2",
+ "requirement": "Provider-neutral AI may suggest classification, line descriptions, or candidate relationships, but no AI suggestion shall establish a confirmed financial finding or amount.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/features/invoice-leak-detector.md"
+ }
+ },
+ {
+ "id": "INT-001",
+ "priority": "P0",
+ "requirement": "Every public API call shall authenticate an `IAM` user or service account; requested API scopes shall be intersected with current `IAM` permissions and `BUA` entitlements and shall never grant authority independently.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-002",
+ "priority": "P0",
+ "requirement": "Every API, webhook-management, connector, import, export, and replay operation shall resolve organization, workspace, project, and resource ownership server-side and enforce the owning subsystem's current authorization before reading or changing protected state.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-003",
+ "priority": "P0",
+ "requirement": "Public API authentication shall consume the `IAM` service-account and credential contract; `INT` shall persist only credential identifiers and safe request metadata, honor `IAM` overlapping rotation, and stop accepting a credential immediately when `IAM` revokes it.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-004",
+ "priority": "P0",
+ "requirement": "Every public mutation shall accept an idempotency key scoped to principal, tenant, method, and route; concurrent identical retries shall produce one effect and the same outcome, while reuse with a different request hash shall return `409 IDEMPOTENCY_KEY_REUSED`.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-005",
+ "priority": "P0",
+ "requirement": "Unbounded public lists shall use opaque cursor pagination with deterministic ordering; a cursor shall bind filters, projection, tenant scope, authorization epoch, and snapshot watermark and shall be rejected when those bindings no longer match.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-006",
+ "priority": "P0",
+ "requirement": "Public REST routes shall use an explicit major version, and OpenAPI, JSON Schema, webhook payloads, connector manifests, and SDK releases shall identify their contract versions; a breaking change shall require a new major contract, and a supported major shall receive a published successor and at least 12 months' deprecation notice before removal.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-007",
+ "priority": "P0",
+ "requirement": "API admission shall enforce rate and concurrency limits by principal, tenant, route cost class, and abuse source, return `429` with `Retry-After`, rate-limit headers, and a structured `RATE_LIMITED` body containing the limiting scope and reset time, and defer commercial quota and usage authority to `BUA`.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-008",
+ "priority": "P0",
+ "requirement": "Every outbound webhook delivery shall have PostgreSQL-backed durable state, be at-least-once, include event ID, delivery ID, schema version, UTC timestamp, attempt number, and signing-key ID, and carry an HMAC-SHA-256 signature over the timestamp and exact raw body using a versioned endpoint secret.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-009",
+ "priority": "P0",
+ "requirement": "Outbound webhook destinations shall pass creation-time and send-time SSRF, private-network, redirect, DNS-rebinding, and scheme validation; payloads shall contain only documented, permission-safe fields and short-lived retrieval references where content is required.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-010",
+ "priority": "P0",
+ "requirement": "Inbound callbacks for `INT`-managed connectors shall verify the signature over exact raw bytes, accepted key version, bounded timestamp, and provider event identity before parsing; authenticated events shall be recorded in the connector's PostgreSQL-backed durable inbox before success acknowledgement and replays shall be deduplicated or quarantined on hash mismatch. Billing and notification callbacks remain owned by `BUA` and `NCO`.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-011",
+ "priority": "P0",
+ "requirement": "A connector shall access only documented public APIs, published feeds or downloads, customer-authorized databases or storage, or other sources the customer is entitled to use; no production capability or release gate shall depend on scraping, browser/session automation, undocumented endpoints, or restricted partner APIs.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-012",
+ "priority": "P0",
+ "requirement": "Connector adapters shall implement the reviewed transport contract, run with declared network and secret capabilities, and call published `IAE`, `DSM`, `JRA`, `DSO`, `IAM`, and `BUA` application contracts instead of accessing their databases, queues, object namespaces, or policy internals directly.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-013",
+ "priority": "P0",
+ "requirement": "Provider access tokens, refresh tokens, passwords, signing secrets, and client secrets shall be held through encrypted secret references, redacted from logs and payloads, supplied just in time to one connection-scoped adapter, and atomically rotated or revoked without exposing plaintext.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-014",
+ "priority": "P0",
+ "requirement": "Connector and API imports shall create ordinary immutable `IAE` artifact versions for bytes or `DSM` DatasetVersions for governed records, with exact `IAE` snapshot references, source external references, connector/schema/mapping versions, capture time, fingerprints, and lineage; retries shall not duplicate a committed source item.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-015",
+ "priority": "P0",
+ "requirement": "API exports and connector exports or destination pushes shall require separate export and destination permissions, applicable `JRA` approval, current `BUA` admission, and the owning `IAE` artifact or `DSM` governed-data export manifest; they shall never silently broaden the source's data classification, project visibility, or data-mode policy.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-016",
+ "priority": "P0",
+ "requirement": "A pull checkpoint shall advance only after all referenced domain commits and outbox records for that page are durable, and a push checkpoint shall advance only after the provider result is confirmed or reconciled; crash recovery shall replay unadvanced work without duplicate committed business records or external effects.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-017",
+ "priority": "P1",
+ "requirement": "Outbound webhook failures shall expose safe attempt history and retry state, use bounded exponential backoff with jitter for a subscription-configured retry window no longer than 72 hours, retain replayable delivery metadata for at least 30 days, and support an authorized manual replay that does not create a new domain event.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-018",
+ "priority": "P1",
+ "requirement": "Webhook documentation shall promise ordering only for an explicitly named ordering key; delivery and replay shall preserve the original event ID, and consumers shall be able to deduplicate without relying on arrival order.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-019",
+ "priority": "P1",
+ "requirement": "After an ambiguous connector-push timeout or connection loss, the adapter shall reconcile through a documented provider read or idempotency mechanism before retrying, or stop for review when neither exists.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-020",
+ "priority": "P1",
+ "requirement": "Provider scope reduction, credential expiry, authorization revocation, rate limiting, and partial access shall place the connection in an explicit degraded or reauthorization state, preserve prior imported artifacts and datasets, publish a content-minimized state event for `NCO`, and report affected capabilities without repeatedly retrying a permanent denial.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-021",
+ "priority": "P1",
+ "requirement": "Public errors shall use stable machine codes, HTTP status, correlation ID, retryability, safe field details, and a localized message key; errors shall not reveal tenant existence, credentials, provider response bodies, or protected source values.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-022",
+ "priority": "P1",
+ "requirement": "Bulk import and export requests shall execute as bounded `JRA` jobs with validated manifests, per-item outcomes, resumable transfer where supported, cancellation checkpoints, and explicit partial-result status instead of holding a synchronous API request open.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-023",
+ "priority": "P2",
+ "requirement": "Official SDKs shall be generated from the published OpenAPI and event schemas, pin a supported major version, expose idempotency, pagination, rate-limit, and signature-verification helpers, and preserve underlying HTTP errors and correlation IDs.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "INT-024",
+ "priority": "P2",
+ "requirement": "Any future third-party connector program shall require signed versioned manifests, publisher identity, declared capabilities, allowed data classifications, synchronization payload classes, isolated execution, contract and security review, revocation, compatibility fixtures, and removal behavior before code can run for a customer.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/foundation/integrations-api-webhooks.md"
+ }
+ },
+ {
+ "id": "JRA-001",
+ "priority": "P0",
+ "requirement": "Every job and step state transition shall be durably committed in PostgreSQL; Redis Streams shall carry dispatch hints only and shall never be the source of truth.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-002",
+ "priority": "P0",
+ "requirement": "Job creation, ready-step creation, any required BUA quota reservation, canonical AUD AuditEvent append, and delivery/dispatch outbox insertion shall occur through the `ExecutionAdmissionCoordinator` in one database transaction and be idempotent by workspace plus caller key; an outbox record shall never substitute for the AuditEvent.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-003",
+ "priority": "P0",
+ "requirement": "Recipes and published recipe versions shall be immutable, content-hashed, and pinned by every job created from them.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-004",
+ "priority": "P0",
+ "requirement": "Executors shall accept only registered typed actions whose versioned schema, capability requirements, side-effect class, and handler digest match the signed envelope.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-005",
+ "priority": "P0",
+ "requirement": "DataBreeze shall not dispatch arbitrary scripts, shell commands, unrestricted URL navigation, arbitrary filesystem paths, or remote keyboard/mouse control.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-006",
+ "priority": "P0",
+ "requirement": "Every job envelope shall be workspace-bound, job/step/attempt-bound, expiry-bound, nonce-protected, and signed by the control plane; devices shall verify it before input access or execution.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-007",
+ "priority": "P0",
+ "requirement": "A worker or device shall claim a time-bounded lease before execution and use attempt-scoped heartbeats; stale completions from superseded attempts shall be rejected.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-008",
+ "priority": "P0",
+ "requirement": "Retried jobs and steps shall not duplicate artifacts, imports, reports, notifications, exports, external actions, or usage charges; effecting handlers shall use stable idempotency keys and receipts.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-009",
+ "priority": "P0",
+ "requirement": "Actions classified as destructive, external-sharing, platform-billing-provider-effecting, publication, or policy-sensitive shall require an unexpired approval when workspace policy says so.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-010",
+ "priority": "P0",
+ "requirement": "Approval eligibility shall use `IAM` at decision time and support separation of duties that prohibits the requester or executor from approving their own action.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-011",
+ "priority": "P0",
+ "requirement": "An approval shall bind the canonical hash of inputs, proposed effects, recipe version, action version, and policy version; any material change shall invalidate it.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-012",
+ "priority": "P0",
+ "requirement": "Job results shall be immutable manifests containing source ArtifactVersion IDs, output IDs and hashes, evidence coverage, handler/engine versions, attempt, reviewer, and approval state.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-013",
+ "priority": "P0",
+ "requirement": "The scheduler and dispatcher shall reconstruct all ready work from PostgreSQL after Redis loss, process restart, or outbox delay.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-014",
+ "priority": "P1",
+ "requirement": "Job states shall be `CREATED`, `QUEUED`, `WAITING_FOR_DEVICE`, `DISPATCHED`, `RUNNING`, `NEEDS_REVIEW`, `AWAITING_APPROVAL`, `SUCCEEDED`, `PARTIALLY_SUCCEEDED`, `FAILED`, `CANCEL_REQUESTED`, `CANCELLED`, or `EXPIRED`, with a documented transition table enforced by the domain layer.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-015",
+ "priority": "P1",
+ "requirement": "Recipe triggers shall include manual, schedule, artifact-created, folder-event, webhook, and approved API trigger types, each with deduplication and authorization context.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-016",
+ "priority": "P1",
+ "requirement": "Review and approval queues shall support assignee, eligible group, due time, escalation rule, reason, and immutable decision history.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-017",
+ "priority": "P1",
+ "requirement": "Progress events shall be monotonic per job, derive from committed state, and tolerate duplicate or out-of-order transport delivery.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-018",
+ "priority": "P1",
+ "requirement": "Cancellation and compensation shall use registered typed handlers, preserve originals and prior results, and expose partial-effect receipts for manual recovery.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-019",
+ "priority": "P1",
+ "requirement": "AI-assisted actions shall record provider/model/configuration versions and confidence, but deterministic validation and explicit approvals shall remain authoritative where required.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-020",
+ "priority": "P1",
+ "requirement": "A direct typed-action job shall meet the same schema, authorization, data-mode, approval, idempotency, and audit rules as a recipe job.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-021",
+ "priority": "P0",
+ "requirement": "Every executor shall isolate temporary source-derived data by tenant and job/attempt, encrypt it at rest, exclude it from telemetry and backup, enforce declared byte/retention limits, and verify cleanup or quarantine after success, cancellation, rejection, crash, and terminal failure.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-022",
+ "priority": "P0",
+ "requirement": "Every typed action shall declare an action risk class of `READ_ONLY`, `LOW`, `CONSEQUENTIAL`, or `RESTRICTED`; policy shall require online authorization and approval for `RESTRICTED` actions and shall invalidate an existing approval after any material change to its bound subject or effect.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-023",
+ "priority": "P0",
+ "requirement": "Processing workers shall claim leases, obtain inputs, send heartbeats, and commit results only through the authenticated internal worker API and job-bound object grants; they shall receive no PostgreSQL credential or workspace-enumeration capability.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-024",
+ "priority": "P0",
+ "requirement": "Offline Desktop work shall be recorded as a `ProvisionalExecution`, never as a canonical Job; synchronization shall re-authorize and idempotently register an accepted execution as one PostgreSQL Job linked by client execution ID, while rejected work remains locally quarantined and exportable when policy permits.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-025",
+ "priority": "P0",
+ "requirement": "`BILLING_PROVIDER_EFFECT` shall be reserved to the BUA adapter for DataBreeze's own subscription account; no feature, connector, or extension may register customer payment, funds-transfer, withholding, reversal, or settlement behavior without a separately approved product-boundary and safety specification.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-026",
+ "priority": "P0",
+ "requirement": "JRA shall own one canonical actionable `Finding` envelope unique by full applicable TenantScope plus source subsystem, finding type, and fingerprint, including immutable diagnostic-detail reference, severity, workflow state, assignment, evidence references, disposition, and history; DSM and feature modules shall own diagnostic detail and link it by `sharedFindingId` rather than creating competing workflow authority.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-027",
+ "priority": "P0",
+ "requirement": "JRA shall own the canonical `ReviewTask` envelope and state; resolution shall reference a versioned correction or disposition created through the subject-owning module contract, and no review completion shall count as an approval unless a distinct valid JRA ApprovalDecision also exists.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-028",
+ "priority": "P0",
+ "requirement": "JRA shall be the only authority for ApprovalPolicy, ApprovalRequest, and ApprovalDecision; a feature may expose an authorized facade and persist a subject binding/projection containing the JRA request ID, exact resource version, and subject hash, but shall not persist an independent decision or weaken eligibility, separation of duties, MFA, expiry, or invalidation.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-029",
+ "priority": "P0",
+ "requirement": "Every asynchronous feature run shall reference one canonical `jraJobId` and pinned JRA result manifest; JRA alone shall own dispatch, progress, cancellation, retry, and terminal execution state, while feature lifecycle state is an idempotent projection from committed JRA results/events with a documented mapping when states differ.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "JRA-030",
+ "priority": "P0",
+ "requirement": "Offline execution shall accept a cached RecipeVersion only with a supported signed RecipePublicationEnvelope binding workspace, recipe ID/version/hash, action definitions and handler/schema hashes, referenced DSM definition hashes, policy references, issue/offline-validity time, schema version, and signer/key version; cache encryption alone shall not establish authenticity.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/foundation/jobs-recipes-approvals.md"
+ }
+ },
+ {
+ "id": "MR-001",
+ "priority": "P0",
+ "requirement": "The system shall create migration projects scoped to one workspace and optionally one client/project, with owner, `dataModeConstraint`, `effectiveDataModePolicyRef`, locale, time zone, `retentionConstraint`, `effectiveRetentionPolicyRef`, and configurable capacity limits; module constraints shall never broaden workspace policy.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-002",
+ "priority": "P0",
+ "requirement": "The system shall bind every run to immutable source artifact versions, `jraJobId`, and a pinned `resultManifestId` and shall verify each source checksum before processing.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-003",
+ "priority": "P0",
+ "requirement": "The system shall ingest CSV, TSV, XLSX, JSON, JSONL, and Parquet sources with explicit encoding, delimiter, header, sheet, decimal, date, and null parsing settings.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-004",
+ "priority": "P0",
+ "requirement": "Desktop shall register local sources through an explicit file or folder grant and shall not broaden that grant without user action.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-005",
+ "priority": "P0",
+ "requirement": "The system shall profile row counts, column types, null rates, distinct counts, min/max values, length and format distributions, candidate keys, and parse failures.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-006",
+ "priority": "P1",
+ "requirement": "The system shall compare fields and identifiers across sources and expose overlap and conflict statistics without merging records.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-007",
+ "priority": "P0",
+ "requirement": "A migration target shall bind an immutable `DSM` `SchemaVersion` containing field identifiers, display names, types, requiredness, cardinality, constraints, and semantic references; new or imported schema drafts shall become canonical only through `DSM` publication.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-008",
+ "priority": "P0",
+ "requirement": "A migration plan shall bind immutable `DSM` `MappingVersion`, `RuleDefinitionVersion`, and `RuleSetVersion` records for direct, constant, lookup, split, combine, parse, normalize, and allowlisted conditional field transformations.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-009",
+ "priority": "P0",
+ "requirement": "Mapping suggestions shall show component signals and confidence; each suggestion shall remain a migration draft until an authorized user publishes it through `DSM` and explicitly binds the resulting immutable version.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-010",
+ "priority": "P0",
+ "requirement": "Before activating a plan binding, the system shall validate mapping completeness, type compatibility, transformation order, cycles, referenced `DSM` versions, missing lookup dataset versions, and unreachable conditions without republishing those definitions.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-011",
+ "priority": "P0",
+ "requirement": "Cleaning shall operate on derived working records and shall never modify an original artifact or its extracted source rows.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-012",
+ "priority": "P0",
+ "requirement": "The system shall provide deterministic normalization for whitespace, Unicode, case, phone numbers, emails, dates, numbers, identifiers, and configured reference values.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-013",
+ "priority": "P0",
+ "requirement": "Duplicate detection shall bind immutable `DSM` rule versions for blocking and matching, combine them with a versioned migration-specific threshold and survivorship policy, and preserve the contribution of every source record.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-014",
+ "priority": "P0",
+ "requirement": "Ambiguous duplicate clusters shall create module review detail and a canonical `JRA` `ReviewTask` reference with side-by-side field evidence and shall not be auto-merged.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-015",
+ "priority": "P1",
+ "requirement": "The system shall auto-resolve exact duplicate clusters only when a published policy identifies the exact fields, normalization version, and survivorship behavior.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-016",
+ "priority": "P0",
+ "requirement": "A dry-run shall generate record-level output candidates and dispositions without external writes or source mutations.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-017",
+ "priority": "P0",
+ "requirement": "Every rejected, excluded, warning, and merged record shall have one or more stable reason codes and evidence references.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-018",
+ "priority": "P0",
+ "requirement": "Users shall filter module review details and assign, comment on, or bulk-resolve homogeneous exceptions through the canonical `JRA` review facade; bulk actions shall preview the affected count and require confirmation.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-019",
+ "priority": "P1",
+ "requirement": "The system shall support scoped manual overrides that identify the record, field, prior value, replacement value, reason, author, and plan/run applicability.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-020",
+ "priority": "P0",
+ "requirement": "The system shall compare two dry-runs by inputs, plan versions, rule results, dispositions, control totals, and changed output fields.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-021",
+ "priority": "P0",
+ "requirement": "Reconciliation shall prove that each input record is ready, merged into a named survivor, rejected, or explicitly excluded and shall flag unexplained count differences.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-022",
+ "priority": "P0",
+ "requirement": "Reconciliation shall calculate configured numeric control totals before and after transformation, with explicit rounding and tolerance rules.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-023",
+ "priority": "P0",
+ "requirement": "A release policy shall block package generation when mandatory validations fail, unresolved required reviews exist, source versions changed, or reconciliation is outside tolerance.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-024",
+ "priority": "P0",
+ "requirement": "Package release shall require a valid `JRA` `ApprovalDecision` for an `ApprovalRequest` bound to the exact requested action and subject type/ID/version/hash; `JRA` shall enforce that the approver is distinct from the last editor when separation-of-duties policy requires it, and the module shall store only `jraApprovalRequestId` plus the subject binding.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-025",
+ "priority": "P0",
+ "requirement": "An export package shall contain versioned output files, rejected-record files, target schema, plan manifest, source and output checksums, reconciliation report, and machine-readable reason-code summary.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-026",
+ "priority": "P0",
+ "requirement": "The default export formats shall be UTF-8 CSV plus JSON manifest; Parquet and JSONL may be enabled per workspace.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-027",
+ "priority": "P0",
+ "requirement": "The system shall not send a destination write job unless a separately configured adapter, permission, release policy, and valid canonical `JRA` approval for the exact requested action and subject version/hash are all present.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-028",
+ "priority": "P0",
+ "requirement": "Runs, module review details, `JRA` review/approval facade actions, downloads, and package staging shall emit or reference immutable audit events from their owning services.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-029",
+ "priority": "P1",
+ "requirement": "An authorized user shall clone a published plan into a new draft while retaining references to its parent version.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-030",
+ "priority": "P1",
+ "requirement": "The system shall support incremental source batches while preserving batch identity and cumulative reconciliation.",
+ "source": {
+ "line": 160,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-031",
+ "priority": "P1",
+ "requirement": "Desktop and cloud execution of the same plan and fixture shall produce equivalent normalized values, dispositions, reason codes, and control totals.",
+ "source": {
+ "line": 161,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-032",
+ "priority": "P1",
+ "requirement": "Users shall export a human-readable migration book covering sources, definitions, mappings, rules, exceptions, reconciliation, and approvals.",
+ "source": {
+ "line": 162,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "MR-033",
+ "priority": "P2",
+ "requirement": "The system shall permit a workspace to register a signed, declarative destination adapter whose capabilities and idempotency behavior are reviewed independently of the migration plan.",
+ "source": {
+ "line": 163,
+ "path": "docs/specs/features/migration-ready.md"
+ }
+ },
+ {
+ "id": "NCO-001",
+ "priority": "P0",
+ "requirement": "Notifications shall be generated only from committed domain state through a transactional outbox and shall never be the authoritative record of the underlying action.",
+ "source": {
+ "line": 85,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-002",
+ "priority": "P0",
+ "requirement": "Each notification intent shall use a deterministic recipient-scoped deduplication key and category-specific window so retries do not create duplicate alerts.",
+ "source": {
+ "line": 86,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-003",
+ "priority": "P0",
+ "requirement": "Push, email, lock-screen, and Desktop notification payloads shall exclude sensitive source content, including file/client names, extracted values, evidence snippets, amounts, paths, and secrets.",
+ "source": {
+ "line": 87,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-004",
+ "priority": "P0",
+ "requirement": "Notification creation, external delivery, live-stream subscription, deep-link opening, and protected detail read shall each enforce active membership and resource authorization.",
+ "source": {
+ "line": 88,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-005",
+ "priority": "P0",
+ "requirement": "A mention shall not grant access; unauthorized or removed recipients shall receive no notification and shall not resolve the target resource.",
+ "source": {
+ "line": 89,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-006",
+ "priority": "P0",
+ "requirement": "User preferences shall be applied per organization/workspace, category, urgency, and channel, with locale-aware quiet hours and digest schedules.",
+ "source": {
+ "line": 90,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-007",
+ "priority": "P0",
+ "requirement": "Mandatory security, ownership, data-loss-risk, and access-suspension notices may not be disabled, but shall still use content-minimized templates.",
+ "source": {
+ "line": 91,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-008",
+ "priority": "P0",
+ "requirement": "Comments shall be tenant- and resource-scoped, versioned on edit, tombstoned on removal, and recorded with author, timestamps, and audit correlation.",
+ "source": {
+ "line": 92,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-009",
+ "priority": "P0",
+ "requirement": "Evidence-anchored comments shall reference a valid `IAE` EvidenceReference and preserve the referenced ArtifactVersion even when newer versions exist.",
+ "source": {
+ "line": 93,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-010",
+ "priority": "P0",
+ "requirement": "Notification actions such as approve, assign, or retry shall deep-link to the application; no external notification button shall perform the privileged action without authenticated re-authorization and applicable MFA.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-011",
+ "priority": "P0",
+ "requirement": "Membership removal, device revocation, or resource-access loss shall suppress unsent deliveries and make prior deep links return a non-disclosing denial.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-012",
+ "priority": "P1",
+ "requirement": "In-app notification states shall be `UNREAD`, `READ`, `ARCHIVED`, or `DISMISSED`; state changes shall synchronize idempotently per user without changing underlying work state.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-013",
+ "priority": "P1",
+ "requirement": "Threads shall support reply, resolve/reopen, reaction, and assignment reference while preserving immutable event history.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-014",
+ "priority": "P1",
+ "requirement": "Notification bundles shall update an existing notification count and last-occurrence time within the deduplication window instead of emitting one alert per event.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-015",
+ "priority": "P1",
+ "requirement": "Approval/review reminders shall be preference-aware within policy bounds, stop after resolution or invalidation, and be limited to one initial, one due-soon, and one overdue delivery unless policy explicitly escalates.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-016",
+ "priority": "P1",
+ "requirement": "Email and push provider webhooks shall be signature-verified, idempotent, and limited to delivery metadata; they shall not mutate domain decisions.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-017",
+ "priority": "P1",
+ "requirement": "Vietnamese shall be the default notification locale with English fallback; templates shall use stable message keys and sanitized parameters.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-018",
+ "priority": "P1",
+ "requirement": "Workspace administrators shall be able to configure allowed external channels and retention without reading private notification content beyond authorized resources.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-019",
+ "priority": "P1",
+ "requirement": "Offline comment, read-state, and dismissal operations shall use stable operation IDs and explicit conflict rules from `DSO`.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "NCO-020",
+ "priority": "P1",
+ "requirement": "The system shall provide accessible, filterable notification and thread views with pagination and no reliance on color or sound alone.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/foundation/notifications-collaboration.md"
+ }
+ },
+ {
+ "id": "OC-001",
+ "priority": "P0",
+ "requirement": "Web shall create form definitions with stable machine field identifiers independent of labels, order, and translation.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-002",
+ "priority": "P0",
+ "requirement": "Published form versions shall be immutable; edits create a draft child version with a machine-readable compatibility diff.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-003",
+ "priority": "P0",
+ "requirement": "The field catalog shall include text, long text, integer, decimal, currency, date, time, date-time, duration, yes/no, single/multiple choice, identifier, barcode/QR, photo/document, file, voice, signature, and consented location.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-004",
+ "priority": "P0",
+ "requirement": "Form logic shall use allowlisted declarative visibility, requiredness, default, validation, and display-calculation expressions and shall reject arbitrary code.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-005",
+ "priority": "P0",
+ "requirement": "Form validation shall reject cycles, hidden required fields without satisfiable paths, incompatible types, unpinned reference data, prohibited field classes, and resource limits.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-006",
+ "priority": "P0",
+ "requirement": "Designers shall preview Web and representative Android layouts and publish a test assignment before production publication.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-007",
+ "priority": "P0",
+ "requirement": "Assignments shall bind form version, assignee/team, optional roster and record keys, availability window, reference versions, `dataModeConstraint`, `effectiveDataModePolicyRef`, and review policy; the module constraint shall never broaden workspace `DSO` policy.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-008",
+ "priority": "P0",
+ "requirement": "Android shall verify form, assignment, and reference-data checksums before declaring an assignment ready offline.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-009",
+ "priority": "P0",
+ "requirement": "Android shall store drafts, responses, media keys, sync journal, and cached definitions in encrypted app-private storage.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-010",
+ "priority": "P0",
+ "requirement": "Every field change shall autosave locally with record ID, field ID, device revision, actor, device time, and monotonic capture sequence.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-011",
+ "priority": "P0",
+ "requirement": "Camera, microphone, file, and location access shall require Android permission plus a visible in-app user action for each capture session.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-012",
+ "priority": "P0",
+ "requirement": "Voice and video/audio capture shall show an unambiguous active-state indicator and stop control and shall obey configurable duration and size limits.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-013",
+ "priority": "P0",
+ "requirement": "Barcode/QR capture shall retain decoded value, symbology, scan time, validation state, and optional image evidence when policy permits.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-014",
+ "priority": "P0",
+ "requirement": "A drawn signature shall retain the immutable stroke/render artifact, signer-entered label, acknowledgement text/version, device timestamp, and evidence metadata without claiming identity verification.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-015",
+ "priority": "P0",
+ "requirement": "Optional location capture shall disclose purpose, capture only on explicit action, record accuracy and provider state, and permit policy-defined unavailable handling.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-016",
+ "priority": "P0",
+ "requirement": "On-device validation shall support requiredness, type, length, range, pattern, reference membership, uniqueness within the draft, and cross-field rules using the published definition.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-017",
+ "priority": "P0",
+ "requirement": "Submission shall be blocked by mandatory validation failures and shall summarize warnings and missing evidence before confirmation.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-018",
+ "priority": "P0",
+ "requirement": "Submitting shall freeze an immutable local submission version and generate a stable idempotency key before network transfer.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-019",
+ "priority": "P0",
+ "requirement": "Sync retries shall create exactly one server submission version for one device submission idempotency key.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-020",
+ "priority": "P0",
+ "requirement": "Media upload shall be resumable, chunk/checksum verified, and associated only after complete object verification.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-021",
+ "priority": "P0",
+ "requirement": "Server acknowledgement shall include durable submission/version IDs and per-attachment status before Android marks local content safely synchronized.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-022",
+ "priority": "P0",
+ "requirement": "Original photos, documents, voice, barcode image evidence, and signature artifacts shall be immutable; processing creates derived artifacts and candidate values.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-023",
+ "priority": "P0",
+ "requirement": "OCR or transcription candidates shall retain adapter/version, language, confidence, and page/region or time-segment evidence.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-024",
+ "priority": "P0",
+ "requirement": "Extracted candidates shall never silently overwrite operator-entered or submitted values; acceptance creates a correction/version event.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-025",
+ "priority": "P0",
+ "requirement": "Review policy shall route validation warnings, low-confidence extraction, duplicates, reconciliation exceptions, and configured sensitive submissions to authorized queues.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-026",
+ "priority": "P0",
+ "requirement": "Reviewers shall use an authorized facade over a canonical `JRA` `ReviewTask` to mark a capture review detail accepted, rejected, or returned, add comments, or create a correction draft with stable reason codes and field-level evidence; the module shall store `jraReviewTaskId` and a projection only, and review acceptance shall not satisfy or bypass a separate `JRA` approval.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-027",
+ "priority": "P0",
+ "requirement": "Changes to an acknowledged submission shall create a new submission version linked to its parent and shall retain a field-level diff.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-028",
+ "priority": "P0",
+ "requirement": "Form-version breaking changes shall not alter existing drafts; a user shall finish on the pinned version or explicitly migrate through a validated preview.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-029",
+ "priority": "P0",
+ "requirement": "Desktop scanner intake shall operate only on explicitly granted folders, wait for stable files, fingerprint inputs, and avoid duplicate imports.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-030",
+ "priority": "P0",
+ "requirement": "Desktop matching shall prioritize deterministic assignment, record, cover-sheet, barcode, and filename keys before confidence-based suggestions.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-031",
+ "priority": "P0",
+ "requirement": "Ambiguous attachment matches and duplicate submissions shall require review and preserve every candidate and decision.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-032",
+ "priority": "P0",
+ "requirement": "Reconciliation shall report expected, received, approved, missing, duplicate, rejected, returned, and waived records with no unexplained submissions.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-033",
+ "priority": "P0",
+ "requirement": "All form publication, assignment, capture, submission, sync, extraction, review, correction, approval-facade, export, and evidence access actions shall be audited by their owning services.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-034",
+ "priority": "P1",
+ "requirement": "Supervisors shall configure bounded bulk assignment, reassignment, return, approval-facade, and export actions with preview and permission checks; every approval item shall retain requested action, exact subject type/ID/version/hash, and `jraApprovalRequestId`.",
+ "source": {
+ "line": 160,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-035",
+ "priority": "P1",
+ "requirement": "Forms shall support repeatable groups with stable item IDs and configurable minimum/maximum occurrences.",
+ "source": {
+ "line": 161,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-036",
+ "priority": "P1",
+ "requirement": "Forms shall support offline reference-data search and dependent choice lists with pinned versions and explicit stale behavior.",
+ "source": {
+ "line": 162,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-037",
+ "priority": "P1",
+ "requirement": "Submissions backed by any required valid `JRA` `ApprovalDecision` for the exact requested action and subject type/ID/version/hash shall be exportable as UTF-8 CSV/JSON plus media/evidence manifest and available to other DataBreeze modules through typed intake.",
+ "source": {
+ "line": 163,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-038",
+ "priority": "P1",
+ "requirement": "Workspace admins shall configure per-form media quality, `retentionConstraint`, offline capacity, sync network, battery, roaming, and local-cache cleanup; `effectiveRetentionPolicyRef` and authoritative deletion shall remain owned by `IAE`.",
+ "source": {
+ "line": 164,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-039",
+ "priority": "P1",
+ "requirement": "Android shall display storage consumption, unsynchronized item count, oldest pending age, last successful sync, and actionable failure reasons.",
+ "source": {
+ "line": 165,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-040",
+ "priority": "P1",
+ "requirement": "Local Desktop and cloud processing of the same media fixture shall produce equivalent deterministic parsing and evidence coordinates within declared adapter tolerances.",
+ "source": {
+ "line": 166,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "OC-041",
+ "priority": "P2",
+ "requirement": "The system shall permit a form package to be imported/exported between workspaces only as an unsigned draft with assignments, responses, secrets, and restricted reference values removed.",
+ "source": {
+ "line": 167,
+ "path": "docs/specs/features/operations-capture.md"
+ }
+ },
+ {
+ "id": "PDA-001",
+ "priority": "P0",
+ "requirement": "The system shall create an analysis-dataset binding to an existing `DSM` `Dataset` and exact immutable `DatasetVersion` records, then record module-specific locale, time zone, sensitivity projection, quality status, `dataModeConstraint`, `effectiveDataModePolicyRef`, `retentionConstraint`, and `effectiveRetentionPolicyRef` without registering a parallel dataset identity or broadening workspace policy.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-002",
+ "priority": "P0",
+ "requirement": "An analysis-semantic binding shall reference exact immutable `DSM` schema, semantic, metric, relationship, calendar, and dataset versions and shall expose only the projections and analysis policies needed for entities, fields, dimensions, filters, aliases, and evidence.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-003",
+ "priority": "P0",
+ "requirement": "Activated analysis-semantic binding versions shall be immutable; a referenced `DSM` definition change shall require a new binding version with a machine-readable compatibility diff rather than republishing the definition.",
+ "source": {
+ "line": 127,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-004",
+ "priority": "P0",
+ "requirement": "Every bound `DSM` `MetricDefinitionVersion` shall declare formula, source fields, base grain, aggregation behavior, unit/currency, null handling, rounding, default filters, allowed dimensions, and description before analysis use.",
+ "source": {
+ "line": 128,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-005",
+ "priority": "P0",
+ "requirement": "A relationship shall declare keys, cardinality, direction, optionality, effective-time behavior, and fan-out policy.",
+ "source": {
+ "line": 129,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-006",
+ "priority": "P0",
+ "requirement": "Analysis-binding validation shall reject missing or incompatible `DSM` versions, cycles, missing keys, ambiguous joins, incompatible units, non-additive aggregation misuse, unknown functions, and unbounded definitions without creating a parallel semantic publisher.",
+ "source": {
+ "line": 130,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-007",
+ "priority": "P0",
+ "requirement": "Users shall ask Vietnamese or English text questions within a selected authorized governed-data scope.",
+ "source": {
+ "line": 131,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-008",
+ "priority": "P0",
+ "requirement": "Android voice questions shall display a transcript for user confirmation before any plan is executed.",
+ "source": {
+ "line": 132,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-009",
+ "priority": "P0",
+ "requirement": "The planner shall represent analysis as a versioned typed intermediate plan, not executable free-form code.",
+ "source": {
+ "line": 133,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-010",
+ "priority": "P0",
+ "requirement": "The typed plan catalog shall support projection, filter, governed join, group, aggregate, sort, top/bottom, period comparison, share-of-total, bounded cohort, and allowlisted statistical operations.",
+ "source": {
+ "line": 134,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-011",
+ "priority": "P0",
+ "requirement": "When a question has multiple material interpretations, the system shall request clarification or present named alternatives and shall not silently select one.",
+ "source": {
+ "line": 135,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-012",
+ "priority": "P0",
+ "requirement": "Before execution, the system shall show selected `DSM` metric/semantic versions, dimensions, filters, date range, time grain, dataset versions or selectors, and material assumptions.",
+ "source": {
+ "line": 136,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-013",
+ "priority": "P0",
+ "requirement": "Plan validation shall enforce permissions, semantic types, join cardinality, grain, units, filter scope, output bounds, resource limits, and quality gates.",
+ "source": {
+ "line": 137,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-014",
+ "priority": "P0",
+ "requirement": "The deterministic engine shall calculate all displayed numeric results; an AI adapter shall not supply or alter numeric result values.",
+ "source": {
+ "line": 138,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-015",
+ "priority": "P0",
+ "requirement": "Every result shall bind exact `DSM` dataset and semantic/metric versions, analysis-semantic binding version, plan version, engine version, execution time, locale, time zone, `jraJobId`, and pinned `resultManifestId`.",
+ "source": {
+ "line": 139,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-016",
+ "priority": "P0",
+ "requirement": "Every table or chart value shall expose calculation provenance and, when permitted, drill-down evidence to contributing source rows/cells or an exact aggregate definition.",
+ "source": {
+ "line": 140,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-017",
+ "priority": "P0",
+ "requirement": "Answers shall disclose coverage period, source freshness, applied filters, exclusions, units, quality warnings, and whether results are complete, sampled, or truncated.",
+ "source": {
+ "line": 141,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-018",
+ "priority": "P0",
+ "requirement": "If authorized data cannot answer the question, the system shall return `INSUFFICIENT_DATA`, `AMBIGUOUS`, `UNAUTHORIZED_SCOPE`, or another stable non-answer reason rather than inventing a result.",
+ "source": {
+ "line": 142,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-019",
+ "priority": "P0",
+ "requirement": "Optional AI narrative shall be generated only from a bounded structured result and provenance package and shall label unsupported requested claims as unavailable.",
+ "source": {
+ "line": 143,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-020",
+ "priority": "P0",
+ "requirement": "Each material numeric narrative claim shall link to one or more result cells; qualitative claims shall link to result/evidence or be labeled as interpretation.",
+ "source": {
+ "line": 144,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-021",
+ "priority": "P0",
+ "requirement": "Users shall inspect and edit a typed plan through governed fields and operations before rerunning; arbitrary SQL or code execution shall not be exposed.",
+ "source": {
+ "line": 145,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-022",
+ "priority": "P0",
+ "requirement": "The system shall render accessible tables and allowlisted bar, line, area, scatter, and pie/donut charts only when the selected fields and grain are compatible.",
+ "source": {
+ "line": 146,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-023",
+ "priority": "P0",
+ "requirement": "Saved analysis versions shall retain question, plan, analysis-semantic binding and underlying `DSM` version references, parameter schema, display, result policy, owner, and parent version.",
+ "source": {
+ "line": 147,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-024",
+ "priority": "P0",
+ "requirement": "A saved analysis shall define whether readers see a frozen snapshot or execute a permission-checked rerun; it shall not silently switch behavior.",
+ "source": {
+ "line": 148,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-025",
+ "priority": "P0",
+ "requirement": "Sharing an analysis shall not grant underlying dataset permissions, raw-evidence access, or broader row/field visibility.",
+ "source": {
+ "line": 149,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-026",
+ "priority": "P0",
+ "requirement": "Certified analyses shall display the exact certified subject version, certification scope, data freshness policy, expiry/invalidation conditions, and permission-filtered canonical `JRA` approval projection; the module shall retain requested action, exact subject type/ID/version/hash, and `jraApprovalRequestId` but no independent certifier decision.",
+ "source": {
+ "line": 150,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-027",
+ "priority": "P0",
+ "requirement": "Breaking changes in referenced `DSM` definitions, failed required quality rules, expired certification, or unavailable input versions shall block a certified rerun until a new compatible binding is reviewed.",
+ "source": {
+ "line": 151,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-028",
+ "priority": "P0",
+ "requirement": "Schedules shall bind a saved analysis version, parameters, input selector, trigger, freshness policy, recipients, and idempotent occurrence key.",
+ "source": {
+ "line": 152,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-029",
+ "priority": "P0",
+ "requirement": "Scheduled answers shall not notify success when input freshness or required quality gates fail; they shall create a visible blocked occurrence.",
+ "source": {
+ "line": 153,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-030",
+ "priority": "P0",
+ "requirement": "All questions, plans, executions, analysis-binding activations, referenced `DSM` publications, certifications, shares, exports, and evidence access shall be audited by the owning services according to workspace policy.",
+ "source": {
+ "line": 154,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-031",
+ "priority": "P1",
+ "requirement": "Analysts shall compare two compatible result snapshots and show absolute, percentage, and contribution changes using declared zero and null behavior.",
+ "source": {
+ "line": 155,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-032",
+ "priority": "P1",
+ "requirement": "Users shall create reusable parameter controls with type, allowed values/range, default, sensitivity, and permission-aware options.",
+ "source": {
+ "line": 156,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-033",
+ "priority": "P1",
+ "requirement": "Result snapshots shall be embeddable in DataBreeze reports with immutable provenance and refresh policy.",
+ "source": {
+ "line": 157,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-034",
+ "priority": "P1",
+ "requirement": "Desktop shall offer a provider-neutral local AI adapter when installed and capable; deterministic plan execution shall work without any AI adapter.",
+ "source": {
+ "line": 158,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-035",
+ "priority": "P1",
+ "requirement": "Workspace admins shall configure which metadata, samples, result rows, and evidence may be sent to each approved AI adapter, with a previewable egress policy.",
+ "source": {
+ "line": 159,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-036",
+ "priority": "P1",
+ "requirement": "A user shall export result data, chart specification, permission-filtered projections of referenced `DSM` semantic definitions, and a provenance manifest in open formats subject to permissions.",
+ "source": {
+ "line": 160,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-037",
+ "priority": "P1",
+ "requirement": "Local and cloud execution of the same typed plan and fixture shall produce equivalent result values, row counts, units, reason codes, and evidence keys.",
+ "source": {
+ "line": 161,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "PDA-038",
+ "priority": "P2",
+ "requirement": "The system may recommend related certified analyses or follow-up questions using authorized metadata, but recommendations shall never imply a result before execution.",
+ "source": {
+ "line": 162,
+ "path": "docs/specs/features/private-data-analyst.md"
+ }
+ },
+ {
+ "id": "QI-001",
+ "priority": "P0",
+ "requirement": "The system shall create a comparison under one workspace and project with a unique, tenant-scoped identifier.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-002",
+ "priority": "P0",
+ "requirement": "The system shall version RFQ requirements and preserve previously used versions.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-003",
+ "priority": "P0",
+ "requirement": "The system shall ingest PDF, common image formats, DOCX, XLSX, and CSV artifacts without modifying originals. An Android-held `ORIGINAL_CONTENT` source that cannot be processed on Android in strict Local mode shall remain on that device with its IAE InboxItem in `NEEDS_REVIEW` and reason `LOCAL_PROCESSOR_REQUIRED` until an explicit `DSO` user-mediated encrypted offline package is exported and imported on a registered Desktop; cloud upload and live relay are forbidden.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-004",
+ "priority": "P0",
+ "requirement": "The system shall retain the supplier name extracted from each quote separately and bind the quote to an exact authorized DSM `BUSINESS_PARTY` ReferenceEntityVersion; QI shall not own supplier aliases, identifiers, visibility, or merge history.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-005",
+ "priority": "P0",
+ "requirement": "The engine shall extract raw and normalized header terms and line items with field-level evidence references.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-006",
+ "priority": "P0",
+ "requirement": "The system shall support Vietnamese and English labels, decimal conventions, dates, currencies, and unit aliases.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-007",
+ "priority": "P0",
+ "requirement": "The system shall normalize units only through a versioned compatible-dimension conversion rule.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-008",
+ "priority": "P0",
+ "requirement": "The system shall support exact, tolerance-based, many-to-one, one-to-many, partial, alternate, and unmatched line states.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-009",
+ "priority": "P0",
+ "requirement": "A user shall be able to confirm, reject, split, merge, or remap a proposed line match without losing the proposal history.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-010",
+ "priority": "P0",
+ "requirement": "The system shall calculate landed cost from a versioned formula and expose every intermediate component.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-011",
+ "priority": "P0",
+ "requirement": "Required but absent cost inputs shall produce an incomplete result, not an assumed zero.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-012",
+ "priority": "P0",
+ "requirement": "Currency conversion shall store source currency, target currency, rate, effective date, provenance, and rounding policy.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-013",
+ "priority": "P0",
+ "requirement": "The system shall distinguish tax-inclusive, tax-exclusive, exempt, unknown, and not-applicable states.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-014",
+ "priority": "P0",
+ "requirement": "Scoring shall use workspace-configurable weights totaling 100%, deterministic normalization functions, and mandatory eligibility gates.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-015",
+ "priority": "P0",
+ "requirement": "The score view shall show raw value, normalized value, weight, contribution, gate result, and policy version.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-016",
+ "priority": "P0",
+ "requirement": "Manual values and overrides shall require a reason and actor and shall remain visually distinct from extracted values.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-017",
+ "priority": "P0",
+ "requirement": "Consequential compliance, cost, and eligibility findings shall be rule-derived and reproducible without an AI provider.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-018",
+ "priority": "P0",
+ "requirement": "Submission to a `JRA` ApprovalRequest shall be blocked while required fields, invalid conversions, or unresolved blocking findings remain.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-019",
+ "priority": "P0",
+ "requirement": "A version released by an accepted `JRA` ApprovalDecision shall be immutable and shall retain source, rule, rate, reviewer, exact subject type/ID/version/hash, and authoritative approval provenance.",
+ "source": {
+ "line": 117,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-020",
+ "priority": "P0",
+ "requirement": "All read, edit, export, submit, and approve-facade operations shall enforce workspace and project permissions plus the applicable `JRA` ApprovalPolicy; Quote Intelligence shall not persist an independent ApprovalDecision.",
+ "source": {
+ "line": 118,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-021",
+ "priority": "P1",
+ "requirement": "Users shall be able to compare multiple landed-cost scenarios without duplicating source artifacts.",
+ "source": {
+ "line": 119,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-022",
+ "priority": "P1",
+ "requirement": "The system shall detect duplicate quote artifacts and superseded supplier revisions while allowing an explicit override.",
+ "source": {
+ "line": 120,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-023",
+ "priority": "P1",
+ "requirement": "The system shall support configurable tolerances for quantity, price, delivery, and specification comparisons.",
+ "source": {
+ "line": 121,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-024",
+ "priority": "P1",
+ "requirement": "Users shall be able to assign extraction or matching questions and receive notifications.",
+ "source": {
+ "line": 122,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-025",
+ "priority": "P1",
+ "requirement": "The system shall export XLSX, PDF, and web decision packs with evidence identifiers and version metadata.",
+ "source": {
+ "line": 123,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-026",
+ "priority": "P1",
+ "requirement": "The system shall expose comparison history for a supplier without leaking data across projects lacking access.",
+ "source": {
+ "line": 124,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-027",
+ "priority": "P1",
+ "requirement": "A copied comparison shall reference its source template but shall receive new independent versions and, when required, new `JRA` ApprovalRequests.",
+ "source": {
+ "line": 125,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "QI-028",
+ "priority": "P2",
+ "requirement": "An AI adapter may suggest semantic matches or plain-language explanations, but suggestions shall require confirmation and never change deterministic calculations.",
+ "source": {
+ "line": 126,
+ "path": "docs/specs/features/quote-intelligence.md"
+ }
+ },
+ {
+ "id": "SA-001",
+ "priority": "P0",
+ "requirement": "The system shall create each audit against an immutable `IAE` artifact version and recorded content hash and shall bind its asynchronous execution to `jraJobId` and a pinned `resultManifestId`.",
+ "source": {
+ "line": 84,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-002",
+ "priority": "P0",
+ "requirement": "The system shall inspect supported workbooks without executing macros, add-ins, queries, external links, or embedded scripts.",
+ "source": {
+ "line": 85,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-003",
+ "priority": "P0",
+ "requirement": "The audit shall inventory sheets, visibility, dimensions, tables, named ranges, formulas, validations, merged cells, links, macros, and calculation settings.",
+ "source": {
+ "line": 86,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-004",
+ "priority": "P0",
+ "requirement": "The parser shall preserve formula text, cached value when present, style identity, cell type, and sheet/cell evidence.",
+ "source": {
+ "line": 87,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-005",
+ "priority": "P0",
+ "requirement": "The engine shall detect parse errors, broken references, incompatible ranges, and unsupported formulas without inventing calculated values.",
+ "source": {
+ "line": 88,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-006",
+ "priority": "P0",
+ "requirement": "The engine shall group structurally equivalent formulas into formula families using relative-reference normalization.",
+ "source": {
+ "line": 89,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-007",
+ "priority": "P0",
+ "requirement": "The engine shall identify formula-family outliers, formula-to-constant overwrites, gaps, and inconsistent range boundaries.",
+ "source": {
+ "line": 90,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-008",
+ "priority": "P0",
+ "requirement": "Audit profiles shall bind immutable `DSM` `RuleDefinitionVersion` records, compatible registered `AuditRuleVersion` engine/plugin implementations, severities, parameters, scope selectors, and suppression policy.",
+ "source": {
+ "line": 91,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-009",
+ "priority": "P0",
+ "requirement": "Every `SpreadsheetFindingDetail` shall be immutable and include the bound `DSM` rule version, diagnostic severity/classification, evidence, affected scope, deterministic reproduction data, stable fingerprint, and `sharedFindingId` when actionable.",
+ "source": {
+ "line": 92,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-010",
+ "priority": "P0",
+ "requirement": "The system shall distinguish confirmed rule violations, heuristic warnings, unsupported checks, and informational observations.",
+ "source": {
+ "line": 93,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-011",
+ "priority": "P0",
+ "requirement": "Users shall be able to assign, comment on, resolve, reopen, or suppress actionable findings through an authorized facade over the canonical `JRA` `Finding` or `ReviewTask`, subject to permission and revision checks.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-012",
+ "priority": "P0",
+ "requirement": "A suppression shall require scope, reason, actor, creation time, and optional expiry; broad suppressions require elevated permission.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-013",
+ "priority": "P0",
+ "requirement": "Repair proposals shall state exact preconditions and exact cell or workbook-part changes.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-014",
+ "priority": "P0",
+ "requirement": "Repairs shall be previewed as a before/after diff and validated on an isolated copy before application.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-015",
+ "priority": "P0",
+ "requirement": "The system shall never mutate an original artifact or overwrite a user file in place.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-016",
+ "priority": "P0",
+ "requirement": "Applying a repair shall create a new artifact version or separately named export with source lineage.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-017",
+ "priority": "P0",
+ "requirement": "A repair plan shall be rejected as stale when its source hash or required preconditions differ.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-018",
+ "priority": "P0",
+ "requirement": "Diagnostic details, `JRA` finding/review facades, and repair actions shall enforce tenant, project, artifact, and capability permissions.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-019",
+ "priority": "P0",
+ "requirement": "Audit sign-off or repair approval shall use a canonical `JRA` `ApprovalRequest` whose exact subject type, ID, version, hash, and requested action bind the audit or repair plan; the module shall store only `jraApprovalRequestId` and that subject binding, and shall not persist an independent actor decision.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-020",
+ "priority": "P0",
+ "requirement": "Reports shall disclose skipped sheets, unsupported features, truncated analysis, and calculation limitations.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-021",
+ "priority": "P1",
+ "requirement": "The engine shall support configurable data rules for uniqueness, nulls, type, format, range, membership, pattern, and cross-column conditions.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-022",
+ "priority": "P1",
+ "requirement": "The engine shall support deterministic reconciliation rules across cells, ranges, sheets, and imported reference datasets.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-023",
+ "priority": "P1",
+ "requirement": "Users shall be able to compare immutable diagnostic-detail changes between two audit runs of the same logical workbook.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-024",
+ "priority": "P1",
+ "requirement": "Desktop shall navigate to a finding in a safe read-only workbook view or launch the user's spreadsheet application at best-effort sheet/cell location.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-025",
+ "priority": "P1",
+ "requirement": "Approved folders shall support debounced recurring audits with hash deduplication and per-folder policy.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-026",
+ "priority": "P1",
+ "requirement": "The system shall export HTML, PDF, JSON, and XLSX finding reports with evidence identifiers.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-027",
+ "priority": "P1",
+ "requirement": "Repair plans shall support selective acceptance and shall recompute plan validity after each selection change.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "SA-028",
+ "priority": "P2",
+ "requirement": "An AI adapter may cluster or explain findings and suggest human-readable rule descriptions, but it shall not create or apply an executable repair without deterministic validation.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/features/spreadsheet-auditor.md"
+ }
+ },
+ {
+ "id": "WEB-001",
+ "priority": "P0",
+ "requirement": "Web shall provide the complete management surface for organizations, workspaces, projects/clients, members, roles, policies, devices, billing, usage, and audit history.",
+ "source": {
+ "line": 94,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-002",
+ "priority": "P0",
+ "requirement": "Every route, query, mutation, download, SSE subscription, and action shall rely on server-side `IAM` authorization; client permission hints shall affect presentation only.",
+ "source": {
+ "line": 95,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-003",
+ "priority": "P0",
+ "requirement": "The application shall use generated OpenAPI types plus runtime validation and shall reject an incompatible control-plane contract before mutating state.",
+ "source": {
+ "line": 96,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-004",
+ "priority": "P0",
+ "requirement": "Browser refresh credentials shall remain in `HttpOnly`, `Secure`, `SameSite` cookies; long-lived bearer tokens, device secrets, and provider secrets shall not be stored in localStorage or JavaScript-readable persistence.",
+ "source": {
+ "line": 97,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-005",
+ "priority": "P0",
+ "requirement": "Original upload controls shall follow the server data-mode decision; a `LOCAL` workspace shall never send original bytes or reconstructable derived content such as previews, OCR/transcripts, row/cell values, thumbnails, source snippets, or chunks from Web.",
+ "source": {
+ "line": 98,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-006",
+ "priority": "P0",
+ "requirement": "Artifact originals shall be presented as immutable versions; corrections, transformations, redactions, and publications shall create explicit new versions.",
+ "source": {
+ "line": 99,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-007",
+ "priority": "P0",
+ "requirement": "Every material extraction, finding, and report value shall expose its evidence state and navigate to an exact authorized `IAE` EvidenceReference or an explicit resolution error.",
+ "source": {
+ "line": 100,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-008",
+ "priority": "P0",
+ "requirement": "Job and recipe controls shall create only registered typed actions through `JRA`; Web shall expose no arbitrary script, shell, filesystem, or remote-control input.",
+ "source": {
+ "line": 101,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-009",
+ "priority": "P0",
+ "requirement": "Approval decisions shall display the bound input/effect hash summary, policy, expiry, and evidence, and shall be re-authorized with MFA when required at submission time.",
+ "source": {
+ "line": 102,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-010",
+ "priority": "P0",
+ "requirement": "Live progress and notifications shall be derived from committed events, tolerate duplicate/out-of-order delivery, and reconcile from REST after reconnect or event-ID gap.",
+ "source": {
+ "line": 103,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-011",
+ "priority": "P0",
+ "requirement": "Billing entitlements, limits, grace, and suspension shall be enforced by the control plane; Web shall render stable denial/remediation states and preserve read/export/delete-request access.",
+ "source": {
+ "line": 104,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-012",
+ "priority": "P0",
+ "requirement": "Destructive actions shall show the exact scope and consequences, require explicit confirmation and recent MFA where specified, and never conflate billing cancellation with data deletion.",
+ "source": {
+ "line": 105,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-013",
+ "priority": "P1",
+ "requirement": "Vietnamese shall be the default complete locale, including dates, numbers, pluralization, validation, notification templates, and accessible names; English shall be a complete fallback.",
+ "source": {
+ "line": 106,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-014",
+ "priority": "P1",
+ "requirement": "Core workflows shall meet WCAG 2.2 AA, support keyboard-only use and screen readers, preserve focus across dialogs/routes, and not rely on color, pointer hover, sound, or motion alone.",
+ "source": {
+ "line": 107,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-015",
+ "priority": "P1",
+ "requirement": "All list and activity views shall use cursor pagination, stable filters encoded in the URL where safe, explicit empty/error states, and virtualized rendering for large result sets.",
+ "source": {
+ "line": 108,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-016",
+ "priority": "P1",
+ "requirement": "Mutations shall use idempotency keys and revision preconditions, show pending state, prevent accidental duplicate submit, and reconcile ambiguous network outcomes.",
+ "source": {
+ "line": 109,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-017",
+ "priority": "P1",
+ "requirement": "The recipe builder shall validate schemas, graph structure, capabilities, data mode, approval requirements, and entitlements before allowing publication.",
+ "source": {
+ "line": 110,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-018",
+ "priority": "P1",
+ "requirement": "Device management shall expose opaque capabilities, status, grants, revocation, conflicts, and data-mode migrations without revealing local paths or enabling filesystem browsing.",
+ "source": {
+ "line": 111,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-019",
+ "priority": "P1",
+ "requirement": "External notifications and shared links shall remain content-minimized; protected details shall load only after authenticated authorization.",
+ "source": {
+ "line": 112,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-020",
+ "priority": "P1",
+ "requirement": "The browser shall retain only content-minimized cached server state by default; source previews and original bytes shall not be available offline unless an explicit encrypted download/export completes.",
+ "source": {
+ "line": 113,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-021",
+ "priority": "P1",
+ "requirement": "Every error shall map to a stable problem code with a Vietnamese user message, correlation ID, safe retry guidance, and no stack trace, tenant existence leak, or source content.",
+ "source": {
+ "line": 114,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-022",
+ "priority": "P1",
+ "requirement": "Feature modules shall register routes, navigation, permissions, message keys, schemas, and telemetry at build time; arbitrary runtime third-party code shall not execute in the application origin.",
+ "source": {
+ "line": 115,
+ "path": "docs/specs/platforms/web.md"
+ }
+ },
+ {
+ "id": "WEB-023",
+ "priority": "P0",
+ "requirement": "For `LOCAL` evidence, Web shall show an open-on-source-device action and explicit device availability; it shall not request or display a live source-derived relay unless the user first publishes a governed Hybrid/Cloud derivative.",
+ "source": {
+ "line": 116,
+ "path": "docs/specs/platforms/web.md"
+ }
+ }
+ ],
+ "version": 1
+}
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 00000000..d1aa84ad
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,36 @@
+import eslint from '@eslint/js';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ {
+ ignores: [
+ '**/build/**',
+ '**/coverage/**',
+ '**/dist/**',
+ '**/node_modules/**',
+ '**/out/**',
+ 'packages/contracts/generated/**',
+ 'tools/repo-cli/test/fixtures/**',
+ ],
+ },
+ eslint.configs.recommended,
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [tseslint.configs.recommendedTypeChecked],
+ languageOptions: {
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+ {
+ files: ['**/*.{cjs,js,mjs}'],
+ languageOptions: {
+ globals: {
+ console: 'readonly',
+ process: 'readonly',
+ },
+ },
+ },
+);
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..9353d58f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "@databreeze/platform",
+ "version": "0.0.0",
+ "private": true,
+ "packageManager": "pnpm@11.18.0",
+ "engines": {
+ "node": "24.17.0",
+ "pnpm": "11.18.0"
+ },
+ "devEngines": {
+ "runtime": {
+ "name": "node",
+ "version": "24.17.0",
+ "onFail": "download"
+ }
+ },
+ "scripts": {
+ "build": "turbo run build",
+ "contracts:check": "corepack pnpm --filter @databreeze/contracts contract:check",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "lint": "eslint . && node tools/repo-cli/src/check-dependency-boundaries.mjs",
+ "repo:bootstrap": "corepack pnpm install --frozen-lockfile",
+ "repo:build": "corepack pnpm build",
+ "repo:check": "corepack pnpm format:check && corepack pnpm lint && corepack pnpm typecheck && corepack pnpm requirements:check && corepack pnpm contracts:check && corepack pnpm test",
+ "repo:dev": "turbo run dev --parallel",
+ "repo:test": "corepack pnpm test",
+ "requirements:check": "node tools/repo-cli/src/generate-requirement-index.mjs --check",
+ "requirements:generate": "node tools/repo-cli/src/generate-requirement-index.mjs",
+ "test": "node --test tools/repo-cli/test/**/*.test.mjs && turbo run test",
+ "typecheck": "tsc --noEmit --project tsconfig.json"
+ },
+ "devDependencies": {
+ "@eslint/js": "9.36.0",
+ "eslint": "9.36.0",
+ "prettier": "3.6.2",
+ "turbo": "2.5.6",
+ "typescript": "5.9.2",
+ "typescript-eslint": "8.43.0"
+ }
+}
diff --git a/packages/config/README.md b/packages/config/README.md
new file mode 100644
index 00000000..aaafe187
--- /dev/null
+++ b/packages/config/README.md
@@ -0,0 +1,66 @@
+# Runtime Configuration
+
+Pure, versioned deployment-configuration loading for DataBreeze. This package validates runtime
+settings before an application or service constructs any adapter. It does not read `process.env`
+itself, contact a provider, or choose product policy.
+
+## Public interface
+
+`@databreeze/config/runtime/v1` exports:
+
+- `loadRuntimeConfigV1`, which accepts an explicit environment record/entry list, optional
+ structured overrides, and the factory-issued secret-reference capability used by enabled
+ credential references, then returns a deeply frozen configuration;
+- the five explicit profiles: `development`, `test`, `preview`, `staging`, and `production`;
+- typed object-storage, email, push, OCR, AI, payments, telemetry, and secrets selections;
+- `ConfigValidationErrorV1`, whose diagnostics contain only safe paths and codes; and
+- canonical `SecretReferenceV1` identifier objects shared with the secrets port. References have no
+ enumerable identifier fields or global extractor and redact string, JSON, and diagnostic
+ inspection; only the matching composition-owned resolver can recover their validated metadata.
+
+There is intentionally no unversioned package root.
+
+## Loading and safety rules
+
+Precedence is:
+
+`explicit overrides -> DATABREEZE_* environment -> profile defaults`
+
+The profile itself is always explicit. Development and test are the only profiles with defaults,
+and those defaults use loopback endpoints, in-memory/local facilities, or disabled providers.
+Preview, staging, and production have no provider-selection defaults: all eight provider modes must
+be declared; object storage and secrets must be remote; every other port may be explicitly disabled.
+
+Environment and override inputs are snapshotted from own data-property descriptors before parsing;
+accessors and failed proxy inspection become bounded, stable, redacted validation diagnostics.
+Unknown DataBreeze keys, duplicate entry-list keys, whitespace or alternate boolean/integer
+spellings, unknown structured override fields, incomplete active providers, and fields attached to
+a disabled provider are rejected. A higher-precedence provider `mode` that changes the selected
+variant atomically replaces the lower-precedence provider record, so local fields cannot leak into
+a disabled or remote selection. Cleartext endpoints are allowed only for an explicitly local
+adapter on loopback in development/test. URLs with credentials and all cleartext nonlocal endpoints
+are rejected. Secret reference paths are canonical non-traversing segments. Configuration accepts
+references, never API keys, passwords, tokens, webhook secrets, or other credential values.
+
+## Forbidden dependencies
+
+- Provider SDKs, cloud SDKs, service implementations, frameworks, filesystem/database/network I/O.
+- Business configuration, feature flags, organization/workspace/project policy, entitlements, or
+ tenant state.
+- Provider credentials or implicit host-environment reads.
+
+The only runtime dependency is the pure versioned provider-contract package used to authenticate a
+scoped secret-reference capability and construct the same opaque reference accepted by
+`SecretsProviderPortV1`.
+
+The product-policy precedence `platform default -> plan/region -> organization -> workspace ->
+project -> recipe/job` remains owned by later domain/application plans. This package covers only
+fail-closed deployment composition.
+
+## Local commands
+
+```text
+corepack pnpm --filter @databreeze/config test
+corepack pnpm --filter @databreeze/config typecheck
+corepack pnpm --filter @databreeze/config build
+```
diff --git a/packages/config/package.json b/packages/config/package.json
new file mode 100644
index 00000000..b3483533
--- /dev/null
+++ b/packages/config/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@databreeze/config",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./runtime/v1": {
+ "types": "./dist/runtime-config/v1.d.ts",
+ "import": "./dist/runtime-config/v1.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json && node test/built-public-api-smoke.mjs",
+ "test": "node --test test/**/*.test.mjs",
+ "typecheck": "tsc --noEmit --project tsconfig.json"
+ },
+ "dependencies": {
+ "@databreeze/provider-ports": "workspace:*"
+ }
+}
diff --git a/packages/config/src/runtime-config/loader-v1.ts b/packages/config/src/runtime-config/loader-v1.ts
new file mode 100644
index 00000000..44c804f4
--- /dev/null
+++ b/packages/config/src/runtime-config/loader-v1.ts
@@ -0,0 +1,1177 @@
+import { ConfigValidationErrorV1, RUNTIME_CONFIG_SCHEMA_VERSION_V1 } from './types-v1.ts';
+import {
+ isSecretReferenceCapabilityV1,
+ isSecretReferenceForCapabilityV1,
+ isSecretReferenceIssuerV1,
+ isSecretReferenceV1,
+} from '@databreeze/provider-ports/v1';
+import type {
+ ActiveDocumentProviderConfigV1,
+ AiConfigV1,
+ ConfigIssueV1,
+ EmailConfigV1,
+ EnvironmentEntriesV1,
+ LoadRuntimeConfigInputV1,
+ ObjectStorageConfigV1,
+ OcrConfigV1,
+ PaymentsConfigV1,
+ ProviderRuntimeConfigV1,
+ PushConfigV1,
+ RuntimeConfigV1,
+ RuntimeProfileV1,
+ SecretReferenceCapabilityV1,
+ SecretReferenceV1,
+ SecretsConfigV1,
+ TelemetryConfigV1,
+} from './types-v1.ts';
+
+type UnknownRecord = Record;
+type EnvironmentValueKind = 'boolean' | 'integer' | 'string';
+
+interface EnvironmentDefinition {
+ readonly path: readonly string[];
+ readonly kind: EnvironmentValueKind;
+}
+
+const profiles = new Set([
+ 'development',
+ 'test',
+ 'preview',
+ 'staging',
+ 'production',
+]);
+
+const strictProfiles = new Set(['preview', 'staging', 'production']);
+
+const providerKeys = [
+ 'objectStorage',
+ 'email',
+ 'push',
+ 'ocr',
+ 'ai',
+ 'payments',
+ 'telemetry',
+ 'secrets',
+] as const;
+
+const environmentDefinitions: Readonly> = {
+ DATABREEZE_PROFILE: { path: ['profile'], kind: 'string' },
+ DATABREEZE_PROVIDER_TIMEOUT_MS: {
+ path: ['providerPolicy', 'timeoutMs'],
+ kind: 'integer',
+ },
+ DATABREEZE_PROVIDER_MAX_ATTEMPTS: {
+ path: ['providerPolicy', 'maxAttempts'],
+ kind: 'integer',
+ },
+ DATABREEZE_OBJECT_STORAGE_MODE: {
+ path: ['providers', 'objectStorage', 'mode'],
+ kind: 'string',
+ },
+ DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL: {
+ path: ['providers', 'objectStorage', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_OBJECT_STORAGE_REGION: {
+ path: ['providers', 'objectStorage', 'region'],
+ kind: 'string',
+ },
+ DATABREEZE_OBJECT_STORAGE_BUCKET: {
+ path: ['providers', 'objectStorage', 'bucket'],
+ kind: 'string',
+ },
+ DATABREEZE_OBJECT_STORAGE_CREDENTIAL_REF: {
+ path: ['providers', 'objectStorage', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_OBJECT_STORAGE_FORCE_PATH_STYLE: {
+ path: ['providers', 'objectStorage', 'forcePathStyle'],
+ kind: 'boolean',
+ },
+ DATABREEZE_EMAIL_MODE: { path: ['providers', 'email', 'mode'], kind: 'string' },
+ DATABREEZE_EMAIL_ENDPOINT_URL: {
+ path: ['providers', 'email', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_EMAIL_FROM_ADDRESS: {
+ path: ['providers', 'email', 'fromAddress'],
+ kind: 'string',
+ },
+ DATABREEZE_EMAIL_CREDENTIAL_REF: {
+ path: ['providers', 'email', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_PUSH_MODE: { path: ['providers', 'push', 'mode'], kind: 'string' },
+ DATABREEZE_PUSH_ENDPOINT_URL: {
+ path: ['providers', 'push', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_PUSH_APPLICATION_ID: {
+ path: ['providers', 'push', 'applicationId'],
+ kind: 'string',
+ },
+ DATABREEZE_PUSH_CREDENTIAL_REF: {
+ path: ['providers', 'push', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_OCR_MODE: { path: ['providers', 'ocr', 'mode'], kind: 'string' },
+ DATABREEZE_OCR_ENDPOINT_URL: {
+ path: ['providers', 'ocr', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_OCR_CREDENTIAL_REF: {
+ path: ['providers', 'ocr', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_AI_MODE: { path: ['providers', 'ai', 'mode'], kind: 'string' },
+ DATABREEZE_AI_ENDPOINT_URL: {
+ path: ['providers', 'ai', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_AI_CREDENTIAL_REF: {
+ path: ['providers', 'ai', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_PAYMENTS_MODE: { path: ['providers', 'payments', 'mode'], kind: 'string' },
+ DATABREEZE_PAYMENTS_ENDPOINT_URL: {
+ path: ['providers', 'payments', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_PAYMENTS_CREDENTIAL_REF: {
+ path: ['providers', 'payments', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_PAYMENTS_WEBHOOK_SECRET_REF: {
+ path: ['providers', 'payments', 'webhookSecretRef'],
+ kind: 'string',
+ },
+ DATABREEZE_TELEMETRY_MODE: {
+ path: ['providers', 'telemetry', 'mode'],
+ kind: 'string',
+ },
+ DATABREEZE_TELEMETRY_ENDPOINT_URL: {
+ path: ['providers', 'telemetry', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_TELEMETRY_CREDENTIAL_REF: {
+ path: ['providers', 'telemetry', 'credentialRef'],
+ kind: 'string',
+ },
+ DATABREEZE_SECRETS_MODE: { path: ['providers', 'secrets', 'mode'], kind: 'string' },
+ DATABREEZE_SECRETS_ENDPOINT_URL: {
+ path: ['providers', 'secrets', 'endpointUrl'],
+ kind: 'string',
+ },
+ DATABREEZE_SECRETS_NAMESPACE: {
+ path: ['providers', 'secrets', 'namespace'],
+ kind: 'string',
+ },
+};
+
+const allowedOverrideKeys: Readonly> = {
+ '': ['profile', 'providerPolicy', 'providers'],
+ providerPolicy: ['timeoutMs', 'maxAttempts'],
+ providers: [...providerKeys],
+ 'providers.objectStorage': [
+ 'mode',
+ 'endpointUrl',
+ 'region',
+ 'bucket',
+ 'credentialRef',
+ 'forcePathStyle',
+ ],
+ 'providers.email': ['mode', 'endpointUrl', 'fromAddress', 'credentialRef'],
+ 'providers.push': ['mode', 'endpointUrl', 'applicationId', 'credentialRef'],
+ 'providers.ocr': ['mode', 'endpointUrl', 'credentialRef'],
+ 'providers.ai': ['mode', 'endpointUrl', 'credentialRef'],
+ 'providers.payments': ['mode', 'endpointUrl', 'credentialRef', 'webhookSecretRef'],
+ 'providers.telemetry': ['mode', 'endpointUrl', 'credentialRef'],
+ 'providers.secrets': ['mode', 'endpointUrl', 'namespace'],
+};
+
+const placeholderSegments = new Set([
+ 'changeme',
+ 'change-me',
+ 'dummy',
+ 'example',
+ 'password',
+ 'placeholder',
+ 'replace-me',
+ 'secret',
+ 'todo',
+]);
+const canonicalSecretSegmentPattern = /^[a-z0-9][a-z0-9._-]{0,62}$/;
+
+function isRecord(value: unknown): value is UnknownRecord {
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
+}
+
+function deepFreeze(value: T): T {
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
+ for (const child of Object.values(value)) {
+ deepFreeze(child);
+ }
+ Object.freeze(value);
+ }
+ return value;
+}
+
+function ownDataDescriptors(value: unknown): Record | undefined {
+ if (value === null || typeof value !== 'object') return undefined;
+ try {
+ return Object.getOwnPropertyDescriptors(value) as Record;
+ } catch {
+ return undefined;
+ }
+}
+
+function isArraySafely(value: unknown): boolean | undefined {
+ try {
+ return Array.isArray(value);
+ } catch {
+ return undefined;
+ }
+}
+
+function hasOnlyArrayIndexDescriptors(
+ descriptors: Record,
+ length: number,
+): boolean {
+ try {
+ return Reflect.ownKeys(descriptors).every(
+ (key) =>
+ typeof key === 'string' &&
+ (key === 'length' || (/^(0|[1-9][0-9]*)$/.test(key) && Number(key) < length)),
+ );
+ } catch {
+ return false;
+ }
+}
+
+function snapshotLoadInput(
+ input: LoadRuntimeConfigInputV1,
+ issues: ConfigIssueV1[],
+): Readonly<{
+ environment?: EnvironmentEntriesV1;
+ overrides?: unknown;
+ secretReferenceCapability?: SecretReferenceCapabilityV1;
+}> {
+ const descriptors = ownDataDescriptors(input);
+ if (descriptors === undefined || isArraySafely(input) !== false) {
+ issues.push({ path: 'configuration.invalid_input', code: 'invalid_string' });
+ return {};
+ }
+ const result: {
+ environment?: EnvironmentEntriesV1;
+ overrides?: unknown;
+ secretReferenceCapability?: SecretReferenceCapabilityV1;
+ } = {};
+ for (const [key, descriptor] of Object.entries(descriptors)) {
+ if (key !== 'environment' && key !== 'overrides' && key !== 'secretReferenceCapability') {
+ issues.push({ path: 'configuration.unknown_key', code: 'unknown_key' });
+ continue;
+ }
+ if (!('value' in descriptor)) {
+ issues.push({ path: 'configuration.invalid_input', code: 'invalid_string' });
+ continue;
+ }
+ if (key === 'environment') result.environment = descriptor.value as EnvironmentEntriesV1;
+ if (key === 'overrides') result.overrides = descriptor.value;
+ if (key === 'secretReferenceCapability') {
+ result.secretReferenceCapability = descriptor.value as SecretReferenceCapabilityV1;
+ }
+ }
+ return result;
+}
+
+function snapshotEnvironment(
+ environment: EnvironmentEntriesV1 | undefined,
+ issues: ConfigIssueV1[],
+): readonly (readonly [string, string | undefined])[] {
+ if (environment === undefined) return [];
+ const descriptors = ownDataDescriptors(environment);
+ if (descriptors === undefined) {
+ issues.push({ path: 'environment.invalid_input', code: 'invalid_string' });
+ return [];
+ }
+ const entries: (readonly [string, string | undefined])[] = [];
+ if (isArraySafely(environment) === false) {
+ for (const [key, descriptor] of Object.entries(descriptors)) {
+ if (
+ !('value' in descriptor) ||
+ (descriptor.value !== undefined && typeof descriptor.value !== 'string')
+ ) {
+ issues.push({ path: 'environment.invalid_input', code: 'invalid_string' });
+ continue;
+ }
+ entries.push([key, descriptor.value as string | undefined]);
+ }
+ return entries;
+ }
+ const lengthDescriptor = descriptors['length'];
+ if (
+ lengthDescriptor === undefined ||
+ !('value' in lengthDescriptor) ||
+ !Number.isSafeInteger(lengthDescriptor.value) ||
+ lengthDescriptor.value < 0 ||
+ lengthDescriptor.value > 1_000
+ ) {
+ issues.push({ path: 'environment.invalid_input', code: 'invalid_string' });
+ return [];
+ }
+ if (!hasOnlyArrayIndexDescriptors(descriptors, lengthDescriptor.value as number)) {
+ issues.push({ path: 'environment.invalid_input', code: 'invalid_string' });
+ return [];
+ }
+ for (let index = 0; index < (lengthDescriptor.value as number); index += 1) {
+ const entryDescriptor = descriptors[String(index)];
+ if (entryDescriptor === undefined || !('value' in entryDescriptor)) {
+ issues.push({ path: 'environment.invalid_entry', code: 'invalid_string' });
+ continue;
+ }
+ const tupleDescriptors = ownDataDescriptors(entryDescriptor.value);
+ const tupleLength = tupleDescriptors?.['length'];
+ const keyDescriptor = tupleDescriptors?.['0'];
+ const valueDescriptor = tupleDescriptors?.['1'];
+ if (
+ tupleDescriptors === undefined ||
+ tupleLength === undefined ||
+ !('value' in tupleLength) ||
+ tupleLength.value !== 2 ||
+ keyDescriptor === undefined ||
+ !('value' in keyDescriptor) ||
+ typeof keyDescriptor.value !== 'string' ||
+ valueDescriptor === undefined ||
+ !('value' in valueDescriptor) ||
+ (valueDescriptor.value !== undefined && typeof valueDescriptor.value !== 'string')
+ ) {
+ issues.push({ path: 'environment.invalid_entry', code: 'invalid_string' });
+ continue;
+ }
+ if (!hasOnlyArrayIndexDescriptors(tupleDescriptors, 2)) {
+ issues.push({ path: 'environment.invalid_entry', code: 'invalid_string' });
+ continue;
+ }
+ entries.push([keyDescriptor.value, valueDescriptor.value as string | undefined]);
+ }
+ return entries;
+}
+
+function snapshotOverrides(value: unknown, issues: ConfigIssueV1[], path = ''): UnknownRecord {
+ const descriptors = ownDataDescriptors(value);
+ if (descriptors === undefined || isArraySafely(value) !== false) {
+ issues.push({ path: 'overrides.invalid_input', code: 'invalid_string' });
+ return {};
+ }
+ const allowed = allowedOverrideKeys[path];
+ if (allowed === undefined) return {};
+ const result: UnknownRecord = {};
+ for (const [key, descriptor] of Object.entries(descriptors)) {
+ if (!allowed.includes(key)) {
+ issues.push({ path: 'overrides.unknown_key', code: 'unknown_key' });
+ continue;
+ }
+ if (!('value' in descriptor)) {
+ issues.push({ path: 'overrides.invalid_input', code: 'invalid_string' });
+ continue;
+ }
+ const childPath = path === '' ? key : `${path}.${key}`;
+ result[key] =
+ allowedOverrideKeys[childPath] === undefined
+ ? descriptor.value
+ : snapshotOverrides(descriptor.value, issues, childPath);
+ }
+ return result;
+}
+
+function setPath(target: UnknownRecord, path: readonly string[], value: unknown): void {
+ let cursor = target;
+ for (const segment of path.slice(0, -1)) {
+ const existing = cursor[segment];
+ if (!isRecord(existing)) {
+ cursor[segment] = {};
+ }
+ cursor = cursor[segment] as UnknownRecord;
+ }
+ const finalSegment = path.at(-1);
+ if (finalSegment !== undefined) {
+ cursor[finalSegment] = value;
+ }
+}
+
+function mergeRecords(base: UnknownRecord, overlay: UnknownRecord, path = ''): UnknownRecord {
+ if (
+ path.startsWith('providers.') &&
+ typeof overlay['mode'] === 'string' &&
+ overlay['mode'] !== base['mode']
+ ) {
+ return { ...overlay };
+ }
+ const result: UnknownRecord = { ...base };
+ for (const [key, value] of Object.entries(overlay)) {
+ const current = result[key];
+ const childPath = path === '' ? key : `${path}.${key}`;
+ result[key] =
+ isRecord(current) && isRecord(value) ? mergeRecords(current, value, childPath) : value;
+ }
+ return result;
+}
+
+function parseEnvironmentValue(
+ value: string,
+ definition: EnvironmentDefinition,
+ issues: ConfigIssueV1[],
+): unknown {
+ const path = definition.path.join('.');
+ if (definition.kind === 'string') {
+ return value;
+ }
+ if (definition.kind === 'boolean') {
+ if (value === 'true') return true;
+ if (value === 'false') return false;
+ issues.push({ path, code: 'invalid_boolean' });
+ return undefined;
+ }
+ if (!/^(0|[1-9][0-9]*)$/.test(value)) {
+ issues.push({ path, code: 'invalid_integer' });
+ return undefined;
+ }
+ const parsed = Number(value);
+ if (!Number.isSafeInteger(parsed)) {
+ issues.push({ path, code: 'invalid_integer' });
+ return undefined;
+ }
+ return parsed;
+}
+
+function readEnvironment(
+ environment: EnvironmentEntriesV1 | undefined,
+ issues: ConfigIssueV1[],
+): UnknownRecord {
+ const seen = new Set();
+ const result: UnknownRecord = {};
+
+ for (const [key, value] of snapshotEnvironment(environment, issues)) {
+ if (seen.has(key)) {
+ issues.push({ path: 'environment.duplicate_key', code: 'duplicate' });
+ continue;
+ }
+ seen.add(key);
+
+ const definition = environmentDefinitions[key];
+ if (definition === undefined) {
+ if (key.startsWith('DATABREEZE_')) {
+ issues.push({ path: 'environment.unknown_key', code: 'unknown_key' });
+ }
+ continue;
+ }
+ if (value === undefined) {
+ continue;
+ }
+ const parsed = parseEnvironmentValue(value, definition, issues);
+ if (parsed !== undefined) {
+ setPath(result, definition.path, parsed);
+ }
+ }
+ return result;
+}
+
+function profileDefaults(profile: RuntimeProfileV1): UnknownRecord {
+ const providerPolicy = { timeoutMs: 10_000, maxAttempts: 3 };
+ if (profile === 'development') {
+ return {
+ profile,
+ providerPolicy,
+ providers: {
+ objectStorage: {
+ mode: 'local',
+ endpointUrl: 'http://127.0.0.1:9000',
+ region: 'local',
+ bucket: 'databreeze-development',
+ forcePathStyle: true,
+ },
+ email: {
+ mode: 'local',
+ endpointUrl: 'smtp://127.0.0.1:1025',
+ fromAddress: 'noreply@databreeze.local',
+ },
+ push: { mode: 'disabled' },
+ ocr: { mode: 'disabled' },
+ ai: { mode: 'disabled' },
+ payments: { mode: 'disabled' },
+ telemetry: { mode: 'local', endpointUrl: 'http://127.0.0.1:4318' },
+ secrets: { mode: 'memory', namespace: 'development' },
+ },
+ };
+ }
+ if (profile === 'test') {
+ return {
+ profile,
+ providerPolicy,
+ providers: {
+ objectStorage: {
+ mode: 'local',
+ endpointUrl: 'http://127.0.0.1:9000',
+ region: 'local',
+ bucket: 'databreeze-test',
+ forcePathStyle: true,
+ },
+ email: { mode: 'disabled' },
+ push: { mode: 'disabled' },
+ ocr: { mode: 'disabled' },
+ ai: { mode: 'disabled' },
+ payments: { mode: 'disabled' },
+ telemetry: { mode: 'disabled' },
+ secrets: { mode: 'memory', namespace: 'test' },
+ },
+ };
+ }
+ return { profile, providerPolicy, providers: {} };
+}
+
+function requiredString(
+ record: UnknownRecord,
+ key: string,
+ path: string,
+ issues: ConfigIssueV1[],
+): string {
+ const value = record[key];
+ if (value === undefined) {
+ issues.push({ path, code: 'required' });
+ return '';
+ }
+ if (typeof value !== 'string' || value.length === 0 || value.trim() !== value) {
+ issues.push({ path, code: 'invalid_string' });
+ return '';
+ }
+ return value;
+}
+
+function requiredSecretNamespace(
+ record: UnknownRecord,
+ path: string,
+ issues: ConfigIssueV1[],
+): string {
+ const value = record['namespace'];
+ if (value === undefined) {
+ issues.push({ path, code: 'required' });
+ return '';
+ }
+ const segments = typeof value === 'string' ? value.split('/') : [];
+ if (
+ typeof value !== 'string' ||
+ value.length === 0 ||
+ value.length > 2_047 ||
+ value.trim() !== value ||
+ segments.length > 32 ||
+ segments.some(
+ (segment) =>
+ !canonicalSecretSegmentPattern.test(segment) || segment === '.' || segment === '..',
+ )
+ ) {
+ issues.push({ path, code: 'invalid_secret_namespace' });
+ return '';
+ }
+ return value;
+}
+
+function requiredBoolean(
+ record: UnknownRecord,
+ key: string,
+ path: string,
+ issues: ConfigIssueV1[],
+): boolean {
+ const value = record[key];
+ if (value === undefined) {
+ issues.push({ path, code: 'required' });
+ return false;
+ }
+ if (typeof value !== 'boolean') {
+ issues.push({ path, code: 'invalid_boolean' });
+ return false;
+ }
+ return value;
+}
+
+function requiredInteger(
+ record: UnknownRecord,
+ key: string,
+ path: string,
+ minimum: number,
+ maximum: number,
+ issues: ConfigIssueV1[],
+): number {
+ const value = record[key];
+ if (
+ typeof value !== 'number' ||
+ !Number.isSafeInteger(value) ||
+ value < minimum ||
+ value > maximum
+ ) {
+ issues.push({ path, code: 'invalid_integer' });
+ return minimum;
+ }
+ return value;
+}
+
+function recordAt(record: UnknownRecord, key: string): UnknownRecord {
+ const value = record[key];
+ return isRecord(value) ? value : {};
+}
+
+function validEndpoint(
+ value: string,
+ path: string,
+ profile: RuntimeProfileV1,
+ mode: string,
+ kind: 'email' | 'network',
+ issues: ConfigIssueV1[],
+): string {
+ let parsed: URL;
+ try {
+ parsed = new URL(value);
+ } catch {
+ issues.push({ path, code: 'unsafe_url' });
+ return value;
+ }
+
+ if (
+ parsed.username !== '' ||
+ parsed.password !== '' ||
+ parsed.search !== '' ||
+ parsed.hash !== ''
+ ) {
+ issues.push({ path, code: 'unsafe_url' });
+ return value;
+ }
+
+ const secureProtocols = kind === 'email' ? new Set(['https:', 'smtps:']) : new Set(['https:']);
+ if (secureProtocols.has(parsed.protocol)) {
+ return value;
+ }
+
+ const localProtocols = kind === 'email' ? new Set(['http:', 'smtp:']) : new Set(['http:']);
+ const hostname = parsed.hostname.toLowerCase();
+ const loopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '[::1]';
+ const safeLocal =
+ (profile === 'development' || profile === 'test') &&
+ mode === 'local' &&
+ loopback &&
+ localProtocols.has(parsed.protocol);
+ if (!safeLocal) {
+ issues.push({ path, code: 'unsafe_url' });
+ }
+ return value;
+}
+
+function isSecretReferenceIssueFunction(
+ value: unknown,
+): value is SecretReferenceCapabilityV1['issuer']['issue'] {
+ return typeof value === 'function';
+}
+
+function invalidSecretReferenceCapability(issues: ConfigIssueV1[]): undefined {
+ if (
+ !issues.some(
+ (issue) =>
+ issue.path === 'configuration.secret_reference_capability' &&
+ issue.code === 'invalid_secret_reference_capability',
+ )
+ ) {
+ issues.push({
+ path: 'configuration.secret_reference_capability',
+ code: 'invalid_secret_reference_capability',
+ });
+ }
+ return undefined;
+}
+
+function secretReference(
+ value: unknown,
+ path: string,
+ issues: ConfigIssueV1[],
+ required: boolean,
+ capability: SecretReferenceCapabilityV1 | undefined,
+): SecretReferenceV1 | undefined {
+ if (value === undefined) {
+ if (required) issues.push({ path, code: 'required' });
+ return undefined;
+ }
+ if (typeof value !== 'string' || value.trim() !== value) {
+ issues.push({ path, code: 'invalid_secret_reference' });
+ return undefined;
+ }
+ const match =
+ /^secret:\/\/([a-z0-9][a-z0-9._-]{0,62})\/([^#]+?)(?:#([a-z0-9][a-z0-9._-]{0,62}))?$/.exec(
+ value,
+ );
+ const pathSegments = match?.[2]?.split('/') ?? [];
+ const canonicalSegments = pathSegments.every(
+ (segment) => /^[a-z0-9][a-z0-9._-]{0,62}$/.test(segment) && segment !== '.' && segment !== '..',
+ );
+ const placeholderTokens = [match?.[1] ?? '', ...pathSegments, match?.[3] ?? ''].flatMap((part) =>
+ part.split(/[._-]+/),
+ );
+ if (
+ match === null ||
+ !canonicalSegments ||
+ value.includes('//', 'secret://'.length) ||
+ value.includes('%') ||
+ placeholderTokens.some((segment) => placeholderSegments.has(segment))
+ ) {
+ issues.push({ path, code: 'invalid_secret_reference' });
+ return undefined;
+ }
+ if (!isSecretReferenceCapabilityV1(capability)) {
+ return invalidSecretReferenceCapability(issues);
+ }
+ const capabilityDescriptors = ownDataDescriptors(capability);
+ const issuerDescriptor = capabilityDescriptors?.['issuer'];
+ const issuerValue: unknown =
+ issuerDescriptor !== undefined && 'value' in issuerDescriptor
+ ? issuerDescriptor.value
+ : undefined;
+ if (!isSecretReferenceIssuerV1(issuerValue)) {
+ return invalidSecretReferenceCapability(issues);
+ }
+ const issuerDescriptors = ownDataDescriptors(issuerValue);
+ const issueDescriptor = issuerDescriptors?.['issue'];
+ const issueValue: unknown =
+ issueDescriptor !== undefined && 'value' in issueDescriptor ? issueDescriptor.value : undefined;
+ if (!isSecretReferenceIssueFunction(issueValue)) {
+ return invalidSecretReferenceCapability(issues);
+ }
+ try {
+ const reference: unknown = Reflect.apply(issueValue, undefined, [
+ {
+ namespace: match[1] as string,
+ pathSegments,
+ ...(match[3] === undefined ? {} : { version: match[3] }),
+ },
+ ]);
+ if (
+ !isSecretReferenceV1(reference) ||
+ !isSecretReferenceForCapabilityV1(capability, reference)
+ ) {
+ return invalidSecretReferenceCapability(issues);
+ }
+ return reference;
+ } catch {
+ return invalidSecretReferenceCapability(issues);
+ }
+}
+
+function modeOf(
+ record: UnknownRecord,
+ path: string,
+ allowed: readonly string[],
+ issues: ConfigIssueV1[],
+): string {
+ const mode = record['mode'];
+ if (mode === undefined) {
+ issues.push({ path: `${path}.mode`, code: 'required' });
+ return '';
+ }
+ if (typeof mode !== 'string' || !allowed.includes(mode)) {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ return '';
+ }
+ return mode;
+}
+
+function forbidDisabledFields(
+ record: UnknownRecord,
+ path: string,
+ fields: readonly string[],
+ issues: ConfigIssueV1[],
+): void {
+ for (const field of fields) {
+ if (record[field] !== undefined) {
+ issues.push({ path: `${path}.${field}`, code: 'forbidden_when_disabled' });
+ }
+ }
+}
+
+function validateObjectStorage(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): ObjectStorageConfigV1 {
+ const path = 'providers.objectStorage';
+ const mode = modeOf(record, path, ['local', 'remote'], issues);
+ if (strictProfiles.has(profile) && mode !== '' && mode !== 'remote') {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ );
+ const region = requiredString(record, 'region', `${path}.region`, issues);
+ const bucket = requiredString(record, 'bucket', `${path}.bucket`, issues);
+ const forcePathStyle = requiredBoolean(
+ record,
+ 'forcePathStyle',
+ `${path}.forcePathStyle`,
+ issues,
+ );
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ mode === 'remote',
+ capability,
+ );
+ return {
+ mode: mode === 'remote' ? 'remote' : 'local',
+ endpointUrl,
+ region,
+ bucket,
+ ...(credentialRef === undefined ? {} : { credentialRef }),
+ forcePathStyle,
+ };
+}
+
+function validateEmail(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): EmailConfigV1 {
+ const path = 'providers.email';
+ const mode = modeOf(record, path, ['disabled', 'local', 'remote'], issues);
+ if (mode === 'disabled') {
+ forbidDisabledFields(record, path, ['endpointUrl', 'fromAddress', 'credentialRef'], issues);
+ return { mode: 'disabled' };
+ }
+ if (strictProfiles.has(profile) && mode === 'local') {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'email',
+ issues,
+ );
+ const fromAddress = requiredString(record, 'fromAddress', `${path}.fromAddress`, issues);
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fromAddress)) {
+ issues.push({ path: `${path}.fromAddress`, code: 'invalid_email' });
+ }
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ mode === 'remote',
+ capability,
+ );
+ return {
+ mode: mode === 'remote' ? 'remote' : 'local',
+ endpointUrl,
+ fromAddress,
+ ...(credentialRef === undefined ? {} : { credentialRef }),
+ };
+}
+
+function validatePush(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): PushConfigV1 {
+ const path = 'providers.push';
+ const mode = modeOf(record, path, ['disabled', 'remote'], issues);
+ if (mode === 'disabled') {
+ forbidDisabledFields(record, path, ['endpointUrl', 'applicationId', 'credentialRef'], issues);
+ return { mode: 'disabled' };
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ );
+ const applicationId = requiredString(record, 'applicationId', `${path}.applicationId`, issues);
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ true,
+ capability,
+ );
+ return {
+ mode: 'remote',
+ endpointUrl,
+ applicationId,
+ credentialRef: credentialRef as SecretReferenceV1,
+ };
+}
+
+function validateDocumentProvider(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ name: 'ocr' | 'ai',
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): ActiveDocumentProviderConfigV1 | { readonly mode: 'disabled' } {
+ const path = `providers.${name}`;
+ const mode = modeOf(record, path, ['disabled', 'local', 'remote'], issues);
+ if (mode === 'disabled') {
+ forbidDisabledFields(record, path, ['endpointUrl', 'credentialRef'], issues);
+ return { mode: 'disabled' };
+ }
+ if (strictProfiles.has(profile) && mode === 'local') {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ );
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ mode === 'remote',
+ capability,
+ );
+ return {
+ mode: mode === 'remote' ? 'remote' : 'local',
+ endpointUrl,
+ ...(credentialRef === undefined ? {} : { credentialRef }),
+ };
+}
+
+function validatePayments(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): PaymentsConfigV1 {
+ const path = 'providers.payments';
+ const mode = modeOf(record, path, ['disabled', 'remote'], issues);
+ if (mode === 'disabled') {
+ forbidDisabledFields(
+ record,
+ path,
+ ['endpointUrl', 'credentialRef', 'webhookSecretRef'],
+ issues,
+ );
+ return { mode: 'disabled' };
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ );
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ true,
+ capability,
+ );
+ const webhookSecretRef = secretReference(
+ record['webhookSecretRef'],
+ `${path}.webhookSecretRef`,
+ issues,
+ true,
+ capability,
+ );
+ return {
+ mode: 'remote',
+ endpointUrl,
+ credentialRef: credentialRef as SecretReferenceV1,
+ webhookSecretRef: webhookSecretRef as SecretReferenceV1,
+ };
+}
+
+function validateTelemetry(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): TelemetryConfigV1 {
+ const path = 'providers.telemetry';
+ const mode = modeOf(record, path, ['disabled', 'local', 'remote'], issues);
+ if (mode === 'disabled') {
+ forbidDisabledFields(record, path, ['endpointUrl', 'credentialRef'], issues);
+ return { mode: 'disabled' };
+ }
+ if (strictProfiles.has(profile) && mode === 'local') {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ }
+ const endpointUrl = validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ );
+ const credentialRef = secretReference(
+ record['credentialRef'],
+ `${path}.credentialRef`,
+ issues,
+ false,
+ capability,
+ );
+ return {
+ mode: mode === 'remote' ? 'remote' : 'local',
+ endpointUrl,
+ ...(credentialRef === undefined ? {} : { credentialRef }),
+ };
+}
+
+function validateSecrets(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+): SecretsConfigV1 {
+ const path = 'providers.secrets';
+ const mode = modeOf(record, path, ['memory', 'remote'], issues);
+ if (strictProfiles.has(profile) && mode !== '' && mode !== 'remote') {
+ issues.push({ path: `${path}.mode`, code: 'invalid_mode' });
+ }
+ const namespace = requiredSecretNamespace(record, `${path}.namespace`, issues);
+ if (mode === 'remote') {
+ return {
+ mode: 'remote',
+ endpointUrl: validEndpoint(
+ requiredString(record, 'endpointUrl', `${path}.endpointUrl`, issues),
+ `${path}.endpointUrl`,
+ profile,
+ mode,
+ 'network',
+ issues,
+ ),
+ namespace,
+ };
+ }
+ if (record['endpointUrl'] !== undefined) {
+ issues.push({ path: `${path}.endpointUrl`, code: 'forbidden_when_disabled' });
+ }
+ return { mode: 'memory', namespace };
+}
+
+function validateProviders(
+ record: UnknownRecord,
+ profile: RuntimeProfileV1,
+ issues: ConfigIssueV1[],
+ capability: SecretReferenceCapabilityV1 | undefined,
+): ProviderRuntimeConfigV1 {
+ return {
+ objectStorage: validateObjectStorage(
+ recordAt(record, 'objectStorage'),
+ profile,
+ issues,
+ capability,
+ ),
+ email: validateEmail(recordAt(record, 'email'), profile, issues, capability),
+ push: validatePush(recordAt(record, 'push'), profile, issues, capability),
+ ocr: validateDocumentProvider(
+ recordAt(record, 'ocr'),
+ profile,
+ 'ocr',
+ issues,
+ capability,
+ ) as OcrConfigV1,
+ ai: validateDocumentProvider(
+ recordAt(record, 'ai'),
+ profile,
+ 'ai',
+ issues,
+ capability,
+ ) as AiConfigV1,
+ payments: validatePayments(recordAt(record, 'payments'), profile, issues, capability),
+ telemetry: validateTelemetry(recordAt(record, 'telemetry'), profile, issues, capability),
+ secrets: validateSecrets(recordAt(record, 'secrets'), profile, issues),
+ };
+}
+
+function selectedProfile(
+ environment: UnknownRecord,
+ overrides: UnknownRecord,
+ issues: ConfigIssueV1[],
+): RuntimeProfileV1 | undefined {
+ const value = overrides['profile'] ?? environment['profile'];
+ if (value === undefined) {
+ issues.push({ path: 'profile', code: 'required' });
+ return undefined;
+ }
+ if (typeof value !== 'string' || !profiles.has(value as RuntimeProfileV1)) {
+ issues.push({ path: 'profile', code: 'invalid_profile' });
+ return undefined;
+ }
+ return value as RuntimeProfileV1;
+}
+
+export function loadRuntimeConfigV1(input: LoadRuntimeConfigInputV1 = {}): RuntimeConfigV1 {
+ const issues: ConfigIssueV1[] = [];
+ const safeInput = snapshotLoadInput(input, issues);
+ if (
+ safeInput.secretReferenceCapability !== undefined &&
+ !isSecretReferenceCapabilityV1(safeInput.secretReferenceCapability)
+ ) {
+ invalidSecretReferenceCapability(issues);
+ }
+ const environment = readEnvironment(safeInput.environment, issues);
+ const overrideRecord =
+ safeInput.overrides === undefined ? {} : snapshotOverrides(safeInput.overrides, issues);
+ const profile = selectedProfile(environment, overrideRecord, issues);
+
+ if (profile === undefined) {
+ throw new ConfigValidationErrorV1(issues);
+ }
+
+ const merged = mergeRecords(mergeRecords(profileDefaults(profile), environment), overrideRecord);
+ const providerPolicyRecord = recordAt(merged, 'providerPolicy');
+ const providerPolicy = {
+ timeoutMs: requiredInteger(
+ providerPolicyRecord,
+ 'timeoutMs',
+ 'providerPolicy.timeoutMs',
+ 100,
+ 120_000,
+ issues,
+ ),
+ maxAttempts: requiredInteger(
+ providerPolicyRecord,
+ 'maxAttempts',
+ 'providerPolicy.maxAttempts',
+ 1,
+ 10,
+ issues,
+ ),
+ };
+ const providers = validateProviders(
+ recordAt(merged, 'providers'),
+ profile,
+ issues,
+ safeInput.secretReferenceCapability,
+ );
+
+ if (issues.length > 0) {
+ throw new ConfigValidationErrorV1(issues);
+ }
+
+ return deepFreeze({
+ schemaVersion: RUNTIME_CONFIG_SCHEMA_VERSION_V1,
+ profile,
+ providerPolicy,
+ providers,
+ });
+}
diff --git a/packages/config/src/runtime-config/types-v1.ts b/packages/config/src/runtime-config/types-v1.ts
new file mode 100644
index 00000000..e55ba94d
--- /dev/null
+++ b/packages/config/src/runtime-config/types-v1.ts
@@ -0,0 +1,150 @@
+import type { SecretReferenceCapabilityV1, SecretReferenceV1 } from '@databreeze/provider-ports/v1';
+
+export type { SecretReferenceCapabilityV1, SecretReferenceV1 } from '@databreeze/provider-ports/v1';
+
+export const RUNTIME_CONFIG_SCHEMA_VERSION_V1 = 1 as const;
+
+export type RuntimeProfileV1 = 'development' | 'test' | 'preview' | 'staging' | 'production';
+
+export type ConfigIssueCodeV1 =
+ | 'duplicate'
+ | 'forbidden_when_disabled'
+ | 'invalid_boolean'
+ | 'invalid_email'
+ | 'invalid_integer'
+ | 'invalid_mode'
+ | 'invalid_profile'
+ | 'invalid_secret_reference'
+ | 'invalid_secret_reference_capability'
+ | 'invalid_secret_namespace'
+ | 'invalid_string'
+ | 'required'
+ | 'unknown_key'
+ | 'unsafe_url';
+
+export interface ConfigIssueV1 {
+ readonly path: string;
+ readonly code: ConfigIssueCodeV1;
+}
+
+export class ConfigValidationErrorV1 extends Error {
+ public readonly issues: readonly ConfigIssueV1[];
+
+ public constructor(issues: readonly ConfigIssueV1[]) {
+ super('Runtime configuration is invalid.');
+ this.name = 'ConfigValidationErrorV1';
+ this.issues = Object.freeze(
+ issues
+ .slice(0, 100)
+ .map((issue) => Object.freeze({ path: issue.path.slice(0, 80), code: issue.code })),
+ );
+ Object.freeze(this);
+ }
+
+ public toJSON(): Readonly<{ name: string; issues: readonly ConfigIssueV1[] }> {
+ return Object.freeze({ name: this.name, issues: this.issues });
+ }
+}
+
+export interface ProviderPolicyConfigV1 {
+ readonly timeoutMs: number;
+ readonly maxAttempts: number;
+}
+
+export interface DisabledProviderConfigV1 {
+ readonly mode: 'disabled';
+}
+
+export interface ObjectStorageConfigV1 {
+ readonly mode: 'local' | 'remote';
+ readonly endpointUrl: string;
+ readonly region: string;
+ readonly bucket: string;
+ readonly credentialRef?: SecretReferenceV1;
+ readonly forcePathStyle: boolean;
+}
+
+export interface ActiveEmailConfigV1 {
+ readonly mode: 'local' | 'remote';
+ readonly endpointUrl: string;
+ readonly fromAddress: string;
+ readonly credentialRef?: SecretReferenceV1;
+}
+
+export type EmailConfigV1 = DisabledProviderConfigV1 | ActiveEmailConfigV1;
+
+export interface ActivePushConfigV1 {
+ readonly mode: 'remote';
+ readonly endpointUrl: string;
+ readonly applicationId: string;
+ readonly credentialRef: SecretReferenceV1;
+}
+
+export type PushConfigV1 = DisabledProviderConfigV1 | ActivePushConfigV1;
+
+export interface ActiveDocumentProviderConfigV1 {
+ readonly mode: 'local' | 'remote';
+ readonly endpointUrl: string;
+ readonly credentialRef?: SecretReferenceV1;
+}
+
+export type OcrConfigV1 = DisabledProviderConfigV1 | ActiveDocumentProviderConfigV1;
+export type AiConfigV1 = DisabledProviderConfigV1 | ActiveDocumentProviderConfigV1;
+
+export interface ActivePaymentsConfigV1 {
+ readonly mode: 'remote';
+ readonly endpointUrl: string;
+ readonly credentialRef: SecretReferenceV1;
+ readonly webhookSecretRef: SecretReferenceV1;
+}
+
+export type PaymentsConfigV1 = DisabledProviderConfigV1 | ActivePaymentsConfigV1;
+
+export interface ActiveTelemetryConfigV1 {
+ readonly mode: 'local' | 'remote';
+ readonly endpointUrl: string;
+ readonly credentialRef?: SecretReferenceV1;
+}
+
+export type TelemetryConfigV1 = DisabledProviderConfigV1 | ActiveTelemetryConfigV1;
+
+export interface MemorySecretsConfigV1 {
+ readonly mode: 'memory';
+ readonly namespace: string;
+}
+
+export interface RemoteSecretsConfigV1 {
+ readonly mode: 'remote';
+ readonly endpointUrl: string;
+ readonly namespace: string;
+}
+
+export type SecretsConfigV1 = MemorySecretsConfigV1 | RemoteSecretsConfigV1;
+
+export interface ProviderRuntimeConfigV1 {
+ readonly objectStorage: ObjectStorageConfigV1;
+ readonly email: EmailConfigV1;
+ readonly push: PushConfigV1;
+ readonly ocr: OcrConfigV1;
+ readonly ai: AiConfigV1;
+ readonly payments: PaymentsConfigV1;
+ readonly telemetry: TelemetryConfigV1;
+ readonly secrets: SecretsConfigV1;
+}
+
+export interface RuntimeConfigV1 {
+ readonly schemaVersion: typeof RUNTIME_CONFIG_SCHEMA_VERSION_V1;
+ readonly profile: RuntimeProfileV1;
+ readonly providerPolicy: ProviderPolicyConfigV1;
+ readonly providers: ProviderRuntimeConfigV1;
+}
+
+export type EnvironmentEntriesV1 =
+ | Readonly>
+ | readonly (readonly [string, string | undefined])[];
+
+export interface LoadRuntimeConfigInputV1 {
+ readonly environment?: EnvironmentEntriesV1;
+ readonly overrides?: unknown;
+ readonly secretReferenceCapability?: SecretReferenceCapabilityV1;
+}
diff --git a/packages/config/src/runtime-config/v1.ts b/packages/config/src/runtime-config/v1.ts
new file mode 100644
index 00000000..3955bf90
--- /dev/null
+++ b/packages/config/src/runtime-config/v1.ts
@@ -0,0 +1,2 @@
+export * from './loader-v1.ts';
+export * from './types-v1.ts';
diff --git a/packages/config/test/built-public-api-smoke.mjs b/packages/config/test/built-public-api-smoke.mjs
new file mode 100644
index 00000000..6c101f4a
--- /dev/null
+++ b/packages/config/test/built-public-api-smoke.mjs
@@ -0,0 +1,18 @@
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const manifest = JSON.parse(readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
+const typesTarget = manifest.exports['./runtime/v1'].types;
+
+assert.equal(typesTarget, './dist/runtime-config/v1.d.ts');
+assert.equal(existsSync(path.resolve(packageDirectory, typesTarget)), true);
+
+const runtime = await import('../dist/runtime-config/v1.js');
+
+assert.equal(runtime.RUNTIME_CONFIG_SCHEMA_VERSION_V1, 1);
+assert.equal(typeof runtime.loadRuntimeConfigV1, 'function');
+assert.equal(runtime.secretReferenceHandleV1, undefined);
+assert.equal(runtime.createSecretReferenceV1, undefined);
diff --git a/packages/config/test/public-api-v1.test.mjs b/packages/config/test/public-api-v1.test.mjs
new file mode 100644
index 00000000..cf05169f
--- /dev/null
+++ b/packages/config/test/public-api-v1.test.mjs
@@ -0,0 +1,26 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+
+test('publishes a versioned runtime configuration loader', async () => {
+ let runtime;
+ try {
+ runtime = await import('../src/runtime-config/v1.ts');
+ } catch {
+ runtime = undefined;
+ }
+
+ assert.ok(runtime, 'the runtime/v1 source entry point must exist');
+ assert.equal(runtime.RUNTIME_CONFIG_SCHEMA_VERSION_V1, 1);
+ assert.equal(typeof runtime.loadRuntimeConfigV1, 'function');
+});
+
+test('exposes only the versioned runtime entry point', async () => {
+ const manifest = JSON.parse(readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
+ assert.deepEqual(Object.keys(manifest.exports), ['./runtime/v1']);
+ await assert.rejects(import('@databreeze/config'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });
+});
diff --git a/packages/config/test/review-regressions-v1.test.mjs b/packages/config/test/review-regressions-v1.test.mjs
new file mode 100644
index 00000000..f3486b51
--- /dev/null
+++ b/packages/config/test/review-regressions-v1.test.mjs
@@ -0,0 +1,497 @@
+import assert from 'node:assert/strict';
+import { inspect } from 'node:util';
+import test from 'node:test';
+
+import { ConfigValidationErrorV1, loadRuntimeConfigV1 } from '../src/runtime-config/v1.ts';
+
+const providerPorts = await import('@databreeze/provider-ports/v1');
+
+function issue(error, path, code) {
+ return (
+ error instanceof ConfigValidationErrorV1 &&
+ error.issues.some((entry) => entry.path === path && entry.code === code)
+ );
+}
+
+function expectSafeConfigFailure(run, path, code, exposed = []) {
+ assert.throws(run, (error) => {
+ assert.ok(issue(error, path, code), inspect(error));
+ for (const value of exposed) {
+ assert.doesNotMatch(String(error), new RegExp(value, 'u'));
+ assert.doesNotMatch(JSON.stringify(error), new RegExp(value, 'u'));
+ assert.doesNotMatch(inspect(error), new RegExp(value, 'u'));
+ }
+ assert.ok(error.issues.every((entry) => entry.path.length <= 80));
+ return true;
+ });
+}
+
+test('an environment mode change replaces the lower-precedence provider record', () => {
+ const disabledEmail = loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_EMAIL_MODE: 'disabled',
+ },
+ });
+ assert.deepEqual(disabledEmail.providers.email, { mode: 'disabled' });
+
+ const disabledTelemetry = loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_TELEMETRY_MODE: 'disabled',
+ },
+ });
+ assert.deepEqual(disabledTelemetry.providers.telemetry, { mode: 'disabled' });
+
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_OBJECT_STORAGE_MODE: 'remote',
+ },
+ }),
+ 'providers.objectStorage.endpointUrl',
+ 'required',
+ );
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_SECRETS_MODE: 'remote',
+ },
+ }),
+ 'providers.secrets.namespace',
+ 'required',
+ );
+});
+
+const activeEnvironmentByProvider = {
+ email: {
+ DATABREEZE_EMAIL_MODE: 'local',
+ DATABREEZE_EMAIL_ENDPOINT_URL: 'smtp://127.0.0.1:1025',
+ DATABREEZE_EMAIL_FROM_ADDRESS: 'notify@databreeze.local',
+ },
+ push: {
+ DATABREEZE_PUSH_MODE: 'remote',
+ DATABREEZE_PUSH_ENDPOINT_URL: 'https://push.example.test',
+ DATABREEZE_PUSH_APPLICATION_ID: 'databreeze',
+ DATABREEZE_PUSH_CREDENTIAL_REF: 'secret://development/push/credential',
+ },
+ ocr: {
+ DATABREEZE_OCR_MODE: 'local',
+ DATABREEZE_OCR_ENDPOINT_URL: 'http://127.0.0.1:8181',
+ },
+ ai: {
+ DATABREEZE_AI_MODE: 'local',
+ DATABREEZE_AI_ENDPOINT_URL: 'http://127.0.0.1:8282',
+ },
+ payments: {
+ DATABREEZE_PAYMENTS_MODE: 'remote',
+ DATABREEZE_PAYMENTS_ENDPOINT_URL: 'https://payments.example.test',
+ DATABREEZE_PAYMENTS_CREDENTIAL_REF: 'secret://development/payments/credential',
+ DATABREEZE_PAYMENTS_WEBHOOK_SECRET_REF: 'secret://development/payments/webhook',
+ },
+ telemetry: {
+ DATABREEZE_TELEMETRY_MODE: 'local',
+ DATABREEZE_TELEMETRY_ENDPOINT_URL: 'http://127.0.0.1:4318',
+ },
+};
+
+for (const [provider, providerEnvironment] of Object.entries(activeEnvironmentByProvider)) {
+ test(`an override mode change replaces the environment ${provider} record`, () => {
+ const config = loadRuntimeConfigV1({
+ environment: { DATABREEZE_PROFILE: 'development', ...providerEnvironment },
+ overrides: { providers: { [provider]: { mode: 'disabled' } } },
+ });
+ assert.deepEqual(config.providers[provider], { mode: 'disabled' });
+ });
+}
+
+test('override mode replacement also closes object-storage and secrets variants', () => {
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: { DATABREEZE_PROFILE: 'development' },
+ overrides: { providers: { objectStorage: { mode: 'remote' } } },
+ }),
+ 'providers.objectStorage.endpointUrl',
+ 'required',
+ );
+
+ const secrets = loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_SECRETS_MODE: 'remote',
+ DATABREEZE_SECRETS_ENDPOINT_URL: 'https://secrets.example.test',
+ DATABREEZE_SECRETS_NAMESPACE: 'remote',
+ },
+ overrides: { providers: { secrets: { mode: 'memory', namespace: 'local' } } },
+ });
+ assert.deepEqual(secrets.providers.secrets, { mode: 'memory', namespace: 'local' });
+});
+
+test('configuration snapshots data properties without invoking accessors', () => {
+ let getterCalls = 0;
+ const environment = {};
+ Object.defineProperty(environment, 'DATABREEZE_PROFILE', {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ throw new Error('environment-getter-secret');
+ },
+ });
+
+ expectSafeConfigFailure(
+ () => loadRuntimeConfigV1({ environment }),
+ 'environment.invalid_input',
+ 'invalid_string',
+ ['environment-getter-secret'],
+ );
+ assert.equal(getterCalls, 0);
+
+ const overrides = { providers: {} };
+ Object.defineProperty(overrides.providers, 'ai', {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ throw new Error('override-getter-secret');
+ },
+ });
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: { DATABREEZE_PROFILE: 'development' },
+ overrides,
+ }),
+ 'overrides.invalid_input',
+ 'invalid_string',
+ ['override-getter-secret'],
+ );
+ assert.equal(getterCalls, 0);
+});
+
+test('configuration converts proxy and malformed tuple failures to bounded redacted diagnostics', () => {
+ const exposed = 'proxy-own-keys-secret';
+ const environment = new Proxy(
+ {},
+ {
+ ownKeys() {
+ throw new Error(exposed);
+ },
+ },
+ );
+ expectSafeConfigFailure(
+ () => loadRuntimeConfigV1({ environment }),
+ 'environment.invalid_input',
+ 'invalid_string',
+ [exposed],
+ );
+
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ [42, { value: 'must-not-leak' }],
+ ],
+ }),
+ 'environment.invalid_entry',
+ 'invalid_string',
+ ['must-not-leak'],
+ );
+});
+
+test('duplicate environment keys use a stable bounded path without exposing attacker text', () => {
+ const exposedKey = 'DATABREEZE_DUPLICATE_SECRET_X9Y8Z7';
+ const exposedValue = 'duplicate-value-X9Y8Z7';
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ [exposedKey, exposedValue],
+ [exposedKey, exposedValue],
+ ],
+ }),
+ 'environment.duplicate_key',
+ 'duplicate',
+ [exposedKey, exposedValue],
+ );
+});
+
+test('environment arrays and tuples reject non-index string and symbol properties', () => {
+ const environment = [['DATABREEZE_PROFILE', 'development']];
+ environment.extra = 'must-not-be-read';
+ expectSafeConfigFailure(
+ () => loadRuntimeConfigV1({ environment }),
+ 'environment.invalid_input',
+ 'invalid_string',
+ ['must-not-be-read'],
+ );
+
+ const tuple = ['DATABREEZE_PROFILE', 'development'];
+ tuple[Symbol('hidden')] = 'must-not-be-read';
+ expectSafeConfigFailure(
+ () => loadRuntimeConfigV1({ environment: [tuple] }),
+ 'environment.invalid_entry',
+ 'invalid_string',
+ ['must-not-be-read'],
+ );
+});
+
+test('configuration snapshots the load request itself and bounds repeated diagnostics', () => {
+ let getterCalls = 0;
+ const input = {};
+ Object.defineProperty(input, 'environment', {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ throw new Error('load-request-secret-X9Y8Z7');
+ },
+ });
+ expectSafeConfigFailure(
+ () => loadRuntimeConfigV1(input),
+ 'configuration.invalid_input',
+ 'invalid_string',
+ ['load-request-secret-X9Y8Z7'],
+ );
+ assert.equal(getterCalls, 0);
+
+ assert.throws(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ...Array.from({ length: 500 }, () => [42, 'invalid']),
+ ],
+ }),
+ (error) => error instanceof ConfigValidationErrorV1 && error.issues.length <= 100,
+ );
+});
+
+test('unknown attacker-controlled keys never become diagnostic field names', () => {
+ const exposedEnvironmentKey = 'DATABREEZE_TOKEN_X9Y8Z7';
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ [exposedEnvironmentKey]: 'raw-value-X9Y8Z7',
+ },
+ }),
+ 'environment.unknown_key',
+ 'unknown_key',
+ [exposedEnvironmentKey, 'raw-value-X9Y8Z7'],
+ );
+
+ const exposedOverrideKey = 'privateKey_X9Y8Z7';
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: { DATABREEZE_PROFILE: 'development' },
+ overrides: { providers: { ai: { mode: 'disabled', [exposedOverrideKey]: 'raw' } } },
+ }),
+ 'overrides.unknown_key',
+ 'unknown_key',
+ [exposedOverrideKey],
+ );
+});
+
+for (const reference of [
+ 'secret://production/.',
+ 'secret://production/..',
+ 'secret://production/a//b',
+ 'secret://production/a/',
+ 'secret://production/a/../b',
+ 'secret://production/a/./b',
+]) {
+ test(`rejects non-canonical secret reference ${reference}`, () => {
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_PUSH_MODE: 'remote',
+ DATABREEZE_PUSH_ENDPOINT_URL: 'https://push.example.test',
+ DATABREEZE_PUSH_APPLICATION_ID: 'databreeze',
+ DATABREEZE_PUSH_CREDENTIAL_REF: reference,
+ },
+ }),
+ 'providers.push.credentialRef',
+ 'invalid_secret_reference',
+ [reference.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&')],
+ );
+ });
+}
+
+for (const namespace of ['', '.', '..', 'team//prod', 'team/', 'team/../prod', 'team/./prod']) {
+ test(`rejects non-canonical secret namespace ${JSON.stringify(namespace)}`, () => {
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_SECRETS_MODE: 'memory',
+ DATABREEZE_SECRETS_NAMESPACE: namespace,
+ },
+ }),
+ 'providers.secrets.namespace',
+ 'invalid_secret_namespace',
+ );
+ });
+}
+
+function enabledPushEnvironment() {
+ return {
+ DATABREEZE_PROFILE: 'development',
+ DATABREEZE_PUSH_MODE: 'remote',
+ DATABREEZE_PUSH_ENDPOINT_URL: 'https://push.example.test',
+ DATABREEZE_PUSH_APPLICATION_ID: 'databreeze',
+ DATABREEZE_PUSH_CREDENTIAL_REF: 'secret://development/push/credential#active',
+ };
+}
+
+test('returns a capability-created opaque secret reference without a raw extractor', async () => {
+ const runtime = await import('../src/runtime-config/v1.ts');
+ const capability = providerPorts.createSecretReferenceCapabilityV1();
+ const config = loadRuntimeConfigV1({
+ environment: enabledPushEnvironment(),
+ secretReferenceCapability: capability,
+ });
+ const reference = config.providers.push.credentialRef;
+ assert.deepEqual(capability.resolver.resolve(reference), {
+ namespace: 'development',
+ pathSegments: ['push', 'credential'],
+ version: 'active',
+ });
+ assert.equal(reference.namespace, undefined);
+ assert.equal(reference.pathSegments, undefined);
+ assert.equal(reference.version, undefined);
+ assert.deepEqual(Reflect.ownKeys(reference), []);
+ assert.doesNotMatch(inspect(reference), /development|push|credential|active/u);
+ assert.equal(runtime.secretReferenceHandleV1, undefined);
+ assert.equal(runtime.createSecretReferenceV1, undefined);
+ assert.equal(JSON.stringify(reference), '"[REDACTED_SECRET_REFERENCE]"');
+ assert.doesNotMatch(JSON.stringify(config), /secretReferenceCapability|secret:\/\//u);
+});
+
+test('rejects fake, hostile, revoked, and foreign secret capabilities without invoking them', () => {
+ const realCapability = providerPorts.createSecretReferenceCapabilityV1();
+ const foreignCapability = providerPorts.createSecretReferenceCapabilityV1();
+ const foreignReference = foreignCapability.issuer.issue({
+ namespace: 'foreign',
+ pathSegments: ['credential'],
+ });
+ let invoked = 0;
+ const attempts = [
+ {
+ name: 'primitive-return',
+ marker: 'primitive-return-marker-X9Y8Z7',
+ value: {
+ issuer: {
+ issue() {
+ invoked += 1;
+ return 42;
+ },
+ },
+ resolver: {},
+ },
+ },
+ {
+ name: 'plain-return',
+ marker: 'plain-return-marker-X9Y8Z7',
+ value: {
+ issuer: {
+ issue() {
+ invoked += 1;
+ return { raw: 'plain-return-marker-X9Y8Z7' };
+ },
+ },
+ resolver: {},
+ },
+ },
+ {
+ name: 'throwing-method',
+ marker: 'throwing-method-marker-X9Y8Z7',
+ value: {
+ issuer: {
+ issue() {
+ invoked += 1;
+ throw new Error('throwing-method-marker-X9Y8Z7');
+ },
+ },
+ resolver: {},
+ },
+ },
+ {
+ name: 'foreign-return',
+ marker: 'foreign-return-marker-X9Y8Z7',
+ value: {
+ issuer: {
+ issue() {
+ invoked += 1;
+ return foreignReference;
+ },
+ },
+ resolver: foreignCapability.resolver,
+ },
+ },
+ ];
+
+ const getterCapability = {};
+ Object.defineProperty(getterCapability, 'issuer', {
+ enumerable: true,
+ get() {
+ invoked += 1;
+ throw new Error('capability-getter-marker-X9Y8Z7');
+ },
+ });
+ attempts.push({
+ name: 'getter',
+ marker: 'capability-getter-marker-X9Y8Z7',
+ value: getterCapability,
+ });
+
+ const hostileProxy = new Proxy(
+ {},
+ {
+ get() {
+ invoked += 1;
+ throw new Error('capability-proxy-marker-X9Y8Z7');
+ },
+ ownKeys() {
+ invoked += 1;
+ throw new Error('capability-proxy-marker-X9Y8Z7');
+ },
+ },
+ );
+ attempts.push({
+ name: 'proxy',
+ marker: 'capability-proxy-marker-X9Y8Z7',
+ value: hostileProxy,
+ });
+
+ const { proxy: revokedProxy, revoke } = Proxy.revocable(realCapability, {});
+ revoke();
+ attempts.push({
+ name: 'revoked-proxy',
+ marker: 'revoked-proxy-marker-X9Y8Z7',
+ value: revokedProxy,
+ });
+
+ for (const attempt of attempts) {
+ expectSafeConfigFailure(
+ () =>
+ loadRuntimeConfigV1({
+ environment: enabledPushEnvironment(),
+ secretReferenceCapability: attempt.value,
+ }),
+ 'configuration.secret_reference_capability',
+ 'invalid_secret_reference_capability',
+ [attempt.marker],
+ );
+ }
+ assert.equal(invoked, 0);
+});
diff --git a/packages/config/test/runtime-v1.test.mjs b/packages/config/test/runtime-v1.test.mjs
new file mode 100644
index 00000000..c400eeea
--- /dev/null
+++ b/packages/config/test/runtime-v1.test.mjs
@@ -0,0 +1,349 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import { ConfigValidationErrorV1, loadRuntimeConfigV1 } from '../src/runtime-config/v1.ts';
+import { createSecretReferenceCapabilityV1 } from '@databreeze/provider-ports/v1';
+
+const secretReferenceCapability = createSecretReferenceCapabilityV1();
+
+function nonLocalEnvironment(profile) {
+ return [
+ ['DATABREEZE_PROFILE', profile],
+ ['DATABREEZE_OBJECT_STORAGE_MODE', 'remote'],
+ ['DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL', 'https://objects.example.test'],
+ ['DATABREEZE_OBJECT_STORAGE_REGION', 'sg-1'],
+ ['DATABREEZE_OBJECT_STORAGE_BUCKET', `databreeze-${profile}`],
+ ['DATABREEZE_OBJECT_STORAGE_CREDENTIAL_REF', `secret://${profile}/object-storage`],
+ ['DATABREEZE_OBJECT_STORAGE_FORCE_PATH_STYLE', 'false'],
+ ['DATABREEZE_EMAIL_MODE', 'disabled'],
+ ['DATABREEZE_PUSH_MODE', 'disabled'],
+ ['DATABREEZE_OCR_MODE', 'disabled'],
+ ['DATABREEZE_AI_MODE', 'disabled'],
+ ['DATABREEZE_PAYMENTS_MODE', 'disabled'],
+ ['DATABREEZE_TELEMETRY_MODE', 'disabled'],
+ ['DATABREEZE_SECRETS_MODE', 'remote'],
+ ['DATABREEZE_SECRETS_ENDPOINT_URL', 'https://secrets.example.test'],
+ ['DATABREEZE_SECRETS_NAMESPACE', `databreeze-${profile}`],
+ ];
+}
+
+function expectConfigIssue(action, expectedPath, expectedCode) {
+ assert.throws(action, (error) => {
+ assert.ok(error instanceof ConfigValidationErrorV1);
+ assert.ok(
+ error.issues.some((issue) => issue.path === expectedPath && issue.code === expectedCode),
+ `expected ${expectedCode} at ${expectedPath}, received ${JSON.stringify(error.issues)}`,
+ );
+ return true;
+ });
+}
+
+test('requires an explicit runtime profile', () => {
+ expectConfigIssue(() => loadRuntimeConfigV1({ environment: [] }), 'profile', 'required');
+});
+
+test('loads safe development defaults only after development is explicit', () => {
+ const config = loadRuntimeConfigV1({
+ environment: [['DATABREEZE_PROFILE', 'development']],
+ });
+
+ assert.equal(config.profile, 'development');
+ assert.deepEqual(config.providerPolicy, { timeoutMs: 10_000, maxAttempts: 3 });
+ assert.deepEqual(config.providers.objectStorage, {
+ mode: 'local',
+ endpointUrl: 'http://127.0.0.1:9000',
+ region: 'local',
+ bucket: 'databreeze-development',
+ forcePathStyle: true,
+ });
+ assert.equal(config.providers.email.mode, 'local');
+ assert.equal(config.providers.push.mode, 'disabled');
+ assert.equal(config.providers.ocr.mode, 'disabled');
+ assert.equal(config.providers.ai.mode, 'disabled');
+ assert.equal(config.providers.payments.mode, 'disabled');
+ assert.equal(config.providers.telemetry.mode, 'local');
+ assert.deepEqual(config.providers.secrets, { mode: 'memory', namespace: 'development' });
+});
+
+test('loads deterministic test defaults distinct from development', () => {
+ const config = loadRuntimeConfigV1({ environment: { DATABREEZE_PROFILE: 'test' } });
+
+ assert.equal(config.profile, 'test');
+ assert.equal(config.providers.objectStorage.bucket, 'databreeze-test');
+ assert.equal(config.providers.email.mode, 'disabled');
+ assert.equal(config.providers.telemetry.mode, 'disabled');
+ assert.deepEqual(config.providers.secrets, { mode: 'memory', namespace: 'test' });
+});
+
+for (const profile of ['preview', 'staging', 'production']) {
+ test(`loads an explicitly complete ${profile} profile`, () => {
+ const config = loadRuntimeConfigV1({
+ environment: nonLocalEnvironment(profile),
+ secretReferenceCapability,
+ });
+
+ assert.equal(config.profile, profile);
+ assert.equal(config.providers.objectStorage.mode, 'remote');
+ assert.equal(config.providers.objectStorage.bucket, `databreeze-${profile}`);
+ assert.deepEqual(
+ secretReferenceCapability.resolver.resolve(config.providers.objectStorage.credentialRef),
+ { namespace: profile, pathSegments: ['object-storage'] },
+ );
+ assert.deepEqual(config.providers.secrets, {
+ mode: 'remote',
+ endpointUrl: 'https://secrets.example.test',
+ namespace: `databreeze-${profile}`,
+ });
+ });
+}
+
+for (const profile of ['preview', 'staging', 'production']) {
+ test(`${profile} fails closed when provider selections are absent`, () => {
+ expectConfigIssue(
+ () => loadRuntimeConfigV1({ environment: [['DATABREEZE_PROFILE', profile]] }),
+ 'providers.objectStorage.mode',
+ 'required',
+ );
+ });
+}
+
+test('applies explicit overrides over environment and environment over local defaults', () => {
+ const config = loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_PROVIDER_TIMEOUT_MS', '2000'],
+ ['DATABREEZE_EMAIL_MODE', 'disabled'],
+ ],
+ overrides: {
+ providerPolicy: { timeoutMs: 3_000, maxAttempts: 4 },
+ providers: {
+ email: {
+ mode: 'local',
+ endpointUrl: 'smtp://localhost:2525',
+ fromAddress: 'notify@databreeze.local',
+ },
+ },
+ },
+ });
+
+ assert.deepEqual(config.providerPolicy, { timeoutMs: 3_000, maxAttempts: 4 });
+ assert.deepEqual(config.providers.email, {
+ mode: 'local',
+ endpointUrl: 'smtp://localhost:2525',
+ fromAddress: 'notify@databreeze.local',
+ });
+});
+
+test('rejects duplicate environment entries instead of choosing one', () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_PROFILE', 'production'],
+ ],
+ }),
+ 'environment.duplicate_key',
+ 'duplicate',
+ );
+});
+
+test('rejects unknown DataBreeze environment keys but ignores host environment keys', () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['PATH', 'not-product-configuration'],
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_UNKNOWN_OPTION', 'true'],
+ ],
+ }),
+ 'environment.unknown_key',
+ 'unknown_key',
+ );
+});
+
+test('rejects unknown nested override keys', () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [['DATABREEZE_PROFILE', 'development']],
+ overrides: { providers: { ai: { mode: 'disabled', apiKey: 'not-allowed' } } },
+ }),
+ 'overrides.unknown_key',
+ 'unknown_key',
+ );
+});
+
+for (const value of ['1e3', '01000', ' 1000', '1000 ']) {
+ test(`rejects ambiguous integer coercion from ${JSON.stringify(value)}`, () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_PROVIDER_TIMEOUT_MS', value],
+ ],
+ }),
+ 'providerPolicy.timeoutMs',
+ 'invalid_integer',
+ );
+ });
+}
+
+for (const value of ['1', 'TRUE', 'yes', 'false ']) {
+ test(`rejects ambiguous boolean coercion from ${JSON.stringify(value)}`, () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_OBJECT_STORAGE_FORCE_PATH_STYLE', value],
+ ],
+ }),
+ 'providers.objectStorage.forcePathStyle',
+ 'invalid_boolean',
+ );
+ });
+}
+
+test('rejects non-loopback cleartext origins even in development', () => {
+ expectConfigIssue(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL', 'http://objects.example.test'],
+ ],
+ }),
+ 'providers.objectStorage.endpointUrl',
+ 'unsafe_url',
+ );
+});
+
+test('rejects cleartext origins in production even when loopback', () => {
+ const environment = nonLocalEnvironment('production').map(([key, value]) =>
+ key === 'DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL'
+ ? [key, 'http://127.0.0.1:9000']
+ : [key, value],
+ );
+
+ expectConfigIssue(
+ () => loadRuntimeConfigV1({ environment }),
+ 'providers.objectStorage.endpointUrl',
+ 'unsafe_url',
+ );
+});
+
+test('rejects URL credentials without echoing them in the error', () => {
+ const exposed = 'top-secret-password';
+
+ assert.throws(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL', `http://user:${exposed}@127.0.0.1:9000`],
+ ],
+ }),
+ (error) => {
+ assert.ok(error instanceof ConfigValidationErrorV1);
+ assert.doesNotMatch(error.message, new RegExp(exposed));
+ assert.doesNotMatch(JSON.stringify(error), new RegExp(exposed));
+ return true;
+ },
+ );
+});
+
+test('rejects endpoint query credentials without echoing them in the error', () => {
+ const exposed = 'query-token-that-must-not-escape';
+
+ assert.throws(
+ () =>
+ loadRuntimeConfigV1({
+ environment: [
+ ['DATABREEZE_PROFILE', 'development'],
+ ['DATABREEZE_OBJECT_STORAGE_ENDPOINT_URL', `http://127.0.0.1:9000?token=${exposed}`],
+ ],
+ }),
+ (error) => {
+ assert.ok(error instanceof ConfigValidationErrorV1);
+ assert.ok(
+ error.issues.some(
+ (issue) =>
+ issue.path === 'providers.objectStorage.endpointUrl' && issue.code === 'unsafe_url',
+ ),
+ );
+ assert.doesNotMatch(error.message, new RegExp(exposed));
+ assert.doesNotMatch(JSON.stringify(error), new RegExp(exposed));
+ return true;
+ },
+ );
+});
+
+for (const reference of ['', 'super-secret-value', 'secret://production/changeme']) {
+ test(`rejects empty, raw, or placeholder credential input ${JSON.stringify(reference)}`, () => {
+ const environment = nonLocalEnvironment('production').map(([key, value]) =>
+ key === 'DATABREEZE_OBJECT_STORAGE_CREDENTIAL_REF' ? [key, reference] : [key, value],
+ );
+
+ assert.throws(
+ () => loadRuntimeConfigV1({ environment }),
+ (error) => {
+ assert.ok(error instanceof ConfigValidationErrorV1);
+ assert.ok(
+ error.issues.some(
+ (issue) =>
+ issue.path === 'providers.objectStorage.credentialRef' &&
+ issue.code === 'invalid_secret_reference',
+ ),
+ );
+ if (reference.length > 0) {
+ assert.doesNotMatch(error.message, new RegExp(reference.replaceAll('/', '\\/')));
+ assert.doesNotMatch(JSON.stringify(error), new RegExp(reference.replaceAll('/', '\\/')));
+ }
+ return true;
+ },
+ );
+ });
+}
+
+test('redacts valid secret references during string and JSON serialization', () => {
+ const config = loadRuntimeConfigV1({
+ environment: nonLocalEnvironment('production'),
+ secretReferenceCapability,
+ });
+ const reference = config.providers.objectStorage.credentialRef;
+
+ assert.equal(String(reference), '[REDACTED_SECRET_REFERENCE]');
+ assert.equal(JSON.stringify(reference), '"[REDACTED_SECRET_REFERENCE]"');
+ assert.doesNotMatch(JSON.stringify(config), /secret:\/\//);
+});
+
+test('returns a deeply immutable configuration graph', () => {
+ const config = loadRuntimeConfigV1({
+ environment: [['DATABREEZE_PROFILE', 'development']],
+ });
+
+ assert.equal(Object.isFrozen(config), true);
+ assert.equal(Object.isFrozen(config.providers), true);
+ assert.equal(Object.isFrozen(config.providers.objectStorage), true);
+ assert.equal(Object.isFrozen(config.providerPolicy), true);
+ assert.throws(() => {
+ config.providers.objectStorage.bucket = 'mutated';
+ }, TypeError);
+ assert.equal(config.providers.objectStorage.bucket, 'databreeze-development');
+});
+
+test('rejects settings on a provider explicitly disabled in a strict profile', () => {
+ const environment = [
+ ...nonLocalEnvironment('staging'),
+ ['DATABREEZE_AI_ENDPOINT_URL', 'https://ai.example.test'],
+ ];
+
+ expectConfigIssue(
+ () => loadRuntimeConfigV1({ environment }),
+ 'providers.ai.endpointUrl',
+ 'forbidden_when_disabled',
+ );
+});
diff --git a/packages/config/tsconfig.build.json b/packages/config/tsconfig.build.json
new file mode 100644
index 00000000..c85c6910
--- /dev/null
+++ b/packages/config/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "lib": ["ES2024", "DOM"],
+ "outDir": "dist",
+ "rewriteRelativeImportExtensions": true,
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json
new file mode 100644
index 00000000..fe24d784
--- /dev/null
+++ b/packages/config/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "lib": ["ES2024", "DOM"],
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts", "test/**/*.ts"]
+}
diff --git a/packages/config/turbo.json b/packages/config/turbo.json
new file mode 100644
index 00000000..25e76c6b
--- /dev/null
+++ b/packages/config/turbo.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "test": {
+ "outputs": []
+ }
+ }
+}
diff --git a/packages/contracts/COMPATIBILITY.md b/packages/contracts/COMPATIBILITY.md
new file mode 100644
index 00000000..5d33953a
--- /dev/null
+++ b/packages/contracts/COMPATIBILITY.md
@@ -0,0 +1,45 @@
+# Contract compatibility policy
+
+DataBreeze v1 contracts are published, conservative, and immutable. A schema's absolute `$id`,
+registry entry, path, and exact source bytes cannot change in place. Version-specific generated
+TypeScript, Python, and Kotlin public outputs are immutable for the same reason. This rule also
+applies to changes that would normally be described as additive or backward compatible: after
+publication, they require a new contract version and new absolute schema IDs.
+
+`public-outputs.json` is the explicit versioned inventory of generated public files and selected
+package JSON surfaces. It includes language-package metadata outside `/vN/` directories, such as the
+Python `pyproject.toml`, package-root `__init__.py`, and `py.typed`, plus the JavaScript package
+name, exports, and runtime dependency map. Every file below `generated/` must appear in the
+inventory.
+
+`compatibility/published.json` locks the SHA-256 digest of each reviewed version baseline. Each
+`compatibility/vN/baseline.json` locks every schema ID/path/byte digest, the version's exact
+public-output inventory entry, every inventoried generated public file, and each selected package
+surface. The check fails for removed or added v1 schemas, changed IDs or bytes, missing, changed,
+added, or unlisted generated outputs, public export drift, missing baselines, and baseline edits
+that do not match the published registry.
+
+Run the read-only checks with:
+
+```sh
+corepack pnpm contracts:check
+```
+
+That root gate checks generated drift, published compatibility, compile-time TypeScript fixture
+consumption, and TypeScript/Python/Kotlin runtime fixture parity.
+
+To publish a reviewed new version after its new schema IDs, generated outputs, and fixtures exist:
+
+```sh
+corepack pnpm --filter @databreeze/contracts compatibility:baseline -- --version 2 --approve-new-version
+```
+
+The update is deterministic and is a no-op when the same version is already current. It refuses to
+rewrite a published version. `--approve-new-version` records the caller's intent; it is not a
+substitute for repository review. An incompatible change therefore requires a new version/ID,
+consumer migration evidence, an updated shared fixture suite, and review of the new immutable
+baseline.
+
+There is deliberately no command that rewrites a published baseline or its registry digest. An
+exceptional coordinated repair remains a repository-review trust boundary; Task 21 must protect the
+inventory, baseline, and registry together in CI and review policy.
diff --git a/packages/contracts/README.md b/packages/contracts/README.md
index 65d19505..8196ab6c 100644
--- a/packages/contracts/README.md
+++ b/packages/contracts/README.md
@@ -1,3 +1,43 @@
# Contracts
Canonical OpenAPI, JSON Schema, event, typed-job, and compatibility definitions used to generate TypeScript, Kotlin, and Pydantic models.
+
+## Public interfaces
+
+- `manifest.json` is the deterministic registry for canonical source schemas.
+- `schemas/v1/*.schema.json` contains closed JSON Schema 2020-12 definitions with stable absolute IDs and references.
+- `@databreeze/contracts/v1` exports structural TypeScript contracts plus the generated
+ `parseV1Contract` runtime validator backed by the canonical schema registry.
+- `generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1` provides standard Kotlin models
+ and a public `parseV1Contract` model/validation entry point.
+- `generated/python/databreeze_contracts/v1` is the Pydantic v2 model package for Python consumers.
+- `public-outputs.json` explicitly inventories every version-relevant generated file and selected
+ package export surface.
+- `compatibility/` contains immutable reviewed hashes for published schema bytes and generated
+ version-specific public outputs; see [COMPATIBILITY.md](COMPATIBILITY.md).
+- Consumers import only the entry points declared in `package.json#exports`.
+
+The v1 base schemas provide UUID identifiers and UTC timestamps (IAM-001), complete tenant ancestry (IAM-019), correlation and actor metadata (AUD-004), RFC-compatible public problems (INT-021), idempotent commands (INT-004), cursor pages (INT-005), and canonical events (AUD-004, AUD-006, and INT-008). This is partial foundation coverage; it does not implement those requirements' persistence or runtime behavior.
+
+## Local commands
+
+```text
+corepack pnpm --filter @databreeze/contracts test
+corepack pnpm --filter @databreeze/contracts build
+corepack pnpm --filter @databreeze/contracts generate
+corepack pnpm --filter @databreeze/contracts generate:check
+corepack pnpm --filter @databreeze/contracts compatibility:check
+corepack pnpm --filter @databreeze/contracts fixtures:check
+```
+
+`generate` is the only supported way to update checked-in language models. Do not edit files below `generated/` by hand. `generate:check` regenerates into a temporary directory, byte-compares the complete expected file set, and reports missing, stale, or unexpected files without changing checked-in output. `test` compiles the real schemas with Ajv's JSON Schema 2020-12 validator and exercises generator behavior plus hand-authored protocol payloads. `build` compiles every manifest entry and checks generated-file drift.
+
+`compatibility:check` rejects in-place changes to a published contract baseline, public-output
+inventory, generated package metadata, or package export surface. `fixtures:check` runs every
+shared payload through the public generated TypeScript and Kotlin parsers and the generated
+Pydantic models.
+Root `pnpm contracts:check` runs generation drift, compatibility, and cross-runtime fixture parity.
+
+## Forbidden dependencies
+
+This package contains protocol definitions only. It must not import application or service implementations, persistence adapters, framework code, or generated consumer models.
diff --git a/packages/contracts/compatibility/published.json b/packages/contracts/compatibility/published.json
new file mode 100644
index 00000000..fbdd0098
--- /dev/null
+++ b/packages/contracts/compatibility/published.json
@@ -0,0 +1,10 @@
+{
+ "policyVersion": 1,
+ "versions": [
+ {
+ "contractVersion": 1,
+ "baseline": "compatibility/v1/baseline.json",
+ "sha256": "cb1e4833e517b96eaa2ca6c0583838619bfa494e3b634b7d855b60eb2fb242ff"
+ }
+ ]
+}
diff --git a/packages/contracts/compatibility/v1/baseline.json b/packages/contracts/compatibility/v1/baseline.json
new file mode 100644
index 00000000..6d257209
--- /dev/null
+++ b/packages/contracts/compatibility/v1/baseline.json
@@ -0,0 +1,151 @@
+{
+ "baselineFormat": 2,
+ "contractVersion": 1,
+ "schemaIdPrefix": "https://schemas.databreeze.dev/contracts/v1/",
+ "schemas": [
+ {
+ "name": "actor-metadata",
+ "id": "https://schemas.databreeze.dev/contracts/v1/actor-metadata",
+ "path": "schemas/v1/actor-metadata.schema.json",
+ "sha256": "fb9d12675478ae805bbe0163866c1cf8e4bb810dbe32ac780b1c1bf4975c856c"
+ },
+ {
+ "name": "command-envelope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/command-envelope",
+ "path": "schemas/v1/command-envelope.schema.json",
+ "sha256": "4bf310647800d038bfdaae8f6d89722862f89200589fe4e5e08a764b16b80002"
+ },
+ {
+ "name": "correlation-metadata",
+ "id": "https://schemas.databreeze.dev/contracts/v1/correlation-metadata",
+ "path": "schemas/v1/correlation-metadata.schema.json",
+ "sha256": "3e0b490036d2c709b0398cb7d3dcb2618d8e67bc6c64e90f62650b8b79d87b79"
+ },
+ {
+ "name": "cursor-page",
+ "id": "https://schemas.databreeze.dev/contracts/v1/cursor-page",
+ "path": "schemas/v1/cursor-page.schema.json",
+ "sha256": "8d009cb3b0e2232e5efdc297dfdd7b89f697410438d610c92414c014a5d44b19"
+ },
+ {
+ "name": "event-envelope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/event-envelope",
+ "path": "schemas/v1/event-envelope.schema.json",
+ "sha256": "54780e954b80a07de08d428a03c972cb9fcfe6682691521fe6d6c357b45753dd"
+ },
+ {
+ "name": "identifier",
+ "id": "https://schemas.databreeze.dev/contracts/v1/identifier",
+ "path": "schemas/v1/identifier.schema.json",
+ "sha256": "a4892d0fd11473356f53638758ae7017ff4b816765de65b2463ce8483fc699bb"
+ },
+ {
+ "name": "problem-details",
+ "id": "https://schemas.databreeze.dev/contracts/v1/problem-details",
+ "path": "schemas/v1/problem-details.schema.json",
+ "sha256": "c1209d3d234e75b13a84e7cfbbd2bec9f6d9f1daa3602ec2443f682770892272"
+ },
+ {
+ "name": "revision",
+ "id": "https://schemas.databreeze.dev/contracts/v1/revision",
+ "path": "schemas/v1/revision.schema.json",
+ "sha256": "6319ea8d21627cf73eb8d49b93e7529b87018e9e8ae5cd982fd70b419e0c7bb1"
+ },
+ {
+ "name": "tenant-scope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/tenant-scope",
+ "path": "schemas/v1/tenant-scope.schema.json",
+ "sha256": "a08f7d59f2fcd9c675298dc11f1fb05ba1c1f18757040980c6b214c3d9fff327"
+ },
+ {
+ "name": "utc-timestamp",
+ "id": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp",
+ "path": "schemas/v1/utc-timestamp.schema.json",
+ "sha256": "904b8736592d6c1e527f084daf0fa452447ada75dbfee8e9395ea55cf7c07dc5"
+ }
+ ],
+ "publicOutputInventory": {
+ "path": "public-outputs.json",
+ "versionEntrySha256": "786630d0f58b04485efedf643f4d9ffce09c2d0e4a3a5bf61f74f7046a1f731a"
+ },
+ "generatedPublicOutputs": [
+ {
+ "path": "generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt",
+ "sha256": "cd60b56f750e382ada6a9bbaba1baefef3a43e8fdb41ee6a12c89e1b8e1f9487"
+ },
+ {
+ "path": "generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt",
+ "sha256": "f4c98e5f568968160687ffaf9ddbba4db51e25266c5baf927cde8b24e6a2442a"
+ },
+ {
+ "path": "generated/python/databreeze_contracts/__init__.py",
+ "sha256": "36705a639b307e118392bd5000315e13aeecc0d605d5afab10c13f35cd79787b"
+ },
+ {
+ "path": "generated/python/databreeze_contracts/py.typed",
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+ },
+ {
+ "path": "generated/python/databreeze_contracts/v1/__init__.py",
+ "sha256": "785ff1b0fde763730070345f43b494203f149b10879683364fcbec0fcc35cb7e"
+ },
+ {
+ "path": "generated/python/databreeze_contracts/v1/_validation.py",
+ "sha256": "843bf675e577d3e9197e3e263a95bd483dac2d2d93c0987dc1b0785cb6a111cf"
+ },
+ {
+ "path": "generated/python/databreeze_contracts/v1/models.py",
+ "sha256": "435a3af5a513fd14fbf232c0f289b901f4b3722a12b9f3a559d59df404fee4ab"
+ },
+ {
+ "path": "generated/python/pyproject.toml",
+ "sha256": "1492e96a4ccb035f17ed777c853cbdbc0bca427bc808e269bbe3917c67c83c0f"
+ },
+ {
+ "path": "generated/typescript/v1/index.ts",
+ "sha256": "59e3b40f806d91a6d82b81a59e8937e6e0716d4eab44dbd5d2740e8eeb14912b"
+ },
+ {
+ "path": "generated/typescript/v1/validation.mjs",
+ "sha256": "0d48072b7dbc919e7ac1d9a3fbd8b864137594a98cf33e900d239564851fda7e"
+ }
+ ],
+ "publicPackageSurfaces": [
+ {
+ "path": "package.json",
+ "values": [
+ {
+ "pointer": "/dependencies",
+ "value": {
+ "ajv": "8.17.1",
+ "ajv-formats": "3.0.1"
+ }
+ },
+ {
+ "pointer": "/exports",
+ "value": {
+ ".": "./manifest.json",
+ "./v1": {
+ "types": "./generated/typescript/v1/index.ts",
+ "import": "./generated/typescript/v1/validation.mjs"
+ },
+ "./v1/actor-metadata": "./schemas/v1/actor-metadata.schema.json",
+ "./v1/command-envelope": "./schemas/v1/command-envelope.schema.json",
+ "./v1/correlation-metadata": "./schemas/v1/correlation-metadata.schema.json",
+ "./v1/cursor-page": "./schemas/v1/cursor-page.schema.json",
+ "./v1/event-envelope": "./schemas/v1/event-envelope.schema.json",
+ "./v1/identifier": "./schemas/v1/identifier.schema.json",
+ "./v1/problem-details": "./schemas/v1/problem-details.schema.json",
+ "./v1/revision": "./schemas/v1/revision.schema.json",
+ "./v1/tenant-scope": "./schemas/v1/tenant-scope.schema.json",
+ "./v1/utc-timestamp": "./schemas/v1/utc-timestamp.schema.json"
+ }
+ },
+ {
+ "pointer": "/name",
+ "value": "@databreeze/contracts"
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt b/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt
new file mode 100644
index 00000000..295949ab
--- /dev/null
+++ b/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt
@@ -0,0 +1,127 @@
+// Generated by @databreeze/contracts. DO NOT EDIT.
+
+package com.databreeze.contracts.v1
+
+public typealias JsonObject = Map
+
+public typealias Identifier = String
+
+public typealias Revision = Long
+
+public typealias UtcTimestamp = String
+
+public sealed interface TenantScope {
+ public val scopeType: String
+}
+
+public data class ActorMetadata(
+ public val actorId: Identifier,
+ public val actorType: String,
+)
+
+public data class CommandEnvelope(
+ public val actor: ActorMetadata,
+ public val commandId: Identifier,
+ public val commandType: String,
+ public val correlation: CorrelationMetadata,
+ public val data: TData,
+ public val idempotencyKey: String,
+ public val issuedAt: UtcTimestamp,
+ public val schemaVersion: Long,
+ public val tenantScope: TenantScope,
+)
+
+public data class CorrelationMetadata(
+ public val causationId: Identifier? = null,
+ public val correlationId: Identifier,
+ public val requestId: Identifier? = null,
+)
+
+public data class CursorPage(
+ public val data: List,
+ public val hasMore: Boolean,
+ public val nextCursor: String? = null,
+ public val snapshotAt: UtcTimestamp,
+) {
+ init {
+ require(if (hasMore) !nextCursor.isNullOrEmpty() else nextCursor == null) {
+ "nextCursor must match hasMore"
+ }
+ }
+}
+
+public data class EventEnvelope(
+ public val actor: ActorMetadata,
+ public val correlation: CorrelationMetadata,
+ public val data: TData,
+ public val entity: EventEnvelopeEntity,
+ public val eventId: Identifier,
+ public val eventType: String,
+ public val occurredAt: UtcTimestamp,
+ public val schemaVersion: Long,
+ public val sourceComponent: String,
+ public val tenantScope: TenantScope,
+)
+
+public data class EventEnvelopeEntity(
+ public val entityId: Identifier,
+ public val entityType: String,
+ public val revision: Revision,
+)
+
+public data class OrganizationScope(
+ public val organizationId: Identifier,
+) : TenantScope {
+ public override val scopeType: String = "organization"
+}
+
+public data class ProblemDetails(
+ public val code: String,
+ public val correlationId: Identifier,
+ public val currentRevision: Revision? = null,
+ public val detail: String? = null,
+ public val fieldErrors: List? = null,
+ public val instance: String? = null,
+ public val messageKey: String? = null,
+ public val rateLimit: ProblemDetailsRateLimit? = null,
+ public val remediationAction: String? = null,
+ public val retryAfterSeconds: Long? = null,
+ public val retryable: Boolean,
+ public val status: Long,
+ public val title: String? = null,
+ public val titleKey: String? = null,
+ public val type: String,
+) {
+ init {
+ require(titleKey != null || messageKey != null) {
+ "at least one required schema alternative must be present"
+ }
+ }
+}
+
+public data class ProblemDetailsFieldErrorsItem(
+ public val code: String,
+ public val field: String,
+)
+
+public data class ProblemDetailsRateLimit(
+ public val limit: Long? = null,
+ public val remaining: Long? = null,
+ public val resetAt: UtcTimestamp,
+ public val scope: String,
+)
+
+public data class ProjectScope(
+ public val organizationId: Identifier,
+ public val projectId: Identifier,
+ public val workspaceId: Identifier,
+) : TenantScope {
+ public override val scopeType: String = "project"
+}
+
+public data class WorkspaceScope(
+ public val organizationId: Identifier,
+ public val workspaceId: Identifier,
+) : TenantScope {
+ public override val scopeType: String = "workspace"
+}
diff --git a/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt b/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt
new file mode 100644
index 00000000..9422343e
--- /dev/null
+++ b/packages/contracts/generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt
@@ -0,0 +1,111 @@
+// Generated by @databreeze/contracts. DO NOT EDIT.
+
+package com.databreeze.contracts.v1
+
+import com.fasterxml.jackson.annotation.JsonSubTypes
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.networknt.schema.InputFormat
+import com.networknt.schema.SchemaLocation
+import com.networknt.schema.SchemaRegistry
+import com.networknt.schema.SpecificationVersion
+import java.util.Base64
+
+@JsonTypeInfo(
+ use = JsonTypeInfo.Id.NAME,
+ include = JsonTypeInfo.As.EXISTING_PROPERTY,
+ property = "scopeType",
+ visible = false,
+)
+@JsonSubTypes(
+ JsonSubTypes.Type(value = OrganizationScope::class, name = "organization"),
+ JsonSubTypes.Type(value = WorkspaceScope::class, name = "workspace"),
+ JsonSubTypes.Type(value = ProjectScope::class, name = "project"),
+)
+private interface TenantScopeMixin
+
+public sealed interface ContractV1ParseResult {
+ public val accepted: Boolean
+}
+
+public data class AcceptedV1Contract(public val value: Any) : ContractV1ParseResult {
+ public override val accepted: Boolean = true
+}
+
+public data object RejectedV1Contract : ContractV1ParseResult {
+ public override val accepted: Boolean = false
+}
+
+private val mapper: ObjectMapper = jacksonObjectMapper()
+ .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
+ .addMixIn(TenantScope::class.java, TenantScopeMixin::class.java)
+
+private fun decodeSchema(encoded: String): String =
+ String(Base64.getDecoder().decode(encoded), Charsets.UTF_8)
+
+private val schemaSources: Map = mapOf(
+ "https://schemas.databreeze.dev/contracts/v1/actor-metadata" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2FjdG9yLW1ldGFkYXRhIiwiJGNvbW1lbnQiOiJTaGFyZWQgYWN0b3IgaWRlbnRpdHkgbWV0YWRhdGEgdXNlZCBieSBjb21tYW5kcyBhbmQgZXZlbnRzOyBzdXBwb3J0cyBBVUQtMDA0LiIsInRpdGxlIjoiQWN0b3IgTWV0YWRhdGEiLCJkZXNjcmlwdGlvbiI6IlRoZSBzdGFibGUgdHlwZSBhbmQgaWRlbnRpZmllciBvZiB0aGUgcHJpbmNpcGFsIHJlc3BvbnNpYmxlIGZvciBhbiBhY3Rpb24uIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicmVxdWlyZWQiOlsiYWN0b3JUeXBlIiwiYWN0b3JJZCJdLCJwcm9wZXJ0aWVzIjp7ImFjdG9yVHlwZSI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6Il5bYS16XVthLXowLTlfLV17MCw2Mn0kIn0sImFjdG9ySWQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvaWRlbnRpZmllciJ9fX0="),
+ "https://schemas.databreeze.dev/contracts/v1/command-envelope" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2NvbW1hbmQtZW52ZWxvcGUiLCIkY29tbWVudCI6IlBhcnRpYWwgZm91bmRhdGlvbiBjb3ZlcmFnZSBmb3IgSU5ULTAwNCBhbmQgSUFNLTAxOS4iLCJ0aXRsZSI6IklkZW1wb3RlbnQgQ29tbWFuZCBFbnZlbG9wZSIsImRlc2NyaXB0aW9uIjoiVGhlIHNoYXJlZCBjbG9zZWQgZW52ZWxvcGUgZm9yIGFuIGlkZW1wb3RlbnQsIHRlbmFudC1zY29wZWQgY29tbWFuZC4iLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJyZXF1aXJlZCI6WyJjb21tYW5kSWQiLCJjb21tYW5kVHlwZSIsInNjaGVtYVZlcnNpb24iLCJ0ZW5hbnRTY29wZSIsImFjdG9yIiwiY29ycmVsYXRpb24iLCJpc3N1ZWRBdCIsImlkZW1wb3RlbmN5S2V5IiwiZGF0YSJdLCJwcm9wZXJ0aWVzIjp7ImNvbW1hbmRJZCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9pZGVudGlmaWVyIn0sImNvbW1hbmRUeXBlIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiXlthLXpdW2EtejAtOV8tXSooXFwuW2Etel1bYS16MC05Xy1dKikrJCJ9LCJzY2hlbWFWZXJzaW9uIjp7InR5cGUiOiJpbnRlZ2VyIiwibWluaW11bSI6MX0sInRlbmFudFNjb3BlIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3RlbmFudC1zY29wZSJ9LCJhY3RvciI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9hY3Rvci1tZXRhZGF0YSJ9LCJjb3JyZWxhdGlvbiI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9jb3JyZWxhdGlvbi1tZXRhZGF0YSJ9LCJpc3N1ZWRBdCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS91dGMtdGltZXN0YW1wIn0sImlkZW1wb3RlbmN5S2V5Ijp7InR5cGUiOiJzdHJpbmciLCJtaW5MZW5ndGgiOjEsIm1heExlbmd0aCI6MjU1fSwiZGF0YSI6eyJ0eXBlIjoib2JqZWN0In19fQ=="),
+ "https://schemas.databreeze.dev/contracts/v1/correlation-metadata" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2NvcnJlbGF0aW9uLW1ldGFkYXRhIiwiJGNvbW1lbnQiOiJQYXJ0aWFsIGZvdW5kYXRpb24gY292ZXJhZ2UgZm9yIEFVRC0wMDQgYW5kIElOVC0wMjEuIiwidGl0bGUiOiJDb3JyZWxhdGlvbiBNZXRhZGF0YSIsImRlc2NyaXB0aW9uIjoiQ29udGVudC1zYWZlIGlkZW50aWZpZXJzIHVzZWQgdG8gam9pbiBhIHJlcXVlc3Qgb3IgZXZlbnQgY2hhaW4uIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicmVxdWlyZWQiOlsiY29ycmVsYXRpb25JZCJdLCJwcm9wZXJ0aWVzIjp7ImNvcnJlbGF0aW9uSWQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvaWRlbnRpZmllciJ9LCJjYXVzYXRpb25JZCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9pZGVudGlmaWVyIn0sInJlcXVlc3RJZCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9pZGVudGlmaWVyIn19fQ=="),
+ "https://schemas.databreeze.dev/contracts/v1/cursor-page" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2N1cnNvci1wYWdlIiwiJGNvbW1lbnQiOiJTaGFyZWQgcGFnaW5hdGlvbiBzaGFwZSBzdXBwb3J0aW5nIElOVC0wMDUuIiwidGl0bGUiOiJDdXJzb3IgUGFnZSBFbnZlbG9wZSIsImRlc2NyaXB0aW9uIjoiVGhlIGNhbm9uaWNhbCBjbG9zZWQgcGFnZSBlbnZlbG9wZSB3aXRoIGEgVVRDIHNuYXBzaG90IGFuZCBvcGFxdWUgY29udGludWF0aW9uIGN1cnNvci4iLCJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJyZXF1aXJlZCI6WyJkYXRhIiwic25hcHNob3RBdCIsImhhc01vcmUiXSwicHJvcGVydGllcyI6eyJkYXRhIjp7InR5cGUiOiJhcnJheSIsIml0ZW1zIjp7fX0sIm5leHRDdXJzb3IiOnsidHlwZSI6InN0cmluZyIsIm1pbkxlbmd0aCI6MSwibWF4TGVuZ3RoIjo0MDk2fSwic25hcHNob3RBdCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS91dGMtdGltZXN0YW1wIn0sImhhc01vcmUiOnsidHlwZSI6ImJvb2xlYW4ifX0sImFsbE9mIjpbeyJpZiI6eyJwcm9wZXJ0aWVzIjp7Imhhc01vcmUiOnsiY29uc3QiOnRydWV9fSwicmVxdWlyZWQiOlsiaGFzTW9yZSJdfSwidGhlbiI6eyJwcm9wZXJ0aWVzIjp7Im5leHRDdXJzb3IiOnRydWV9LCJyZXF1aXJlZCI6WyJuZXh0Q3Vyc29yIl19LCJlbHNlIjp7Im5vdCI6eyJwcm9wZXJ0aWVzIjp7Im5leHRDdXJzb3IiOnRydWV9LCJyZXF1aXJlZCI6WyJuZXh0Q3Vyc29yIl19fX1dfQ=="),
+ "https://schemas.databreeze.dev/contracts/v1/event-envelope" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2V2ZW50LWVudmVsb3BlIiwiJGNvbW1lbnQiOiJDYW5vbmljYWwgZXZlbnQgYmFzZSBzdXBwb3J0aW5nIEFVRC0wMDQsIEFVRC0wMDYsIElBTS0wMTksIGFuZCBJTlQtMDA4LiIsInRpdGxlIjoiQ2Fub25pY2FsIEV2ZW50IEVudmVsb3BlIiwiZGVzY3JpcHRpb24iOiJUaGUgc2hhcmVkIGNsb3NlZCBlbnZlbG9wZSBmb3IgYSB2ZXJzaW9uZWQsIHRlbmFudC1zY29wZWQgZG9tYWluIGV2ZW50LiIsInR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2UsInJlcXVpcmVkIjpbImV2ZW50SWQiLCJldmVudFR5cGUiLCJzY2hlbWFWZXJzaW9uIiwidGVuYW50U2NvcGUiLCJlbnRpdHkiLCJhY3RvciIsImNvcnJlbGF0aW9uIiwic291cmNlQ29tcG9uZW50Iiwib2NjdXJyZWRBdCIsImRhdGEiXSwicHJvcGVydGllcyI6eyJldmVudElkIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIifSwiZXZlbnRUeXBlIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiXlthLXpdW2EtejAtOV8tXSooXFwuW2Etel1bYS16MC05Xy1dKikrJCJ9LCJzY2hlbWFWZXJzaW9uIjp7InR5cGUiOiJpbnRlZ2VyIiwibWluaW11bSI6MX0sInRlbmFudFNjb3BlIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3RlbmFudC1zY29wZSJ9LCJlbnRpdHkiOnsidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicmVxdWlyZWQiOlsiZW50aXR5VHlwZSIsImVudGl0eUlkIiwicmV2aXNpb24iXSwicHJvcGVydGllcyI6eyJlbnRpdHlUeXBlIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiXlthLXpdW2EtejAtOV8tXXswLDYyfSQifSwiZW50aXR5SWQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvaWRlbnRpZmllciJ9LCJyZXZpc2lvbiI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9yZXZpc2lvbiJ9fX0sImFjdG9yIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2FjdG9yLW1ldGFkYXRhIn0sImNvcnJlbGF0aW9uIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2NvcnJlbGF0aW9uLW1ldGFkYXRhIn0sInNvdXJjZUNvbXBvbmVudCI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6Il5bYS16XVthLXowLTlfLV17MCw2Mn0kIn0sIm9jY3VycmVkQXQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvdXRjLXRpbWVzdGFtcCJ9LCJkYXRhIjp7InR5cGUiOiJvYmplY3QifX19"),
+ "https://schemas.databreeze.dev/contracts/v1/identifier" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIiLCIkY29tbWVudCI6IlBhcnRpYWwgZm91bmRhdGlvbiBjb3ZlcmFnZSBmb3IgSUFNLTAwMS4iLCJ0aXRsZSI6IlN0YWJsZSBVVUlEIElkZW50aWZpZXIiLCJkZXNjcmlwdGlvbiI6IkFuIG9wYXF1ZSBzdGFibGUgVVVJRCBpZGVudGlmaWVyLiIsInR5cGUiOiJzdHJpbmciLCJmb3JtYXQiOiJ1dWlkIn0="),
+ "https://schemas.databreeze.dev/contracts/v1/problem-details" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3Byb2JsZW0tZGV0YWlscyIsIiRjb21tZW50IjoiUkZDIDc4MDctY29tcGF0aWJsZSBiYXNlIHdpdGggdGhlIHNhZmUgcHVibGljIGVycm9yIG1ldGFkYXRhIHJlcXVpcmVkIGJ5IElOVC0wMjEgYW5kIFdFQi0wMjEuIiwidGl0bGUiOiJQcm9ibGVtIERldGFpbHMiLCJkZXNjcmlwdGlvbiI6IkEgY2xvc2VkIFJGQyA3ODA3LWNvbXBhdGlibGUgcHJvYmxlbSBkb2N1bWVudCB3aXRoIERhdGFCcmVlemUgcHVibGljIGVycm9yIGV4dGVuc2lvbnMuIiwidHlwZSI6Im9iamVjdCIsImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZSwicmVxdWlyZWQiOlsidHlwZSIsInN0YXR1cyIsImNvZGUiLCJjb3JyZWxhdGlvbklkIiwicmV0cnlhYmxlIl0sImFueU9mIjpbeyJwcm9wZXJ0aWVzIjp7InRpdGxlS2V5Ijp0cnVlfSwicmVxdWlyZWQiOlsidGl0bGVLZXkiXX0seyJwcm9wZXJ0aWVzIjp7Im1lc3NhZ2VLZXkiOnRydWV9LCJyZXF1aXJlZCI6WyJtZXNzYWdlS2V5Il19XSwicHJvcGVydGllcyI6eyJ0eXBlIjp7InR5cGUiOiJzdHJpbmciLCJmb3JtYXQiOiJ1cmktcmVmZXJlbmNlIn0sInRpdGxlIjp7InR5cGUiOiJzdHJpbmciLCJtaW5MZW5ndGgiOjF9LCJ0aXRsZUtleSI6eyJ0eXBlIjoic3RyaW5nIiwibWluTGVuZ3RoIjoxLCJtYXhMZW5ndGgiOjI1NX0sInN0YXR1cyI6eyJ0eXBlIjoiaW50ZWdlciIsIm1pbmltdW0iOjEwMCwibWF4aW11bSI6NTk5fSwiZGV0YWlsIjp7InR5cGUiOiJzdHJpbmcifSwiaW5zdGFuY2UiOnsidHlwZSI6InN0cmluZyIsImZvcm1hdCI6InVyaS1yZWZlcmVuY2UifSwiY29kZSI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6Il5bQS1aXVtBLVowLTlfXXswLDEyN30kIn0sImNvcnJlbGF0aW9uSWQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvaWRlbnRpZmllciJ9LCJyZXRyeWFibGUiOnsidHlwZSI6ImJvb2xlYW4ifSwibWVzc2FnZUtleSI6eyJ0eXBlIjoic3RyaW5nIiwibWluTGVuZ3RoIjoxLCJtYXhMZW5ndGgiOjI1NX0sImZpZWxkRXJyb3JzIjp7InR5cGUiOiJhcnJheSIsIm1heEl0ZW1zIjoxMDAsIml0ZW1zIjp7InR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2UsInJlcXVpcmVkIjpbImZpZWxkIiwiY29kZSJdLCJwcm9wZXJ0aWVzIjp7ImZpZWxkIjp7InR5cGUiOiJzdHJpbmciLCJtaW5MZW5ndGgiOjEsIm1heExlbmd0aCI6MjU1fSwiY29kZSI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6Il5bQS1aXVtBLVowLTlfXXswLDEyN30kIn19fX0sInJldHJ5QWZ0ZXJTZWNvbmRzIjp7InR5cGUiOiJpbnRlZ2VyIiwibWluaW11bSI6MH0sImN1cnJlbnRSZXZpc2lvbiI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9yZXZpc2lvbiJ9LCJyZW1lZGlhdGlvbkFjdGlvbiI6eyJ0eXBlIjoic3RyaW5nIiwibWluTGVuZ3RoIjoxLCJtYXhMZW5ndGgiOjI1NX0sInJhdGVMaW1pdCI6eyJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJyZXF1aXJlZCI6WyJzY29wZSIsInJlc2V0QXQiXSwicHJvcGVydGllcyI6eyJzY29wZSI6eyJ0eXBlIjoic3RyaW5nIiwibWluTGVuZ3RoIjoxLCJtYXhMZW5ndGgiOjI1NX0sImxpbWl0Ijp7InR5cGUiOiJpbnRlZ2VyIiwibWluaW11bSI6MH0sInJlbWFpbmluZyI6eyJ0eXBlIjoiaW50ZWdlciIsIm1pbmltdW0iOjB9LCJyZXNldEF0Ijp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3V0Yy10aW1lc3RhbXAifX19fX0="),
+ "https://schemas.databreeze.dev/contracts/v1/revision" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3JldmlzaW9uIiwiJGNvbW1lbnQiOiJTdXBwb3J0cyBvcHRpbWlzdGljLWNvbmN1cnJlbmN5IHJldmlzaW9ucyBkZXNjcmliZWQgYnkgdGhlIGRvbWFpbiBhbmQgZGF0YSBtb2RlbC4iLCJ0aXRsZSI6IkVudGl0eSBSZXZpc2lvbiIsImRlc2NyaXB0aW9uIjoiQSBwb3NpdGl2ZSwgbW9ub3RvbmljYWxseSBpbmNyZWFzaW5nIGVudGl0eSByZXZpc2lvbi4iLCJ0eXBlIjoiaW50ZWdlciIsIm1pbmltdW0iOjF9"),
+ "https://schemas.databreeze.dev/contracts/v1/tenant-scope" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3RlbmFudC1zY29wZSIsIiRjb21tZW50IjoiUGFydGlhbCBmb3VuZGF0aW9uIGNvdmVyYWdlIGZvciBJQU0tMDE5LiIsInRpdGxlIjoiVGVuYW50IFNjb3BlIiwiZGVzY3JpcHRpb24iOiJBIGRpc2NyaW1pbmF0ZWQgdGVuYW50IHNjb3BlIGNvbnRhaW5pbmcgdGhlIGNvbXBsZXRlIGFuY2VzdHJ5IHJlcXVpcmVkIGF0IGl0cyBsZXZlbC4iLCJvbmVPZiI6W3siJHJlZiI6IiMvJGRlZnMvb3JnYW5pemF0aW9uU2NvcGUifSx7IiRyZWYiOiIjLyRkZWZzL3dvcmtzcGFjZVNjb3BlIn0seyIkcmVmIjoiIy8kZGVmcy9wcm9qZWN0U2NvcGUifV0sIiRkZWZzIjp7Im9yZ2FuaXphdGlvblNjb3BlIjp7InR5cGUiOiJvYmplY3QiLCJhZGRpdGlvbmFsUHJvcGVydGllcyI6ZmFsc2UsInJlcXVpcmVkIjpbInNjb3BlVHlwZSIsIm9yZ2FuaXphdGlvbklkIl0sInByb3BlcnRpZXMiOnsic2NvcGVUeXBlIjp7ImNvbnN0Ijoib3JnYW5pemF0aW9uIn0sIm9yZ2FuaXphdGlvbklkIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIifX19LCJ3b3Jrc3BhY2VTY29wZSI6eyJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJyZXF1aXJlZCI6WyJzY29wZVR5cGUiLCJvcmdhbml6YXRpb25JZCIsIndvcmtzcGFjZUlkIl0sInByb3BlcnRpZXMiOnsic2NvcGVUeXBlIjp7ImNvbnN0Ijoid29ya3NwYWNlIn0sIm9yZ2FuaXphdGlvbklkIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIifSwid29ya3NwYWNlSWQiOnsiJHJlZiI6Imh0dHBzOi8vc2NoZW1hcy5kYXRhYnJlZXplLmRldi9jb250cmFjdHMvdjEvaWRlbnRpZmllciJ9fX0sInByb2plY3RTY29wZSI6eyJ0eXBlIjoib2JqZWN0IiwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlLCJyZXF1aXJlZCI6WyJzY29wZVR5cGUiLCJvcmdhbml6YXRpb25JZCIsIndvcmtzcGFjZUlkIiwicHJvamVjdElkIl0sInByb3BlcnRpZXMiOnsic2NvcGVUeXBlIjp7ImNvbnN0IjoicHJvamVjdCJ9LCJvcmdhbml6YXRpb25JZCI6eyIkcmVmIjoiaHR0cHM6Ly9zY2hlbWFzLmRhdGFicmVlemUuZGV2L2NvbnRyYWN0cy92MS9pZGVudGlmaWVyIn0sIndvcmtzcGFjZUlkIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIifSwicHJvamVjdElkIjp7IiRyZWYiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL2lkZW50aWZpZXIifX19fX0="),
+ "https://schemas.databreeze.dev/contracts/v1/utc-timestamp" to decodeSchema("eyIkc2NoZW1hIjoiaHR0cHM6Ly9qc29uLXNjaGVtYS5vcmcvZHJhZnQvMjAyMC0xMi9zY2hlbWEiLCIkaWQiOiJodHRwczovL3NjaGVtYXMuZGF0YWJyZWV6ZS5kZXYvY29udHJhY3RzL3YxL3V0Yy10aW1lc3RhbXAiLCIkY29tbWVudCI6IlBhcnRpYWwgZm91bmRhdGlvbiBjb3ZlcmFnZSBmb3IgSUFNLTAwMSBhbmQgSU5ULTAwOC4iLCJ0aXRsZSI6IlVUQyBUaW1lc3RhbXAiLCJkZXNjcmlwdGlvbiI6IkFuIFJGQyAzMzM5IGRhdGUtdGltZSBub3JtYWxpemVkIHRvIFVUQyBhbmQgdGVybWluYXRlZCBieSB1cHBlcmNhc2UgWi4iLCJ0eXBlIjoic3RyaW5nIiwiZm9ybWF0IjoiZGF0ZS10aW1lIiwicGF0dGVybiI6IlokIn0="),
+)
+
+private val schemaRegistry: SchemaRegistry =
+ SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12) { builder ->
+ builder.schemas(schemaSources)
+ }
+
+private fun constructGeneratedModel(schemaId: String, payload: JsonNode): Any = when (schemaId) {
+ "https://schemas.databreeze.dev/contracts/v1/actor-metadata" -> mapper.treeToValue(payload, ActorMetadata::class.java)
+ "https://schemas.databreeze.dev/contracts/v1/command-envelope" -> mapper.convertValue(
+ payload,
+ object : TypeReference>() {},
+ )
+ "https://schemas.databreeze.dev/contracts/v1/correlation-metadata" -> mapper.treeToValue(payload, CorrelationMetadata::class.java)
+ "https://schemas.databreeze.dev/contracts/v1/cursor-page" -> mapper.convertValue(
+ payload,
+ object : TypeReference>() {},
+ )
+ "https://schemas.databreeze.dev/contracts/v1/event-envelope" -> mapper.convertValue(
+ payload,
+ object : TypeReference>() {},
+ )
+ "https://schemas.databreeze.dev/contracts/v1/identifier" -> mapper.treeToValue(payload, String::class.java)
+ "https://schemas.databreeze.dev/contracts/v1/problem-details" -> mapper.treeToValue(payload, ProblemDetails::class.java)
+ "https://schemas.databreeze.dev/contracts/v1/revision" -> payload.longValue()
+ "https://schemas.databreeze.dev/contracts/v1/tenant-scope" -> when (payload.required("scopeType").asText()) {
+ "organization" -> mapper.treeToValue(payload, OrganizationScope::class.java)
+ "workspace" -> mapper.treeToValue(payload, WorkspaceScope::class.java)
+ "project" -> mapper.treeToValue(payload, ProjectScope::class.java)
+ else -> error("Unknown scopeType discriminator")
+ }
+ "https://schemas.databreeze.dev/contracts/v1/utc-timestamp" -> mapper.treeToValue(payload, String::class.java)
+ else -> error("No generated Kotlin model for $schemaId")
+}
+
+public fun parseV1Contract(schemaId: String, payloadSource: String): ContractV1ParseResult {
+ require(schemaId in schemaSources) { "Unknown v1 contract schema: $schemaId" }
+ return try {
+ val payload = mapper.readTree(payloadSource)
+ val model = constructGeneratedModel(schemaId, payload)
+ val schema = schemaRegistry.getSchema(SchemaLocation.of(schemaId))
+ val errors = schema.validate(payloadSource, InputFormat.JSON) { executionContext ->
+ executionContext.executionConfig { configuration ->
+ configuration.formatAssertionsEnabled(true)
+ }
+ }
+ if (errors.isEmpty()) AcceptedV1Contract(model) else RejectedV1Contract
+ } catch (_: Exception) {
+ RejectedV1Contract
+ }
+}
diff --git a/packages/contracts/generated/python/databreeze_contracts/__init__.py b/packages/contracts/generated/python/databreeze_contracts/__init__.py
new file mode 100644
index 00000000..c11cb5fe
--- /dev/null
+++ b/packages/contracts/generated/python/databreeze_contracts/__init__.py
@@ -0,0 +1,5 @@
+# Generated by @databreeze/contracts. DO NOT EDIT.
+
+from . import v1
+
+__all__ = ["v1"]
diff --git a/packages/contracts/generated/python/databreeze_contracts/py.typed b/packages/contracts/generated/python/databreeze_contracts/py.typed
new file mode 100644
index 00000000..e69de29b
diff --git a/packages/contracts/generated/python/databreeze_contracts/v1/__init__.py b/packages/contracts/generated/python/databreeze_contracts/v1/__init__.py
new file mode 100644
index 00000000..4d609bc1
--- /dev/null
+++ b/packages/contracts/generated/python/databreeze_contracts/v1/__init__.py
@@ -0,0 +1,39 @@
+# Generated by @databreeze/contracts. DO NOT EDIT.
+
+from .models import (
+ ActorMetadata,
+ CommandEnvelope,
+ CorrelationMetadata,
+ CursorPage,
+ EventEnvelope,
+ EventEnvelopeEntity,
+ Identifier,
+ OrganizationScope,
+ ProblemDetails,
+ ProblemDetailsFieldErrorsItem,
+ ProblemDetailsRateLimit,
+ ProjectScope,
+ Revision,
+ TenantScope,
+ UtcTimestamp,
+ WorkspaceScope,
+)
+
+__all__ = [
+ "ActorMetadata",
+ "CommandEnvelope",
+ "CorrelationMetadata",
+ "CursorPage",
+ "EventEnvelope",
+ "EventEnvelopeEntity",
+ "Identifier",
+ "OrganizationScope",
+ "ProblemDetails",
+ "ProblemDetailsFieldErrorsItem",
+ "ProblemDetailsRateLimit",
+ "ProjectScope",
+ "Revision",
+ "TenantScope",
+ "UtcTimestamp",
+ "WorkspaceScope",
+]
diff --git a/packages/contracts/generated/python/databreeze_contracts/v1/_validation.py b/packages/contracts/generated/python/databreeze_contracts/v1/_validation.py
new file mode 100644
index 00000000..84db6c31
--- /dev/null
+++ b/packages/contracts/generated/python/databreeze_contracts/v1/_validation.py
@@ -0,0 +1,73 @@
+# Generated by @databreeze/contracts. DO NOT EDIT.
+
+from __future__ import annotations
+
+from collections.abc import Mapping
+from typing import Any
+
+
+def _require_string(value: Any, format_name: str) -> str:
+ if not isinstance(value, str):
+ raise TypeError(f"{format_name} must be a string")
+ return value
+
+
+def validate_uuid(value: Any) -> str:
+ from uuid import UUID
+
+ value = _require_string(value, "uuid")
+ candidate = value[9:] if value.lower().startswith("urn:uuid:") else value
+ try:
+ parsed = UUID(candidate)
+ except ValueError as error:
+ raise ValueError("value must be a UUID string") from error
+ if str(parsed) != candidate.lower():
+ raise ValueError("value must be a UUID string")
+ return value
+
+
+def validate_utc_timestamp(value: Any) -> str:
+ from rfc3339_validator import validate_rfc3339
+
+ value = _require_string(value, "date-time")
+ valid = bool(validate_rfc3339(value))
+ if not valid:
+ prefix, separator, seconds = value.rpartition(":")
+ valid = bool(
+ separator
+ and prefix.endswith(("T23:59", "t23:59"))
+ and seconds.startswith("60")
+ and validate_rfc3339(f"{prefix}:59{seconds[2:]}")
+ )
+ if not value.endswith("Z") or not valid:
+ raise ValueError("value must be an RFC 3339 date-time ending in uppercase Z")
+ return value
+
+
+def validate_uri_reference(value: Any) -> str:
+ from rfc3986_validator import validate_rfc3986
+
+ value = _require_string(value, "uri-reference")
+ if validate_rfc3986(value, rule="URI_reference") is None:
+ raise ValueError("value must be an RFC 3986 URI reference")
+ return value
+
+
+def reject_explicit_null_properties(
+ value: Any, field_names: frozenset[str]
+) -> Any:
+ if isinstance(value, Mapping):
+ null_fields = sorted(
+ field_name
+ for field_name in field_names
+ if field_name in value and value[field_name] is None
+ )
+ if null_fields:
+ raise ValueError(f"null is not allowed for: {', '.join(null_fields)}")
+ return value
+
+
+def serialization_options(options: dict[str, Any]) -> dict[str, Any]:
+ result = dict(options)
+ result.setdefault("exclude_unset", True)
+ return result
diff --git a/packages/contracts/generated/python/databreeze_contracts/v1/models.py b/packages/contracts/generated/python/databreeze_contracts/v1/models.py
new file mode 100644
index 00000000..00438505
--- /dev/null
+++ b/packages/contracts/generated/python/databreeze_contracts/v1/models.py
@@ -0,0 +1,163 @@
+# Generated by @databreeze/contracts. DO NOT EDIT.
+
+from __future__ import annotations
+
+from typing import Annotated, Any, Generic, Literal, Self, TypeAlias, TypeVar
+
+from pydantic import (
+ AfterValidator,
+ BaseModel,
+ ConfigDict,
+ Field,
+ StrictBool,
+ StrictStr,
+ StringConstraints,
+ model_validator,
+)
+
+from ._validation import (
+ reject_explicit_null_properties,
+ serialization_options,
+ validate_uri_reference,
+ validate_utc_timestamp,
+ validate_uuid,
+)
+
+JsonObject: TypeAlias = dict[str, Any]
+TData = TypeVar("TData")
+TItem = TypeVar("TItem")
+
+
+class ClosedModel(BaseModel):
+ model_config = ConfigDict(extra="forbid")
+
+ @model_validator(mode="before")
+ @classmethod
+ def reject_explicit_nulls(cls, value: Any) -> Any:
+ return reject_explicit_null_properties(value, frozenset(cls.model_fields))
+
+ def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:
+ return super().model_dump(*args, **serialization_options(kwargs))
+
+ def model_dump_json(self, *args: Any, **kwargs: Any) -> str:
+ return super().model_dump_json(*args, **serialization_options(kwargs))
+
+Identifier: TypeAlias = Annotated[StrictStr, AfterValidator(validate_uuid)]
+
+Revision: TypeAlias = Annotated[int, Field(strict=True, ge=1)]
+
+UtcTimestamp: TypeAlias = Annotated[StrictStr, AfterValidator(validate_utc_timestamp)]
+
+class ActorMetadata(ClosedModel):
+ actorId: Identifier
+ actorType: Annotated[StrictStr, StringConstraints(pattern=r"^[a-z][a-z0-9_-]{0,62}$")]
+
+class CommandEnvelope(ClosedModel, Generic[TData]):
+ actor: ActorMetadata
+ commandId: Identifier
+ commandType: Annotated[StrictStr, StringConstraints(pattern=r"^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$")]
+ correlation: CorrelationMetadata
+ data: TData
+ idempotencyKey: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)]
+ issuedAt: UtcTimestamp
+ schemaVersion: Annotated[int, Field(strict=True, ge=1)]
+ tenantScope: TenantScope
+
+class CorrelationMetadata(ClosedModel):
+ causationId: Identifier | None = None
+ correlationId: Identifier
+ requestId: Identifier | None = None
+
+class CursorPage(ClosedModel, Generic[TItem]):
+ data: list[TItem]
+ hasMore: StrictBool
+ nextCursor: Annotated[StrictStr, StringConstraints(min_length=1, max_length=4096)] | None = None
+ snapshotAt: UtcTimestamp
+
+ @model_validator(mode="after")
+ def enforce_conditional_fields(self) -> Self:
+ if self.hasMore is True and self.nextCursor is None:
+ raise ValueError("nextCursor is required for this hasMore value")
+ if self.hasMore is False and self.nextCursor is not None:
+ raise ValueError("nextCursor is forbidden for this hasMore value")
+ return self
+
+class EventEnvelope(ClosedModel, Generic[TData]):
+ actor: ActorMetadata
+ correlation: CorrelationMetadata
+ data: TData
+ entity: EventEnvelopeEntity
+ eventId: Identifier
+ eventType: Annotated[StrictStr, StringConstraints(pattern=r"^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$")]
+ occurredAt: UtcTimestamp
+ schemaVersion: Annotated[int, Field(strict=True, ge=1)]
+ sourceComponent: Annotated[StrictStr, StringConstraints(pattern=r"^[a-z][a-z0-9_-]{0,62}$")]
+ tenantScope: TenantScope
+
+class EventEnvelopeEntity(ClosedModel):
+ entityId: Identifier
+ entityType: Annotated[StrictStr, StringConstraints(pattern=r"^[a-z][a-z0-9_-]{0,62}$")]
+ revision: Revision
+
+class OrganizationScope(ClosedModel):
+ organizationId: Identifier
+ scopeType: Literal["organization"]
+
+class ProblemDetails(ClosedModel):
+ code: Annotated[StrictStr, StringConstraints(pattern=r"^[A-Z][A-Z0-9_]{0,127}$")]
+ correlationId: Identifier
+ currentRevision: Revision | None = None
+ detail: StrictStr | None = None
+ fieldErrors: Annotated[list[ProblemDetailsFieldErrorsItem], Field(max_length=100)] | None = None
+ instance: Annotated[StrictStr, AfterValidator(validate_uri_reference)] | None = None
+ messageKey: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)] | None = None
+ rateLimit: ProblemDetailsRateLimit | None = None
+ remediationAction: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)] | None = None
+ retryAfterSeconds: Annotated[int, Field(strict=True, ge=0)] | None = None
+ retryable: StrictBool
+ status: Annotated[int, Field(strict=True, ge=100, le=599)]
+ title: Annotated[StrictStr, StringConstraints(min_length=1)] | None = None
+ titleKey: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)] | None = None
+ type: Annotated[StrictStr, AfterValidator(validate_uri_reference)]
+
+ @model_validator(mode="after")
+ def require_schema_alternative(self) -> Self:
+ if not ((self.titleKey is not None) or (self.messageKey is not None)):
+ raise ValueError("at least one required schema alternative must be present")
+ return self
+
+class ProblemDetailsFieldErrorsItem(ClosedModel):
+ code: Annotated[StrictStr, StringConstraints(pattern=r"^[A-Z][A-Z0-9_]{0,127}$")]
+ field: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)]
+
+class ProblemDetailsRateLimit(ClosedModel):
+ limit: Annotated[int, Field(strict=True, ge=0)] | None = None
+ remaining: Annotated[int, Field(strict=True, ge=0)] | None = None
+ resetAt: UtcTimestamp
+ scope: Annotated[StrictStr, StringConstraints(min_length=1, max_length=255)]
+
+class ProjectScope(ClosedModel):
+ organizationId: Identifier
+ projectId: Identifier
+ scopeType: Literal["project"]
+ workspaceId: Identifier
+
+class WorkspaceScope(ClosedModel):
+ organizationId: Identifier
+ scopeType: Literal["workspace"]
+ workspaceId: Identifier
+
+TenantScope: TypeAlias = Annotated[OrganizationScope | WorkspaceScope | ProjectScope, Field(discriminator="scopeType")]
+
+ActorMetadata.model_rebuild()
+CommandEnvelope.model_rebuild()
+CorrelationMetadata.model_rebuild()
+CursorPage.model_rebuild()
+EventEnvelope.model_rebuild()
+EventEnvelopeEntity.model_rebuild()
+OrganizationScope.model_rebuild()
+ProblemDetails.model_rebuild()
+ProblemDetailsFieldErrorsItem.model_rebuild()
+ProblemDetailsRateLimit.model_rebuild()
+ProjectScope.model_rebuild()
+WorkspaceScope.model_rebuild()
diff --git a/packages/contracts/generated/python/pyproject.toml b/packages/contracts/generated/python/pyproject.toml
new file mode 100644
index 00000000..9abdceb1
--- /dev/null
+++ b/packages/contracts/generated/python/pyproject.toml
@@ -0,0 +1,11 @@
+# Generated by @databreeze/contracts. DO NOT EDIT.
+
+[project]
+name = "databreeze-contracts"
+version = "1.0.0"
+requires-python = ">=3.13"
+dependencies = [
+ "pydantic==2.13.4",
+ "rfc3339-validator==0.1.4",
+ "rfc3986-validator==0.1.1",
+]
diff --git a/packages/contracts/generated/typescript/v1/index.ts b/packages/contracts/generated/typescript/v1/index.ts
new file mode 100644
index 00000000..17114417
--- /dev/null
+++ b/packages/contracts/generated/typescript/v1/index.ts
@@ -0,0 +1,128 @@
+// Generated by @databreeze/contracts. DO NOT EDIT.
+
+export type JsonPrimitive = boolean | number | string | null;
+export type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;
+export type JsonObject = { readonly [key: string]: JsonValue };
+
+export interface ActorMetadata {
+ readonly actorId: Identifier;
+ readonly actorType: string;
+}
+
+export interface CommandEnvelope {
+ readonly actor: ActorMetadata;
+ readonly commandId: Identifier;
+ readonly commandType: string;
+ readonly correlation: CorrelationMetadata;
+ readonly data: TData;
+ readonly idempotencyKey: string;
+ readonly issuedAt: UtcTimestamp;
+ readonly schemaVersion: number;
+ readonly tenantScope: TenantScope;
+}
+
+export interface CorrelationMetadata {
+ readonly causationId?: Identifier;
+ readonly correlationId: Identifier;
+ readonly requestId?: Identifier;
+}
+
+export interface CursorPageFields {
+ readonly data: readonly TItem[];
+ readonly hasMore: boolean;
+ readonly nextCursor?: string;
+ readonly snapshotAt: UtcTimestamp;
+}
+
+export type CursorPage = Omit, "hasMore" | "nextCursor"> & (
+ | { readonly hasMore: true; readonly nextCursor: string }
+ | { readonly hasMore: false; readonly nextCursor?: never }
+);
+
+export interface EventEnvelope {
+ readonly actor: ActorMetadata;
+ readonly correlation: CorrelationMetadata;
+ readonly data: TData;
+ readonly entity: EventEnvelopeEntity;
+ readonly eventId: Identifier;
+ readonly eventType: string;
+ readonly occurredAt: UtcTimestamp;
+ readonly schemaVersion: number;
+ readonly sourceComponent: string;
+ readonly tenantScope: TenantScope;
+}
+
+export interface EventEnvelopeEntity {
+ readonly entityId: Identifier;
+ readonly entityType: string;
+ readonly revision: Revision;
+}
+
+export type Identifier = string;
+
+export interface OrganizationScope {
+ readonly organizationId: Identifier;
+ readonly scopeType: "organization";
+}
+
+export interface ProblemDetailsFields {
+ readonly code: string;
+ readonly correlationId: Identifier;
+ readonly currentRevision?: Revision;
+ readonly detail?: string;
+ readonly fieldErrors?: readonly ProblemDetailsFieldErrorsItem[];
+ readonly instance?: string;
+ readonly messageKey?: string;
+ readonly rateLimit?: ProblemDetailsRateLimit;
+ readonly remediationAction?: string;
+ readonly retryAfterSeconds?: number;
+ readonly retryable: boolean;
+ readonly status: number;
+ readonly title?: string;
+ readonly titleKey?: string;
+ readonly type: string;
+}
+
+export type ProblemDetails = ProblemDetailsFields & ({ readonly titleKey: string } | { readonly messageKey: string });
+
+export interface ProblemDetailsFieldErrorsItem {
+ readonly code: string;
+ readonly field: string;
+}
+
+export interface ProblemDetailsRateLimit {
+ readonly limit?: number;
+ readonly remaining?: number;
+ readonly resetAt: UtcTimestamp;
+ readonly scope: string;
+}
+
+export interface ProjectScope {
+ readonly organizationId: Identifier;
+ readonly projectId: Identifier;
+ readonly scopeType: "project";
+ readonly workspaceId: Identifier;
+}
+
+export type Revision = number;
+
+export type TenantScope = OrganizationScope | WorkspaceScope | ProjectScope;
+
+export type UtcTimestamp = string;
+
+export interface WorkspaceScope {
+ readonly organizationId: Identifier;
+ readonly scopeType: "workspace";
+ readonly workspaceId: Identifier;
+}
+
+export type ContractV1SchemaId = "https://schemas.databreeze.dev/contracts/v1/actor-metadata" | "https://schemas.databreeze.dev/contracts/v1/command-envelope" | "https://schemas.databreeze.dev/contracts/v1/correlation-metadata" | "https://schemas.databreeze.dev/contracts/v1/cursor-page" | "https://schemas.databreeze.dev/contracts/v1/event-envelope" | "https://schemas.databreeze.dev/contracts/v1/identifier" | "https://schemas.databreeze.dev/contracts/v1/problem-details" | "https://schemas.databreeze.dev/contracts/v1/revision" | "https://schemas.databreeze.dev/contracts/v1/tenant-scope" | "https://schemas.databreeze.dev/contracts/v1/utc-timestamp";
+
+export type ContractV1ParseResult =
+ | { readonly accepted: true; readonly value: TValue }
+ | { readonly accepted: false };
+
+export declare function parseV1Contract(
+ schemaId: ContractV1SchemaId,
+ payload: unknown,
+): ContractV1ParseResult;
diff --git a/packages/contracts/generated/typescript/v1/validation.mjs b/packages/contracts/generated/typescript/v1/validation.mjs
new file mode 100644
index 00000000..b4cc1597
--- /dev/null
+++ b/packages/contracts/generated/typescript/v1/validation.mjs
@@ -0,0 +1,35 @@
+// Generated by @databreeze/contracts. DO NOT EDIT.
+
+import Ajv2020 from 'ajv/dist/2020.js';
+import addFormats from 'ajv-formats';
+
+const schemas = [
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/actor-metadata","$comment":"Shared actor identity metadata used by commands and events; supports AUD-004.","title":"Actor Metadata","description":"The stable type and identifier of the principal responsible for an action.","type":"object","additionalProperties":false,"required":["actorType","actorId"],"properties":{"actorType":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,62}$"},"actorId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/command-envelope","$comment":"Partial foundation coverage for INT-004 and IAM-019.","title":"Idempotent Command Envelope","description":"The shared closed envelope for an idempotent, tenant-scoped command.","type":"object","additionalProperties":false,"required":["commandId","commandType","schemaVersion","tenantScope","actor","correlation","issuedAt","idempotencyKey","data"],"properties":{"commandId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"commandType":{"type":"string","pattern":"^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$"},"schemaVersion":{"type":"integer","minimum":1},"tenantScope":{"$ref":"https://schemas.databreeze.dev/contracts/v1/tenant-scope"},"actor":{"$ref":"https://schemas.databreeze.dev/contracts/v1/actor-metadata"},"correlation":{"$ref":"https://schemas.databreeze.dev/contracts/v1/correlation-metadata"},"issuedAt":{"$ref":"https://schemas.databreeze.dev/contracts/v1/utc-timestamp"},"idempotencyKey":{"type":"string","minLength":1,"maxLength":255},"data":{"type":"object"}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/correlation-metadata","$comment":"Partial foundation coverage for AUD-004 and INT-021.","title":"Correlation Metadata","description":"Content-safe identifiers used to join a request or event chain.","type":"object","additionalProperties":false,"required":["correlationId"],"properties":{"correlationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"causationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"requestId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/cursor-page","$comment":"Shared pagination shape supporting INT-005.","title":"Cursor Page Envelope","description":"The canonical closed page envelope with a UTC snapshot and opaque continuation cursor.","type":"object","additionalProperties":false,"required":["data","snapshotAt","hasMore"],"properties":{"data":{"type":"array","items":{}},"nextCursor":{"type":"string","minLength":1,"maxLength":4096},"snapshotAt":{"$ref":"https://schemas.databreeze.dev/contracts/v1/utc-timestamp"},"hasMore":{"type":"boolean"}},"allOf":[{"if":{"properties":{"hasMore":{"const":true}},"required":["hasMore"]},"then":{"properties":{"nextCursor":true},"required":["nextCursor"]},"else":{"not":{"properties":{"nextCursor":true},"required":["nextCursor"]}}}]},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/event-envelope","$comment":"Canonical event base supporting AUD-004, AUD-006, IAM-019, and INT-008.","title":"Canonical Event Envelope","description":"The shared closed envelope for a versioned, tenant-scoped domain event.","type":"object","additionalProperties":false,"required":["eventId","eventType","schemaVersion","tenantScope","entity","actor","correlation","sourceComponent","occurredAt","data"],"properties":{"eventId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"eventType":{"type":"string","pattern":"^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$"},"schemaVersion":{"type":"integer","minimum":1},"tenantScope":{"$ref":"https://schemas.databreeze.dev/contracts/v1/tenant-scope"},"entity":{"type":"object","additionalProperties":false,"required":["entityType","entityId","revision"],"properties":{"entityType":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,62}$"},"entityId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"revision":{"$ref":"https://schemas.databreeze.dev/contracts/v1/revision"}}},"actor":{"$ref":"https://schemas.databreeze.dev/contracts/v1/actor-metadata"},"correlation":{"$ref":"https://schemas.databreeze.dev/contracts/v1/correlation-metadata"},"sourceComponent":{"type":"string","pattern":"^[a-z][a-z0-9_-]{0,62}$"},"occurredAt":{"$ref":"https://schemas.databreeze.dev/contracts/v1/utc-timestamp"},"data":{"type":"object"}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/identifier","$comment":"Partial foundation coverage for IAM-001.","title":"Stable UUID Identifier","description":"An opaque stable UUID identifier.","type":"string","format":"uuid"},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/problem-details","$comment":"RFC 7807-compatible base with the safe public error metadata required by INT-021 and WEB-021.","title":"Problem Details","description":"A closed RFC 7807-compatible problem document with DataBreeze public error extensions.","type":"object","additionalProperties":false,"required":["type","status","code","correlationId","retryable"],"anyOf":[{"properties":{"titleKey":true},"required":["titleKey"]},{"properties":{"messageKey":true},"required":["messageKey"]}],"properties":{"type":{"type":"string","format":"uri-reference"},"title":{"type":"string","minLength":1},"titleKey":{"type":"string","minLength":1,"maxLength":255},"status":{"type":"integer","minimum":100,"maximum":599},"detail":{"type":"string"},"instance":{"type":"string","format":"uri-reference"},"code":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"},"correlationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"retryable":{"type":"boolean"},"messageKey":{"type":"string","minLength":1,"maxLength":255},"fieldErrors":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["field","code"],"properties":{"field":{"type":"string","minLength":1,"maxLength":255},"code":{"type":"string","pattern":"^[A-Z][A-Z0-9_]{0,127}$"}}}},"retryAfterSeconds":{"type":"integer","minimum":0},"currentRevision":{"$ref":"https://schemas.databreeze.dev/contracts/v1/revision"},"remediationAction":{"type":"string","minLength":1,"maxLength":255},"rateLimit":{"type":"object","additionalProperties":false,"required":["scope","resetAt"],"properties":{"scope":{"type":"string","minLength":1,"maxLength":255},"limit":{"type":"integer","minimum":0},"remaining":{"type":"integer","minimum":0},"resetAt":{"$ref":"https://schemas.databreeze.dev/contracts/v1/utc-timestamp"}}}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/revision","$comment":"Supports optimistic-concurrency revisions described by the domain and data model.","title":"Entity Revision","description":"A positive, monotonically increasing entity revision.","type":"integer","minimum":1},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/tenant-scope","$comment":"Partial foundation coverage for IAM-019.","title":"Tenant Scope","description":"A discriminated tenant scope containing the complete ancestry required at its level.","oneOf":[{"$ref":"#/$defs/organizationScope"},{"$ref":"#/$defs/workspaceScope"},{"$ref":"#/$defs/projectScope"}],"$defs":{"organizationScope":{"type":"object","additionalProperties":false,"required":["scopeType","organizationId"],"properties":{"scopeType":{"const":"organization"},"organizationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"}}},"workspaceScope":{"type":"object","additionalProperties":false,"required":["scopeType","organizationId","workspaceId"],"properties":{"scopeType":{"const":"workspace"},"organizationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"workspaceId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"}}},"projectScope":{"type":"object","additionalProperties":false,"required":["scopeType","organizationId","workspaceId","projectId"],"properties":{"scopeType":{"const":"project"},"organizationId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"workspaceId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"},"projectId":{"$ref":"https://schemas.databreeze.dev/contracts/v1/identifier"}}}}},
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://schemas.databreeze.dev/contracts/v1/utc-timestamp","$comment":"Partial foundation coverage for IAM-001 and INT-008.","title":"UTC Timestamp","description":"An RFC 3339 date-time normalized to UTC and terminated by uppercase Z.","type":"string","format":"date-time","pattern":"Z$"},
+];
+
+const ajv = new Ajv2020({ allErrors: true, strict: true });
+addFormats(ajv);
+for (const schema of schemas) ajv.addSchema(schema);
+
+const validators = new Map(
+ schemas.map((schema) => {
+ const validate = ajv.getSchema(schema.$id);
+ if (!validate) throw new Error(`No generated validator for ${schema.$id}`);
+ return [schema.$id, validate];
+ }),
+);
+
+export function parseV1Contract(schemaId, payload) {
+ const validate = validators.get(schemaId);
+ if (!validate) throw new TypeError(`Unknown v1 contract schema: ${schemaId}`);
+ return validate(payload) ? { accepted: true, value: payload } : { accepted: false };
+}
diff --git a/packages/contracts/manifest.json b/packages/contracts/manifest.json
new file mode 100644
index 00000000..e76ffe63
--- /dev/null
+++ b/packages/contracts/manifest.json
@@ -0,0 +1,56 @@
+{
+ "draft": "https://json-schema.org/draft/2020-12/schema",
+ "version": 1,
+ "schemas": [
+ {
+ "name": "actor-metadata",
+ "id": "https://schemas.databreeze.dev/contracts/v1/actor-metadata",
+ "path": "schemas/v1/actor-metadata.schema.json"
+ },
+ {
+ "name": "command-envelope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/command-envelope",
+ "path": "schemas/v1/command-envelope.schema.json"
+ },
+ {
+ "name": "correlation-metadata",
+ "id": "https://schemas.databreeze.dev/contracts/v1/correlation-metadata",
+ "path": "schemas/v1/correlation-metadata.schema.json"
+ },
+ {
+ "name": "cursor-page",
+ "id": "https://schemas.databreeze.dev/contracts/v1/cursor-page",
+ "path": "schemas/v1/cursor-page.schema.json"
+ },
+ {
+ "name": "event-envelope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/event-envelope",
+ "path": "schemas/v1/event-envelope.schema.json"
+ },
+ {
+ "name": "identifier",
+ "id": "https://schemas.databreeze.dev/contracts/v1/identifier",
+ "path": "schemas/v1/identifier.schema.json"
+ },
+ {
+ "name": "problem-details",
+ "id": "https://schemas.databreeze.dev/contracts/v1/problem-details",
+ "path": "schemas/v1/problem-details.schema.json"
+ },
+ {
+ "name": "revision",
+ "id": "https://schemas.databreeze.dev/contracts/v1/revision",
+ "path": "schemas/v1/revision.schema.json"
+ },
+ {
+ "name": "tenant-scope",
+ "id": "https://schemas.databreeze.dev/contracts/v1/tenant-scope",
+ "path": "schemas/v1/tenant-scope.schema.json"
+ },
+ {
+ "name": "utc-timestamp",
+ "id": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp",
+ "path": "schemas/v1/utc-timestamp.schema.json"
+ }
+ ]
+}
diff --git a/packages/contracts/package.json b/packages/contracts/package.json
new file mode 100644
index 00000000..2abb1818
--- /dev/null
+++ b/packages/contracts/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "@databreeze/contracts",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ ".": "./manifest.json",
+ "./v1": {
+ "types": "./generated/typescript/v1/index.ts",
+ "import": "./generated/typescript/v1/validation.mjs"
+ },
+ "./v1/actor-metadata": "./schemas/v1/actor-metadata.schema.json",
+ "./v1/command-envelope": "./schemas/v1/command-envelope.schema.json",
+ "./v1/correlation-metadata": "./schemas/v1/correlation-metadata.schema.json",
+ "./v1/cursor-page": "./schemas/v1/cursor-page.schema.json",
+ "./v1/event-envelope": "./schemas/v1/event-envelope.schema.json",
+ "./v1/identifier": "./schemas/v1/identifier.schema.json",
+ "./v1/problem-details": "./schemas/v1/problem-details.schema.json",
+ "./v1/revision": "./schemas/v1/revision.schema.json",
+ "./v1/tenant-scope": "./schemas/v1/tenant-scope.schema.json",
+ "./v1/utc-timestamp": "./schemas/v1/utc-timestamp.schema.json"
+ },
+ "scripts": {
+ "build": "node scripts/build.mjs && node scripts/generate-models.mjs --check",
+ "compatibility:baseline": "node scripts/contract-compatibility.mjs update",
+ "compatibility:check": "node scripts/contract-compatibility.mjs check",
+ "contract:check": "node scripts/generate-models.mjs --check && node scripts/contract-compatibility.mjs check && node ../../tools/fixture-validation/src/run-contract-parity.mjs",
+ "fixtures:check": "node ../../tools/fixture-validation/src/run-contract-parity.mjs",
+ "generate": "node scripts/generate-models.mjs",
+ "generate:check": "node scripts/generate-models.mjs --check",
+ "test": "node --test \"test/**/*.test.mjs\"",
+ "test:python-formats": "node test/python-format-runtime-probe.mjs"
+ },
+ "dependencies": {
+ "ajv": "8.17.1",
+ "ajv-formats": "3.0.1"
+ }
+}
diff --git a/packages/contracts/public-outputs.json b/packages/contracts/public-outputs.json
new file mode 100644
index 00000000..723bbe51
--- /dev/null
+++ b/packages/contracts/public-outputs.json
@@ -0,0 +1,26 @@
+{
+ "inventoryFormat": 1,
+ "versions": [
+ {
+ "contractVersion": 1,
+ "generatedFiles": [
+ "generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt",
+ "generated/kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt",
+ "generated/python/databreeze_contracts/__init__.py",
+ "generated/python/databreeze_contracts/py.typed",
+ "generated/python/databreeze_contracts/v1/__init__.py",
+ "generated/python/databreeze_contracts/v1/_validation.py",
+ "generated/python/databreeze_contracts/v1/models.py",
+ "generated/python/pyproject.toml",
+ "generated/typescript/v1/index.ts",
+ "generated/typescript/v1/validation.mjs"
+ ],
+ "jsonSurfaces": [
+ {
+ "path": "package.json",
+ "pointers": ["/dependencies", "/exports", "/name"]
+ }
+ ]
+ }
+ ]
+}
diff --git a/packages/contracts/schemas/v1/actor-metadata.schema.json b/packages/contracts/schemas/v1/actor-metadata.schema.json
new file mode 100644
index 00000000..00848e63
--- /dev/null
+++ b/packages/contracts/schemas/v1/actor-metadata.schema.json
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/actor-metadata",
+ "$comment": "Shared actor identity metadata used by commands and events; supports AUD-004.",
+ "title": "Actor Metadata",
+ "description": "The stable type and identifier of the principal responsible for an action.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["actorType", "actorId"],
+ "properties": {
+ "actorType": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]{0,62}$"
+ },
+ "actorId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/command-envelope.schema.json b/packages/contracts/schemas/v1/command-envelope.schema.json
new file mode 100644
index 00000000..63d8cf16
--- /dev/null
+++ b/packages/contracts/schemas/v1/command-envelope.schema.json
@@ -0,0 +1,53 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/command-envelope",
+ "$comment": "Partial foundation coverage for INT-004 and IAM-019.",
+ "title": "Idempotent Command Envelope",
+ "description": "The shared closed envelope for an idempotent, tenant-scoped command.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "commandId",
+ "commandType",
+ "schemaVersion",
+ "tenantScope",
+ "actor",
+ "correlation",
+ "issuedAt",
+ "idempotencyKey",
+ "data"
+ ],
+ "properties": {
+ "commandId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "commandType": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$"
+ },
+ "schemaVersion": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "tenantScope": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/tenant-scope"
+ },
+ "actor": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/actor-metadata"
+ },
+ "correlation": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/correlation-metadata"
+ },
+ "issuedAt": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp"
+ },
+ "idempotencyKey": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/correlation-metadata.schema.json b/packages/contracts/schemas/v1/correlation-metadata.schema.json
new file mode 100644
index 00000000..36b260f7
--- /dev/null
+++ b/packages/contracts/schemas/v1/correlation-metadata.schema.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/correlation-metadata",
+ "$comment": "Partial foundation coverage for AUD-004 and INT-021.",
+ "title": "Correlation Metadata",
+ "description": "Content-safe identifiers used to join a request or event chain.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["correlationId"],
+ "properties": {
+ "correlationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "causationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "requestId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/cursor-page.schema.json b/packages/contracts/schemas/v1/cursor-page.schema.json
new file mode 100644
index 00000000..59c9ed72
--- /dev/null
+++ b/packages/contracts/schemas/v1/cursor-page.schema.json
@@ -0,0 +1,53 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/cursor-page",
+ "$comment": "Shared pagination shape supporting INT-005.",
+ "title": "Cursor Page Envelope",
+ "description": "The canonical closed page envelope with a UTC snapshot and opaque continuation cursor.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["data", "snapshotAt", "hasMore"],
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {}
+ },
+ "nextCursor": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4096
+ },
+ "snapshotAt": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "hasMore": {
+ "const": true
+ }
+ },
+ "required": ["hasMore"]
+ },
+ "then": {
+ "properties": {
+ "nextCursor": true
+ },
+ "required": ["nextCursor"]
+ },
+ "else": {
+ "not": {
+ "properties": {
+ "nextCursor": true
+ },
+ "required": ["nextCursor"]
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/contracts/schemas/v1/event-envelope.schema.json b/packages/contracts/schemas/v1/event-envelope.schema.json
new file mode 100644
index 00000000..79cb0e8b
--- /dev/null
+++ b/packages/contracts/schemas/v1/event-envelope.schema.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/event-envelope",
+ "$comment": "Canonical event base supporting AUD-004, AUD-006, IAM-019, and INT-008.",
+ "title": "Canonical Event Envelope",
+ "description": "The shared closed envelope for a versioned, tenant-scoped domain event.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "eventId",
+ "eventType",
+ "schemaVersion",
+ "tenantScope",
+ "entity",
+ "actor",
+ "correlation",
+ "sourceComponent",
+ "occurredAt",
+ "data"
+ ],
+ "properties": {
+ "eventId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "eventType": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]*(\\.[a-z][a-z0-9_-]*)+$"
+ },
+ "schemaVersion": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "tenantScope": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/tenant-scope"
+ },
+ "entity": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["entityType", "entityId", "revision"],
+ "properties": {
+ "entityType": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]{0,62}$"
+ },
+ "entityId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "revision": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/revision"
+ }
+ }
+ },
+ "actor": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/actor-metadata"
+ },
+ "correlation": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/correlation-metadata"
+ },
+ "sourceComponent": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_-]{0,62}$"
+ },
+ "occurredAt": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/identifier.schema.json b/packages/contracts/schemas/v1/identifier.schema.json
new file mode 100644
index 00000000..aee9f95e
--- /dev/null
+++ b/packages/contracts/schemas/v1/identifier.schema.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/identifier",
+ "$comment": "Partial foundation coverage for IAM-001.",
+ "title": "Stable UUID Identifier",
+ "description": "An opaque stable UUID identifier.",
+ "type": "string",
+ "format": "uuid"
+}
diff --git a/packages/contracts/schemas/v1/problem-details.schema.json b/packages/contracts/schemas/v1/problem-details.schema.json
new file mode 100644
index 00000000..9d921ef1
--- /dev/null
+++ b/packages/contracts/schemas/v1/problem-details.schema.json
@@ -0,0 +1,121 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/problem-details",
+ "$comment": "RFC 7807-compatible base with the safe public error metadata required by INT-021 and WEB-021.",
+ "title": "Problem Details",
+ "description": "A closed RFC 7807-compatible problem document with DataBreeze public error extensions.",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["type", "status", "code", "correlationId", "retryable"],
+ "anyOf": [
+ {
+ "properties": {
+ "titleKey": true
+ },
+ "required": ["titleKey"]
+ },
+ {
+ "properties": {
+ "messageKey": true
+ },
+ "required": ["messageKey"]
+ }
+ ],
+ "properties": {
+ "type": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1
+ },
+ "titleKey": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "status": {
+ "type": "integer",
+ "minimum": 100,
+ "maximum": 599
+ },
+ "detail": {
+ "type": "string"
+ },
+ "instance": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "code": {
+ "type": "string",
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
+ },
+ "correlationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "retryable": {
+ "type": "boolean"
+ },
+ "messageKey": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "fieldErrors": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["field", "code"],
+ "properties": {
+ "field": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "code": {
+ "type": "string",
+ "pattern": "^[A-Z][A-Z0-9_]{0,127}$"
+ }
+ }
+ }
+ },
+ "retryAfterSeconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "currentRevision": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/revision"
+ },
+ "remediationAction": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "rateLimit": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["scope", "resetAt"],
+ "properties": {
+ "scope": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "remaining": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "resetAt": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/revision.schema.json b/packages/contracts/schemas/v1/revision.schema.json
new file mode 100644
index 00000000..54ce1ab3
--- /dev/null
+++ b/packages/contracts/schemas/v1/revision.schema.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/revision",
+ "$comment": "Supports optimistic-concurrency revisions described by the domain and data model.",
+ "title": "Entity Revision",
+ "description": "A positive, monotonically increasing entity revision.",
+ "type": "integer",
+ "minimum": 1
+}
diff --git a/packages/contracts/schemas/v1/tenant-scope.schema.json b/packages/contracts/schemas/v1/tenant-scope.schema.json
new file mode 100644
index 00000000..ecb25b97
--- /dev/null
+++ b/packages/contracts/schemas/v1/tenant-scope.schema.json
@@ -0,0 +1,68 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/tenant-scope",
+ "$comment": "Partial foundation coverage for IAM-019.",
+ "title": "Tenant Scope",
+ "description": "A discriminated tenant scope containing the complete ancestry required at its level.",
+ "oneOf": [
+ {
+ "$ref": "#/$defs/organizationScope"
+ },
+ {
+ "$ref": "#/$defs/workspaceScope"
+ },
+ {
+ "$ref": "#/$defs/projectScope"
+ }
+ ],
+ "$defs": {
+ "organizationScope": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["scopeType", "organizationId"],
+ "properties": {
+ "scopeType": {
+ "const": "organization"
+ },
+ "organizationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ }
+ }
+ },
+ "workspaceScope": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["scopeType", "organizationId", "workspaceId"],
+ "properties": {
+ "scopeType": {
+ "const": "workspace"
+ },
+ "organizationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "workspaceId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ }
+ }
+ },
+ "projectScope": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["scopeType", "organizationId", "workspaceId", "projectId"],
+ "properties": {
+ "scopeType": {
+ "const": "project"
+ },
+ "organizationId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "workspaceId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ },
+ "projectId": {
+ "$ref": "https://schemas.databreeze.dev/contracts/v1/identifier"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/contracts/schemas/v1/utc-timestamp.schema.json b/packages/contracts/schemas/v1/utc-timestamp.schema.json
new file mode 100644
index 00000000..61ac477b
--- /dev/null
+++ b/packages/contracts/schemas/v1/utc-timestamp.schema.json
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.databreeze.dev/contracts/v1/utc-timestamp",
+ "$comment": "Partial foundation coverage for IAM-001 and INT-008.",
+ "title": "UTC Timestamp",
+ "description": "An RFC 3339 date-time normalized to UTC and terminated by uppercase Z.",
+ "type": "string",
+ "format": "date-time",
+ "pattern": "Z$"
+}
diff --git a/packages/contracts/scripts/build.mjs b/packages/contracts/scripts/build.mjs
new file mode 100644
index 00000000..d029818d
--- /dev/null
+++ b/packages/contracts/scripts/build.mjs
@@ -0,0 +1,27 @@
+import { readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import Ajv2020 from 'ajv/dist/2020.js';
+import addFormats from 'ajv-formats';
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const manifest = JSON.parse(readFileSync(resolve(packageRoot, 'manifest.json'), 'utf8'));
+const ajv = new Ajv2020({ allErrors: true, strict: true });
+addFormats(ajv);
+
+for (const entry of manifest.schemas) {
+ const schema = JSON.parse(readFileSync(resolve(packageRoot, entry.path), 'utf8'));
+ if (schema.$id !== entry.id) {
+ throw new Error(`Manifest ID does not match ${entry.path}`);
+ }
+ ajv.addSchema(schema);
+}
+
+for (const entry of manifest.schemas) {
+ if (!ajv.getSchema(entry.id)) {
+ throw new Error(`Schema did not compile: ${entry.id}`);
+ }
+}
+
+console.log(`Compiled ${manifest.schemas.length} canonical JSON Schemas.`);
diff --git a/packages/contracts/scripts/contract-compatibility.mjs b/packages/contracts/scripts/contract-compatibility.mjs
new file mode 100644
index 00000000..f923c9da
--- /dev/null
+++ b/packages/contracts/scripts/contract-compatibility.mjs
@@ -0,0 +1,446 @@
+import { createHash } from 'node:crypto';
+import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
+import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const defaultRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const publishedRegistryPath = 'compatibility/published.json';
+const publicOutputInventoryPath = 'public-outputs.json';
+
+function fail(message) {
+ throw new Error(message);
+}
+
+function compareStrings(left, right) {
+ return left.localeCompare(right, 'en');
+}
+
+function toPosix(path) {
+ return path.replaceAll('\\', '/');
+}
+
+function parseJson(path, label) {
+ try {
+ return JSON.parse(readFileSync(path, 'utf8'));
+ } catch (error) {
+ fail(`${label} is not valid JSON: ${error.message}`);
+ }
+}
+
+function formatJson(value) {
+ return `${JSON.stringify(value, null, 2)}\n`;
+}
+
+function sha256Bytes(bytes) {
+ return createHash('sha256').update(bytes).digest('hex');
+}
+
+function sha256File(path) {
+ return sha256Bytes(readFileSync(path));
+}
+
+function listFiles(root, directory = root) {
+ if (!existsSync(directory)) return [];
+ return readdirSync(directory, { withFileTypes: true })
+ .flatMap((entry) => {
+ const path = resolve(directory, entry.name);
+ return entry.isDirectory() ? listFiles(root, path) : [toPosix(relative(root, path))];
+ })
+ .sort(compareStrings);
+}
+
+function resolveInventoryPath(root, path, label) {
+ if (typeof path !== 'string' || !path || path.includes('\\')) {
+ fail(`${label} must be a non-empty POSIX path`);
+ }
+ const packageRoot = resolve(root);
+ const destination = resolve(packageRoot, ...path.split('/'));
+ const relativePath = relative(packageRoot, destination);
+ if (relativePath === '..' || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
+ fail(`${label} escapes the contract package: ${path}`);
+ }
+ return destination;
+}
+
+function assertSortedUniqueStrings(values, label) {
+ if (
+ !Array.isArray(values) ||
+ values.length === 0 ||
+ !values.every((value) => typeof value === 'string')
+ ) {
+ fail(`${label} must be a non-empty array of strings`);
+ }
+ const sorted = [...values].sort(compareStrings);
+ if (new Set(values).size !== values.length || JSON.stringify(values) !== JSON.stringify(sorted)) {
+ fail(`${label} must contain unique values in stable order`);
+ }
+}
+
+function readPublicOutputInventory(root) {
+ const path = resolve(root, publicOutputInventoryPath);
+ if (!existsSync(path)) fail(`Public-output inventory is missing: ${publicOutputInventoryPath}`);
+ const inventory = parseJson(path, 'Public-output inventory');
+ if (inventory.inventoryFormat !== 1 || !Array.isArray(inventory.versions)) {
+ fail('Public-output inventory has an unsupported shape');
+ }
+ const versions = new Set();
+ for (const entry of inventory.versions) {
+ if (!Number.isInteger(entry.contractVersion) || entry.contractVersion < 1) {
+ fail('Public-output inventory contractVersion must be a positive integer');
+ }
+ if (versions.has(entry.contractVersion)) {
+ fail(`Duplicate public-output inventory entry for v${entry.contractVersion}`);
+ }
+ versions.add(entry.contractVersion);
+ assertSortedUniqueStrings(
+ entry.generatedFiles,
+ `Public-output inventory v${entry.contractVersion} generatedFiles`,
+ );
+ for (const generatedFile of entry.generatedFiles) {
+ if (!generatedFile.startsWith('generated/')) {
+ fail(`Generated public output must be below generated/: ${generatedFile}`);
+ }
+ resolveInventoryPath(root, generatedFile, 'Generated public output');
+ }
+ if (!Array.isArray(entry.jsonSurfaces) || entry.jsonSurfaces.length === 0) {
+ fail(`Public-output inventory v${entry.contractVersion} jsonSurfaces must be non-empty`);
+ }
+ const surfacePaths = entry.jsonSurfaces.map((surface) => surface.path);
+ assertSortedUniqueStrings(
+ surfacePaths,
+ `Public-output inventory v${entry.contractVersion} json surface paths`,
+ );
+ for (const surface of entry.jsonSurfaces) {
+ resolveInventoryPath(root, surface.path, 'Public JSON surface');
+ assertSortedUniqueStrings(
+ surface.pointers,
+ `Public-output inventory ${surface.path} pointers`,
+ );
+ if (!surface.pointers.every((pointer) => pointer.startsWith('/'))) {
+ fail(`Public JSON surface pointers must use JSON Pointer syntax: ${surface.path}`);
+ }
+ }
+ }
+ return inventory;
+}
+
+function versionPublicOutputEntry(root, version, published = false) {
+ const inventory = readPublicOutputInventory(root);
+ const entry = inventory.versions.find((candidate) => candidate.contractVersion === version);
+ if (!entry) {
+ fail(
+ published
+ ? `Published public-output inventory changed in place for v${version}`
+ : `Public-output inventory has no v${version} entry`,
+ );
+ }
+ return { entry, inventory };
+}
+
+function verifyAllGeneratedFilesAreDeclared(root, inventory) {
+ const declared = new Set(inventory.versions.flatMap((entry) => entry.generatedFiles));
+ const generatedRoot = resolve(root, 'generated');
+ const undeclared = listFiles(generatedRoot)
+ .map((path) => `generated/${path}`)
+ .find((path) => !declared.has(path));
+ if (undeclared) {
+ fail(`Generated output is not declared in public-output inventory: ${undeclared}`);
+ }
+}
+
+function jsonPointerValue(document, pointer, label) {
+ let current = document;
+ for (const token of pointer.slice(1).split('/')) {
+ const key = token.replaceAll('~1', '/').replaceAll('~0', '~');
+ if (current === null || typeof current !== 'object' || !Object.hasOwn(current, key)) {
+ fail(`${label} is missing JSON pointer ${pointer}`);
+ }
+ current = current[key];
+ }
+ return current;
+}
+
+function buildPublicPackageSurfaces(root, entry) {
+ return entry.jsonSurfaces.map((surface) => {
+ const path = resolveInventoryPath(root, surface.path, 'Public JSON surface');
+ if (!existsSync(path)) fail(`Published package surface is missing: ${surface.path}`);
+ const document = parseJson(path, `Public package surface ${surface.path}`);
+ return {
+ path: surface.path,
+ values: surface.pointers.map((pointer) => ({
+ pointer,
+ value: jsonPointerValue(document, pointer, surface.path),
+ })),
+ };
+ });
+}
+
+function versionSchemaEntries(root, version) {
+ const manifestPath = resolve(root, 'manifest.json');
+ if (!existsSync(manifestPath)) fail('Canonical contract manifest is missing: manifest.json');
+ const manifest = parseJson(manifestPath, 'Canonical contract manifest');
+ if (!Array.isArray(manifest.schemas))
+ fail('Canonical contract manifest schemas must be an array');
+
+ const versionPrefix = `schemas/v${version}/`;
+ const entries = manifest.schemas
+ .filter((entry) => typeof entry.path === 'string' && entry.path.startsWith(versionPrefix))
+ .sort((left, right) => compareStrings(left.name, right.name));
+ if (entries.length === 0) fail(`No canonical schemas found for v${version}`);
+ return entries;
+}
+
+function buildBaseline(root, version) {
+ const expectedIdPrefix = `https://schemas.databreeze.dev/contracts/v${version}/`;
+ const schemaEntries = versionSchemaEntries(root, version);
+ const registeredPaths = new Set(schemaEntries.map((entry) => entry.path));
+ const schemaDirectory = resolve(root, `schemas/v${version}`);
+ const unregisteredSchemas = listFiles(schemaDirectory)
+ .map((path) => `schemas/v${version}/${path}`)
+ .filter((path) => !registeredPaths.has(path));
+ if (unregisteredSchemas.length > 0) {
+ fail(`Unregistered schema in published v${version}: ${unregisteredSchemas[0]}`);
+ }
+
+ const schemas = schemaEntries.map((entry) => {
+ if (!entry.id.startsWith(expectedIdPrefix)) {
+ fail(`Schema ID must use the v${version} namespace: ${entry.id}`);
+ }
+ const schemaPath = resolve(root, entry.path);
+ if (!existsSync(schemaPath)) fail(`Published schema is missing: ${entry.path}`);
+ const schema = parseJson(schemaPath, `Schema ${entry.name}`);
+ if (schema.$id !== entry.id) fail(`Manifest ID does not match ${entry.path}`);
+ return {
+ name: entry.name,
+ id: entry.id,
+ path: entry.path,
+ sha256: sha256File(schemaPath),
+ };
+ });
+
+ const { entry: publicOutputEntry, inventory } = versionPublicOutputEntry(root, version);
+ verifyAllGeneratedFilesAreDeclared(root, inventory);
+ const outputPaths = publicOutputEntry.generatedFiles;
+ if (outputPaths.length === 0) fail(`No generated public outputs found for v${version}`);
+ const generatedPublicOutputs = outputPaths.map((path) => ({
+ path,
+ sha256: sha256File(resolveInventoryPath(root, path, 'Generated public output')),
+ }));
+
+ return {
+ baselineFormat: 2,
+ contractVersion: version,
+ schemaIdPrefix: expectedIdPrefix,
+ schemas,
+ publicOutputInventory: {
+ path: publicOutputInventoryPath,
+ versionEntrySha256: sha256Bytes(formatJson(publicOutputEntry)),
+ },
+ generatedPublicOutputs,
+ publicPackageSurfaces: buildPublicPackageSurfaces(root, publicOutputEntry),
+ };
+}
+
+function readPublishedRegistry(root) {
+ const path = resolve(root, publishedRegistryPath);
+ if (!existsSync(path)) {
+ fail(`Published compatibility registry is missing: ${publishedRegistryPath}`);
+ }
+ const registry = parseJson(path, 'Published compatibility registry');
+ if (registry.policyVersion !== 1 || !Array.isArray(registry.versions)) {
+ fail('Published compatibility registry has an unsupported shape');
+ }
+ return registry;
+}
+
+function verifySchemaBaseline(root, version, baseline) {
+ const currentEntries = versionSchemaEntries(root, version);
+ const currentByName = new Map(currentEntries.map((entry) => [entry.name, entry]));
+ const baselineNames = new Set(baseline.schemas.map((entry) => entry.name));
+
+ for (const expected of baseline.schemas) {
+ const current = currentByName.get(expected.name);
+ if (!current) fail(`Published schema was removed from v${version}: ${expected.name}`);
+ if (current.id !== expected.id) {
+ fail(`Published schema ID changed in place: ${expected.name}`);
+ }
+ if (current.path !== expected.path) {
+ fail(`Published schema path changed in place: ${expected.name}`);
+ }
+ const sourcePath = resolve(root, expected.path);
+ if (!existsSync(sourcePath)) fail(`Published schema is missing: ${expected.path}`);
+ if (sha256File(sourcePath) !== expected.sha256) {
+ fail(`Published schema bytes changed in place: ${expected.name}`);
+ }
+ }
+
+ const added = currentEntries.find((entry) => !baselineNames.has(entry.name));
+ if (added) fail(`Schema added to published v${version}: ${added.name}`);
+
+ const registeredPaths = new Set(currentEntries.map((entry) => entry.path));
+ const schemaRoot = resolve(root, `schemas/v${version}`);
+ const unregistered = listFiles(schemaRoot)
+ .map((path) => `schemas/v${version}/${path}`)
+ .find((path) => !registeredPaths.has(path));
+ if (unregistered) fail(`Unregistered schema in published v${version}: ${unregistered}`);
+}
+
+function verifyGeneratedBaseline(root, version, baseline) {
+ const { entry: publicOutputEntry, inventory } = versionPublicOutputEntry(root, version, true);
+ verifyAllGeneratedFilesAreDeclared(root, inventory);
+ if (
+ baseline.publicOutputInventory?.path !== publicOutputInventoryPath ||
+ baseline.publicOutputInventory?.versionEntrySha256 !==
+ sha256Bytes(formatJson(publicOutputEntry))
+ ) {
+ fail(`Published public-output inventory changed in place for v${version}`);
+ }
+
+ const baselinePaths = new Set(baseline.generatedPublicOutputs.map((entry) => entry.path));
+ for (const expected of baseline.generatedPublicOutputs) {
+ const outputPath = resolveInventoryPath(root, expected.path, 'Generated public output');
+ if (!existsSync(outputPath)) {
+ fail(`Published public output is missing: ${expected.path}`);
+ }
+ if (sha256File(outputPath) !== expected.sha256) {
+ fail(`Published public output changed in place: ${expected.path}`);
+ }
+ }
+ const added = publicOutputEntry.generatedFiles.find((path) => !baselinePaths.has(path));
+ if (added) fail(`Public output added to published v${version}: ${added}`);
+ const removed = baseline.generatedPublicOutputs.find(
+ ({ path }) => !publicOutputEntry.generatedFiles.includes(path),
+ );
+ if (removed) fail(`Public output removed from published v${version}: ${removed.path}`);
+
+ const currentSurfaces = buildPublicPackageSurfaces(root, publicOutputEntry);
+ if (JSON.stringify(currentSurfaces) !== JSON.stringify(baseline.publicPackageSurfaces)) {
+ fail(`Published package surface changed in place for v${version}`);
+ }
+}
+
+function checkCompatibility(root) {
+ const registry = readPublishedRegistry(root);
+ if (registry.versions.length === 0) fail('Published compatibility registry has no versions');
+
+ for (const published of [...registry.versions].sort(
+ (left, right) => left.contractVersion - right.contractVersion,
+ )) {
+ const version = published.contractVersion;
+ const baselinePath = resolve(root, ...published.baseline.split('/'));
+ if (!existsSync(baselinePath)) {
+ fail(`Published baseline is missing: ${published.baseline}`);
+ }
+ if (sha256File(baselinePath) !== published.sha256) {
+ fail(`Published baseline drift detected for v${version}`);
+ }
+ const baseline = parseJson(baselinePath, `Published v${version} baseline`);
+ if (baseline.baselineFormat !== 2 || baseline.contractVersion !== version) {
+ fail(`Published baseline version mismatch for v${version}`);
+ }
+ verifySchemaBaseline(root, version, baseline);
+ verifyGeneratedBaseline(root, version, baseline);
+ }
+}
+
+function updateBaseline(root, version, approved) {
+ const baseline = buildBaseline(root, version);
+ const baselineContent = formatJson(baseline);
+ const registryPath = resolve(root, publishedRegistryPath);
+ const registry = existsSync(registryPath)
+ ? readPublishedRegistry(root)
+ : { policyVersion: 1, versions: [] };
+ const published = registry.versions.find((entry) => entry.contractVersion === version);
+
+ if (published) {
+ const baselinePath = resolve(root, ...published.baseline.split('/'));
+ if (!existsSync(baselinePath)) {
+ fail(`Published baseline is missing: ${published.baseline}`);
+ }
+ if (
+ sha256File(baselinePath) !== published.sha256 ||
+ readFileSync(baselinePath, 'utf8') !== baselineContent
+ ) {
+ fail(`Refusing to rewrite published v${version}; publish new schema IDs and a new version`);
+ }
+ return false;
+ }
+
+ if (!approved) {
+ fail(`Creating v${version} requires --approve-new-version after review`);
+ }
+
+ const relativeBaselinePath = `compatibility/v${version}/baseline.json`;
+ const baselinePath = resolve(root, ...relativeBaselinePath.split('/'));
+ if (existsSync(baselinePath)) {
+ fail(`Unregistered baseline already exists: ${relativeBaselinePath}`);
+ }
+ mkdirSync(dirname(baselinePath), { recursive: true });
+ writeFileSync(baselinePath, baselineContent, 'utf8');
+
+ const nextRegistry = {
+ policyVersion: 1,
+ versions: [
+ ...registry.versions,
+ {
+ contractVersion: version,
+ baseline: relativeBaselinePath,
+ sha256: sha256Bytes(baselineContent),
+ },
+ ].sort((left, right) => left.contractVersion - right.contractVersion),
+ };
+ mkdirSync(dirname(registryPath), { recursive: true });
+ writeFileSync(registryPath, formatJson(nextRegistry), 'utf8');
+ return true;
+}
+
+function readArguments(argumentsList) {
+ const command = argumentsList[0];
+ if (!['check', 'update'].includes(command)) {
+ fail('Usage: contract-compatibility.mjs [--root PATH] [--version N]');
+ }
+ const options = {
+ approved: false,
+ command,
+ root: defaultRoot,
+ version: undefined,
+ };
+ for (let index = 1; index < argumentsList.length; index += 1) {
+ const argument = argumentsList[index];
+ if (argument === '--approve-new-version') {
+ options.approved = true;
+ } else if (argument === '--root' || argument === '--version') {
+ const value = argumentsList[index + 1];
+ if (!value || value.startsWith('--')) fail(`${argument} requires a value`);
+ if (argument === '--root') options.root = resolve(value);
+ else options.version = Number(value);
+ index += 1;
+ } else {
+ fail(`Unknown argument: ${argument}`);
+ }
+ }
+ if (options.command === 'update') {
+ if (!Number.isSafeInteger(options.version) || options.version < 1) {
+ fail('update requires a positive integer --version');
+ }
+ }
+ return options;
+}
+
+try {
+ const options = readArguments(process.argv.slice(2));
+ if (options.command === 'check') {
+ checkCompatibility(options.root);
+ console.log('Published contract compatibility baseline is unchanged.');
+ } else {
+ const created = updateBaseline(options.root, options.version, options.approved);
+ console.log(
+ created
+ ? `Created reviewed compatibility baseline for v${options.version}.`
+ : `Published v${options.version} baseline is already up to date.`,
+ );
+ }
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ process.exitCode = 1;
+}
diff --git a/packages/contracts/scripts/contract-generator.mjs b/packages/contracts/scripts/contract-generator.mjs
new file mode 100644
index 00000000..18f1ee60
--- /dev/null
+++ b/packages/contracts/scripts/contract-generator.mjs
@@ -0,0 +1,1203 @@
+import { Buffer } from 'node:buffer';
+import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
+
+const DRAFT = 'https://json-schema.org/draft/2020-12/schema';
+const HEADER = 'Generated by @databreeze/contracts. DO NOT EDIT.';
+const SUPPORTED_KEYWORDS = new Set([
+ '$comment',
+ '$defs',
+ '$id',
+ '$ref',
+ '$schema',
+ 'additionalProperties',
+ 'allOf',
+ 'anyOf',
+ 'const',
+ 'description',
+ 'else',
+ 'format',
+ 'if',
+ 'items',
+ 'maxItems',
+ 'maxLength',
+ 'maximum',
+ 'minLength',
+ 'minimum',
+ 'not',
+ 'oneOf',
+ 'pattern',
+ 'properties',
+ 'required',
+ 'then',
+ 'title',
+ 'type',
+]);
+const SUPPORTED_TYPES = new Set(['array', 'boolean', 'integer', 'object', 'string']);
+const SUPPORTED_FORMATS = new Set(['date-time', 'uri-reference', 'uuid']);
+
+function fail(message) {
+ throw new Error(message);
+}
+
+function parseJson(path) {
+ try {
+ return JSON.parse(readFileSync(path, 'utf8'));
+ } catch (error) {
+ fail(`Could not parse ${path}: ${error.message}`);
+ }
+}
+
+function assertPlainObject(value, label) {
+ if (value === null || Array.isArray(value) || typeof value !== 'object') {
+ fail(`${label} must be an object`);
+ }
+}
+
+function validateSchemaNode(node, sourcePath, location = '#') {
+ if (node === true) return;
+ if (node === false) {
+ fail(`Unsupported boolean JSON Schema false in ${sourcePath}${location}`);
+ }
+ assertPlainObject(node, `${sourcePath}${location}`);
+
+ for (const keyword of Object.keys(node)) {
+ if (!SUPPORTED_KEYWORDS.has(keyword)) {
+ fail(`Unsupported JSON Schema keyword "${keyword}" in ${sourcePath}${location}`);
+ }
+ }
+
+ if ('type' in node && !SUPPORTED_TYPES.has(node.type)) {
+ fail(`Unsupported JSON Schema type ${JSON.stringify(node.type)} in ${sourcePath}${location}`);
+ }
+ if ('format' in node && !SUPPORTED_FORMATS.has(node.format)) {
+ fail(
+ `Unsupported JSON Schema format ${JSON.stringify(node.format)} in ${sourcePath}${location}`,
+ );
+ }
+ if ('additionalProperties' in node && node.additionalProperties !== false) {
+ fail(`Unsupported additionalProperties value in ${sourcePath}${location}`);
+ }
+ if ('properties' in node) {
+ assertPlainObject(node.properties, `${sourcePath}${location}/properties`);
+ for (const [name, property] of Object.entries(node.properties)) {
+ validateSchemaNode(property, sourcePath, `${location}/properties/${name}`);
+ }
+ }
+ if ('$defs' in node) {
+ assertPlainObject(node.$defs, `${sourcePath}${location}/$defs`);
+ for (const [name, definition] of Object.entries(node.$defs)) {
+ validateSchemaNode(definition, sourcePath, `${location}/$defs/${name}`);
+ }
+ }
+ if ('items' in node) validateSchemaNode(node.items, sourcePath, `${location}/items`);
+ for (const keyword of ['oneOf', 'anyOf', 'allOf']) {
+ if (!(keyword in node)) continue;
+ if (!Array.isArray(node[keyword]) || node[keyword].length === 0) {
+ fail(`${sourcePath}${location}/${keyword} must be a non-empty array`);
+ }
+ node[keyword].forEach((child, index) =>
+ validateSchemaNode(child, sourcePath, `${location}/${keyword}/${index}`),
+ );
+ }
+ for (const keyword of ['if', 'then', 'else', 'not']) {
+ if (keyword in node) validateSchemaNode(node[keyword], sourcePath, `${location}/${keyword}`);
+ }
+ if ('required' in node && (!Array.isArray(node.required) || !node.required.every(isString))) {
+ fail(`${sourcePath}${location}/required must be an array of strings`);
+ }
+ if ('const' in node && !['boolean', 'number', 'string'].includes(typeof node.const)) {
+ fail(`Unsupported const value in ${sourcePath}${location}`);
+ }
+}
+
+function isString(value) {
+ return typeof value === 'string';
+}
+
+function pascalCase(value) {
+ const result = value
+ .split(/[^A-Za-z0-9]+/u)
+ .filter(Boolean)
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
+ .join('');
+ if (!result || !/^[A-Za-z][A-Za-z0-9]*$/u.test(result)) {
+ fail(`Cannot derive a model name from ${JSON.stringify(value)}`);
+ }
+ return result;
+}
+
+function loadRegistry(sourceRoot) {
+ const root = resolve(sourceRoot);
+ const manifestPath = resolve(root, 'manifest.json');
+ const manifest = parseJson(manifestPath);
+ assertPlainObject(manifest, manifestPath);
+ if (manifest.draft !== DRAFT || manifest.version !== 1 || !Array.isArray(manifest.schemas)) {
+ fail(`${manifestPath} must declare the supported JSON Schema draft and v1 schema registry`);
+ }
+
+ const names = new Set();
+ const ids = new Set();
+ const entries = manifest.schemas.map((entry, index) => {
+ assertPlainObject(entry, `${manifestPath}#/schemas/${index}`);
+ if (!isString(entry.name) || !isString(entry.id) || !isString(entry.path)) {
+ fail(`${manifestPath}#/schemas/${index} must contain string name, id, and path values`);
+ }
+ if (names.has(entry.name) || ids.has(entry.id))
+ fail(`Duplicate schema registry entry: ${entry.name}`);
+ names.add(entry.name);
+ ids.add(entry.id);
+ const schemaPath = resolve(root, entry.path);
+ const relativePath = relative(root, schemaPath);
+ if (relativePath.startsWith(`..${sep}`) || relativePath === '..' || isAbsolute(relativePath)) {
+ fail(`Schema path escapes the source root: ${entry.path}`);
+ }
+ const schema = parseJson(schemaPath);
+ validateSchemaNode(schema, entry.path);
+ if (schema.$schema !== manifest.draft) fail(`Schema draft does not match ${entry.path}`);
+ if (schema.$id !== entry.id) fail(`Manifest ID does not match ${entry.path}`);
+ return { ...entry, modelName: pascalCase(entry.name), schema, schemaPath: entry.path };
+ });
+ entries.sort((left, right) => compareStrings(left.name, right.name));
+
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
+ for (const entry of entries) validateReferences(entry.schema, entry, byId);
+ return { entries, byId, root };
+}
+
+function validateReferences(node, entry, byId, location = '#') {
+ if (typeof node === 'boolean') return;
+ if (node.$ref) {
+ if (node.$ref.startsWith('#/$defs/')) {
+ const name = node.$ref.slice('#/$defs/'.length);
+ if (!entry.schema.$defs?.[name])
+ fail(`Unresolved schema reference ${node.$ref} in ${entry.path}`);
+ } else if (!byId.has(node.$ref)) {
+ fail(`Unresolved schema reference ${node.$ref} in ${entry.path}`);
+ }
+ }
+ for (const [name, child] of Object.entries(node.properties ?? {})) {
+ validateReferences(child, entry, byId, `${location}/properties/${name}`);
+ }
+ for (const [name, child] of Object.entries(node.$defs ?? {})) {
+ validateReferences(child, entry, byId, `${location}/$defs/${name}`);
+ }
+ if (node.items) validateReferences(node.items, entry, byId, `${location}/items`);
+ for (const keyword of ['oneOf', 'anyOf', 'allOf']) {
+ (node[keyword] ?? []).forEach((child, index) =>
+ validateReferences(child, entry, byId, `${location}/${keyword}/${index}`),
+ );
+ }
+ for (const keyword of ['if', 'then', 'else', 'not']) {
+ if (node[keyword]) validateReferences(node[keyword], entry, byId, `${location}/${keyword}`);
+ }
+}
+
+function buildModelContext(registry) {
+ const nameByNode = new Map();
+ const entryByNode = new Map();
+ const nodesByName = new Map();
+ const unionMembership = new Map();
+
+ function register(node, name, entry) {
+ const existing = nodesByName.get(name);
+ if (existing && existing !== node) fail(`Generated model name collision: ${name}`);
+ nameByNode.set(node, name);
+ entryByNode.set(node, entry);
+ nodesByName.set(name, node);
+ }
+
+ for (const entry of registry.entries) {
+ register(entry.schema, entry.modelName, entry);
+ for (const [definitionName, definition] of Object.entries(entry.schema.$defs ?? {}).sort(
+ compareEntries,
+ )) {
+ register(definition, pascalCase(definitionName), entry);
+ }
+ }
+
+ for (const entry of registry.entries) {
+ const root = entry.schema;
+ if (root.oneOf) {
+ for (const alternative of root.oneOf) {
+ const target = resolveReference(alternative.$ref, entry, registry, nameByNode);
+ unionMembership.set(target.node, entry.modelName);
+ }
+ }
+ }
+
+ function collect(node, ownerName, entry) {
+ if (typeof node === 'boolean') return;
+ for (const [propertyName, property] of Object.entries(node.properties ?? {}).sort(
+ compareEntries,
+ )) {
+ if (isNamedObjectCandidate(property) && !nameByNode.has(property)) {
+ register(property, `${ownerName}${pascalCase(propertyName)}`, entry);
+ }
+ if (
+ property.type === 'array' &&
+ isNamedObjectCandidate(property.items) &&
+ !nameByNode.has(property.items)
+ ) {
+ register(property.items, `${ownerName}${pascalCase(propertyName)}Item`, entry);
+ }
+ collect(property, nameByNode.get(property) ?? ownerName, entry);
+ }
+ for (const [definitionName, definition] of Object.entries(node.$defs ?? {}).sort(
+ compareEntries,
+ )) {
+ collect(definition, nameByNode.get(definition) ?? pascalCase(definitionName), entry);
+ }
+ if (node.items && typeof node.items === 'object') collect(node.items, ownerName, entry);
+ }
+ for (const entry of registry.entries) collect(entry.schema, entry.modelName, entry);
+
+ return {
+ ...registry,
+ entryByNode,
+ nameByNode,
+ nodesByName,
+ unionMembership,
+ };
+}
+
+function compareEntries([left], [right]) {
+ return compareStrings(left, right);
+}
+
+function compareStrings(left, right) {
+ return left < right ? -1 : left > right ? 1 : 0;
+}
+
+function isNamedObjectCandidate(schema) {
+ return schema && typeof schema === 'object' && schema.type === 'object' && schema.properties;
+}
+
+function resolveReference(reference, currentEntry, context, nameByNode = context.nameByNode) {
+ if (!reference) fail(`Expected a schema reference in ${currentEntry.path}`);
+ if (reference.startsWith('#/$defs/')) {
+ const node = currentEntry.schema.$defs[reference.slice('#/$defs/'.length)];
+ return { node, name: nameByNode.get(node), entry: currentEntry };
+ }
+ const entry = context.byId.get(reference);
+ if (!entry) fail(`Unresolved schema reference ${reference} in ${currentEntry.path}`);
+ return { node: entry.schema, name: entry.modelName, entry };
+}
+
+function genericParameters(node) {
+ const parameters = [];
+ for (const [propertyName, property] of Object.entries(node.properties ?? {}).sort(
+ compareEntries,
+ )) {
+ if (isOpenObject(property)) {
+ parameters.push({ child: property, kind: 'object', name: `T${pascalCase(propertyName)}` });
+ } else if (property.type === 'array' && isEmptySchema(property.items)) {
+ parameters.push({ child: property.items, kind: 'item', name: 'TItem' });
+ }
+ }
+ const seen = new Set();
+ return parameters.filter(({ name }) => !seen.has(name) && seen.add(name));
+}
+
+function isOpenObject(schema) {
+ return (
+ schema &&
+ typeof schema === 'object' &&
+ schema.type === 'object' &&
+ !schema.properties &&
+ !schema.$ref
+ );
+}
+
+function isEmptySchema(schema) {
+ return schema && typeof schema === 'object' && Object.keys(schema).length === 0;
+}
+
+function quoted(value) {
+ return JSON.stringify(value);
+}
+
+function renderTypeScript(context) {
+ const lines = [
+ `// ${HEADER}`,
+ '',
+ 'export type JsonPrimitive = boolean | number | string | null;',
+ 'export type JsonValue = JsonPrimitive | readonly JsonValue[] | JsonObject;',
+ 'export type JsonObject = { readonly [key: string]: JsonValue };',
+ '',
+ ];
+
+ for (const [name, node] of [...context.nodesByName.entries()].sort(compareEntries)) {
+ const entry = context.entryByNode.get(node);
+ if (node.oneOf) {
+ const alternatives = node.oneOf.map(
+ (alternative) => resolveReference(alternative.$ref, entry, context).name,
+ );
+ lines.push(`export type ${name} = ${alternatives.join(' | ')};`, '');
+ continue;
+ }
+ if (node.type !== 'object') {
+ lines.push(`export type ${name} = ${typescriptType(node, entry, context, new Map())};`, '');
+ continue;
+ }
+
+ const parameters = genericParameters(node);
+ const parameterMap = new Map(parameters.map((parameter) => [parameter.child, parameter.name]));
+ const declaration = renderTypeScriptGenericDeclaration(parameters);
+ if (node.anyOf) {
+ const fieldsName = `${name}Fields`;
+ lines.push(
+ ...renderTypeScriptInterface(fieldsName, node, entry, context, parameters, parameterMap),
+ );
+ const genericUse = parameters.length
+ ? `<${parameters.map(({ name: parameter }) => parameter).join(', ')}>`
+ : '';
+ const alternatives = requiredAlternatives(node, entry.path).map(
+ (required) =>
+ `{ ${required
+ .map(
+ (property) =>
+ `readonly ${property}: ${typescriptType(node.properties[property], entry, context, parameterMap)}`,
+ )
+ .join('; ')} }`,
+ );
+ lines.push(
+ `export type ${name}${declaration} = ${fieldsName}${genericUse} & (${alternatives.join(' | ')});`,
+ '',
+ );
+ } else if (node.allOf) {
+ const fieldsName = `${name}Fields`;
+ lines.push(
+ ...renderTypeScriptInterface(fieldsName, node, entry, context, parameters, parameterMap),
+ );
+ const genericUse = parameters.length
+ ? `<${parameters.map(({ name: parameter }) => parameter).join(', ')}>`
+ : '';
+ const conditional = conditionalConstraint(node, entry.path);
+ const base = `Omit<${fieldsName}${genericUse}, ${quoted(conditional.discriminator)} | ${quoted(conditional.dependent)}>`;
+ const dependentType = typescriptType(
+ node.properties[conditional.dependent],
+ entry,
+ context,
+ parameterMap,
+ );
+ lines.push(
+ `export type ${name}${declaration} = ${base} & (`,
+ ` | { readonly ${conditional.discriminator}: ${quoted(conditional.value)}; readonly ${conditional.dependent}: ${dependentType} }`,
+ ` | { readonly ${conditional.discriminator}: ${quoted(!conditional.value)}; readonly ${conditional.dependent}?: never }`,
+ ');',
+ '',
+ );
+ } else {
+ lines.push(
+ ...renderTypeScriptInterface(name, node, entry, context, parameters, parameterMap),
+ );
+ }
+ }
+ const schemaIds = context.entries.map(({ id }) => quoted(id));
+ lines.push(
+ `export type ContractV1SchemaId = ${schemaIds.join(' | ')};`,
+ '',
+ 'export type ContractV1ParseResult =',
+ ' | { readonly accepted: true; readonly value: TValue }',
+ ' | { readonly accepted: false };',
+ '',
+ 'export declare function parseV1Contract(',
+ ' schemaId: ContractV1SchemaId,',
+ ' payload: unknown,',
+ '): ContractV1ParseResult;',
+ '',
+ );
+ return `${lines.join('\n').trimEnd()}\n`;
+}
+
+function renderTypeScriptValidation(context) {
+ const schemas = context.entries.map(({ schema }) => ` ${JSON.stringify(schema)},`);
+ return `${[
+ `// ${HEADER}`,
+ '',
+ "import Ajv2020 from 'ajv/dist/2020.js';",
+ "import addFormats from 'ajv-formats';",
+ '',
+ 'const schemas = [',
+ ...schemas,
+ '];',
+ '',
+ 'const ajv = new Ajv2020({ allErrors: true, strict: true });',
+ 'addFormats(ajv);',
+ 'for (const schema of schemas) ajv.addSchema(schema);',
+ '',
+ 'const validators = new Map(',
+ ' schemas.map((schema) => {',
+ ' const validate = ajv.getSchema(schema.$id);',
+ ' if (!validate) throw new Error(`No generated validator for ${schema.$id}`);',
+ ' return [schema.$id, validate];',
+ ' }),',
+ ');',
+ '',
+ 'export function parseV1Contract(schemaId, payload) {',
+ ' const validate = validators.get(schemaId);',
+ ' if (!validate) throw new TypeError(`Unknown v1 contract schema: ${schemaId}`);',
+ ' return validate(payload) ? { accepted: true, value: payload } : { accepted: false };',
+ '}',
+ ].join('\n')}\n`;
+}
+
+function renderTypeScriptGenericDeclaration(parameters) {
+ if (!parameters.length) return '';
+ return `<${parameters
+ .map(({ kind, name }) =>
+ kind === 'object' ? `${name} extends object = JsonObject` : `${name} = unknown`,
+ )
+ .join(', ')}>`;
+}
+
+function renderTypeScriptInterface(name, node, entry, context, parameters, parameterMap) {
+ const lines = [`export interface ${name}${renderTypeScriptGenericDeclaration(parameters)} {`];
+ const required = new Set(node.required ?? []);
+ for (const [propertyName, property] of Object.entries(node.properties ?? {}).sort(
+ compareEntries,
+ )) {
+ const optional = required.has(propertyName) ? '' : '?';
+ lines.push(
+ ` readonly ${propertyName}${optional}: ${typescriptType(property, entry, context, parameterMap)};`,
+ );
+ }
+ lines.push('}', '');
+ return lines;
+}
+
+function typescriptType(node, entry, context, parameters) {
+ if (parameters.has(node)) return parameters.get(node);
+ if (typeof node === 'boolean' || isEmptySchema(node)) return 'unknown';
+ if (node.$ref) return resolveReference(node.$ref, entry, context).name;
+ if ('const' in node) return quoted(node.const);
+ if (node.oneOf) {
+ return node.oneOf
+ .map((alternative) => typescriptType(alternative, entry, context, parameters))
+ .join(' | ');
+ }
+ if (node.type === 'string') return 'string';
+ if (node.type === 'integer') return 'number';
+ if (node.type === 'boolean') return 'boolean';
+ if (node.type === 'array') {
+ const item = typescriptType(node.items, entry, context, parameters);
+ return `readonly ${item.includes(' | ') ? `(${item})` : item}[]`;
+ }
+ if (node.type === 'object') {
+ if (context.nameByNode.has(node)) return context.nameByNode.get(node);
+ return 'JsonObject';
+ }
+ fail(`Cannot render TypeScript type from ${entry.path}`);
+}
+
+function renderKotlin(context) {
+ const lines = [
+ `// ${HEADER}`,
+ '',
+ 'package com.databreeze.contracts.v1',
+ '',
+ 'public typealias JsonObject = Map',
+ '',
+ ];
+ const models = [...context.nodesByName.entries()].sort(compareEntries);
+
+ for (const [name, node] of models) {
+ if (node.type !== 'object' && !node.oneOf) {
+ const entry = context.entryByNode.get(node);
+ lines.push(`public typealias ${name} = ${kotlinType(node, entry, context, new Map())}`, '');
+ }
+ }
+ for (const [name, node] of models) {
+ if (!node.oneOf) continue;
+ const entry = context.entryByNode.get(node);
+ const discriminator = unionDiscriminator(node, entry, context);
+ lines.push(
+ `public sealed interface ${name} {`,
+ ` public val ${discriminator}: String`,
+ '}',
+ '',
+ );
+ }
+ for (const [name, node] of models) {
+ if (node.type !== 'object') continue;
+ const entry = context.entryByNode.get(node);
+ const parameters = genericParameters(node);
+ const parameterMap = new Map(parameters.map((parameter) => [parameter.child, parameter.name]));
+ const generic = parameters.length
+ ? `<${parameters.map(({ name: parameter }) => parameter).join(', ')}>`
+ : '';
+ const parent = context.unionMembership.get(node);
+ const discriminator = parent
+ ? unionDiscriminator(context.nodesByName.get(parent), entry, context)
+ : undefined;
+ const properties = Object.entries(node.properties ?? {}).sort(compareEntries);
+ const constructorProperties = properties.filter(
+ ([propertyName]) => propertyName !== discriminator,
+ );
+ const required = new Set(node.required ?? []);
+ lines.push(`public data class ${name}${generic}(`);
+ for (const [propertyName, property] of constructorProperties) {
+ const optional = required.has(propertyName) ? '' : '?';
+ const defaultValue = required.has(propertyName) ? '' : ' = null';
+ lines.push(
+ ` public val ${propertyName}: ${kotlinType(property, entry, context, parameterMap)}${optional}${defaultValue},`,
+ );
+ }
+ const body = [];
+ if (parent) {
+ const literal = node.properties[discriminator].const;
+ body.push(` public override val ${discriminator}: String = ${quoted(literal)}`);
+ }
+ if (node.anyOf) {
+ const alternatives = requiredAlternatives(node, entry.path).map((requiredProperties) =>
+ requiredProperties.map((property) => `${property} != null`).join(' && '),
+ );
+ if (body.length) body.push('');
+ body.push(
+ ' init {',
+ ` require(${alternatives.join(' || ')}) {`,
+ ' "at least one required schema alternative must be present"',
+ ' }',
+ ' }',
+ );
+ }
+ if (node.allOf) {
+ const conditional = conditionalConstraint(node, entry.path);
+ const present =
+ node.properties[conditional.dependent].type === 'string'
+ ? `!${conditional.dependent}.isNullOrEmpty()`
+ : `${conditional.dependent} != null`;
+ const discriminator = conditional.value
+ ? conditional.discriminator
+ : `!${conditional.discriminator}`;
+ if (body.length) body.push('');
+ body.push(
+ ' init {',
+ ` require(if (${discriminator}) ${present} else ${conditional.dependent} == null) {`,
+ ` "${conditional.dependent} must match ${conditional.discriminator}"`,
+ ' }',
+ ' }',
+ );
+ }
+ if (body.length) {
+ lines.push(parent ? `) : ${parent} {` : ') {', ...body, '}', '');
+ } else {
+ lines.push(')', '');
+ }
+ }
+ return `${lines.join('\n').trimEnd()}\n`;
+}
+
+function renderKotlinModelExpression(entry, context) {
+ const node = entry.schema;
+ if (node.oneOf) {
+ const discriminator = unionDiscriminator(node, entry, context);
+ const alternatives = node.oneOf.map((alternative) => {
+ const target = resolveReference(alternative.$ref, entry, context);
+ return {
+ literal: target.node.properties[discriminator].const,
+ modelName: target.name,
+ };
+ });
+ return [
+ `when (payload.required(${quoted(discriminator)}).asText()) {`,
+ ...alternatives.map(
+ ({ literal, modelName }) =>
+ ` ${quoted(literal)} -> mapper.treeToValue(payload, ${modelName}::class.java)`,
+ ),
+ ` else -> error("Unknown ${discriminator} discriminator")`,
+ ' }',
+ ];
+ }
+ if (node.type === 'string') return ['mapper.treeToValue(payload, String::class.java)'];
+ if (node.type === 'integer') return ['payload.longValue()'];
+ if (node.type === 'boolean') return ['payload.booleanValue()'];
+ if (node.type !== 'object') fail(`Cannot render Kotlin parser for ${entry.path}`);
+
+ const parameters = genericParameters(node);
+ if (!parameters.length) {
+ return [`mapper.treeToValue(payload, ${entry.modelName}::class.java)`];
+ }
+ const argumentsList = parameters.map(({ kind }) =>
+ kind === 'object' ? 'JsonObject' : 'JsonNode',
+ );
+ return [
+ 'mapper.convertValue(',
+ ' payload,',
+ ` object : TypeReference<${entry.modelName}<${argumentsList.join(', ')}>>() {},`,
+ ' )',
+ ];
+}
+
+function renderKotlinValidation(context) {
+ const schemaEntries = context.entries.map(({ id, schema }) => {
+ const encoded = Buffer.from(JSON.stringify(schema), 'utf8').toString('base64');
+ return ` ${quoted(id)} to decodeSchema(${quoted(encoded)}),`;
+ });
+ const modelCases = context.entries.flatMap((entry) => {
+ const [first, ...rest] = renderKotlinModelExpression(entry, context);
+ return [` ${quoted(entry.id)} -> ${first}`, ...rest.map((line) => ` ${line}`)];
+ });
+ const unions = [...context.nodesByName.entries()]
+ .filter(([, node]) => node.oneOf)
+ .sort(compareEntries)
+ .map(([name, node]) => {
+ const entry = context.entryByNode.get(node);
+ const discriminator = unionDiscriminator(node, entry, context);
+ const alternatives = node.oneOf.map((alternative) => {
+ const target = resolveReference(alternative.$ref, entry, context);
+ return {
+ literal: target.node.properties[discriminator].const,
+ modelName: target.name,
+ };
+ });
+ return { alternatives, discriminator, name };
+ });
+ const unionImports = unions.length
+ ? [
+ 'import com.fasterxml.jackson.annotation.JsonSubTypes',
+ 'import com.fasterxml.jackson.annotation.JsonTypeInfo',
+ ]
+ : [];
+ const unionMixins = unions.flatMap(({ alternatives, discriminator, name }) => [
+ '@JsonTypeInfo(',
+ ' use = JsonTypeInfo.Id.NAME,',
+ ' include = JsonTypeInfo.As.EXISTING_PROPERTY,',
+ ` property = ${quoted(discriminator)},`,
+ ' visible = false,',
+ ')',
+ '@JsonSubTypes(',
+ ...alternatives.map(
+ ({ literal, modelName }) =>
+ ` JsonSubTypes.Type(value = ${modelName}::class, name = ${quoted(literal)}),`,
+ ),
+ ')',
+ `private interface ${name}Mixin`,
+ '',
+ ]);
+ const mapperMixins = unions.map(
+ ({ name }) => ` .addMixIn(${name}::class.java, ${name}Mixin::class.java)`,
+ );
+ return `${[
+ `// ${HEADER}`,
+ '',
+ 'package com.databreeze.contracts.v1',
+ '',
+ ...unionImports,
+ 'import com.fasterxml.jackson.core.type.TypeReference',
+ 'import com.fasterxml.jackson.databind.DeserializationFeature',
+ 'import com.fasterxml.jackson.databind.JsonNode',
+ 'import com.fasterxml.jackson.databind.ObjectMapper',
+ 'import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper',
+ 'import com.networknt.schema.InputFormat',
+ 'import com.networknt.schema.SchemaLocation',
+ 'import com.networknt.schema.SchemaRegistry',
+ 'import com.networknt.schema.SpecificationVersion',
+ 'import java.util.Base64',
+ '',
+ ...unionMixins,
+ 'public sealed interface ContractV1ParseResult {',
+ ' public val accepted: Boolean',
+ '}',
+ '',
+ 'public data class AcceptedV1Contract(public val value: Any) : ContractV1ParseResult {',
+ ' public override val accepted: Boolean = true',
+ '}',
+ '',
+ 'public data object RejectedV1Contract : ContractV1ParseResult {',
+ ' public override val accepted: Boolean = false',
+ '}',
+ '',
+ 'private val mapper: ObjectMapper = jacksonObjectMapper()',
+ ' .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)',
+ ...mapperMixins,
+ '',
+ 'private fun decodeSchema(encoded: String): String =',
+ ' String(Base64.getDecoder().decode(encoded), Charsets.UTF_8)',
+ '',
+ 'private val schemaSources: Map = mapOf(',
+ ...schemaEntries,
+ ')',
+ '',
+ 'private val schemaRegistry: SchemaRegistry =',
+ ' SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12) { builder ->',
+ ' builder.schemas(schemaSources)',
+ ' }',
+ '',
+ 'private fun constructGeneratedModel(schemaId: String, payload: JsonNode): Any = when (schemaId) {',
+ ...modelCases,
+ ' else -> error("No generated Kotlin model for $schemaId")',
+ '}',
+ '',
+ 'public fun parseV1Contract(schemaId: String, payloadSource: String): ContractV1ParseResult {',
+ ' require(schemaId in schemaSources) { "Unknown v1 contract schema: $schemaId" }',
+ ' return try {',
+ ' val payload = mapper.readTree(payloadSource)',
+ ' val model = constructGeneratedModel(schemaId, payload)',
+ ' val schema = schemaRegistry.getSchema(SchemaLocation.of(schemaId))',
+ ' val errors = schema.validate(payloadSource, InputFormat.JSON) { executionContext ->',
+ ' executionContext.executionConfig { configuration ->',
+ ' configuration.formatAssertionsEnabled(true)',
+ ' }',
+ ' }',
+ ' if (errors.isEmpty()) AcceptedV1Contract(model) else RejectedV1Contract',
+ ' } catch (_: Exception) {',
+ ' RejectedV1Contract',
+ ' }',
+ '}',
+ ].join('\n')}\n`;
+}
+
+function kotlinType(node, entry, context, parameters) {
+ if (parameters.has(node)) return parameters.get(node);
+ if (typeof node === 'boolean' || isEmptySchema(node)) return 'Any?';
+ if (node.$ref) return resolveReference(node.$ref, entry, context).name;
+ if ('const' in node) {
+ if (typeof node.const === 'boolean') return 'Boolean';
+ if (typeof node.const === 'number') return 'Long';
+ return 'String';
+ }
+ if (node.oneOf) return context.nameByNode.get(node);
+ if (node.type === 'string') return 'String';
+ if (node.type === 'integer') return 'Long';
+ if (node.type === 'boolean') return 'Boolean';
+ if (node.type === 'array') return `List<${kotlinType(node.items, entry, context, parameters)}>`;
+ if (node.type === 'object') return context.nameByNode.get(node) ?? 'JsonObject';
+ fail(`Cannot render Kotlin type from ${entry.path}`);
+}
+
+function unionDiscriminator(node, entry, context) {
+ const alternatives = node.oneOf.map(
+ (alternative) => resolveReference(alternative.$ref, entry, context).node,
+ );
+ const candidates = Object.keys(alternatives[0].properties ?? {}).filter((property) =>
+ alternatives.every((alternative) => 'const' in (alternative.properties?.[property] ?? {})),
+ );
+ if (candidates.length !== 1) fail(`A closed union must have one discriminator in ${entry.path}`);
+ return candidates[0];
+}
+
+function renderPython(context) {
+ const models = [...context.nodesByName.entries()].sort(compareEntries);
+ const genericNames = [
+ ...new Set(
+ models.flatMap(([, node]) =>
+ genericParameters(node).map(({ kind, name }) => `${name}:${kind}`),
+ ),
+ ),
+ ].sort();
+ const lines = [
+ `# ${HEADER}`,
+ '',
+ 'from __future__ import annotations',
+ '',
+ 'from typing import Annotated, Any, Generic, Literal, Self, TypeAlias, TypeVar',
+ '',
+ 'from pydantic import (',
+ ' AfterValidator,',
+ ' BaseModel,',
+ ' ConfigDict,',
+ ' Field,',
+ ' StrictBool,',
+ ' StrictStr,',
+ ' StringConstraints,',
+ ' model_validator,',
+ ')',
+ '',
+ 'from ._validation import (',
+ ' reject_explicit_null_properties,',
+ ' serialization_options,',
+ ' validate_uri_reference,',
+ ' validate_utc_timestamp,',
+ ' validate_uuid,',
+ ')',
+ '',
+ 'JsonObject: TypeAlias = dict[str, Any]',
+ ];
+ for (const generic of genericNames) {
+ const [name] = generic.split(':');
+ lines.push(`${name} = TypeVar("${name}")`);
+ }
+ lines.push(
+ '',
+ '',
+ 'class ClosedModel(BaseModel):',
+ ' model_config = ConfigDict(extra="forbid")',
+ '',
+ ' @model_validator(mode="before")',
+ ' @classmethod',
+ ' def reject_explicit_nulls(cls, value: Any) -> Any:',
+ ' return reject_explicit_null_properties(value, frozenset(cls.model_fields))',
+ '',
+ ' def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]:',
+ ' return super().model_dump(*args, **serialization_options(kwargs))',
+ '',
+ ' def model_dump_json(self, *args: Any, **kwargs: Any) -> str:',
+ ' return super().model_dump_json(*args, **serialization_options(kwargs))',
+ '',
+ );
+
+ for (const [name, node] of models) {
+ if (node.type !== 'object' && !node.oneOf) {
+ const entry = context.entryByNode.get(node);
+ lines.push(`${name}: TypeAlias = ${pythonType(node, entry, context, new Map())}`, '');
+ }
+ }
+ for (const [name, node] of models) {
+ if (node.type !== 'object') continue;
+ const entry = context.entryByNode.get(node);
+ const parameters = genericParameters(node);
+ const parameterMap = new Map(parameters.map((parameter) => [parameter.child, parameter.name]));
+ const bases = ['ClosedModel'];
+ if (parameters.length)
+ bases.push(`Generic[${parameters.map(({ name: parameter }) => parameter).join(', ')}]`);
+ lines.push(`class ${name}(${bases.join(', ')}):`);
+ const properties = Object.entries(node.properties ?? {}).sort(compareEntries);
+ const required = new Set(node.required ?? []);
+ if (!properties.length) {
+ lines.push(' pass');
+ } else {
+ for (const [propertyName, property] of properties) {
+ const optional = required.has(propertyName) ? '' : ' | None';
+ const defaultValue = required.has(propertyName) ? '' : ' = None';
+ lines.push(
+ ` ${propertyName}: ${pythonType(property, entry, context, parameterMap)}${optional}${defaultValue}`,
+ );
+ }
+ }
+ if (node.anyOf) {
+ const alternatives = requiredAlternatives(node, entry.path);
+ lines.push(
+ '',
+ ' @model_validator(mode="after")',
+ ' def require_schema_alternative(self) -> Self:',
+ ` if not (${alternatives
+ .map((requiredProperties) =>
+ requiredProperties.map((property) => `self.${property} is not None`).join(' and '),
+ )
+ .map((condition) => `(${condition})`)
+ .join(' or ')}):`,
+ ' raise ValueError("at least one required schema alternative must be present")',
+ ' return self',
+ );
+ }
+ if (node.allOf) {
+ const conditional = conditionalConstraint(node, entry.path);
+ lines.push(
+ '',
+ ' @model_validator(mode="after")',
+ ' def enforce_conditional_fields(self) -> Self:',
+ ` if self.${conditional.discriminator} is ${conditional.value ? 'True' : 'False'} and self.${conditional.dependent} is None:`,
+ ` raise ValueError("${conditional.dependent} is required for this ${conditional.discriminator} value")`,
+ ` if self.${conditional.discriminator} is ${conditional.value ? 'False' : 'True'} and self.${conditional.dependent} is not None:`,
+ ` raise ValueError("${conditional.dependent} is forbidden for this ${conditional.discriminator} value")`,
+ ' return self',
+ );
+ }
+ lines.push('');
+ }
+ for (const [name, node] of models) {
+ if (!node.oneOf) continue;
+ const entry = context.entryByNode.get(node);
+ const discriminator = unionDiscriminator(node, entry, context);
+ const alternatives = node.oneOf.map(
+ (alternative) => resolveReference(alternative.$ref, entry, context).name,
+ );
+ lines.push(
+ `${name}: TypeAlias = Annotated[${alternatives.join(' | ')}, Field(discriminator=${quoted(discriminator)})]`,
+ '',
+ );
+ }
+ for (const [name, node] of models) {
+ if (node.type === 'object') lines.push(`${name}.model_rebuild()`);
+ }
+ return `${lines.join('\n').trimEnd()}\n`;
+}
+
+function pythonType(node, entry, context, parameters) {
+ if (parameters.has(node)) return parameters.get(node);
+ if (typeof node === 'boolean' || isEmptySchema(node)) return 'Any';
+ if (node.$ref) return resolveReference(node.$ref, entry, context).name;
+ if ('const' in node) return `Literal[${quoted(node.const)}]`;
+ if (node.oneOf) return context.nameByNode.get(node);
+ if (node.type === 'string') {
+ if (node.format === 'uuid') {
+ return 'Annotated[StrictStr, AfterValidator(validate_uuid)]';
+ }
+ if (node.format === 'date-time') {
+ return 'Annotated[StrictStr, AfterValidator(validate_utc_timestamp)]';
+ }
+ if (node.format === 'uri-reference') {
+ return 'Annotated[StrictStr, AfterValidator(validate_uri_reference)]';
+ }
+ const argumentsList = [];
+ if (node.minLength !== undefined) argumentsList.push(`min_length=${node.minLength}`);
+ if (node.maxLength !== undefined) argumentsList.push(`max_length=${node.maxLength}`);
+ if (node.pattern !== undefined) argumentsList.push(`pattern=${pythonRawString(node.pattern)}`);
+ return argumentsList.length
+ ? `Annotated[StrictStr, StringConstraints(${argumentsList.join(', ')})]`
+ : 'StrictStr';
+ }
+ if (node.type === 'integer') {
+ const argumentsList = ['strict=True'];
+ if (node.minimum !== undefined) argumentsList.push(`ge=${node.minimum}`);
+ if (node.maximum !== undefined) argumentsList.push(`le=${node.maximum}`);
+ return `Annotated[int, Field(${argumentsList.join(', ')})]`;
+ }
+ if (node.type === 'boolean') return 'StrictBool';
+ if (node.type === 'array') {
+ const list = `list[${pythonType(node.items, entry, context, parameters)}]`;
+ return node.maxItems !== undefined
+ ? `Annotated[${list}, Field(max_length=${node.maxItems})]`
+ : list;
+ }
+ if (node.type === 'object') return context.nameByNode.get(node) ?? 'JsonObject';
+ fail(`Cannot render Python type from ${entry.path}`);
+}
+
+function pythonRawString(value) {
+ return `r${quoted(value).replaceAll('\\\\', '\\')}`;
+}
+
+function requiredAlternatives(node, sourcePath) {
+ return node.anyOf.map((alternative) => {
+ const keys = Object.keys(alternative);
+ if (
+ !keys.every((key) => key === 'properties' || key === 'required') ||
+ !alternative.required?.length ||
+ !alternative.required.every((property) => alternative.properties?.[property] === true)
+ ) {
+ fail(`Unsupported anyOf shape in ${sourcePath}`);
+ }
+ return [...alternative.required].sort();
+ });
+}
+
+function conditionalConstraint(node, sourcePath) {
+ if (node.allOf.length !== 1) fail(`Unsupported allOf shape in ${sourcePath}`);
+ const conditional = node.allOf[0];
+ const discriminator = conditional.if?.required?.[0];
+ const dependent = conditional.then?.required?.[0];
+ const value = conditional.if?.properties?.[discriminator]?.const;
+ const forbidden = conditional.else?.not?.required?.[0];
+ if (
+ conditional.if?.required?.length !== 1 ||
+ conditional.then?.required?.length !== 1 ||
+ conditional.else?.not?.required?.length !== 1 ||
+ typeof value !== 'boolean' ||
+ dependent !== forbidden ||
+ !node.properties?.[discriminator] ||
+ !node.properties?.[dependent]
+ ) {
+ fail(`Unsupported allOf shape in ${sourcePath}`);
+ }
+ return { dependent, discriminator, value };
+}
+
+function renderPythonPackage(context) {
+ const publicNames = [...context.nodesByName.keys()].sort(compareStrings);
+ const versionInit = [
+ `# ${HEADER}`,
+ '',
+ 'from .models import (',
+ ...publicNames.map((name) => ` ${name},`),
+ ')',
+ '',
+ '__all__ = [',
+ ...publicNames.map((name) => ` ${quoted(name)},`),
+ ']',
+ '',
+ ].join('\n');
+ const packageInit = [`# ${HEADER}`, '', 'from . import v1', '', '__all__ = ["v1"]', ''].join(
+ '\n',
+ );
+ return { packageInit, versionInit };
+}
+
+function renderPythonValidation() {
+ return `${[
+ `# ${HEADER}`,
+ '',
+ 'from __future__ import annotations',
+ '',
+ 'from collections.abc import Mapping',
+ 'from typing import Any',
+ '',
+ '',
+ 'def _require_string(value: Any, format_name: str) -> str:',
+ ' if not isinstance(value, str):',
+ ' raise TypeError(f"{format_name} must be a string")',
+ ' return value',
+ '',
+ '',
+ 'def validate_uuid(value: Any) -> str:',
+ ' from uuid import UUID',
+ '',
+ ' value = _require_string(value, "uuid")',
+ ' candidate = value[9:] if value.lower().startswith("urn:uuid:") else value',
+ ' try:',
+ ' parsed = UUID(candidate)',
+ ' except ValueError as error:',
+ ' raise ValueError("value must be a UUID string") from error',
+ ' if str(parsed) != candidate.lower():',
+ ' raise ValueError("value must be a UUID string")',
+ ' return value',
+ '',
+ '',
+ 'def validate_utc_timestamp(value: Any) -> str:',
+ ' from rfc3339_validator import validate_rfc3339',
+ '',
+ ' value = _require_string(value, "date-time")',
+ ' valid = bool(validate_rfc3339(value))',
+ ' if not valid:',
+ ' prefix, separator, seconds = value.rpartition(":")',
+ ' valid = bool(',
+ ' separator',
+ ' and prefix.endswith(("T23:59", "t23:59"))',
+ ' and seconds.startswith("60")',
+ ' and validate_rfc3339(f"{prefix}:59{seconds[2:]}")',
+ ' )',
+ ' if not value.endswith("Z") or not valid:',
+ ' raise ValueError("value must be an RFC 3339 date-time ending in uppercase Z")',
+ ' return value',
+ '',
+ '',
+ 'def validate_uri_reference(value: Any) -> str:',
+ ' from rfc3986_validator import validate_rfc3986',
+ '',
+ ' value = _require_string(value, "uri-reference")',
+ ' if validate_rfc3986(value, rule="URI_reference") is None:',
+ ' raise ValueError("value must be an RFC 3986 URI reference")',
+ ' return value',
+ '',
+ '',
+ 'def reject_explicit_null_properties(',
+ ' value: Any, field_names: frozenset[str]',
+ ') -> Any:',
+ ' if isinstance(value, Mapping):',
+ ' null_fields = sorted(',
+ ' field_name',
+ ' for field_name in field_names',
+ ' if field_name in value and value[field_name] is None',
+ ' )',
+ ' if null_fields:',
+ ' raise ValueError(f"null is not allowed for: {\', \'.join(null_fields)}")',
+ ' return value',
+ '',
+ '',
+ 'def serialization_options(options: dict[str, Any]) -> dict[str, Any]:',
+ ' result = dict(options)',
+ ' result.setdefault("exclude_unset", True)',
+ ' return result',
+ ]
+ .join('\n')
+ .trimEnd()}\n`;
+}
+
+function renderPythonProject() {
+ return `${[
+ `# ${HEADER}`,
+ '',
+ '[project]',
+ 'name = "databreeze-contracts"',
+ 'version = "1.0.0"',
+ 'requires-python = ">=3.13"',
+ 'dependencies = [',
+ ' "pydantic==2.13.4",',
+ ' "rfc3339-validator==0.1.4",',
+ ' "rfc3986-validator==0.1.1",',
+ ']',
+ ].join('\n')}\n`;
+}
+
+export function generateContractFiles(sourceRoot) {
+ const context = buildModelContext(loadRegistry(sourceRoot));
+ const { packageInit, versionInit } = renderPythonPackage(context);
+ return new Map([
+ ['kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt', renderKotlin(context)],
+ [
+ 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt',
+ renderKotlinValidation(context),
+ ],
+ ['python/databreeze_contracts/__init__.py', packageInit],
+ ['python/databreeze_contracts/py.typed', ''],
+ ['python/databreeze_contracts/v1/__init__.py', versionInit],
+ ['python/databreeze_contracts/v1/_validation.py', renderPythonValidation()],
+ ['python/databreeze_contracts/v1/models.py', renderPython(context)],
+ ['python/pyproject.toml', renderPythonProject()],
+ ['typescript/v1/index.ts', renderTypeScript(context)],
+ ['typescript/v1/validation.mjs', renderTypeScriptValidation(context)],
+ ]);
+}
+
+export function writeContractFiles(sourceRoot, outputRoot) {
+ const source = resolve(sourceRoot);
+ const output = resolve(outputRoot);
+ const sourceFromOutput = relative(output, source);
+ if (
+ sourceFromOutput === '' ||
+ (!sourceFromOutput.startsWith(`..${sep}`) &&
+ sourceFromOutput !== '..' &&
+ !isAbsolute(sourceFromOutput))
+ ) {
+ fail(`Refusing to replace a source tree with generated output: ${output}`);
+ }
+ const files = generateContractFiles(sourceRoot);
+ rmSync(output, { recursive: true, force: true });
+ for (const [path, content] of files) {
+ const destination = resolve(output, ...path.split('/'));
+ mkdirSync(dirname(destination), { recursive: true });
+ writeFileSync(destination, content, 'utf8');
+ }
+ return files.size;
+}
+
+export function checkContractDrift(sourceRoot, outputRoot) {
+ const temporaryRoot = mkdtempSync(resolve(tmpdir(), 'databreeze-contracts-check-'));
+ try {
+ writeContractFiles(sourceRoot, temporaryRoot);
+ const expected = listRelativeFiles(temporaryRoot);
+ const actual = listRelativeFiles(outputRoot);
+ const expectedSet = new Set(expected);
+ const actualSet = new Set(actual);
+ const missing = expected.filter((path) => !actualSet.has(path));
+ const unexpected = actual.filter((path) => !expectedSet.has(path));
+ const stale = expected.filter(
+ (path) =>
+ actualSet.has(path) &&
+ !readFileSync(resolve(temporaryRoot, ...path.split('/'))).equals(
+ readFileSync(resolve(outputRoot, ...path.split('/'))),
+ ),
+ );
+ if (missing.length || stale.length || unexpected.length) {
+ const details = [
+ ...missing.map((path) => `missing: ${path}`),
+ ...stale.map((path) => `stale: ${path}`),
+ ...unexpected.map((path) => `unexpected: ${path}`),
+ ];
+ fail(`Generated contract drift detected:\n${details.join('\n')}`);
+ }
+ } finally {
+ rmSync(temporaryRoot, { recursive: true, force: true });
+ }
+}
+
+function listRelativeFiles(root, directory = resolve(root)) {
+ let entries;
+ try {
+ entries = readdirSync(directory, { withFileTypes: true });
+ } catch (error) {
+ if (error.code === 'ENOENT') return [];
+ throw error;
+ }
+ return entries
+ .flatMap((entry) => {
+ const path = resolve(directory, entry.name);
+ return entry.isDirectory()
+ ? listRelativeFiles(root, path)
+ : [relative(resolve(root), path).replaceAll('\\', '/')];
+ })
+ .sort(compareStrings);
+}
diff --git a/packages/contracts/scripts/generate-models.mjs b/packages/contracts/scripts/generate-models.mjs
new file mode 100644
index 00000000..b344e0bf
--- /dev/null
+++ b/packages/contracts/scripts/generate-models.mjs
@@ -0,0 +1,42 @@
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+import { checkContractDrift, writeContractFiles } from './contract-generator.mjs';
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+
+function readArguments(argumentsList) {
+ const options = {
+ check: false,
+ output: resolve(packageRoot, 'generated'),
+ source: packageRoot,
+ };
+ for (let index = 0; index < argumentsList.length; index += 1) {
+ const argument = argumentsList[index];
+ if (argument === '--check') {
+ options.check = true;
+ } else if (argument === '--source' || argument === '--output') {
+ const value = argumentsList[index + 1];
+ if (!value) throw new Error(`${argument} requires a path`);
+ options[argument.slice(2)] = resolve(value);
+ index += 1;
+ } else {
+ throw new Error(`Unknown argument: ${argument}`);
+ }
+ }
+ return options;
+}
+
+try {
+ const options = readArguments(process.argv.slice(2));
+ if (options.check) {
+ checkContractDrift(options.source, options.output);
+ console.log('Generated contract files are up to date.');
+ } else {
+ const count = writeContractFiles(options.source, options.output);
+ console.log(`Generated ${count} contract files.`);
+ }
+} catch (error) {
+ console.error(error instanceof Error ? error.message : error);
+ process.exitCode = 1;
+}
diff --git a/packages/contracts/test/compatibility.test.mjs b/packages/contracts/test/compatibility.test.mjs
new file mode 100644
index 00000000..a5321db7
--- /dev/null
+++ b/packages/contracts/test/compatibility.test.mjs
@@ -0,0 +1,218 @@
+import assert from 'node:assert/strict';
+import {
+ appendFileSync,
+ cpSync,
+ existsSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import test from 'node:test';
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const compatibilityScript = resolve(packageRoot, 'scripts/contract-compatibility.mjs');
+
+function runCompatibility(root, command, extraArguments = []) {
+ return runCompatibilityArguments(command, '--root', root, ...extraArguments);
+}
+
+function runCompatibilityArguments(...argumentsList) {
+ return spawnSync(process.execPath, [compatibilityScript, ...argumentsList], {
+ cwd: packageRoot,
+ encoding: 'utf8',
+ });
+}
+
+function withPackageCopy(callback) {
+ const temporaryRoot = mkdtempSync(resolve(tmpdir(), 'databreeze-contract-compatibility-'));
+ const copyRoot = resolve(temporaryRoot, 'contracts');
+ cpSync(packageRoot, copyRoot, { recursive: true });
+ try {
+ callback(copyRoot);
+ } finally {
+ rmSync(temporaryRoot, { recursive: true, force: true });
+ }
+}
+
+test('the checked-in published v1 compatibility baseline accepts unchanged contracts', () => {
+ const run = runCompatibility(packageRoot, 'check');
+ assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stdout, /Published contract compatibility baseline is unchanged/u);
+});
+
+test('compatibility options reject another flag where a value is required', () => {
+ for (const { argumentsList, option } of [
+ { argumentsList: ['check', '--root', '--version', '2'], option: '--root' },
+ {
+ argumentsList: ['update', '--root', packageRoot, '--version', '--approve-new-version'],
+ option: '--version',
+ },
+ ]) {
+ const run = runCompatibilityArguments(...argumentsList);
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, new RegExp(`${option} requires a value`, 'u'));
+ }
+});
+
+test('compatibility check rejects a missing published schema', () => {
+ withPackageCopy((copyRoot) => {
+ rmSync(resolve(copyRoot, 'schemas/v1/identifier.schema.json'));
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published schema is missing: schemas\/v1\/identifier\.schema\.json/u);
+ });
+});
+
+test('compatibility check rejects changed schema bytes in place', () => {
+ withPackageCopy((copyRoot) => {
+ appendFileSync(resolve(copyRoot, 'schemas/v1/revision.schema.json'), '\n', 'utf8');
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published schema bytes changed in place: revision/u);
+ });
+});
+
+test('compatibility check rejects changed generated public output in place', () => {
+ withPackageCopy((copyRoot) => {
+ appendFileSync(resolve(copyRoot, 'generated/typescript/v1/index.ts'), '\n', 'utf8');
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(
+ run.stderr,
+ /Published public output changed in place: generated\/typescript\/v1\/index\.ts/u,
+ );
+ });
+});
+
+test('compatibility check covers Python package metadata and root package markers', () => {
+ for (const path of [
+ 'generated/python/pyproject.toml',
+ 'generated/python/databreeze_contracts/__init__.py',
+ 'generated/python/databreeze_contracts/py.typed',
+ ]) {
+ withPackageCopy((copyRoot) => {
+ appendFileSync(resolve(copyRoot, path), '# changed\n', 'utf8');
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${path}\n${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published public output changed in place/u);
+ });
+ }
+});
+
+test('compatibility check rejects added and removed public outputs', () => {
+ withPackageCopy((copyRoot) => {
+ writeFileSync(
+ resolve(copyRoot, 'generated/python/databreeze_contracts/public_api.py'),
+ '# unexpected public output\n',
+ 'utf8',
+ );
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Generated output is not declared in public-output inventory/u);
+ });
+
+ withPackageCopy((copyRoot) => {
+ rmSync(resolve(copyRoot, 'generated/python/databreeze_contracts/py.typed'));
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published public output is missing/u);
+ });
+});
+
+test('compatibility check rejects public package export mapping drift', () => {
+ withPackageCopy((copyRoot) => {
+ const packagePath = resolve(copyRoot, 'package.json');
+ const packageManifest = JSON.parse(readFileSync(packagePath, 'utf8'));
+ packageManifest.exports['./v1'].import = './generated/typescript/v1/not-public.mjs';
+ writeFileSync(packagePath, `${JSON.stringify(packageManifest, null, 2)}\n`, 'utf8');
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published package surface changed in place/u);
+ });
+});
+
+test('compatibility check rejects a changed public-output inventory', () => {
+ withPackageCopy((copyRoot) => {
+ writeFileSync(
+ resolve(copyRoot, 'public-outputs.json'),
+ `${JSON.stringify({ inventoryFormat: 1, versions: [] }, null, 2)}\n`,
+ 'utf8',
+ );
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published public-output inventory changed in place/u);
+ });
+});
+
+test('compatibility check rejects a missing baseline', () => {
+ withPackageCopy((copyRoot) => {
+ rmSync(resolve(copyRoot, 'compatibility/v1/baseline.json'), { force: true });
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published baseline is missing: compatibility\/v1\/baseline\.json/u);
+ });
+});
+
+test('compatibility check rejects unauthorized baseline drift', () => {
+ withPackageCopy((copyRoot) => {
+ const baselinePath = resolve(copyRoot, 'compatibility/v1/baseline.json');
+ if (existsSync(baselinePath)) appendFileSync(baselinePath, '\n', 'utf8');
+
+ const run = runCompatibility(copyRoot, 'check');
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(run.stderr, /Published baseline drift detected for v1/u);
+ });
+});
+
+test('baseline update is deterministic for a reviewed unpublished version', () => {
+ withPackageCopy((copyRoot) => {
+ rmSync(resolve(copyRoot, 'compatibility'), { recursive: true, force: true });
+
+ const first = runCompatibility(copyRoot, 'update', ['--version', '1', '--approve-new-version']);
+ assert.equal(first.status, 0, `${first.stdout}\n${first.stderr}`);
+ const firstPublished = readFileSync(resolve(copyRoot, 'compatibility/published.json'), 'utf8');
+ const firstBaseline = readFileSync(resolve(copyRoot, 'compatibility/v1/baseline.json'), 'utf8');
+
+ const second = runCompatibility(copyRoot, 'update', [
+ '--version',
+ '1',
+ '--approve-new-version',
+ ]);
+ assert.equal(second.status, 0, `${second.stdout}\n${second.stderr}`);
+ assert.equal(
+ readFileSync(resolve(copyRoot, 'compatibility/published.json'), 'utf8'),
+ firstPublished,
+ );
+ assert.equal(
+ readFileSync(resolve(copyRoot, 'compatibility/v1/baseline.json'), 'utf8'),
+ firstBaseline,
+ );
+ });
+});
+
+test('baseline update refuses to rewrite an already published v1 contract', () => {
+ withPackageCopy((copyRoot) => {
+ appendFileSync(resolve(copyRoot, 'schemas/v1/revision.schema.json'), '\n', 'utf8');
+
+ const run = runCompatibility(copyRoot, 'update', ['--version', '1', '--approve-new-version']);
+ assert.equal(run.status, 1, `${run.stdout}\n${run.stderr}`);
+ assert.match(
+ run.stderr,
+ /Refusing to rewrite published v1; publish new schema IDs and a new version/u,
+ );
+ });
+});
diff --git a/packages/contracts/test/fixtures/generator/manifest.json b/packages/contracts/test/fixtures/generator/manifest.json
new file mode 100644
index 00000000..8445a2d6
--- /dev/null
+++ b/packages/contracts/test/fixtures/generator/manifest.json
@@ -0,0 +1,16 @@
+{
+ "draft": "https://json-schema.org/draft/2020-12/schema",
+ "version": 1,
+ "schemas": [
+ {
+ "name": "sample-envelope",
+ "id": "https://schemas.example.test/contracts/v1/sample-envelope",
+ "path": "schemas/v1/sample-envelope.schema.json"
+ },
+ {
+ "name": "alpha",
+ "id": "https://schemas.example.test/contracts/v1/alpha",
+ "path": "schemas/v1/alpha.schema.json"
+ }
+ ]
+}
diff --git a/packages/contracts/test/fixtures/generator/schemas/v1/alpha.schema.json b/packages/contracts/test/fixtures/generator/schemas/v1/alpha.schema.json
new file mode 100644
index 00000000..088f2170
--- /dev/null
+++ b/packages/contracts/test/fixtures/generator/schemas/v1/alpha.schema.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.example.test/contracts/v1/alpha",
+ "title": "Alpha Identifier",
+ "type": "string",
+ "pattern": "^[a-z]+$"
+}
diff --git a/packages/contracts/test/fixtures/generator/schemas/v1/sample-envelope.schema.json b/packages/contracts/test/fixtures/generator/schemas/v1/sample-envelope.schema.json
new file mode 100644
index 00000000..580b52ee
--- /dev/null
+++ b/packages/contracts/test/fixtures/generator/schemas/v1/sample-envelope.schema.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://schemas.example.test/contracts/v1/sample-envelope",
+ "title": "Sample Envelope",
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["kind", "id", "data"],
+ "properties": {
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "kind": {
+ "const": "sample"
+ },
+ "id": {
+ "$ref": "https://schemas.example.test/contracts/v1/alpha"
+ },
+ "data": {
+ "type": "object"
+ }
+ }
+}
diff --git a/packages/contracts/test/generation.test.mjs b/packages/contracts/test/generation.test.mjs
new file mode 100644
index 00000000..8d746a8c
--- /dev/null
+++ b/packages/contracts/test/generation.test.mjs
@@ -0,0 +1,427 @@
+import assert from 'node:assert/strict';
+import {
+ appendFileSync,
+ cpSync,
+ existsSync,
+ mkdtempSync,
+ readFileSync,
+ readdirSync,
+ rmSync,
+ unlinkSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join, relative, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import test from 'node:test';
+import Ajv2020 from 'ajv/dist/2020.js';
+import addFormats from 'ajv-formats';
+
+import { resolvePythonInterpreter } from './test-runtime-tools.mjs';
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const generatorPath = resolve(packageRoot, 'scripts/generate-models.mjs');
+const fixtureRoot = resolve(packageRoot, 'test/fixtures/generator');
+const generatedRoot = resolve(packageRoot, 'generated');
+const pythonInterpreter = resolvePythonInterpreter();
+const expectedFiles = [
+ 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt',
+ 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Validation.kt',
+ 'python/databreeze_contracts/__init__.py',
+ 'python/databreeze_contracts/py.typed',
+ 'python/databreeze_contracts/v1/__init__.py',
+ 'python/databreeze_contracts/v1/_validation.py',
+ 'python/databreeze_contracts/v1/models.py',
+ 'python/pyproject.toml',
+ 'typescript/v1/index.ts',
+ 'typescript/v1/validation.mjs',
+];
+
+function runGenerator(...args) {
+ return spawnSync(process.execPath, [generatorPath, ...args], {
+ cwd: packageRoot,
+ encoding: 'utf8',
+ });
+}
+
+function listFiles(root, directory = root) {
+ if (!existsSync(directory)) return [];
+
+ return readdirSync(directory, { withFileTypes: true })
+ .flatMap((entry) => {
+ const path = join(directory, entry.name);
+ return entry.isDirectory()
+ ? listFiles(root, path)
+ : [relative(root, path).replaceAll('\\', '/')];
+ })
+ .sort();
+}
+
+function snapshot(root) {
+ return listFiles(root).map((path) => [
+ path,
+ readFileSync(resolve(root, path)).toString('base64'),
+ ]);
+}
+
+function withTemporaryDirectory(run) {
+ const directory = mkdtempSync(join(tmpdir(), 'databreeze-contracts-test-'));
+ try {
+ return run(directory);
+ } finally {
+ rmSync(directory, { recursive: true, force: true });
+ }
+}
+
+function runPythonValidationProgram(program) {
+ const source = resolve(generatedRoot, 'python/databreeze_contracts/v1/_validation.py');
+ assert.equal(existsSync(source), true, 'generated Python validation helpers are missing');
+ return spawnSync(pythonInterpreter, ['-c', program, source], {
+ cwd: packageRoot,
+ encoding: 'utf8',
+ });
+}
+
+test('generates byte-stable language packages from a controlled registry', () => {
+ withTemporaryDirectory((directory) => {
+ const first = resolve(directory, 'first');
+ const second = resolve(directory, 'second');
+ const firstRun = runGenerator('--source', fixtureRoot, '--output', first);
+ const secondRun = runGenerator('--source', fixtureRoot, '--output', second);
+
+ assert.equal(firstRun.status, 0, firstRun.stderr);
+ assert.equal(secondRun.status, 0, secondRun.stderr);
+ assert.deepEqual(listFiles(first), expectedFiles);
+ assert.deepEqual(snapshot(first), snapshot(second));
+
+ for (const path of expectedFiles.filter((entry) => !entry.endsWith('py.typed'))) {
+ const content = readFileSync(resolve(first, path), 'utf8');
+ assert.match(content, /^[/#].*Generated by @databreeze\/contracts\. DO NOT EDIT\./);
+ assert.equal(content.includes('\r'), false, `${path} must use LF newlines`);
+ assert.equal(content.endsWith('\n'), true, `${path} must end with a newline`);
+ }
+
+ const typescript = readFileSync(resolve(first, 'typescript/v1/index.ts'), 'utf8');
+ assert.match(typescript, /export type Alpha = string;/);
+ assert.match(typescript, /export interface SampleEnvelope/);
+ assert.ok(
+ typescript.indexOf('export type Alpha') <
+ typescript.indexOf('export interface SampleEnvelope'),
+ );
+ assert.ok(
+ typescript.indexOf('readonly data: TData;') < typescript.indexOf('readonly id: Alpha;'),
+ );
+ assert.match(typescript, /readonly kind: "sample";/);
+ assert.match(typescript, /readonly labels\?: readonly string\[\];/);
+
+ const kotlin = readFileSync(
+ resolve(first, 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt'),
+ 'utf8',
+ );
+ assert.match(kotlin, /package com\.databreeze\.contracts\.v1/);
+ assert.match(kotlin, /public typealias Alpha = String/);
+ assert.match(kotlin, /public data class SampleEnvelope/);
+
+ const python = readFileSync(resolve(first, 'python/databreeze_contracts/v1/models.py'), 'utf8');
+ assert.match(python, /class ClosedModel\(BaseModel\):/);
+ assert.match(python, /model_config = ConfigDict\(extra="forbid"\)/);
+ assert.match(python, /TData = TypeVar\("TData"\)/);
+ assert.match(python, /class SampleEnvelope\(ClosedModel, Generic\[TData\]\):/);
+
+ const check = runGenerator('--check', '--source', fixtureRoot, '--output', first);
+ assert.equal(check.status, 0, check.stderr);
+ assert.match(check.stdout, /Generated contract files are up to date\./);
+ });
+});
+
+test('fails loudly when a schema uses an unsupported construct', () => {
+ withTemporaryDirectory((directory) => {
+ const source = resolve(directory, 'source');
+ cpSync(fixtureRoot, source, { recursive: true });
+ const schemaPath = resolve(source, 'schemas/v1/alpha.schema.json');
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
+ schema.enum = ['alpha'];
+ writeFileSync(schemaPath, `${JSON.stringify(schema, null, 2)}\n`);
+
+ const result = runGenerator('--source', source, '--output', resolve(directory, 'output'));
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /Unsupported JSON Schema keyword "enum"/);
+ assert.match(result.stderr, /alpha\.schema\.json/);
+ });
+});
+
+test('fails loudly when a schema contains the unsatisfiable false schema', () => {
+ withTemporaryDirectory((directory) => {
+ const source = resolve(directory, 'source');
+ cpSync(fixtureRoot, source, { recursive: true });
+ const schemaPath = resolve(source, 'schemas/v1/sample-envelope.schema.json');
+ const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
+ schema.properties.labels.items = false;
+ writeFileSync(schemaPath, `${JSON.stringify(schema, null, 2)}\n`);
+
+ const result = runGenerator('--source', source, '--output', resolve(directory, 'output'));
+
+ assert.notEqual(result.status, 0);
+ assert.match(result.stderr, /Unsupported boolean JSON Schema false/);
+ assert.match(result.stderr, /sample-envelope\.schema\.json#\/properties\/labels\/items/);
+ });
+});
+
+test('drift check reports missing stale and unexpected files without mutation', () => {
+ withTemporaryDirectory((directory) => {
+ const output = resolve(directory, 'output');
+ const generate = runGenerator('--source', fixtureRoot, '--output', output);
+ assert.equal(generate.status, 0, generate.stderr);
+
+ const missing = 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt';
+ const stale = 'typescript/v1/index.ts';
+ const unexpected = 'typescript/v1/obsolete.ts';
+ unlinkSync(resolve(output, missing));
+ appendFileSync(resolve(output, stale), '// stale\n');
+ writeFileSync(resolve(output, unexpected), '// unexpected\n');
+ const before = snapshot(output);
+
+ const check = runGenerator('--check', '--source', fixtureRoot, '--output', output);
+
+ assert.notEqual(check.status, 0);
+ assert.match(check.stderr, new RegExp(`missing: ${missing.replaceAll('/', '\\/')}`));
+ assert.match(check.stderr, new RegExp(`stale: ${stale.replaceAll('/', '\\/')}`));
+ assert.match(check.stderr, new RegExp(`unexpected: ${unexpected.replaceAll('/', '\\/')}`));
+ assert.deepEqual(snapshot(output), before);
+ });
+});
+
+test('generation refuses to replace its schema source tree', () => {
+ withTemporaryDirectory((directory) => {
+ const source = resolve(directory, 'source');
+ cpSync(fixtureRoot, source, { recursive: true });
+ const before = snapshot(source);
+
+ const generate = runGenerator('--source', source, '--output', source);
+
+ assert.notEqual(generate.status, 0);
+ assert.match(generate.stderr, /Refusing to replace a source tree with generated output/);
+ assert.deepEqual(snapshot(source), before);
+ });
+});
+
+test('the checked-in TypeScript contracts compile in strict mode', () => {
+ const source = resolve(generatedRoot, 'typescript/v1/index.ts');
+ assert.equal(existsSync(source), true, 'generated TypeScript entry point is missing');
+ withTemporaryDirectory((directory) => {
+ let importPath = relative(directory, source).replaceAll('\\', '/').replace(/\.ts$/u, '.js');
+ if (!importPath.startsWith('.')) importPath = `./${importPath}`;
+ const consumer = resolve(directory, 'consumer.ts');
+ writeFileSync(
+ consumer,
+ [
+ `import type { CommandEnvelope, EventEnvelope } from ${JSON.stringify(importPath)};`,
+ '',
+ 'interface DomainPayload {',
+ ' readonly displayName: string;',
+ '}',
+ '',
+ 'declare const command: CommandEnvelope;',
+ 'declare const event: EventEnvelope;',
+ 'const commandName: string = command.data.displayName;',
+ 'const eventName: string = event.data.displayName;',
+ 'void commandName;',
+ 'void eventName;',
+ '',
+ ].join('\n'),
+ );
+ const compiler = resolve(packageRoot, '../../node_modules/typescript/bin/tsc');
+ const result = spawnSync(
+ process.execPath,
+ [
+ compiler,
+ '--noEmit',
+ '--strict',
+ '--target',
+ 'ES2024',
+ '--module',
+ 'NodeNext',
+ '--moduleResolution',
+ 'NodeNext',
+ '--skipLibCheck',
+ consumer,
+ ],
+ { cwd: packageRoot, encoding: 'utf8' },
+ );
+
+ assert.equal(result.status, 0, `${result.error ?? ''}\n${result.stdout}\n${result.stderr}`);
+ });
+});
+
+test('the checked-in Python package compiles with the available interpreter', () => {
+ const files = expectedFiles
+ .filter((path) => path.endsWith('.py'))
+ .map((path) => resolve(generatedRoot, path));
+ for (const path of files) assert.equal(existsSync(path), true, `${path} is missing`);
+ const program = [
+ 'from pathlib import Path',
+ 'import sys',
+ 'for name in sys.argv[1:]:',
+ ' source = Path(name).read_text(encoding="utf-8")',
+ ' compile(source, name, "exec")',
+ ].join('\n');
+ const result = spawnSync(pythonInterpreter, ['-c', program, ...files], {
+ cwd: packageRoot,
+ encoding: 'utf8',
+ });
+
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
+});
+
+test('generated Pydantic models reject explicit nulls and omit absent properties by default', () => {
+ const models = readFileSync(
+ resolve(generatedRoot, 'python/databreeze_contracts/v1/models.py'),
+ 'utf8',
+ );
+ assert.match(
+ models,
+ /return reject_explicit_null_properties\(value, frozenset\(cls\.model_fields\)\)/,
+ );
+ assert.match(
+ models,
+ /return super\(\)\.model_dump\(\*args, \*\*serialization_options\(kwargs\)\)/,
+ );
+ assert.match(
+ models,
+ /return super\(\)\.model_dump_json\(\*args, \*\*serialization_options\(kwargs\)\)/,
+ );
+
+ const program = [
+ 'import json',
+ 'import runpy',
+ 'import sys',
+ 'helpers = runpy.run_path(sys.argv[1])',
+ 'reject = helpers["reject_explicit_null_properties"]',
+ 'options = helpers["serialization_options"]',
+ 'missing = reject({"status": 400}, frozenset({"status", "detail"}))',
+ 'try:',
+ ' reject({"status": 400, "detail": None}, frozenset({"status", "detail"}))',
+ ' explicit_null_rejected = False',
+ 'except ValueError:',
+ ' explicit_null_rejected = True',
+ 'print(json.dumps([missing, explicit_null_rejected, options({}), options({"exclude_unset": False})], sort_keys=True))',
+ ].join('\n');
+ const result = runPythonValidationProgram(program);
+
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
+ assert.equal(
+ result.stdout.trim(),
+ '[{"status": 400}, true, {"exclude_unset": true}, {"exclude_unset": false}]',
+ );
+});
+
+test('generated Python helpers preserve formats and strict primitive declarations', () => {
+ const models = readFileSync(
+ resolve(generatedRoot, 'python/databreeze_contracts/v1/models.py'),
+ 'utf8',
+ );
+ assert.match(
+ models,
+ /Identifier: TypeAlias = Annotated\[StrictStr, AfterValidator\(validate_uuid\)\]/,
+ );
+ assert.match(
+ models,
+ /UtcTimestamp: TypeAlias = Annotated\[StrictStr, AfterValidator\(validate_utc_timestamp\)\]/,
+ );
+ assert.match(models, /status: Annotated\[int, Field\(strict=True, ge=100, le=599\)\]/);
+ assert.match(models, /retryable: StrictBool/);
+ assert.match(models, /type: Annotated\[StrictStr, AfterValidator\(validate_uri_reference\)\]/);
+ assert.match(
+ models,
+ /instance: Annotated\[StrictStr, AfterValidator\(validate_uri_reference\)\] \| None = None/,
+ );
+ const pyprojectPath = resolve(generatedRoot, 'python/pyproject.toml');
+ assert.equal(existsSync(pyprojectPath), true, 'generated Python dependency manifest is missing');
+ const pyproject = readFileSync(pyprojectPath, 'utf8');
+ assert.match(pyproject, /"pydantic==2\.13\.4"/);
+ assert.match(pyproject, /"rfc3339-validator==0\.1\.4"/);
+ assert.match(pyproject, /"rfc3986-validator==0\.1\.1"/);
+
+ const helpers = readFileSync(
+ resolve(generatedRoot, 'python/databreeze_contracts/v1/_validation.py'),
+ 'utf8',
+ );
+ assert.match(helpers, /from uuid import UUID/);
+ assert.match(helpers, /from rfc3339_validator import validate_rfc3339/);
+ assert.match(helpers, /from rfc3986_validator import validate_rfc3986/);
+ assert.match(helpers, /UUID\(candidate\)/);
+ assert.match(helpers, /validate_rfc3339\(value\)/);
+ assert.match(helpers, /validate_rfc3986\(value, rule="URI_reference"\)/);
+ assert.doesNotMatch(helpers, /datetime\.fromisoformat|urlsplit|_UTC_TIMESTAMP|_UUID =/);
+
+ const ajv = new Ajv2020({ strict: true });
+ addFormats(ajv);
+ const validators = {
+ 'date-time': ajv.compile({ type: 'string', format: 'date-time', pattern: 'Z$' }),
+ 'uri-reference': ajv.compile({ type: 'string', format: 'uri-reference' }),
+ uuid: ajv.compile({ type: 'string', format: 'uuid' }),
+ };
+ const cases = [
+ ['uri-reference', 'abc]'],
+ ['uri-reference', 'http://example.com/[]'],
+ ['uri-reference', 'foo#bar#baz'],
+ ['date-time', '2016-12-31T23:59:60Z'],
+ ['date-time', '2016-12-31T23:60:00Z'],
+ ['uuid', 'urn:uuid:018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc01'],
+ ];
+ assert.deepEqual(
+ cases.map(([format, value]) => validators[format](value)),
+ [false, false, false, true, false, true],
+ );
+});
+
+test('generated Pydantic tenant discriminators remain required', () => {
+ const models = readFileSync(
+ resolve(generatedRoot, 'python/databreeze_contracts/v1/models.py'),
+ 'utf8',
+ );
+
+ assert.match(models, /scopeType: Literal\["organization"\]\n/);
+ assert.match(models, /scopeType: Literal\["workspace"\]\n/);
+ assert.match(models, /scopeType: Literal\["project"\]\n/);
+ assert.doesNotMatch(models, /scopeType: Literal\[[^\]]+\] =/);
+});
+
+test('generated Kotlin models preserve cursor and problem constructor invariants', () => {
+ const kotlin = readFileSync(
+ resolve(generatedRoot, 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt'),
+ 'utf8',
+ );
+
+ assert.match(
+ kotlin,
+ /require\(if \(hasMore\) !nextCursor\.isNullOrEmpty\(\) else nextCursor == null\)/,
+ );
+ assert.doesNotMatch(kotlin, /nextCursor\.isNullOrBlank\(\)/);
+ assert.equal(' '.length >= 1, true, 'JSON Schema minLength: 1 accepts a whitespace cursor');
+ assert.match(kotlin, /require\(titleKey != null \|\| messageKey != null\)/);
+});
+
+test('the checked-in Kotlin models have a deterministic standard-Kotlin structure', () => {
+ const source = resolve(
+ generatedRoot,
+ 'kotlin/src/main/kotlin/com/databreeze/contracts/v1/Models.kt',
+ );
+ assert.equal(existsSync(source), true, 'generated Kotlin source is missing');
+ const kotlin = readFileSync(source, 'utf8');
+
+ assert.match(kotlin, /^\/\/ Generated by @databreeze\/contracts\. DO NOT EDIT\.\n/);
+ assert.match(kotlin, /package com\.databreeze\.contracts\.v1/);
+ assert.match(kotlin, /public sealed interface TenantScope/);
+ assert.match(kotlin, /public data class OrganizationScope/);
+ assert.match(kotlin, /public data class WorkspaceScope/);
+ assert.match(kotlin, /public data class ProjectScope/);
+ assert.match(kotlin, /public data class CommandEnvelope/);
+ assert.match(kotlin, /public data class CursorPage/);
+ assert.match(kotlin, /public data class EventEnvelope/);
+ assert.doesNotMatch(kotlin, /android\.|androidx\.|kotlinx\./);
+ assert.equal((kotlin.match(/\{/g) ?? []).length, (kotlin.match(/\}/g) ?? []).length);
+ assert.equal((kotlin.match(/\(/g) ?? []).length, (kotlin.match(/\)/g) ?? []).length);
+});
diff --git a/packages/contracts/test/python-format-runtime-probe.mjs b/packages/contracts/test/python-format-runtime-probe.mjs
new file mode 100644
index 00000000..0ee6619d
--- /dev/null
+++ b/packages/contracts/test/python-format-runtime-probe.mjs
@@ -0,0 +1,107 @@
+import assert from 'node:assert/strict';
+import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import Ajv2020 from 'ajv/dist/2020.js';
+import addFormats from 'ajv-formats';
+
+import { resolvePythonInterpreter } from './test-runtime-tools.mjs';
+
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const generatedPython = resolve(packageRoot, 'generated/python');
+const pythonInterpreter = resolvePythonInterpreter();
+const pyproject = readFileSync(resolve(generatedPython, 'pyproject.toml'), 'utf8');
+const dependenciesSection = /dependencies\s*=\s*\[([\s\S]*?)\]/u.exec(pyproject);
+assert.ok(dependenciesSection, 'generated pyproject.toml must declare project dependencies');
+const dependencies = [...dependenciesSection[1].matchAll(/"([^"]+)"/gu)].map((match) => match[1]);
+assert.deepEqual(dependencies, [
+ 'pydantic==2.13.4',
+ 'rfc3339-validator==0.1.4',
+ 'rfc3986-validator==0.1.1',
+]);
+
+const cases = [
+ { format: 'uri-reference', value: 'abc]' },
+ { format: 'uri-reference', value: 'http://example.com/[]' },
+ { format: 'uri-reference', value: 'foo#bar#baz' },
+ { format: 'date-time', value: '2016-12-31T23:59:60Z' },
+ { format: 'date-time', value: '2016-12-31T12:34:60Z' },
+ { format: 'date-time', value: '2016-12-31T23:60:00Z' },
+ { format: 'uuid', value: 'urn:uuid:018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc01' },
+];
+const ajv = new Ajv2020({ strict: true });
+addFormats(ajv);
+const ajvValidators = {
+ 'date-time': ajv.compile({ type: 'string', format: 'date-time', pattern: 'Z$' }),
+ 'uri-reference': ajv.compile({ type: 'string', format: 'uri-reference' }),
+ uuid: ajv.compile({ type: 'string', format: 'uuid' }),
+};
+const expected = cases.map(({ format, value }) => ({
+ accepted: ajvValidators[format](value),
+ returnedOriginal: ajvValidators[format](value) ? true : null,
+}));
+assert.deepEqual(
+ expected.map(({ accepted }) => accepted),
+ [false, false, false, true, false, false, true],
+ 'canonical Ajv expectations changed',
+);
+
+const pythonProgram = [
+ 'import importlib.util',
+ 'import json',
+ 'import sys',
+ 'sys.dont_write_bytecode = True',
+ 'sys.path.insert(0, sys.argv[1])',
+ 'spec = importlib.util.spec_from_file_location("generated_validation", sys.argv[2])',
+ 'module = importlib.util.module_from_spec(spec)',
+ 'spec.loader.exec_module(module)',
+ 'cases = json.loads(sys.argv[3])',
+ 'function_names = {"date-time": "validate_utc_timestamp", "uri-reference": "validate_uri_reference", "uuid": "validate_uuid"}',
+ 'results = []',
+ 'for case in cases:',
+ ' function = getattr(module, function_names[case["format"]])',
+ ' try:',
+ ' result = function(case["value"])',
+ ' results.append({"accepted": True, "returnedOriginal": result == case["value"]})',
+ ' except (TypeError, ValueError):',
+ ' results.append({"accepted": False, "returnedOriginal": None})',
+ 'print(json.dumps(results))',
+].join('\n');
+
+const temporaryRoot = mkdtempSync(resolve(tmpdir(), 'databreeze-python-formats-'));
+try {
+ const dependenciesRoot = resolve(temporaryRoot, 'site-packages');
+ const install = spawnSync(
+ pythonInterpreter,
+ [
+ '-m',
+ 'pip',
+ 'install',
+ '--disable-pip-version-check',
+ '--quiet',
+ '--target',
+ dependenciesRoot,
+ ...dependencies,
+ ],
+ { cwd: packageRoot, encoding: 'utf8' },
+ );
+ assert.equal(install.status, 0, `${install.stdout}\n${install.stderr}`);
+
+ const validationPath = resolve(generatedPython, 'databreeze_contracts/v1/_validation.py');
+ const probe = spawnSync(
+ pythonInterpreter,
+ ['-c', pythonProgram, dependenciesRoot, validationPath, JSON.stringify(cases)],
+ {
+ cwd: packageRoot,
+ encoding: 'utf8',
+ env: { ...process.env, PYTHONDONTWRITEBYTECODE: '1' },
+ },
+ );
+ assert.equal(probe.status, 0, `${probe.stdout}\n${probe.stderr}`);
+ assert.deepEqual(JSON.parse(probe.stdout), expected);
+ console.log(`Python format runtime parity probe passed for ${cases.length} cases.`);
+} finally {
+ rmSync(temporaryRoot, { recursive: true, force: true });
+}
diff --git a/packages/contracts/test/schemas.test.mjs b/packages/contracts/test/schemas.test.mjs
new file mode 100644
index 00000000..a3e6f633
--- /dev/null
+++ b/packages/contracts/test/schemas.test.mjs
@@ -0,0 +1,359 @@
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync } from 'node:fs';
+import { dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import test from 'node:test';
+
+import Ajv2020 from 'ajv/dist/2020.js';
+import addFormats from 'ajv-formats';
+
+// Partial foundation coverage: IAM-001, IAM-019, AUD-004, AUD-006,
+// INT-004, INT-005, INT-008, and INT-021.
+const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const manifestPath = resolve(packageRoot, 'manifest.json');
+const schemaBase = 'https://schemas.databreeze.dev/contracts/v1';
+
+const ids = {
+ actorMetadata: `${schemaBase}/actor-metadata`,
+ commandEnvelope: `${schemaBase}/command-envelope`,
+ correlationMetadata: `${schemaBase}/correlation-metadata`,
+ cursorPage: `${schemaBase}/cursor-page`,
+ eventEnvelope: `${schemaBase}/event-envelope`,
+ identifier: `${schemaBase}/identifier`,
+ problemDetails: `${schemaBase}/problem-details`,
+ revision: `${schemaBase}/revision`,
+ tenantScope: `${schemaBase}/tenant-scope`,
+ utcTimestamp: `${schemaBase}/utc-timestamp`,
+};
+
+const organizationId = '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc01';
+const workspaceId = '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc02';
+const projectId = '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc03';
+const actorId = '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc04';
+const correlationId = '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc05';
+
+function loadContracts() {
+ assert.equal(existsSync(manifestPath), true, 'canonical schema manifest must exist');
+
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
+ const schemas = manifest.schemas.map((entry) => {
+ const schemaPath = resolve(packageRoot, entry.path);
+ assert.equal(existsSync(schemaPath), true, `schema source must exist: ${entry.path}`);
+ return JSON.parse(readFileSync(schemaPath, 'utf8'));
+ });
+
+ const ajv = new Ajv2020({ allErrors: true, strict: true });
+ addFormats(ajv);
+ for (const schema of schemas) {
+ ajv.addSchema(schema);
+ }
+
+ return { ajv, manifest, schemas };
+}
+
+function validatorFor(id) {
+ const { ajv } = loadContracts();
+ const validate = ajv.getSchema(id);
+ assert.ok(validate, `manifest must register ${id}`);
+ return validate;
+}
+
+test('publishes the complete deterministic v1 registry and compiles every real schema', () => {
+ const { ajv, manifest, schemas } = loadContracts();
+ const expectedNames = [
+ 'actor-metadata',
+ 'command-envelope',
+ 'correlation-metadata',
+ 'cursor-page',
+ 'event-envelope',
+ 'identifier',
+ 'problem-details',
+ 'revision',
+ 'tenant-scope',
+ 'utc-timestamp',
+ ];
+
+ assert.equal(manifest.draft, 'https://json-schema.org/draft/2020-12/schema');
+ assert.equal(manifest.version, 1);
+ assert.deepEqual(
+ manifest.schemas.map((entry) => entry.name),
+ expectedNames,
+ );
+ assert.deepEqual(
+ manifest.schemas.map((entry) => entry.id),
+ expectedNames.map((name) => `${schemaBase}/${name}`),
+ );
+ assert.deepEqual(
+ schemas.map((schema) => schema.$id),
+ manifest.schemas.map((entry) => entry.id),
+ );
+
+ for (const entry of manifest.schemas) {
+ assert.ok(ajv.getSchema(entry.id), `schema must compile: ${entry.name}`);
+ }
+});
+
+test('exports only declared registry schema and generated TypeScript entry points', () => {
+ const packageJson = JSON.parse(readFileSync(resolve(packageRoot, 'package.json'), 'utf8'));
+
+ assert.deepEqual(Object.keys(packageJson.exports), [
+ '.',
+ './v1',
+ './v1/actor-metadata',
+ './v1/command-envelope',
+ './v1/correlation-metadata',
+ './v1/cursor-page',
+ './v1/event-envelope',
+ './v1/identifier',
+ './v1/problem-details',
+ './v1/revision',
+ './v1/tenant-scope',
+ './v1/utc-timestamp',
+ ]);
+ for (const target of Object.values(packageJson.exports)) {
+ const paths = typeof target === 'string' ? [target] : Object.values(target);
+ for (const path of paths) {
+ assert.equal(
+ existsSync(resolve(packageRoot, path)),
+ true,
+ `export target must exist: ${path}`,
+ );
+ }
+ }
+});
+
+test('rejects a malformed UUID identifier', () => {
+ const validate = validatorFor(ids.identifier);
+
+ assert.equal(validate('not-a-uuid'), false);
+ assert.equal(validate(organizationId), true);
+});
+
+test('rejects a timestamp that is not expressed with UTC Z', () => {
+ const validate = validatorFor(ids.utcTimestamp);
+
+ assert.equal(validate('2026-08-01T08:30:00+07:00'), false);
+ assert.equal(validate('2026-08-01T01:30:00.125Z'), true);
+});
+
+test('accepts only positive entity revisions', () => {
+ const validate = validatorFor(ids.revision);
+
+ assert.equal(validate(0), false);
+ assert.equal(validate(1), true);
+});
+
+test('accepts explicit organization, workspace, and project tenant ancestry', () => {
+ const validate = validatorFor(ids.tenantScope);
+
+ assert.equal(validate({ scopeType: 'organization', organizationId }), true);
+ assert.equal(validate({ scopeType: 'workspace', organizationId, workspaceId }), true);
+ assert.equal(validate({ scopeType: 'project', organizationId, workspaceId, projectId }), true);
+});
+
+test('rejects incomplete or discriminator-mismatched tenant ancestry', () => {
+ const validate = validatorFor(ids.tenantScope);
+
+ assert.equal(validate({ scopeType: 'project', organizationId, projectId }), false);
+ assert.equal(validate({ scopeType: 'workspace', organizationId, workspaceId, projectId }), false);
+});
+
+test('accepts closed correlation metadata and rejects undeclared context', () => {
+ const validate = validatorFor(ids.correlationMetadata);
+
+ assert.equal(validate({ correlationId }), true);
+ assert.equal(validate({ correlationId, customerEmail: 'sensitive@example.test' }), false);
+});
+
+test('rejects RFC problem details with an invalid HTTP status', () => {
+ const validate = validatorFor(ids.problemDetails);
+ const problem = {
+ type: 'https://api.databreeze.dev/problems/validation-failed',
+ title: 'Request validation failed',
+ status: 422,
+ detail: 'One or more request fields are invalid.',
+ instance: '/requests/018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc09',
+ code: 'VALIDATION_FAILED',
+ correlationId,
+ retryable: false,
+ messageKey: 'errors.validationFailed',
+ fieldErrors: [{ field: 'name', code: 'REQUIRED' }],
+ };
+
+ assert.equal(validate(problem), true);
+ assert.equal(validate({ ...problem, status: 99 }), false);
+ assert.equal(validate({ ...problem, status: 600 }), false);
+});
+
+test('accepts the documented Web problem shape with title localization and revision recovery', () => {
+ const validate = validatorFor(ids.problemDetails);
+ const webProblem = {
+ type: 'https://api.databreeze.dev/problems/revision-conflict',
+ titleKey: 'errors.revisionConflict.title',
+ status: 409,
+ code: 'REVISION_CONFLICT',
+ correlationId,
+ retryable: false,
+ currentRevision: 7,
+ remediationAction: 'refresh-and-retry',
+ };
+
+ assert.equal(validate(webProblem), true);
+});
+
+test('accepts the documented rate-limit problem shape with message localization', () => {
+ const validate = validatorFor(ids.problemDetails);
+ const rateLimitProblem = {
+ type: 'https://api.databreeze.dev/problems/rate-limit-exceeded',
+ messageKey: 'errors.rateLimitExceeded',
+ status: 429,
+ code: 'RATE_LIMIT_EXCEEDED',
+ correlationId,
+ retryable: true,
+ retryAfterSeconds: 30,
+ rateLimit: {
+ scope: 'principal',
+ limit: 100,
+ remaining: 0,
+ resetAt: '2026-08-01T01:31:00Z',
+ },
+ };
+
+ assert.equal(validate(rateLimitProblem), true);
+});
+
+test('requires at least one problem localization key', () => {
+ const validate = validatorFor(ids.problemDetails);
+ const problem = {
+ type: 'https://api.databreeze.dev/problems/access-denied',
+ status: 403,
+ code: 'ACCESS_DENIED',
+ correlationId,
+ retryable: false,
+ };
+
+ assert.equal(validate(problem), false);
+});
+
+test('rejects unknown outer and nested problem fields', () => {
+ const validate = validatorFor(ids.problemDetails);
+ const problem = {
+ type: 'https://api.databreeze.dev/problems/access-denied',
+ status: 403,
+ code: 'ACCESS_DENIED',
+ correlationId,
+ retryable: false,
+ };
+
+ assert.equal(validate({ ...problem, titleKey: 'errors.accessDenied', stack: 'secret' }), false);
+ assert.equal(
+ validate({
+ ...problem,
+ messageKey: 'errors.accessDenied',
+ rateLimit: {
+ scope: 'principal',
+ resetAt: '2026-08-01T01:31:00Z',
+ credential: 'secret',
+ },
+ }),
+ false,
+ );
+});
+
+test('rejects a command envelope with no idempotency key', () => {
+ const validate = validatorFor(ids.commandEnvelope);
+ const command = {
+ commandId: '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc06',
+ commandType: 'iam.workspace.rename',
+ schemaVersion: 1,
+ tenantScope: { scopeType: 'workspace', organizationId, workspaceId },
+ actor: { actorType: 'user', actorId },
+ correlation: { correlationId },
+ issuedAt: '2026-08-01T01:30:00.125Z',
+ idempotencyKey: 'rename-workspace-018f47f2',
+ data: { displayName: 'Operations' },
+ };
+
+ assert.equal(validate(command), true);
+ const withoutIdempotency = { ...command };
+ delete withoutIdempotency.idempotencyKey;
+ assert.equal(validate(withoutIdempotency), false);
+});
+
+test('accepts the authoritative continuing and terminal cursor page shapes', () => {
+ const validate = validatorFor(ids.cursorPage);
+
+ assert.equal(
+ validate({
+ data: [{ id: workspaceId }],
+ nextCursor: 'opaque-cursor',
+ snapshotAt: '2026-08-01T01:30:00Z',
+ hasMore: true,
+ }),
+ true,
+ );
+ assert.equal(validate({ data: [], snapshotAt: '2026-08-01T01:30:00Z', hasMore: false }), true);
+});
+
+test('requires a continuation cursor only while more data exists', () => {
+ const validate = validatorFor(ids.cursorPage);
+
+ assert.equal(validate({ data: [], snapshotAt: '2026-08-01T01:30:00Z', hasMore: true }), false);
+ assert.equal(
+ validate({
+ data: [],
+ nextCursor: 'stale-cursor',
+ snapshotAt: '2026-08-01T01:30:00Z',
+ hasMore: false,
+ }),
+ false,
+ );
+});
+
+test('rejects a cursor page with a non-UTC snapshot', () => {
+ const validate = validatorFor(ids.cursorPage);
+
+ assert.equal(
+ validate({ data: [], snapshotAt: '2026-08-01T08:30:00+07:00', hasMore: false }),
+ false,
+ );
+});
+
+test('rejects unknown cursor page fields', () => {
+ const validate = validatorFor(ids.cursorPage);
+
+ assert.equal(
+ validate({
+ data: [],
+ snapshotAt: '2026-08-01T01:30:00Z',
+ hasMore: false,
+ pageInfo: {},
+ }),
+ false,
+ );
+});
+
+test('requires event type and positive entity revision in the canonical event envelope', () => {
+ const validate = validatorFor(ids.eventEnvelope);
+ const event = {
+ eventId: '018f47f2-5ee1-7d8d-a4c2-8f0e19e4cc07',
+ eventType: 'iam.workspace.renamed',
+ schemaVersion: 1,
+ tenantScope: { scopeType: 'workspace', organizationId, workspaceId },
+ entity: { entityType: 'workspace', entityId: workspaceId, revision: 2 },
+ actor: { actorType: 'user', actorId },
+ correlation: { correlationId },
+ sourceComponent: 'iam',
+ occurredAt: '2026-08-01T01:30:00.125Z',
+ data: { changedFields: ['displayName'] },
+ };
+
+ assert.equal(validate(event), true);
+ const withoutEventType = { ...event };
+ delete withoutEventType.eventType;
+ assert.equal(validate(withoutEventType), false);
+ assert.equal(
+ validate({ ...event, entity: { entityType: 'workspace', entityId: workspaceId } }),
+ false,
+ );
+});
diff --git a/packages/contracts/test/test-runtime-tools.mjs b/packages/contracts/test/test-runtime-tools.mjs
new file mode 100644
index 00000000..09a0a31a
--- /dev/null
+++ b/packages/contracts/test/test-runtime-tools.mjs
@@ -0,0 +1,15 @@
+import { spawnSync } from 'node:child_process';
+
+const defaultCandidates =
+ process.platform === 'win32' ? ['python', 'python3'] : ['python3', 'python'];
+
+export function resolvePythonInterpreter(candidates = defaultCandidates) {
+ for (const candidate of candidates) {
+ const result = spawnSync(candidate, ['--version'], { encoding: 'utf8' });
+ if (result.status === 0 && /^Python 3\./u.test(`${result.stdout}${result.stderr}`)) {
+ return candidate;
+ }
+ }
+
+ throw new Error(`Python 3 interpreter is required; tried: ${candidates.join(', ')}`);
+}
diff --git a/packages/contracts/test/test-runtime-tools.test.mjs b/packages/contracts/test/test-runtime-tools.test.mjs
new file mode 100644
index 00000000..87f45c54
--- /dev/null
+++ b/packages/contracts/test/test-runtime-tools.test.mjs
@@ -0,0 +1,20 @@
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import test from 'node:test';
+
+import { resolvePythonInterpreter } from './test-runtime-tools.mjs';
+
+test('resolves an available Python 3 interpreter for contract probes', () => {
+ const interpreter = resolvePythonInterpreter();
+ const result = spawnSync(interpreter, ['--version'], { encoding: 'utf8' });
+
+ assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`);
+ assert.match(`${result.stdout}${result.stderr}`, /^Python 3\./u);
+});
+
+test('reports every attempted interpreter when Python 3 is unavailable', () => {
+ assert.throws(
+ () => resolvePythonInterpreter(['databreeze-missing-python-a', 'databreeze-missing-python-b']),
+ /Python 3 interpreter is required; tried: databreeze-missing-python-a, databreeze-missing-python-b/u,
+ );
+});
diff --git a/packages/contracts/turbo.json b/packages/contracts/turbo.json
new file mode 100644
index 00000000..7409b09e
--- /dev/null
+++ b/packages/contracts/turbo.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build": {
+ "outputs": []
+ },
+ "test": {
+ "dependsOn": ["^build"],
+ "outputs": []
+ }
+ }
+}
diff --git a/packages/design-tokens/README.md b/packages/design-tokens/README.md
index d008e8b0..be359c70 100644
--- a/packages/design-tokens/README.md
+++ b/packages/design-tokens/README.md
@@ -1,3 +1,15 @@
# Design Tokens
Platform-neutral DataBreeze color, typography, spacing, motion, and icon tokens with generated outputs for React and Android.
+
+## Canonical brand assets
+
+`brand/source/` contains the three immutable legacy assets. Never edit those files. Their approved dimensions and SHA-256 values live in `brand/manifest.json` and are checked before any derivative is created.
+
+The declarative `brand/derivative-plan.json` records every Web, Windows Desktop, and Android output, including its source, purpose, dimensions, content box, and safe-zone policy. The generator permits only approved source cropping, aspect-preserving resizing, transparent padding, an approved source-color background, Android alpha-mask extraction, and PNG/ICO container conversion. It does not redraw, recolor, or distort a presented logo.
+
+Run `pnpm brand:generate` after an approved plan or pipeline change. Run `pnpm brand:check` to regenerate into a temporary clean directory and byte-compare the result with `brand/generated/` and `brand/derivatives.json`. `pnpm build` performs the same drift check.
+
+Wordmark derivatives already contain the DataBreeze name and must not be placed beside duplicate visible “DataBreeze” text. Standalone-mark derivatives may be paired with product text only when the surrounding interface or accessible name requires it.
+
+Android notification PNGs are platform-ready white alpha masks derived from the approved mark’s alpha geometry. Their white RGB bytes are non-presentational mask data; Android controls the runtime tint. The pipeline verifies that mask alpha matches the approved mark geometry exactly.
diff --git a/packages/design-tokens/brand/derivative-plan.json b/packages/design-tokens/brand/derivative-plan.json
new file mode 100644
index 00000000..4fc25e9d
--- /dev/null
+++ b/packages/design-tokens/brand/derivative-plan.json
@@ -0,0 +1,337 @@
+{
+ "schemaVersion": 1,
+ "approval": {
+ "status": "plan-approved",
+ "reviewedOn": "2026-08-01",
+ "reviewSource": "approved Task 11 plan and DataBreeze brand specification",
+ "specReference": "docs/product/brand-and-experience.md#1-brand-continuity",
+ "taskReference": "docs/plans/010-engineering-foundation.md#task-11-reproducible-brand-derivatives",
+ "cropRationale": "The blue mark is the left 1155x1155 square of the approved blue wordmark; cropping removes only the adjacent DataBreeze letters and does not redraw geometry.",
+ "sourceHashes": {
+ "databreeze-mark-dark.png": "5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4",
+ "databreeze-wordmark-black.png": "4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98",
+ "databreeze-wordmark-blue.png": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ }
+ },
+ "pipeline": {
+ "engine": "sharp",
+ "engineVersion": "0.35.3",
+ "libvipsVersion": "8.18.3",
+ "pngVersion": "1.6.58",
+ "pixelPolicy": "sRGB brand colors are preserved; allowed operations are approved cropping, aspect-preserving contain resize, transparent padding, approved-source-color background compositing, Android alpha-mask extraction, and PNG/ICO container conversion",
+ "png": {
+ "adaptiveFiltering": false,
+ "compressionLevel": 9,
+ "effort": 10,
+ "palette": false
+ }
+ },
+ "sources": {
+ "blueMark": {
+ "crop": { "height": 1155, "left": 0, "top": 0, "width": 1155 },
+ "file": "databreeze-wordmark-blue.png"
+ },
+ "blueWordmark": { "file": "databreeze-wordmark-blue.png" },
+ "blackWordmark": { "file": "databreeze-wordmark-black.png" },
+ "darkMark": { "file": "databreeze-mark-dark.png" }
+ },
+ "assets": [
+ {
+ "file": "android/adaptive-foreground-432.png",
+ "platform": "android",
+ "purpose": "Adaptive launcher foreground source",
+ "source": "blueMark",
+ "width": 432,
+ "height": 432,
+ "contentBox": { "x": 84, "y": 84, "width": 264, "height": 264 },
+ "safeZone": "android-adaptive-66dp-within-108dp",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/launcher-hdpi-72.png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (hdpi)",
+ "source": "blueMark",
+ "width": 72,
+ "height": 72,
+ "contentBox": { "x": 7, "y": 7, "width": 58, "height": 58 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/launcher-mdpi-48.png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (mdpi)",
+ "source": "blueMark",
+ "width": 48,
+ "height": 48,
+ "contentBox": { "x": 5, "y": 5, "width": 38, "height": 38 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/launcher-xhdpi-96.png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xhdpi)",
+ "source": "blueMark",
+ "width": 96,
+ "height": 96,
+ "contentBox": { "x": 10, "y": 10, "width": 76, "height": 76 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/launcher-xxhdpi-144.png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xxhdpi)",
+ "source": "blueMark",
+ "width": 144,
+ "height": 144,
+ "contentBox": { "x": 14, "y": 14, "width": 116, "height": 116 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/launcher-xxxhdpi-192.png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xxxhdpi)",
+ "source": "blueMark",
+ "width": 192,
+ "height": 192,
+ "contentBox": { "x": 19, "y": 19, "width": 154, "height": 154 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/notification-hdpi-36.png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (hdpi)",
+ "source": "blueMark",
+ "outputMode": "android-alpha-mask",
+ "width": 36,
+ "height": 36,
+ "contentBox": { "x": 6, "y": 6, "width": 24, "height": 24 },
+ "safeZone": "one-sixth-per-edge",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/notification-mdpi-24.png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (mdpi)",
+ "source": "blueMark",
+ "outputMode": "android-alpha-mask",
+ "width": 24,
+ "height": 24,
+ "contentBox": { "x": 4, "y": 4, "width": 16, "height": 16 },
+ "safeZone": "one-sixth-per-edge",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/notification-xhdpi-48.png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xhdpi)",
+ "source": "blueMark",
+ "outputMode": "android-alpha-mask",
+ "width": 48,
+ "height": 48,
+ "contentBox": { "x": 8, "y": 8, "width": 32, "height": 32 },
+ "safeZone": "one-sixth-per-edge",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/notification-xxhdpi-72.png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xxhdpi)",
+ "source": "blueMark",
+ "outputMode": "android-alpha-mask",
+ "width": 72,
+ "height": 72,
+ "contentBox": { "x": 12, "y": 12, "width": 48, "height": 48 },
+ "safeZone": "one-sixth-per-edge",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "android/notification-xxxhdpi-96.png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xxxhdpi)",
+ "source": "blueMark",
+ "outputMode": "android-alpha-mask",
+ "width": 96,
+ "height": 96,
+ "contentBox": { "x": 16, "y": 16, "width": 64, "height": 64 },
+ "safeZone": "one-sixth-per-edge",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "desktop/application-256.png",
+ "platform": "desktop",
+ "purpose": "Windows application icon PNG",
+ "source": "darkMark",
+ "width": 256,
+ "height": 256,
+ "contentBox": { "x": 13, "y": 13, "width": 230, "height": 230 },
+ "safeZone": "5-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "desktop/application.ico",
+ "platform": "desktop",
+ "purpose": "Windows application multi-resolution icon",
+ "source": "blueMark",
+ "width": 256,
+ "height": 256,
+ "contentBox": { "x": 26, "y": 26, "width": 204, "height": 204 },
+ "safeZone": "10-percent-minimum",
+ "frames": [16, 24, 32, 48, 64, 128, 256],
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "desktop/installer.ico",
+ "platform": "desktop",
+ "purpose": "Windows installer multi-resolution icon",
+ "source": "blueMark",
+ "width": 256,
+ "height": 256,
+ "contentBox": { "x": 26, "y": 26, "width": 204, "height": 204 },
+ "safeZone": "10-percent-minimum",
+ "frames": [16, 24, 32, 48, 64, 128, 256],
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "desktop/notification-32.png",
+ "platform": "desktop",
+ "purpose": "Windows notification icon PNG",
+ "source": "blueMark",
+ "width": 32,
+ "height": 32,
+ "contentBox": { "x": 5, "y": 5, "width": 22, "height": 22 },
+ "safeZone": "15-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "desktop/updater.ico",
+ "platform": "desktop",
+ "purpose": "Windows updater multi-resolution icon",
+ "source": "blueMark",
+ "width": 256,
+ "height": 256,
+ "contentBox": { "x": 26, "y": 26, "width": 204, "height": 204 },
+ "safeZone": "10-percent-minimum",
+ "frames": [16, 24, 32, 48, 64, 128, 256],
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/apple-touch-icon-180.png",
+ "platform": "web",
+ "purpose": "Apple touch install icon",
+ "source": "blueMark",
+ "width": 180,
+ "height": 180,
+ "contentBox": { "x": 18, "y": 18, "width": 144, "height": 144 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/favicon-16.png",
+ "platform": "web",
+ "purpose": "Browser favicon (16px)",
+ "source": "blueMark",
+ "width": 16,
+ "height": 16,
+ "contentBox": { "x": 2, "y": 2, "width": 12, "height": 12 },
+ "safeZone": "12.5-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/favicon-32.png",
+ "platform": "web",
+ "purpose": "Browser favicon (32px)",
+ "source": "blueMark",
+ "width": 32,
+ "height": 32,
+ "contentBox": { "x": 4, "y": 4, "width": 24, "height": 24 },
+ "safeZone": "12.5-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/install-icon-192.png",
+ "platform": "web",
+ "purpose": "Web application install icon (192px)",
+ "source": "blueMark",
+ "width": 192,
+ "height": 192,
+ "contentBox": { "x": 19, "y": 19, "width": 154, "height": 154 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/install-icon-512.png",
+ "platform": "web",
+ "purpose": "Web application install icon (512px)",
+ "source": "blueMark",
+ "width": 512,
+ "height": 512,
+ "contentBox": { "x": 51, "y": 51, "width": 410, "height": 410 },
+ "safeZone": "10-percent-minimum",
+ "containsWordmark": false,
+ "adjacentProductNamePolicy": "accessible-context-only"
+ },
+ {
+ "file": "web/navigation-wordmark-black-204x50.png",
+ "platform": "web",
+ "purpose": "Black navigation wordmark for light surfaces",
+ "source": "blackWordmark",
+ "width": 204,
+ "height": 50,
+ "contentBox": { "x": 10, "y": 5, "width": 184, "height": 40 },
+ "safeZone": "minimum-5px-vertical-and-20px-fitted-horizontal",
+ "containsWordmark": true,
+ "adjacentProductNamePolicy": "forbidden"
+ },
+ {
+ "file": "web/navigation-wordmark-blue-204x50.png",
+ "platform": "web",
+ "purpose": "Primary blue navigation wordmark for light surfaces",
+ "source": "blueWordmark",
+ "width": 204,
+ "height": 50,
+ "contentBox": { "x": 10, "y": 5, "width": 184, "height": 40 },
+ "safeZone": "minimum-5px-vertical-and-20px-fitted-horizontal",
+ "containsWordmark": true,
+ "adjacentProductNamePolicy": "forbidden"
+ },
+ {
+ "file": "web/social-card-1200x630.png",
+ "platform": "web",
+ "purpose": "Social metadata image without adjacent duplicate product text",
+ "source": "blueWordmark",
+ "backgroundColor": { "red": 4, "green": 9, "blue": 32, "alpha": 1 },
+ "width": 1200,
+ "height": 630,
+ "contentBox": { "x": 120, "y": 126, "width": 960, "height": 378 },
+ "safeZone": "10-percent-horizontal-and-20-percent-vertical",
+ "containsWordmark": true,
+ "adjacentProductNamePolicy": "forbidden"
+ }
+ ]
+}
diff --git a/packages/design-tokens/brand/derivatives.json b/packages/design-tokens/brand/derivatives.json
new file mode 100644
index 00000000..38ef590b
--- /dev/null
+++ b/packages/design-tokens/brand/derivatives.json
@@ -0,0 +1,1057 @@
+{
+ "schemaVersion": 1,
+ "approval": {
+ "status": "plan-approved",
+ "reviewedOn": "2026-08-01",
+ "reviewSource": "approved Task 11 plan and DataBreeze brand specification",
+ "specReference": "docs/product/brand-and-experience.md#1-brand-continuity",
+ "taskReference": "docs/plans/010-engineering-foundation.md#task-11-reproducible-brand-derivatives",
+ "cropRationale": "The blue mark is the left 1155x1155 square of the approved blue wordmark; cropping removes only the adjacent DataBreeze letters and does not redraw geometry.",
+ "sourceHashes": {
+ "databreeze-mark-dark.png": "5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4",
+ "databreeze-wordmark-black.png": "4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98",
+ "databreeze-wordmark-blue.png": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ }
+ },
+ "generator": {
+ "engine": "sharp",
+ "engineVersion": "0.35.3",
+ "icoContainer": "png-frame-ico-v1",
+ "png": {
+ "adaptiveFiltering": false,
+ "compressionLevel": 9,
+ "effort": 10,
+ "palette": false
+ },
+ "runtime": {
+ "libvips": "8.18.3",
+ "png": "1.6.58",
+ "sharp": "0.35.3"
+ }
+ },
+ "sourceManifestSha256": "78F718C9B32303F5C2C746BFF5F7698740CCC1565F5D0242AF0E4B83263BF7A0",
+ "assets": [
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 84,
+ "y": 84,
+ "width": 264,
+ "height": 264
+ },
+ "file": "android/adaptive-foreground-432.png",
+ "fittedBox": {
+ "x": 84,
+ "y": 84,
+ "width": 264,
+ "height": 264
+ },
+ "visibleBounds": {
+ "maxX": 347,
+ "maxY": 347,
+ "minX": 84,
+ "minY": 84
+ },
+ "height": 432,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Adaptive launcher foreground source",
+ "safeZone": "android-adaptive-66dp-within-108dp",
+ "sha256": "2342B1B56C9D16E009EFA5D285D030C8BD37DBB2135223A9D819DB098B86732B",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "C16B2F8154035627EE4BC1970B6BFC21C557AE8D44F1A33F85DE3C3377A7E645",
+ "width": 432
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 7,
+ "y": 7,
+ "width": 58,
+ "height": 58
+ },
+ "file": "android/launcher-hdpi-72.png",
+ "fittedBox": {
+ "x": 7,
+ "y": 7,
+ "width": 58,
+ "height": 58
+ },
+ "visibleBounds": {
+ "maxX": 64,
+ "maxY": 64,
+ "minX": 7,
+ "minY": 7
+ },
+ "height": 72,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (hdpi)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "37FB331CA337961A759DF4D937D780CA291305B3B0FB0DDC9A61306127278448",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "C850208368B49DD40EB5206145DC8044A0AD97AD5A15222B5AAA14458ACBD626",
+ "width": 72
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 5,
+ "y": 5,
+ "width": 38,
+ "height": 38
+ },
+ "file": "android/launcher-mdpi-48.png",
+ "fittedBox": {
+ "x": 5,
+ "y": 5,
+ "width": 38,
+ "height": 38
+ },
+ "visibleBounds": {
+ "maxX": 42,
+ "maxY": 42,
+ "minX": 5,
+ "minY": 5
+ },
+ "height": 48,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (mdpi)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "DCF3ACFC20775B7F23EB70383032AEC11B0CFAC9D7149B6CE3E945EC570AB295",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "2249A9C233228CDF3E7CD648AD7E011F1369837C00BA34F4407B71FA2C134216",
+ "width": 48
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 10,
+ "y": 10,
+ "width": 76,
+ "height": 76
+ },
+ "file": "android/launcher-xhdpi-96.png",
+ "fittedBox": {
+ "x": 10,
+ "y": 10,
+ "width": 76,
+ "height": 76
+ },
+ "visibleBounds": {
+ "maxX": 85,
+ "maxY": 85,
+ "minX": 10,
+ "minY": 10
+ },
+ "height": 96,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xhdpi)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "7F03E51CD277518B9DBA1BEAC381F68E29B9385BBCF76630E8C373249DC12961",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "D8C304EDE847F7F8B370F3477EBE155E951644F7406FE77FA44596CBB4497EBB",
+ "width": 96
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 14,
+ "y": 14,
+ "width": 116,
+ "height": 116
+ },
+ "file": "android/launcher-xxhdpi-144.png",
+ "fittedBox": {
+ "x": 14,
+ "y": 14,
+ "width": 116,
+ "height": 116
+ },
+ "visibleBounds": {
+ "maxX": 129,
+ "maxY": 129,
+ "minX": 14,
+ "minY": 14
+ },
+ "height": 144,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xxhdpi)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "25FDB53E6BDF4E433B8F9A4036E86A94BE457BE6BDD65BDD06B83037395CF93A",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "56AAE8DD4F4DDA38B333325F56D2CF0064E7866DF22C15C7564D294B8E08377E",
+ "width": 144
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 19,
+ "y": 19,
+ "width": 154,
+ "height": 154
+ },
+ "file": "android/launcher-xxxhdpi-192.png",
+ "fittedBox": {
+ "x": 19,
+ "y": 19,
+ "width": 154,
+ "height": 154
+ },
+ "visibleBounds": {
+ "maxX": 172,
+ "maxY": 172,
+ "minX": 19,
+ "minY": 19
+ },
+ "height": 192,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Legacy launcher source (xxxhdpi)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "8AD6B9B8D6441E24A97F28ED8B6492779AAB24639DBAB972CF67A04F05170C2B",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "B83E179BC03B00A0C6B9ED6CD14B89BDBB792730799FD1890DC1B6769DE1367B",
+ "width": 192
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 6,
+ "y": 6,
+ "width": 24,
+ "height": 24
+ },
+ "file": "android/notification-hdpi-36.png",
+ "fittedBox": {
+ "x": 6,
+ "y": 6,
+ "width": 24,
+ "height": 24
+ },
+ "visibleBounds": {
+ "maxX": 29,
+ "maxY": 29,
+ "minX": 6,
+ "minY": 6
+ },
+ "height": 36,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (hdpi)",
+ "safeZone": "one-sixth-per-edge",
+ "sha256": "451F41912F10904EBF98DE21406A64A1560F3B513A2EB5C71A9605D4447B7B98",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "alpha-mask-from-approved-geometry",
+ "outputMode": "android-alpha-mask",
+ "visualSha256": "73F2DA8D89A22CEEDF5CFC85303FE80114A5F7158AB5BC585FDCF0BFC5D885C7",
+ "width": 36
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 4,
+ "y": 4,
+ "width": 16,
+ "height": 16
+ },
+ "file": "android/notification-mdpi-24.png",
+ "fittedBox": {
+ "x": 4,
+ "y": 4,
+ "width": 16,
+ "height": 16
+ },
+ "visibleBounds": {
+ "maxX": 19,
+ "maxY": 19,
+ "minX": 4,
+ "minY": 4
+ },
+ "height": 24,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (mdpi)",
+ "safeZone": "one-sixth-per-edge",
+ "sha256": "330D591C013421A0B158C95FEFDC97B8A87028CE8DDEA2D01D705DB7A3B14E13",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "alpha-mask-from-approved-geometry",
+ "outputMode": "android-alpha-mask",
+ "visualSha256": "3B6BFDF2EB3696325AD644F268CD7DE9F8014B19C0DA2698BD19BE6E5984F717",
+ "width": 24
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 8,
+ "y": 8,
+ "width": 32,
+ "height": 32
+ },
+ "file": "android/notification-xhdpi-48.png",
+ "fittedBox": {
+ "x": 8,
+ "y": 8,
+ "width": 32,
+ "height": 32
+ },
+ "visibleBounds": {
+ "maxX": 39,
+ "maxY": 39,
+ "minX": 8,
+ "minY": 8
+ },
+ "height": 48,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xhdpi)",
+ "safeZone": "one-sixth-per-edge",
+ "sha256": "B02A36C9E1690F54B68E91C1D0E24A8596CCC7797D7DB62A4C56B1178C8A7CA3",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "alpha-mask-from-approved-geometry",
+ "outputMode": "android-alpha-mask",
+ "visualSha256": "BEEDF672DC8861EB6BAE1C47A5B669205C5B46B27B6870C7F78EAD6715519E56",
+ "width": 48
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 12,
+ "y": 12,
+ "width": 48,
+ "height": 48
+ },
+ "file": "android/notification-xxhdpi-72.png",
+ "fittedBox": {
+ "x": 12,
+ "y": 12,
+ "width": 48,
+ "height": 48
+ },
+ "visibleBounds": {
+ "maxX": 59,
+ "maxY": 59,
+ "minX": 12,
+ "minY": 12
+ },
+ "height": 72,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xxhdpi)",
+ "safeZone": "one-sixth-per-edge",
+ "sha256": "51B1EFEC750833CC2BCBB3B5C5319CDA3348B471BA23D7A4301E5CF6FD2866A5",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "alpha-mask-from-approved-geometry",
+ "outputMode": "android-alpha-mask",
+ "visualSha256": "DFE4ED9EB98092732874F42629A5359259D9D8B4EC9307131EC625C991891290",
+ "width": 72
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 16,
+ "y": 16,
+ "width": 64,
+ "height": 64
+ },
+ "file": "android/notification-xxxhdpi-96.png",
+ "fittedBox": {
+ "x": 16,
+ "y": 16,
+ "width": 64,
+ "height": 64
+ },
+ "visibleBounds": {
+ "maxX": 79,
+ "maxY": 79,
+ "minX": 16,
+ "minY": 16
+ },
+ "height": 96,
+ "mediaType": "image/png",
+ "platform": "android",
+ "purpose": "Android runtime-tinted notification alpha mask (xxxhdpi)",
+ "safeZone": "one-sixth-per-edge",
+ "sha256": "9539808522823869BE44BA3AE079A603E94E4AD95AFB2F0206F71CBADC8160F4",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "alpha-mask-from-approved-geometry",
+ "outputMode": "android-alpha-mask",
+ "visualSha256": "B555B71BBBC56556AA0B650D1BE63296A2EF7CCFB3528D88FEE23F6EE6D708BF",
+ "width": 96
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 13,
+ "y": 13,
+ "width": 230,
+ "height": 230
+ },
+ "file": "desktop/application-256.png",
+ "fittedBox": {
+ "x": 13,
+ "y": 13,
+ "width": 230,
+ "height": 230
+ },
+ "visibleBounds": {
+ "maxX": 242,
+ "maxY": 242,
+ "minX": 13,
+ "minY": 13
+ },
+ "height": 256,
+ "mediaType": "image/png",
+ "platform": "desktop",
+ "purpose": "Windows application icon PNG",
+ "safeZone": "5-percent-minimum",
+ "sha256": "E8E5118585FCAC39C63A85A537B6F6930F5675D0F2960CD9186197FD7CBC417E",
+ "source": {
+ "file": "databreeze-mark-dark.png",
+ "sha256": "5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "E38BD7B30191F57F9399D39972649B252AD8C30708FCB476A77C77A0A4E4E373",
+ "width": 256
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "file": "desktop/application.ico",
+ "fittedBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "visibleBounds": {
+ "maxX": 229,
+ "maxY": 229,
+ "minX": 26,
+ "minY": 26
+ },
+ "frames": [
+ 16,
+ 24,
+ 32,
+ 48,
+ 64,
+ 128,
+ 256
+ ],
+ "height": 256,
+ "mediaType": "image/x-icon",
+ "platform": "desktop",
+ "purpose": "Windows application multi-resolution icon",
+ "safeZone": "10-percent-minimum",
+ "sha256": "01151CE213CEF964112854C3E09195A38C6AA4457359B89F90EE63ECD62DB00C",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "width": 256
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "file": "desktop/installer.ico",
+ "fittedBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "visibleBounds": {
+ "maxX": 229,
+ "maxY": 229,
+ "minX": 26,
+ "minY": 26
+ },
+ "frames": [
+ 16,
+ 24,
+ 32,
+ 48,
+ 64,
+ 128,
+ 256
+ ],
+ "height": 256,
+ "mediaType": "image/x-icon",
+ "platform": "desktop",
+ "purpose": "Windows installer multi-resolution icon",
+ "safeZone": "10-percent-minimum",
+ "sha256": "01151CE213CEF964112854C3E09195A38C6AA4457359B89F90EE63ECD62DB00C",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "width": 256
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 5,
+ "y": 5,
+ "width": 22,
+ "height": 22
+ },
+ "file": "desktop/notification-32.png",
+ "fittedBox": {
+ "x": 5,
+ "y": 5,
+ "width": 22,
+ "height": 22
+ },
+ "visibleBounds": {
+ "maxX": 26,
+ "maxY": 26,
+ "minX": 5,
+ "minY": 5
+ },
+ "height": 32,
+ "mediaType": "image/png",
+ "platform": "desktop",
+ "purpose": "Windows notification icon PNG",
+ "safeZone": "15-percent-minimum",
+ "sha256": "F6A0FC62CBD49DEDC1702F17D42B2D65448A8FDE98770069A5A6D312802246A4",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "38903201463667946F1232BF5235841B937208520E39568C564F0BC2E3F64ADD",
+ "width": 32
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "file": "desktop/updater.ico",
+ "fittedBox": {
+ "x": 26,
+ "y": 26,
+ "width": 204,
+ "height": 204
+ },
+ "visibleBounds": {
+ "maxX": 229,
+ "maxY": 229,
+ "minX": 26,
+ "minY": 26
+ },
+ "frames": [
+ 16,
+ 24,
+ 32,
+ 48,
+ 64,
+ 128,
+ 256
+ ],
+ "height": 256,
+ "mediaType": "image/x-icon",
+ "platform": "desktop",
+ "purpose": "Windows updater multi-resolution icon",
+ "safeZone": "10-percent-minimum",
+ "sha256": "01151CE213CEF964112854C3E09195A38C6AA4457359B89F90EE63ECD62DB00C",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "width": 256
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 18,
+ "y": 18,
+ "width": 144,
+ "height": 144
+ },
+ "file": "web/apple-touch-icon-180.png",
+ "fittedBox": {
+ "x": 18,
+ "y": 18,
+ "width": 144,
+ "height": 144
+ },
+ "visibleBounds": {
+ "maxX": 161,
+ "maxY": 161,
+ "minX": 18,
+ "minY": 18
+ },
+ "height": 180,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Apple touch install icon",
+ "safeZone": "10-percent-minimum",
+ "sha256": "2809E173878DB399BCFB974799F05B4930F85DFE6C198E64F7A2117DD20E0927",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "4049EC9E808406AB767E07EC5F11A37021453D1ED601E04ED2C910612B8248CB",
+ "width": 180
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 2,
+ "y": 2,
+ "width": 12,
+ "height": 12
+ },
+ "file": "web/favicon-16.png",
+ "fittedBox": {
+ "x": 2,
+ "y": 2,
+ "width": 12,
+ "height": 12
+ },
+ "visibleBounds": {
+ "maxX": 13,
+ "maxY": 13,
+ "minX": 2,
+ "minY": 2
+ },
+ "height": 16,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Browser favicon (16px)",
+ "safeZone": "12.5-percent-minimum",
+ "sha256": "F471D3E2CA6BF8A3ACA2E95A3FBA5FA97E6ABF56E713D280361A7B15993600E9",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "A7DC5A14885ABC87316C39E803D9C8BEE2FB560189C1096F2CC3E323CE175327",
+ "width": 16
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 4,
+ "y": 4,
+ "width": 24,
+ "height": 24
+ },
+ "file": "web/favicon-32.png",
+ "fittedBox": {
+ "x": 4,
+ "y": 4,
+ "width": 24,
+ "height": 24
+ },
+ "visibleBounds": {
+ "maxX": 27,
+ "maxY": 27,
+ "minX": 4,
+ "minY": 4
+ },
+ "height": 32,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Browser favicon (32px)",
+ "safeZone": "12.5-percent-minimum",
+ "sha256": "267894B71A948F7673004ACF910D7673AEE925635E84078BB5967161EC2FFE39",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "C01AA26FD1B9DDC0CAE0175EB7AC7A16E0CF802C354AB1DE7A76D922EB17702C",
+ "width": 32
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 19,
+ "y": 19,
+ "width": 154,
+ "height": 154
+ },
+ "file": "web/install-icon-192.png",
+ "fittedBox": {
+ "x": 19,
+ "y": 19,
+ "width": 154,
+ "height": 154
+ },
+ "visibleBounds": {
+ "maxX": 172,
+ "maxY": 172,
+ "minX": 19,
+ "minY": 19
+ },
+ "height": 192,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Web application install icon (192px)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "8AD6B9B8D6441E24A97F28ED8B6492779AAB24639DBAB972CF67A04F05170C2B",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "B83E179BC03B00A0C6B9ED6CD14B89BDBB792730799FD1890DC1B6769DE1367B",
+ "width": 192
+ },
+ {
+ "adjacentProductNamePolicy": "accessible-context-only",
+ "containsWordmark": false,
+ "contentBox": {
+ "x": 51,
+ "y": 51,
+ "width": 410,
+ "height": 410
+ },
+ "file": "web/install-icon-512.png",
+ "fittedBox": {
+ "x": 51,
+ "y": 51,
+ "width": 410,
+ "height": 410
+ },
+ "visibleBounds": {
+ "maxX": 460,
+ "maxY": 460,
+ "minX": 51,
+ "minY": 51
+ },
+ "height": 512,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Web application install icon (512px)",
+ "safeZone": "10-percent-minimum",
+ "sha256": "8ED8E34F3D7867B028485FF75F23802B23200DEC2AB2823C5A0C9C4082BF36C6",
+ "source": {
+ "crop": {
+ "height": 1155,
+ "left": 0,
+ "top": 0,
+ "width": 1155
+ },
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "F3FC6E0C0F32F40C26961A5570321D025540313693793C44B58B7B8718621792",
+ "width": 512
+ },
+ {
+ "adjacentProductNamePolicy": "forbidden",
+ "containsWordmark": true,
+ "contentBox": {
+ "x": 10,
+ "y": 5,
+ "width": 184,
+ "height": 40
+ },
+ "file": "web/navigation-wordmark-black-204x50.png",
+ "fittedBox": {
+ "x": 20,
+ "y": 5,
+ "width": 163,
+ "height": 40
+ },
+ "visibleBounds": {
+ "maxX": 182,
+ "maxY": 44,
+ "minX": 20,
+ "minY": 5
+ },
+ "height": 50,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Black navigation wordmark for light surfaces",
+ "safeZone": "minimum-5px-vertical-and-20px-fitted-horizontal",
+ "sha256": "658DFFF140DC5BE0E18D37B845656A9B9C5C2C6E1150EC8BB79FD93B9987B2F3",
+ "source": {
+ "file": "databreeze-wordmark-black.png",
+ "sha256": "4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "468507ED94F13BABE083361984247E177C92C4514D03536862A2F0D102FD5688",
+ "width": 204
+ },
+ {
+ "adjacentProductNamePolicy": "forbidden",
+ "containsWordmark": true,
+ "contentBox": {
+ "x": 10,
+ "y": 5,
+ "width": 184,
+ "height": 40
+ },
+ "file": "web/navigation-wordmark-blue-204x50.png",
+ "fittedBox": {
+ "x": 20,
+ "y": 5,
+ "width": 163,
+ "height": 40
+ },
+ "visibleBounds": {
+ "maxX": 182,
+ "maxY": 44,
+ "minX": 20,
+ "minY": 5
+ },
+ "height": 50,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Primary blue navigation wordmark for light surfaces",
+ "safeZone": "minimum-5px-vertical-and-20px-fitted-horizontal",
+ "sha256": "905FA49ADC0810A57067728615658AF00DDD47A6653733FA485BF2246C122DBF",
+ "source": {
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "CD44FB5AD502B125863C26331FF3BB0C6E88804D6B6FC0261DE05E66400AFC62",
+ "width": 204
+ },
+ {
+ "adjacentProductNamePolicy": "forbidden",
+ "containsWordmark": true,
+ "contentBox": {
+ "x": 120,
+ "y": 126,
+ "width": 960,
+ "height": 378
+ },
+ "file": "web/social-card-1200x630.png",
+ "fittedBox": {
+ "x": 120,
+ "y": 197,
+ "width": 960,
+ "height": 235
+ },
+ "visibleBounds": {
+ "maxX": 1079,
+ "maxY": 431,
+ "minX": 120,
+ "minY": 197
+ },
+ "backgroundColor": {
+ "red": 4,
+ "green": 9,
+ "blue": 32,
+ "alpha": 1
+ },
+ "height": 630,
+ "mediaType": "image/png",
+ "platform": "web",
+ "purpose": "Social metadata image without adjacent duplicate product text",
+ "safeZone": "10-percent-horizontal-and-20-percent-vertical",
+ "sha256": "34AE6C90BFD7FF1AC72E82244AEC77AF1105FA3179EE9E35D5D4D63244C4E800",
+ "source": {
+ "file": "databreeze-wordmark-blue.png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D"
+ },
+ "transform": "aspect-preserving-contain",
+ "visualSha256": "C890808C04E3A41315B7471C2BA25B7F0438D26E37367308525D29F0AD1AF531",
+ "width": 1200
+ }
+ ]
+}
diff --git a/packages/design-tokens/brand/generated/android/adaptive-foreground-432.png b/packages/design-tokens/brand/generated/android/adaptive-foreground-432.png
new file mode 100644
index 00000000..131ce949
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/adaptive-foreground-432.png differ
diff --git a/packages/design-tokens/brand/generated/android/launcher-hdpi-72.png b/packages/design-tokens/brand/generated/android/launcher-hdpi-72.png
new file mode 100644
index 00000000..5ce8774f
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/launcher-hdpi-72.png differ
diff --git a/packages/design-tokens/brand/generated/android/launcher-mdpi-48.png b/packages/design-tokens/brand/generated/android/launcher-mdpi-48.png
new file mode 100644
index 00000000..0241046e
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/launcher-mdpi-48.png differ
diff --git a/packages/design-tokens/brand/generated/android/launcher-xhdpi-96.png b/packages/design-tokens/brand/generated/android/launcher-xhdpi-96.png
new file mode 100644
index 00000000..c9ca398e
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/launcher-xhdpi-96.png differ
diff --git a/packages/design-tokens/brand/generated/android/launcher-xxhdpi-144.png b/packages/design-tokens/brand/generated/android/launcher-xxhdpi-144.png
new file mode 100644
index 00000000..03fdae8b
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/launcher-xxhdpi-144.png differ
diff --git a/packages/design-tokens/brand/generated/android/launcher-xxxhdpi-192.png b/packages/design-tokens/brand/generated/android/launcher-xxxhdpi-192.png
new file mode 100644
index 00000000..bd7fc5ac
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/launcher-xxxhdpi-192.png differ
diff --git a/packages/design-tokens/brand/generated/android/notification-hdpi-36.png b/packages/design-tokens/brand/generated/android/notification-hdpi-36.png
new file mode 100644
index 00000000..b4395269
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/notification-hdpi-36.png differ
diff --git a/packages/design-tokens/brand/generated/android/notification-mdpi-24.png b/packages/design-tokens/brand/generated/android/notification-mdpi-24.png
new file mode 100644
index 00000000..6a4c7e0e
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/notification-mdpi-24.png differ
diff --git a/packages/design-tokens/brand/generated/android/notification-xhdpi-48.png b/packages/design-tokens/brand/generated/android/notification-xhdpi-48.png
new file mode 100644
index 00000000..31b9926c
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/notification-xhdpi-48.png differ
diff --git a/packages/design-tokens/brand/generated/android/notification-xxhdpi-72.png b/packages/design-tokens/brand/generated/android/notification-xxhdpi-72.png
new file mode 100644
index 00000000..291514fa
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/notification-xxhdpi-72.png differ
diff --git a/packages/design-tokens/brand/generated/android/notification-xxxhdpi-96.png b/packages/design-tokens/brand/generated/android/notification-xxxhdpi-96.png
new file mode 100644
index 00000000..9c467363
Binary files /dev/null and b/packages/design-tokens/brand/generated/android/notification-xxxhdpi-96.png differ
diff --git a/packages/design-tokens/brand/generated/desktop/application-256.png b/packages/design-tokens/brand/generated/desktop/application-256.png
new file mode 100644
index 00000000..f1884d66
Binary files /dev/null and b/packages/design-tokens/brand/generated/desktop/application-256.png differ
diff --git a/packages/design-tokens/brand/generated/desktop/application.ico b/packages/design-tokens/brand/generated/desktop/application.ico
new file mode 100644
index 00000000..56d63640
Binary files /dev/null and b/packages/design-tokens/brand/generated/desktop/application.ico differ
diff --git a/packages/design-tokens/brand/generated/desktop/installer.ico b/packages/design-tokens/brand/generated/desktop/installer.ico
new file mode 100644
index 00000000..56d63640
Binary files /dev/null and b/packages/design-tokens/brand/generated/desktop/installer.ico differ
diff --git a/packages/design-tokens/brand/generated/desktop/notification-32.png b/packages/design-tokens/brand/generated/desktop/notification-32.png
new file mode 100644
index 00000000..a9cc8b0b
Binary files /dev/null and b/packages/design-tokens/brand/generated/desktop/notification-32.png differ
diff --git a/packages/design-tokens/brand/generated/desktop/updater.ico b/packages/design-tokens/brand/generated/desktop/updater.ico
new file mode 100644
index 00000000..56d63640
Binary files /dev/null and b/packages/design-tokens/brand/generated/desktop/updater.ico differ
diff --git a/packages/design-tokens/brand/generated/web/apple-touch-icon-180.png b/packages/design-tokens/brand/generated/web/apple-touch-icon-180.png
new file mode 100644
index 00000000..7801f9c2
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/apple-touch-icon-180.png differ
diff --git a/packages/design-tokens/brand/generated/web/favicon-16.png b/packages/design-tokens/brand/generated/web/favicon-16.png
new file mode 100644
index 00000000..e214b386
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/favicon-16.png differ
diff --git a/packages/design-tokens/brand/generated/web/favicon-32.png b/packages/design-tokens/brand/generated/web/favicon-32.png
new file mode 100644
index 00000000..000e9368
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/favicon-32.png differ
diff --git a/packages/design-tokens/brand/generated/web/install-icon-192.png b/packages/design-tokens/brand/generated/web/install-icon-192.png
new file mode 100644
index 00000000..bd7fc5ac
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/install-icon-192.png differ
diff --git a/packages/design-tokens/brand/generated/web/install-icon-512.png b/packages/design-tokens/brand/generated/web/install-icon-512.png
new file mode 100644
index 00000000..0f459f70
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/install-icon-512.png differ
diff --git a/packages/design-tokens/brand/generated/web/navigation-wordmark-black-204x50.png b/packages/design-tokens/brand/generated/web/navigation-wordmark-black-204x50.png
new file mode 100644
index 00000000..d3dce7df
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/navigation-wordmark-black-204x50.png differ
diff --git a/packages/design-tokens/brand/generated/web/navigation-wordmark-blue-204x50.png b/packages/design-tokens/brand/generated/web/navigation-wordmark-blue-204x50.png
new file mode 100644
index 00000000..76b0b887
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/navigation-wordmark-blue-204x50.png differ
diff --git a/packages/design-tokens/brand/generated/web/social-card-1200x630.png b/packages/design-tokens/brand/generated/web/social-card-1200x630.png
new file mode 100644
index 00000000..fc30bb88
Binary files /dev/null and b/packages/design-tokens/brand/generated/web/social-card-1200x630.png differ
diff --git a/packages/design-tokens/brand/manifest.json b/packages/design-tokens/brand/manifest.json
new file mode 100644
index 00000000..14fb1d1d
--- /dev/null
+++ b/packages/design-tokens/brand/manifest.json
@@ -0,0 +1,29 @@
+{
+ "schemaVersion": 1,
+ "assets": [
+ {
+ "file": "databreeze-mark-dark.png",
+ "height": 1973,
+ "intendedUse": "Standalone application and product mark on dark backgrounds",
+ "mediaType": "image/png",
+ "sha256": "5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4",
+ "width": 1974
+ },
+ {
+ "file": "databreeze-wordmark-black.png",
+ "height": 1155,
+ "intendedUse": "Monochrome DataBreeze wordmark on light backgrounds",
+ "mediaType": "image/png",
+ "sha256": "4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98",
+ "width": 4710
+ },
+ {
+ "file": "databreeze-wordmark-blue.png",
+ "height": 1155,
+ "intendedUse": "Primary DataBreeze wordmark on light backgrounds",
+ "mediaType": "image/png",
+ "sha256": "B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D",
+ "width": 4710
+ }
+ ]
+}
diff --git a/packages/design-tokens/brand/source/databreeze-mark-dark.png b/packages/design-tokens/brand/source/databreeze-mark-dark.png
new file mode 100644
index 00000000..e3e6e55f
Binary files /dev/null and b/packages/design-tokens/brand/source/databreeze-mark-dark.png differ
diff --git a/packages/design-tokens/brand/source/databreeze-wordmark-black.png b/packages/design-tokens/brand/source/databreeze-wordmark-black.png
new file mode 100644
index 00000000..aa1b762c
Binary files /dev/null and b/packages/design-tokens/brand/source/databreeze-wordmark-black.png differ
diff --git a/packages/design-tokens/brand/source/databreeze-wordmark-blue.png b/packages/design-tokens/brand/source/databreeze-wordmark-blue.png
new file mode 100644
index 00000000..60ecac72
Binary files /dev/null and b/packages/design-tokens/brand/source/databreeze-wordmark-blue.png differ
diff --git a/packages/design-tokens/package.json b/packages/design-tokens/package.json
new file mode 100644
index 00000000..38f7a105
--- /dev/null
+++ b/packages/design-tokens/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "@databreeze/design-tokens",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./brand/derivatives.json": "./brand/derivatives.json",
+ "./brand/generated/*": "./brand/generated/*",
+ "./brand/manifest.json": "./brand/manifest.json",
+ "./brand/source/*": "./brand/source/*"
+ },
+ "scripts": {
+ "brand:check": "node scripts/generate-brand-derivatives.mjs --check",
+ "brand:generate": "node scripts/generate-brand-derivatives.mjs",
+ "build": "corepack pnpm brand:check",
+ "test": "node --test test/**/*.test.mjs"
+ },
+ "devDependencies": {
+ "sharp": "0.35.3"
+ }
+}
diff --git a/packages/design-tokens/scripts/generate-brand-derivatives.mjs b/packages/design-tokens/scripts/generate-brand-derivatives.mjs
new file mode 100644
index 00000000..fbe721b0
--- /dev/null
+++ b/packages/design-tokens/scripts/generate-brand-derivatives.mjs
@@ -0,0 +1,945 @@
+import { Buffer } from 'node:buffer';
+import { createHash } from 'node:crypto';
+import {
+ lstat,
+ mkdir,
+ mkdtemp,
+ open,
+ readFile,
+ readdir,
+ realpath,
+ rename,
+ rm,
+} from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { dirname, isAbsolute, join, parse, relative, resolve, sep } from 'node:path';
+import process from 'node:process';
+import { fileURLToPath, URL } from 'node:url';
+import sharp from 'sharp';
+
+const packageDirectory = fileURLToPath(new URL('../', import.meta.url));
+const brandDirectory = join(packageDirectory, 'brand');
+const sourceDirectory = join(brandDirectory, 'source');
+const sourceManifestPath = join(brandDirectory, 'manifest.json');
+const planPath = join(brandDirectory, 'derivative-plan.json');
+const defaultOutputDirectory = join(brandDirectory, 'generated');
+const defaultManifestPath = join(brandDirectory, 'derivatives.json');
+
+const PIXEL_POLICY =
+ 'sRGB brand colors are preserved; allowed operations are approved cropping, aspect-preserving contain resize, transparent padding, approved-source-color background compositing, Android alpha-mask extraction, and PNG/ICO container conversion';
+const APPROVAL_METADATA = {
+ cropRationale:
+ 'The blue mark is the left 1155x1155 square of the approved blue wordmark; cropping removes only the adjacent DataBreeze letters and does not redraw geometry.',
+ reviewSource: 'approved Task 11 plan and DataBreeze brand specification',
+ reviewedOn: '2026-08-01',
+ specReference: 'docs/product/brand-and-experience.md#1-brand-continuity',
+ status: 'plan-approved',
+ taskReference: 'docs/plans/010-engineering-foundation.md#task-11-reproducible-brand-derivatives',
+};
+const REQUIRED_SOURCE_DEFINITIONS = {
+ blackWordmark: { file: 'databreeze-wordmark-black.png' },
+ blueMark: {
+ crop: { height: 1155, left: 0, top: 0, width: 1155 },
+ file: 'databreeze-wordmark-blue.png',
+ },
+ blueWordmark: { file: 'databreeze-wordmark-blue.png' },
+ darkMark: { file: 'databreeze-mark-dark.png' },
+};
+const REQUIRED_ASSET_FILES = [
+ 'android/adaptive-foreground-432.png',
+ 'android/launcher-hdpi-72.png',
+ 'android/launcher-mdpi-48.png',
+ 'android/launcher-xhdpi-96.png',
+ 'android/launcher-xxhdpi-144.png',
+ 'android/launcher-xxxhdpi-192.png',
+ 'android/notification-hdpi-36.png',
+ 'android/notification-mdpi-24.png',
+ 'android/notification-xhdpi-48.png',
+ 'android/notification-xxhdpi-72.png',
+ 'android/notification-xxxhdpi-96.png',
+ 'desktop/application-256.png',
+ 'desktop/application.ico',
+ 'desktop/installer.ico',
+ 'desktop/notification-32.png',
+ 'desktop/updater.ico',
+ 'web/apple-touch-icon-180.png',
+ 'web/favicon-16.png',
+ 'web/favicon-32.png',
+ 'web/install-icon-192.png',
+ 'web/install-icon-512.png',
+ 'web/navigation-wordmark-black-204x50.png',
+ 'web/navigation-wordmark-blue-204x50.png',
+ 'web/social-card-1200x630.png',
+];
+const PLATFORM_VALUES = new Set(['android', 'desktop', 'web']);
+const ADJACENT_NAME_POLICIES = new Set(['accessible-context-only', 'forbidden']);
+const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
+
+sharp.cache(false);
+sharp.concurrency(1);
+sharp.simd(false);
+
+function sha256(bytes) {
+ return createHash('sha256').update(bytes).digest('hex').toUpperCase();
+}
+
+export function assertApprovedSourceBytes({ approvedSha256, bytes, file }) {
+ if (sha256(bytes) !== approvedSha256) {
+ throw new Error(`Approved source checksum mismatch: ${file}`);
+ }
+}
+
+function stableJson(value) {
+ return `${JSON.stringify(value, null, 2)}\n`;
+}
+
+function isWithin(parent, child) {
+ const pathFromParent = relative(resolve(parent), resolve(child));
+ return (
+ pathFromParent === '' || (!pathFromParent.startsWith(`..${sep}`) && pathFromParent !== '..')
+ );
+}
+
+function assertInteger(value, label, minimum = 0) {
+ if (!Number.isInteger(value) || value < minimum) {
+ throw new Error(`${label} must be an integer greater than or equal to ${minimum}`);
+ }
+}
+
+function assertPlainObject(value, label) {
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+ throw new Error(`${label} must be an object`);
+ }
+}
+
+function assertExactKeys(value, expectedKeys, label) {
+ assertPlainObject(value, label);
+ const expected = [...expectedKeys].sort();
+ const actual = Object.keys(value).sort();
+ const unknown = actual.filter((key) => !expected.includes(key));
+ const missing = expected.filter((key) => !actual.includes(key));
+ if (unknown.length > 0) throw new Error(`${label} has unknown key "${unknown[0]}"`);
+ if (missing.length > 0) throw new Error(`${label} is missing key "${missing[0]}"`);
+}
+
+function assertNonEmptyString(value, label) {
+ if (typeof value !== 'string' || value.trim() !== value || value.length === 0) {
+ throw new Error(`${label} must be a non-empty trimmed string`);
+ }
+}
+
+function assertPortableAssetPath(file) {
+ if (typeof file !== 'string' || isAbsolute(file) || /^[A-Za-z]:/u.test(file)) {
+ throw new Error(`Derivative file must be a portable relative path: ${String(file)}`);
+ }
+ if (file.includes('\\')) {
+ throw new Error(`Derivative file must be a portable POSIX path: ${file}`);
+ }
+ const segments = file.split('/');
+ if (
+ segments.length < 2 ||
+ segments.some(
+ (segment) =>
+ segment.length === 0 ||
+ segment === '.' ||
+ segment === '..' ||
+ !/^[A-Za-z0-9._-]+$/u.test(segment),
+ )
+ ) {
+ throw new Error(`Derivative file must be a portable normalized path: ${file}`);
+ }
+}
+
+function approvedSourceMap(sourceManifest) {
+ assertExactKeys(sourceManifest, ['assets', 'schemaVersion'], 'source manifest');
+ if (sourceManifest.schemaVersion !== 1 || !Array.isArray(sourceManifest.assets)) {
+ throw new Error('Unsupported source manifest');
+ }
+ return new Map(sourceManifest.assets.map((asset) => [asset.file, asset]));
+}
+
+function validatePipeline(pipeline) {
+ assertExactKeys(
+ pipeline,
+ ['engine', 'engineVersion', 'libvipsVersion', 'pixelPolicy', 'png', 'pngVersion'],
+ 'pipeline',
+ );
+ if (pipeline.engine !== 'sharp') throw new Error('Unsupported brand derivative plan engine');
+ const runtimePins = [
+ ['Sharp', pipeline.engineVersion, sharp.versions.sharp],
+ ['libvips', pipeline.libvipsVersion, sharp.versions.vips],
+ ['PNG', pipeline.pngVersion, sharp.versions.png],
+ ];
+ for (const [label, planned, loaded] of runtimePins) {
+ if (planned !== loaded)
+ throw new Error(`${label} runtime requires ${planned}; loaded ${loaded}`);
+ }
+ if (pipeline.pixelPolicy !== PIXEL_POLICY) {
+ throw new Error('Pipeline pixel policy must use the approved aspect-preserving transform');
+ }
+ assertExactKeys(
+ pipeline.png,
+ ['adaptiveFiltering', 'compressionLevel', 'effort', 'palette'],
+ 'pipeline.png',
+ );
+ if (
+ pipeline.png.adaptiveFiltering !== false ||
+ pipeline.png.compressionLevel !== 9 ||
+ pipeline.png.effort !== 10 ||
+ pipeline.png.palette !== false
+ ) {
+ throw new Error('Pipeline PNG options must match the deterministic encoder policy');
+ }
+}
+
+function validateSources(sources, approvedSources) {
+ assertExactKeys(sources, Object.keys(REQUIRED_SOURCE_DEFINITIONS), 'source keys');
+ for (const [key, expected] of Object.entries(REQUIRED_SOURCE_DEFINITIONS)) {
+ const source = sources[key];
+ assertExactKeys(source, expected.crop ? ['crop', 'file'] : ['file'], `source ${key}`);
+ if (source.file !== expected.file) throw new Error(`Source ${key} must use ${expected.file}`);
+ const approved = approvedSources.get(source.file);
+ if (!approved) throw new Error(`Source ${key} is not approved: ${source.file}`);
+ if (expected.crop) {
+ assertExactKeys(source.crop, ['height', 'left', 'top', 'width'], `source ${key} crop`);
+ for (const field of ['height', 'left', 'top', 'width']) {
+ assertInteger(
+ source.crop[field],
+ `source ${key} crop.${field}`,
+ field === 'height' || field === 'width' ? 1 : 0,
+ );
+ }
+ if (
+ source.crop.left + source.crop.width > approved.width ||
+ source.crop.top + source.crop.height > approved.height
+ ) {
+ throw new Error(`Source ${key} crop exceeds approved source bounds`);
+ }
+ if (JSON.stringify(source.crop) !== JSON.stringify(expected.crop)) {
+ throw new Error(`Source ${key} crop must match the approved extraction`);
+ }
+ }
+ }
+}
+
+function validateApproval(approval, approvedSources) {
+ assertExactKeys(
+ approval,
+ [
+ 'cropRationale',
+ 'reviewSource',
+ 'reviewedOn',
+ 'sourceHashes',
+ 'specReference',
+ 'status',
+ 'taskReference',
+ ],
+ 'approval',
+ );
+ for (const [field, expected] of Object.entries(APPROVAL_METADATA)) {
+ if (approval[field] !== expected)
+ throw new Error(`Approval ${field} must match plan provenance`);
+ }
+ assertExactKeys(approval.sourceHashes, [...approvedSources.keys()], 'approval sourceHashes');
+ for (const [file, approved] of approvedSources) {
+ if (approval.sourceHashes[file] !== approved.sha256) {
+ throw new Error(`Approval source hash must match ${file}`);
+ }
+ }
+}
+
+function validateAsset(asset, sources, seenFiles) {
+ assertPlainObject(asset, 'asset');
+ const hasFrames = Object.prototype.hasOwnProperty.call(asset, 'frames');
+ const hasOutputMode = Object.prototype.hasOwnProperty.call(asset, 'outputMode');
+ const hasBackground = Object.prototype.hasOwnProperty.call(asset, 'backgroundColor');
+ assertExactKeys(
+ asset,
+ [
+ 'adjacentProductNamePolicy',
+ 'containsWordmark',
+ 'contentBox',
+ 'file',
+ ...(hasFrames ? ['frames'] : []),
+ ...(hasBackground ? ['backgroundColor'] : []),
+ 'height',
+ ...(hasOutputMode ? ['outputMode'] : []),
+ 'platform',
+ 'purpose',
+ 'safeZone',
+ 'source',
+ 'width',
+ ],
+ `asset ${String(asset?.file)}`,
+ );
+ assertPortableAssetPath(asset.file);
+ const foldedFile = asset.file.toLowerCase();
+ if (seenFiles.has(foldedFile)) throw new Error(`Duplicate derivative output path: ${asset.file}`);
+ seenFiles.add(foldedFile);
+
+ if (!PLATFORM_VALUES.has(asset.platform)) throw new Error(`Invalid platform for ${asset.file}`);
+ if (!asset.file.startsWith(`${asset.platform}/`)) {
+ throw new Error(`Asset path must begin with its platform for ${asset.file}`);
+ }
+ const isAndroidNotification = asset.file.startsWith('android/notification-');
+ if (isAndroidNotification) {
+ if (asset.outputMode !== 'android-alpha-mask') {
+ throw new Error(`${asset.file} outputMode must be android-alpha-mask`);
+ }
+ } else if (hasOutputMode) {
+ throw new Error(`outputMode is not allowed for ${asset.file}`);
+ }
+ const isSocialCard = asset.file === 'web/social-card-1200x630.png';
+ if (isSocialCard) {
+ assertExactKeys(
+ asset.backgroundColor,
+ ['alpha', 'blue', 'green', 'red'],
+ `${asset.file} backgroundColor`,
+ );
+ const expectedBackground = { alpha: 1, blue: 32, green: 9, red: 4 };
+ for (const [channel, expected] of Object.entries(expectedBackground)) {
+ if (asset.backgroundColor[channel] !== expected) {
+ throw new Error(`${asset.file} backgroundColor must use the approved dark mark color`);
+ }
+ }
+ } else if (hasBackground) {
+ throw new Error(`backgroundColor is not allowed for ${asset.file}`);
+ }
+ assertNonEmptyString(asset.purpose, `${asset.file} purpose`);
+ assertNonEmptyString(asset.safeZone, `${asset.file} safeZone`);
+ if (!Object.prototype.hasOwnProperty.call(sources, asset.source)) {
+ throw new Error(`Unknown derivative source "${String(asset.source)}" for ${asset.file}`);
+ }
+ if (typeof asset.containsWordmark !== 'boolean') {
+ throw new Error(`${asset.file} containsWordmark must be boolean`);
+ }
+ if (!ADJACENT_NAME_POLICIES.has(asset.adjacentProductNamePolicy)) {
+ throw new Error(`Invalid adjacent product name policy for ${asset.file}`);
+ }
+ if (asset.containsWordmark && asset.adjacentProductNamePolicy !== 'forbidden') {
+ throw new Error(`Wordmark ${asset.file} must forbid adjacent duplicate product text`);
+ }
+ if (!asset.containsWordmark && asset.adjacentProductNamePolicy !== 'accessible-context-only') {
+ throw new Error(
+ `Standalone mark ${asset.file} may add product text only for accessible context`,
+ );
+ }
+
+ assertInteger(asset.width, `${asset.file} width`, 1);
+ assertInteger(asset.height, `${asset.file} height`, 1);
+ assertExactKeys(asset.contentBox, ['height', 'width', 'x', 'y'], `${asset.file} contentBox`);
+ for (const field of ['x', 'y', 'width', 'height']) {
+ assertInteger(
+ asset.contentBox[field],
+ `${asset.file} contentBox.${field}`,
+ field === 'width' || field === 'height' ? 1 : 0,
+ );
+ }
+ if (
+ asset.contentBox.x + asset.contentBox.width > asset.width ||
+ asset.contentBox.y + asset.contentBox.height > asset.height
+ ) {
+ throw new Error(`Content box exceeds output bounds for ${asset.file}`);
+ }
+
+ if (hasFrames) {
+ if (!asset.file.endsWith('.ico') || !Array.isArray(asset.frames) || asset.frames.length === 0) {
+ throw new Error(`Only non-empty ICO frame lists are supported for ${asset.file}`);
+ }
+ let previous = 0;
+ for (const frame of asset.frames) {
+ assertInteger(frame, `${asset.file} frame`, 1);
+ if (frame > 256) throw new Error(`${asset.file} frame must be at most 256`);
+ if (frame <= previous) throw new Error(`${asset.file} frames must be unique and ascending`);
+ previous = frame;
+ }
+ } else if (!asset.file.endsWith('.png')) {
+ throw new Error(`PNG output required for ${asset.file}`);
+ }
+}
+
+export function validateDerivativePlan(plan, { sourceManifest } = {}) {
+ assertExactKeys(plan, ['approval', 'assets', 'pipeline', 'schemaVersion', 'sources'], 'plan');
+ if (plan.schemaVersion !== 1) throw new Error('Unsupported brand derivative plan schema');
+ validatePipeline(plan.pipeline);
+ const approvedSources = approvedSourceMap(sourceManifest);
+ validateApproval(plan.approval, approvedSources);
+ validateSources(plan.sources, approvedSources);
+ if (!Array.isArray(plan.assets) || plan.assets.length === 0) {
+ throw new Error('Derivative plan must contain the complete platform inventory');
+ }
+ const seenFiles = new Set();
+ for (const asset of plan.assets) validateAsset(asset, plan.sources, seenFiles);
+ const actualFiles = plan.assets.map((asset) => asset.file).sort();
+ if (JSON.stringify(actualFiles) !== JSON.stringify([...REQUIRED_ASSET_FILES].sort())) {
+ throw new Error('Derivative plan must contain the complete required platform inventory');
+ }
+ return plan;
+}
+
+async function loadInputs() {
+ const [planBytes, sourceManifestBytes] = await Promise.all([
+ readFile(planPath),
+ readFile(sourceManifestPath),
+ ]);
+ const sourceManifest = JSON.parse(sourceManifestBytes.toString('utf8'));
+ const plan = validateDerivativePlan(JSON.parse(planBytes.toString('utf8')), { sourceManifest });
+ const approvedSources = new Map(sourceManifest.assets.map((asset) => [asset.file, asset]));
+ const sourceBytes = new Map();
+
+ for (const source of Object.values(plan.sources)) {
+ const approved = approvedSources.get(source.file);
+ if (!approved) throw new Error(`Derivative plan references unapproved source ${source.file}`);
+ if (!sourceBytes.has(source.file)) {
+ const bytes = await readFile(join(sourceDirectory, source.file));
+ assertApprovedSourceBytes({ approvedSha256: approved.sha256, bytes, file: source.file });
+ sourceBytes.set(source.file, bytes);
+ }
+ }
+
+ return { plan, sourceBytes, sourceManifestBytes, approvedSources };
+}
+
+async function renderPng(sourceBytes, source, output, pngOptions) {
+ let image = sharp(sourceBytes, { failOn: 'error', limitInputPixels: 64_000_000 });
+ if (source.crop) image = image.extract(source.crop);
+
+ const resized = await image
+ .resize({
+ width: output.contentBox.width,
+ height: output.contentBox.height,
+ fit: 'contain',
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ kernel: sharp.kernel.lanczos3,
+ })
+ .png(pngOptions)
+ .toBuffer();
+
+ const background = output.backgroundColor
+ ? {
+ r: output.backgroundColor.red,
+ g: output.backgroundColor.green,
+ b: output.backgroundColor.blue,
+ alpha: output.backgroundColor.alpha,
+ }
+ : { r: 0, g: 0, b: 0, alpha: 0 };
+ return sharp({
+ create: {
+ width: output.width,
+ height: output.height,
+ channels: 4,
+ background,
+ },
+ })
+ .composite([{ input: resized, left: output.contentBox.x, top: output.contentBox.y }])
+ .png(pngOptions)
+ .toBuffer();
+}
+
+async function convertToAndroidAlphaMask(pngBytes, pngOptions) {
+ const { data, info } = await sharp(pngBytes)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+ for (let offset = 0; offset < data.length; offset += 4) {
+ data[offset] = 255;
+ data[offset + 1] = 255;
+ data[offset + 2] = 255;
+ }
+ return sharp(data, {
+ raw: { channels: 4, height: info.height, width: info.width },
+ })
+ .png(pngOptions)
+ .toBuffer();
+}
+
+function scaleContentBox(asset, size) {
+ const width = Math.max(1, Math.round((asset.contentBox.width / asset.width) * size));
+ const height = Math.max(1, Math.round((asset.contentBox.height / asset.height) * size));
+ return {
+ x: Math.floor((size - width) / 2),
+ y: Math.floor((size - height) / 2),
+ width,
+ height,
+ };
+}
+
+function fittedBoxFor(source, approvedSource, contentBox) {
+ const sourceWidth = source.crop?.width ?? approvedSource.width;
+ const sourceHeight = source.crop?.height ?? approvedSource.height;
+ const sourceAspect = sourceWidth / sourceHeight;
+ const boxAspect = contentBox.width / contentBox.height;
+ const width =
+ sourceAspect >= boxAspect ? contentBox.width : Math.round(contentBox.height * sourceAspect);
+ const height =
+ sourceAspect >= boxAspect ? Math.round(contentBox.width / sourceAspect) : contentBox.height;
+ return {
+ x: contentBox.x + Math.floor((contentBox.width - width) / 2),
+ y: contentBox.y + Math.floor((contentBox.height - height) / 2),
+ width,
+ height,
+ };
+}
+
+function visibleBoundsFor(source, approvedSource, contentBox) {
+ const box = fittedBoxFor(source, approvedSource, contentBox);
+ return {
+ maxX: box.x + box.width - 1,
+ maxY: box.y + box.height - 1,
+ minX: box.x,
+ minY: box.y,
+ };
+}
+
+function createIco(pngFrames) {
+ const directoryLength = 6 + pngFrames.length * 16;
+ const header = Buffer.alloc(directoryLength);
+ header.writeUInt16LE(0, 0);
+ header.writeUInt16LE(1, 2);
+ header.writeUInt16LE(pngFrames.length, 4);
+
+ let offset = directoryLength;
+ for (let index = 0; index < pngFrames.length; index += 1) {
+ const { bytes, size } = pngFrames[index];
+ const entry = 6 + index * 16;
+ header.writeUInt8(size === 256 ? 0 : size, entry);
+ header.writeUInt8(size === 256 ? 0 : size, entry + 1);
+ header.writeUInt8(0, entry + 2);
+ header.writeUInt8(0, entry + 3);
+ header.writeUInt16LE(1, entry + 4);
+ header.writeUInt16LE(32, entry + 6);
+ header.writeUInt32LE(bytes.length, entry + 8);
+ header.writeUInt32LE(offset, entry + 12);
+ offset += bytes.length;
+ }
+
+ return Buffer.concat([header, ...pngFrames.map(({ bytes }) => bytes)]);
+}
+
+export function parseAndValidateIco(bytes, expectedSizes) {
+ if (!Buffer.isBuffer(bytes) || bytes.length < 22) throw new Error('ICO is truncated');
+ if (bytes.readUInt16LE(0) !== 0) throw new Error('ICO reserved header must be zero');
+ if (bytes.readUInt16LE(2) !== 1) throw new Error('ICO type must be icon');
+ const count = bytes.readUInt16LE(4);
+ if (count === 0) throw new Error('ICO must contain at least one frame');
+ const directoryEnd = 6 + count * 16;
+ if (directoryEnd > bytes.length) throw new Error('ICO directory is truncated');
+
+ const frames = [];
+ let expectedOffset = directoryEnd;
+ let previousSize = 0;
+ for (let index = 0; index < count; index += 1) {
+ const entry = 6 + index * 16;
+ const width = bytes.readUInt8(entry) || 256;
+ const height = bytes.readUInt8(entry + 1) || 256;
+ const colorCount = bytes.readUInt8(entry + 2);
+ const reserved = bytes.readUInt8(entry + 3);
+ const planes = bytes.readUInt16LE(entry + 4);
+ const bitDepth = bytes.readUInt16LE(entry + 6);
+ const length = bytes.readUInt32LE(entry + 8);
+ const offset = bytes.readUInt32LE(entry + 12);
+ if (width !== height || width <= previousSize) {
+ throw new Error('ICO frame sizes must be square, unique, and strictly ascending');
+ }
+ if (colorCount !== 0 || reserved !== 0)
+ throw new Error('ICO entry reserved fields must be zero');
+ if (planes !== 1) throw new Error('ICO frame planes must equal one');
+ if (bitDepth !== 32) throw new Error('ICO frames must use 32-bit depth');
+ if (length < 24 || offset !== expectedOffset || offset + length > bytes.length) {
+ throw new Error('ICO frame offsets must be ordered, contiguous, and within bounds');
+ }
+ const frameBytes = bytes.subarray(offset, offset + length);
+ if (!frameBytes.subarray(0, 8).equals(PNG_SIGNATURE)) {
+ throw new Error('ICO frame must contain PNG bytes');
+ }
+ if (
+ frameBytes.subarray(12, 16).toString('ascii') !== 'IHDR' ||
+ frameBytes.readUInt32BE(16) !== width ||
+ frameBytes.readUInt32BE(20) !== height
+ ) {
+ throw new Error('ICO frame PNG dimensions must match its directory entry');
+ }
+ frames.push({ bytes: frameBytes, height, width });
+ previousSize = width;
+ expectedOffset = offset + length;
+ }
+ if (expectedOffset !== bytes.length) throw new Error('ICO must not contain trailing bytes');
+ if (
+ expectedSizes &&
+ JSON.stringify(frames.map((frame) => frame.width)) !== JSON.stringify(expectedSizes)
+ ) {
+ throw new Error('ICO frame inventory does not match the derivative plan');
+ }
+ return frames;
+}
+
+async function visualHash(pngBytes) {
+ const pixels = await sharp(pngBytes)
+ .ensureAlpha()
+ .resize({ width: 32, height: 32, fit: 'fill', kernel: sharp.kernel.lanczos3 })
+ .raw()
+ .toBuffer();
+ return sha256(pixels);
+}
+
+async function inspectPathWithoutLinks(targetPath, { label, leafType = 'any' }) {
+ const absolutePath = resolve(targetPath);
+ const root = parse(absolutePath).root;
+ const segments = relative(root, absolutePath).split(sep).filter(Boolean);
+ let lexicalPath = root;
+ let canonicalPath = await realpath(root);
+
+ for (let index = 0; index < segments.length; index += 1) {
+ lexicalPath = join(lexicalPath, segments[index]);
+ let stats;
+ try {
+ stats = await lstat(lexicalPath);
+ } catch (error) {
+ if (error?.code !== 'ENOENT') throw error;
+ return {
+ canonicalPath: resolve(canonicalPath, ...segments.slice(index)),
+ exists: false,
+ };
+ }
+ if (stats.isSymbolicLink()) {
+ throw new Error(`${label} must not use symbolic link, junction, or reparse-point ancestry`);
+ }
+ const isLeaf = index === segments.length - 1;
+ if (!isLeaf && !stats.isDirectory()) throw new Error(`${label} ancestry must be directories`);
+ if (isLeaf && leafType === 'directory' && !stats.isDirectory()) {
+ throw new Error(`${label} must be a directory`);
+ }
+ if (isLeaf && leafType === 'file' && !stats.isFile()) {
+ throw new Error(`${label} must be a regular file`);
+ }
+ canonicalPath = await realpath(lexicalPath);
+ }
+ return { canonicalPath, exists: true };
+}
+
+function pathsEqual(left, right) {
+ const normalize = (value) =>
+ process.platform === 'win32' ? resolve(value).toLowerCase() : resolve(value);
+ return normalize(left) === normalize(right);
+}
+
+let temporaryWriteSequence = 0;
+
+async function writeFileSafely(targetPath, bytes, targetContext) {
+ await validateOutputTargets(targetContext);
+ await inspectPathWithoutLinks(targetPath, { label: 'Derivative write target', leafType: 'file' });
+ const parentPath = dirname(targetPath);
+ await inspectPathWithoutLinks(parentPath, {
+ label: 'Derivative write parent',
+ leafType: 'directory',
+ });
+
+ temporaryWriteSequence += 1;
+ const temporaryPath = `${targetPath}.tmp-${process.pid}-${temporaryWriteSequence}`;
+ let handle;
+ try {
+ handle = await open(temporaryPath, 'wx', 0o600);
+ await handle.writeFile(bytes);
+ await handle.sync();
+ await handle.close();
+ handle = undefined;
+ await validateOutputTargets(targetContext);
+ await inspectPathWithoutLinks(targetPath, {
+ label: 'Derivative write target',
+ leafType: 'file',
+ });
+ await inspectPathWithoutLinks(parentPath, {
+ label: 'Derivative write parent',
+ leafType: 'directory',
+ });
+ await rename(temporaryPath, targetPath);
+ } finally {
+ if (handle) await handle.close();
+ await rm(temporaryPath, { force: true });
+ }
+ await inspectPathWithoutLinks(targetPath, {
+ label: 'Written derivative',
+ leafType: 'file',
+ });
+}
+
+async function ensureSafeDirectory(directoryPath, targetContext) {
+ await validateOutputTargets(targetContext);
+ await inspectPathWithoutLinks(directoryPath, {
+ label: 'Derivative directory',
+ leafType: 'directory',
+ });
+ await mkdir(directoryPath, { recursive: true });
+ await inspectPathWithoutLinks(directoryPath, {
+ label: 'Derivative directory',
+ leafType: 'directory',
+ });
+ await validateOutputTargets(targetContext);
+}
+
+export async function generateBrandDerivatives({ outputDirectory, manifestPath }) {
+ const { plan, sourceBytes, sourceManifestBytes, approvedSources } = await loadInputs();
+ const assetFiles = plan.assets.map((asset) => asset.file);
+ const targetContext = { assetFiles, manifestPath, outputDirectory };
+ await validateOutputTargets(targetContext);
+ const manifestAssets = [];
+
+ for (const asset of plan.assets) {
+ const source = plan.sources[asset.source];
+ const approvedSource = approvedSources.get(source.file);
+ const bytes = sourceBytes.get(source.file);
+ let outputBytes;
+ let visualBytes;
+
+ if (asset.frames) {
+ const frames = [];
+ for (const size of asset.frames) {
+ const frameAsset = {
+ ...asset,
+ width: size,
+ height: size,
+ contentBox: scaleContentBox(asset, size),
+ };
+ frames.push({
+ bytes: await renderPng(bytes, source, frameAsset, plan.pipeline.png),
+ size,
+ });
+ }
+ outputBytes = createIco(frames);
+ parseAndValidateIco(outputBytes, asset.frames);
+ visualBytes = frames.at(-1).bytes;
+ } else {
+ outputBytes = await renderPng(bytes, source, asset, plan.pipeline.png);
+ if (asset.outputMode === 'android-alpha-mask') {
+ outputBytes = await convertToAndroidAlphaMask(outputBytes, plan.pipeline.png);
+ }
+ visualBytes = outputBytes;
+ }
+
+ const outputPath = join(outputDirectory, asset.file);
+ await ensureSafeDirectory(dirname(outputPath), targetContext);
+ await writeFileSafely(outputPath, outputBytes, targetContext);
+ manifestAssets.push({
+ adjacentProductNamePolicy: asset.adjacentProductNamePolicy,
+ containsWordmark: asset.containsWordmark,
+ contentBox: asset.contentBox,
+ file: asset.file,
+ fittedBox: fittedBoxFor(source, approvedSource, asset.contentBox),
+ visibleBounds: visibleBoundsFor(source, approvedSource, asset.contentBox),
+ ...(asset.frames ? { frames: asset.frames } : {}),
+ ...(asset.backgroundColor ? { backgroundColor: asset.backgroundColor } : {}),
+ height: asset.height,
+ mediaType: asset.frames ? 'image/x-icon' : 'image/png',
+ platform: asset.platform,
+ purpose: asset.purpose,
+ safeZone: asset.safeZone,
+ sha256: sha256(outputBytes),
+ source: {
+ ...(source.crop ? { crop: source.crop } : {}),
+ file: source.file,
+ sha256: approvedSource.sha256,
+ },
+ transform:
+ asset.outputMode === 'android-alpha-mask'
+ ? 'alpha-mask-from-approved-geometry'
+ : 'aspect-preserving-contain',
+ ...(asset.outputMode ? { outputMode: asset.outputMode } : {}),
+ visualSha256: await visualHash(visualBytes),
+ width: asset.width,
+ });
+ }
+
+ const manifest = {
+ schemaVersion: 1,
+ approval: plan.approval,
+ generator: {
+ engine: plan.pipeline.engine,
+ engineVersion: plan.pipeline.engineVersion,
+ icoContainer: 'png-frame-ico-v1',
+ png: plan.pipeline.png,
+ runtime: {
+ libvips: sharp.versions.vips,
+ png: sharp.versions.png,
+ sharp: sharp.versions.sharp,
+ },
+ },
+ sourceManifestSha256: sha256(sourceManifestBytes),
+ assets: manifestAssets,
+ };
+ await ensureSafeDirectory(dirname(manifestPath), targetContext);
+ await writeFileSafely(manifestPath, stableJson(manifest), targetContext);
+ return manifest;
+}
+
+async function listGeneratedTree(directory, prefix = '') {
+ const entries = await readdir(directory, { withFileTypes: true });
+ const files = [];
+ const directories = [];
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
+ const entryPath = join(directory, entry.name);
+ const stats = await lstat(entryPath);
+ if (stats.isSymbolicLink()) {
+ throw new Error(`Unexpected generated asset entry: ${relativePath}`);
+ }
+ if (stats.isDirectory()) {
+ directories.push(relativePath);
+ const child = await listGeneratedTree(entryPath, relativePath);
+ files.push(...child.files);
+ directories.push(...child.directories);
+ } else if (stats.isFile()) {
+ files.push(relativePath);
+ } else {
+ throw new Error(`Unexpected generated asset entry: ${relativePath}`);
+ }
+ }
+ return { directories, files };
+}
+
+async function assertSameFile(expectedPath, actualPath, label) {
+ const [expected, actual] = await Promise.all([readFile(expectedPath), readFile(actualPath)]);
+ if (!expected.equals(actual)) throw new Error(`Brand derivative drift detected: ${label}`);
+}
+
+export async function validateOutputTargets({ assetFiles = [], manifestPath, outputDirectory }) {
+ if (!Array.isArray(assetFiles)) throw new Error('Generated asset inventory must be an array');
+ if (
+ isWithin(sourceDirectory, outputDirectory) ||
+ isWithin(outputDirectory, sourceDirectory) ||
+ resolve(outputDirectory) === resolve(brandDirectory)
+ ) {
+ throw new Error(
+ 'Derivative output directory must not contain or overwrite immutable brand sources',
+ );
+ }
+ if (
+ isWithin(sourceDirectory, manifestPath) ||
+ resolve(manifestPath) === resolve(sourceManifestPath)
+ ) {
+ throw new Error('Derivative manifest must not contain or overwrite immutable brand sources');
+ }
+ for (const file of assetFiles) {
+ if (pathsEqual(manifestPath, join(outputDirectory, file))) {
+ throw new Error(`Derivative manifest must not collide with generated asset ${file}`);
+ }
+ }
+ if (isWithin(outputDirectory, manifestPath)) {
+ throw new Error('Derivative manifest must not be nested inside the generated output');
+ }
+
+ const [sourceResult, sourceManifestResult, outputResult, manifestResult] = await Promise.all([
+ inspectPathWithoutLinks(sourceDirectory, {
+ label: 'Immutable brand source directory',
+ leafType: 'directory',
+ }),
+ inspectPathWithoutLinks(sourceManifestPath, {
+ label: 'Immutable brand source manifest',
+ leafType: 'file',
+ }),
+ inspectPathWithoutLinks(outputDirectory, {
+ label: 'Derivative output directory',
+ leafType: 'directory',
+ }),
+ inspectPathWithoutLinks(manifestPath, {
+ label: 'Derivative manifest',
+ leafType: 'file',
+ }),
+ ]);
+ if (
+ isWithin(sourceResult.canonicalPath, outputResult.canonicalPath) ||
+ isWithin(outputResult.canonicalPath, sourceResult.canonicalPath)
+ ) {
+ throw new Error(
+ 'Derivative output directory must not contain or overwrite immutable brand sources',
+ );
+ }
+ if (
+ isWithin(sourceResult.canonicalPath, manifestResult.canonicalPath) ||
+ pathsEqual(sourceManifestResult.canonicalPath, manifestResult.canonicalPath)
+ ) {
+ throw new Error('Derivative manifest must not contain or overwrite immutable brand sources');
+ }
+ if (isWithin(outputResult.canonicalPath, manifestResult.canonicalPath)) {
+ throw new Error('Derivative manifest must not be nested inside the generated output');
+ }
+}
+
+export async function compareBrandDerivatives({
+ expectedManifestPath = defaultManifestPath,
+ expectedOutputDirectory = defaultOutputDirectory,
+} = {}) {
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-check-'));
+ try {
+ const temporaryOutput = join(temporaryRoot, 'generated');
+ const temporaryManifest = join(temporaryRoot, 'derivatives.json');
+ await generateBrandDerivatives({
+ outputDirectory: temporaryOutput,
+ manifestPath: temporaryManifest,
+ });
+
+ const [committedTree, generatedTree] = await Promise.all([
+ listGeneratedTree(expectedOutputDirectory),
+ listGeneratedTree(temporaryOutput),
+ ]);
+ if (JSON.stringify(committedTree) !== JSON.stringify(generatedTree)) {
+ throw new Error('Brand derivative inventory drift detected');
+ }
+ await Promise.all(
+ generatedTree.files.map((file) =>
+ assertSameFile(join(expectedOutputDirectory, file), join(temporaryOutput, file), file),
+ ),
+ );
+ await assertSameFile(expectedManifestPath, temporaryManifest, 'derivatives.json');
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+}
+
+export async function checkBrandDerivatives(options) {
+ await compareBrandDerivatives(options);
+}
+
+function parseArguments(argv) {
+ const options = {
+ check: false,
+ outputDirectory: defaultOutputDirectory,
+ manifestPath: defaultManifestPath,
+ };
+ for (let index = 0; index < argv.length; index += 1) {
+ const argument = argv[index];
+ if (argument === '--check') {
+ options.check = true;
+ } else if (argument === '--output' || argument === '--manifest') {
+ const value = argv[index + 1];
+ if (!value || value.startsWith('--')) throw new Error(`${argument} requires a path`);
+ index += 1;
+ if (argument === '--output') options.outputDirectory = resolve(value);
+ else options.manifestPath = resolve(value);
+ } else {
+ throw new Error(`Unknown argument: ${argument}`);
+ }
+ }
+ return options;
+}
+
+async function main() {
+ const options = parseArguments(process.argv.slice(2));
+ if (options.check) {
+ await checkBrandDerivatives({
+ expectedManifestPath: options.manifestPath,
+ expectedOutputDirectory: options.outputDirectory,
+ });
+ process.stdout.write('Brand derivatives are reproducible and current.\n');
+ } else {
+ await generateBrandDerivatives(options);
+ process.stdout.write(`Generated brand derivatives in ${options.outputDirectory}.\n`);
+ }
+}
+
+const invokedPath = process.argv[1] ? resolve(process.argv[1]) : '';
+if (invokedPath === fileURLToPath(import.meta.url)) {
+ main().catch((error) => {
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
+ process.exitCode = 1;
+ });
+}
diff --git a/packages/design-tokens/test/brand-derivative-security.test.mjs b/packages/design-tokens/test/brand-derivative-security.test.mjs
new file mode 100644
index 00000000..701f41d4
--- /dev/null
+++ b/packages/design-tokens/test/brand-derivative-security.test.mjs
@@ -0,0 +1,216 @@
+import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
+import { mkdir, mkdtemp, readFile, rm, symlink } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import process from 'node:process';
+import { test } from 'node:test';
+import { fileURLToPath, URL } from 'node:url';
+
+const packageDirectory = fileURLToPath(new URL('../', import.meta.url));
+const brandDirectory = join(packageDirectory, 'brand');
+const sourceDirectory = join(brandDirectory, 'source');
+const committedOutputDirectory = join(brandDirectory, 'generated');
+const committedManifestPath = join(brandDirectory, 'derivatives.json');
+
+function cloneJson(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+async function loadValidationInputs() {
+ const [plan, sourceManifest] = await Promise.all([
+ readFile(join(brandDirectory, 'derivative-plan.json'), 'utf8').then(JSON.parse),
+ readFile(join(brandDirectory, 'manifest.json'), 'utf8').then(JSON.parse),
+ ]);
+ return { plan, sourceManifest };
+}
+
+test('the derivative plan is a closed, typed, complete portable contract', async (context) => {
+ const { validateDerivativePlan } = await import('../scripts/generate-brand-derivatives.mjs');
+ const { plan, sourceManifest } = await loadValidationInputs();
+
+ const mutations = [
+ ['unknown root key', (value) => (value.unexpected = true), /unknown key/i],
+ ['unknown pipeline key', (value) => (value.pipeline.unexpected = true), /unknown key/i],
+ ['wrong libvips runtime', (value) => (value.pipeline.libvipsVersion = '0.0.0'), /libvips/i],
+ ['wrong PNG runtime', (value) => (value.pipeline.pngVersion = '0.0.0'), /PNG/i],
+ ['empty asset inventory', (value) => (value.assets = []), /complete.*inventory/i],
+ ['missing required asset', (value) => value.assets.pop(), /complete.*inventory/i],
+ ['backslash path', (value) => (value.assets[0].file = 'android\\icon.png'), /portable.*path/i],
+ [
+ 'dot path segment',
+ (value) => (value.assets[0].file = 'android/./icon.png'),
+ /portable.*path/i,
+ ],
+ [
+ 'case-folded duplicate path',
+ (value) => (value.assets[1].file = value.assets[0].file.toUpperCase()),
+ /duplicate.*path/i,
+ ],
+ [
+ 'unknown source key',
+ (value) => (value.sources.extra = value.sources.blueMark),
+ /source keys/i,
+ ],
+ ['unknown source field', (value) => (value.sources.blueMark.extra = true), /unknown key/i],
+ [
+ 'out-of-bounds source crop',
+ (value) => (value.sources.blueMark.crop.width = 99999),
+ /crop.*bounds/i,
+ ],
+ ['unknown asset field', (value) => (value.assets[0].extra = true), /unknown key/i],
+ ['non-object asset', (value) => (value.assets[0] = null), /asset.*object/i],
+ ['invalid platform', (value) => (value.assets[0].platform = 'ios'), /platform/i],
+ ['empty purpose', (value) => (value.assets[0].purpose = ''), /purpose/i],
+ ['empty safe zone', (value) => (value.assets[0].safeZone = ''), /safeZone/i],
+ [
+ 'wrong transform policy',
+ (value) => (value.pipeline.pixelPolicy = 'stretch'),
+ /pixel policy|transform/i,
+ ],
+ [
+ 'unordered ICO frames',
+ (value) => (value.assets.find((asset) => asset.frames).frames = [16, 32, 24]),
+ /frames.*ascending/i,
+ ],
+ [
+ 'duplicate ICO frames',
+ (value) => (value.assets.find((asset) => asset.frames).frames = [16, 16, 32]),
+ /frames.*ascending/i,
+ ],
+ [
+ 'oversized ICO frame',
+ (value) => (value.assets.find((asset) => asset.frames).frames = [16, 257]),
+ /frame.*256/i,
+ ],
+ ];
+
+ for (const [name, mutate, expected] of mutations) {
+ await context.test(name, () => {
+ const changed = cloneJson(plan);
+ mutate(changed);
+ assert.throws(() => validateDerivativePlan(changed, { sourceManifest }), expected);
+ });
+ }
+});
+
+test('manifest provenance pins the loaded Sharp libvips and PNG runtimes', async () => {
+ const [{ default: sharp }, manifest] = await Promise.all([
+ import('sharp'),
+ readFile(committedManifestPath, 'utf8').then(JSON.parse),
+ ]);
+ assert.deepEqual(manifest.generator.runtime, {
+ libvips: sharp.versions.vips,
+ png: sharp.versions.png,
+ sharp: sharp.versions.sharp,
+ });
+});
+
+test('filesystem-aware target validation rejects linked ancestry and output/manifest collisions', async () => {
+ const { validateOutputTargets } = await import('../scripts/generate-brand-derivatives.mjs');
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-targets-'));
+ try {
+ const linkPath = join(temporaryRoot, 'linked-source');
+ await symlink(sourceDirectory, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
+
+ await assert.rejects(
+ validateOutputTargets({
+ assetFiles: [],
+ manifestPath: join(temporaryRoot, 'derivatives.json'),
+ outputDirectory: join(linkPath, 'generated'),
+ }),
+ /symbolic link|junction|reparse/i,
+ );
+
+ await assert.rejects(
+ validateOutputTargets({
+ assetFiles: [],
+ manifestPath: join(temporaryRoot, 'generated', 'derivatives.json'),
+ outputDirectory: join(temporaryRoot, 'generated'),
+ }),
+ /manifest.*output/i,
+ );
+
+ await assert.rejects(
+ validateOutputTargets({
+ assetFiles: ['web/favicon-16.png'],
+ manifestPath: join(temporaryRoot, 'generated', 'web', 'favicon-16.png'),
+ outputDirectory: join(temporaryRoot, 'generated'),
+ }),
+ /manifest.*generated asset/i,
+ );
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('generation refuses a junction output ancestor before writing through it', async () => {
+ const { generateBrandDerivatives } = await import('../scripts/generate-brand-derivatives.mjs');
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-write-link-'));
+ try {
+ const externalTarget = join(temporaryRoot, 'external');
+ const linkPath = join(temporaryRoot, 'linked');
+ await mkdir(externalTarget);
+ await symlink(externalTarget, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
+ await assert.rejects(
+ generateBrandDerivatives({
+ manifestPath: join(temporaryRoot, 'derivatives.json'),
+ outputDirectory: join(linkPath, 'generated'),
+ }),
+ /symbolic link|junction|reparse/i,
+ );
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('drift comparison rejects extra empty directories', async () => {
+ const { compareBrandDerivatives, generateBrandDerivatives } = await import(
+ '../scripts/generate-brand-derivatives.mjs'
+ );
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-empty-dir-'));
+ try {
+ const outputDirectory = join(temporaryRoot, 'generated');
+ const manifestPath = join(temporaryRoot, 'derivatives.json');
+ await generateBrandDerivatives({ manifestPath, outputDirectory });
+ await mkdir(join(outputDirectory, 'unexpected-empty'));
+ await assert.rejects(
+ compareBrandDerivatives({
+ expectedManifestPath: manifestPath,
+ expectedOutputDirectory: outputDirectory,
+ }),
+ /inventory drift/i,
+ );
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('ICO validation rejects malformed headers, directory entries, offsets, and trailing bytes', async () => {
+ const { parseAndValidateIco } = await import('../scripts/generate-brand-derivatives.mjs');
+ assert.equal(typeof parseAndValidateIco, 'function');
+ const original = await readFile(join(committedOutputDirectory, 'desktop', 'application.ico'));
+ const mutate = (callback) => {
+ const bytes = Buffer.from(original);
+ callback(bytes);
+ return bytes;
+ };
+
+ const cases = [
+ ['reserved header', mutate((bytes) => bytes.writeUInt16LE(1, 0))],
+ ['icon type', mutate((bytes) => bytes.writeUInt16LE(2, 2))],
+ ['reserved entry', mutate((bytes) => bytes.writeUInt8(1, 9))],
+ ['planes', mutate((bytes) => bytes.writeUInt16LE(2, 10))],
+ ['bit depth', mutate((bytes) => bytes.writeUInt16LE(24, 12))],
+ ['overlapping offset', mutate((bytes) => bytes.writeUInt32LE(6, 18))],
+ ['out-of-bounds length', mutate((bytes) => bytes.writeUInt32LE(0xffffffff, 14))],
+ ['trailing bytes', Buffer.concat([original, Buffer.from([0])])],
+ ];
+ for (const [name, bytes] of cases) {
+ assert.throws(
+ () => parseAndValidateIco(bytes, [16, 24, 32, 48, 64, 128, 256]),
+ undefined,
+ name,
+ );
+ }
+});
diff --git a/packages/design-tokens/test/brand-derivatives.test.mjs b/packages/design-tokens/test/brand-derivatives.test.mjs
new file mode 100644
index 00000000..c7497931
--- /dev/null
+++ b/packages/design-tokens/test/brand-derivatives.test.mjs
@@ -0,0 +1,316 @@
+import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
+import { execFile } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { promisify } from 'node:util';
+import { test } from 'node:test';
+import { fileURLToPath, URL } from 'node:url';
+
+const execFileAsync = promisify(execFile);
+const packageDirectory = fileURLToPath(new URL('../', import.meta.url));
+const generatorPath = join(packageDirectory, 'scripts', 'generate-brand-derivatives.mjs');
+const brandDirectory = join(packageDirectory, 'brand');
+const committedOutputDirectory = join(brandDirectory, 'generated');
+const committedManifestPath = join(brandDirectory, 'derivatives.json');
+const goldenPath = join(packageDirectory, 'test', 'fixtures', 'brand-visual-golden.json');
+
+const expectedInventory = [
+ ['android/adaptive-foreground-432.png', 432, 432],
+ ['android/launcher-hdpi-72.png', 72, 72],
+ ['android/launcher-mdpi-48.png', 48, 48],
+ ['android/launcher-xhdpi-96.png', 96, 96],
+ ['android/launcher-xxhdpi-144.png', 144, 144],
+ ['android/launcher-xxxhdpi-192.png', 192, 192],
+ ['android/notification-hdpi-36.png', 36, 36],
+ ['android/notification-mdpi-24.png', 24, 24],
+ ['android/notification-xhdpi-48.png', 48, 48],
+ ['android/notification-xxhdpi-72.png', 72, 72],
+ ['android/notification-xxxhdpi-96.png', 96, 96],
+ ['desktop/application-256.png', 256, 256],
+ ['desktop/application.ico', 256, 256],
+ ['desktop/installer.ico', 256, 256],
+ ['desktop/notification-32.png', 32, 32],
+ ['desktop/updater.ico', 256, 256],
+ ['web/apple-touch-icon-180.png', 180, 180],
+ ['web/favicon-16.png', 16, 16],
+ ['web/favicon-32.png', 32, 32],
+ ['web/install-icon-192.png', 192, 192],
+ ['web/install-icon-512.png', 512, 512],
+ ['web/navigation-wordmark-black-204x50.png', 204, 50],
+ ['web/navigation-wordmark-blue-204x50.png', 204, 50],
+ ['web/social-card-1200x630.png', 1200, 630],
+];
+
+function runGenerator(args) {
+ return execFileAsync(process.execPath, [generatorPath, ...args], {
+ cwd: packageDirectory,
+ encoding: 'utf8',
+ });
+}
+
+function hash(bytes) {
+ return createHash('sha256').update(bytes).digest('hex').toUpperCase();
+}
+
+function cloneJson(value) {
+ return JSON.parse(JSON.stringify(value));
+}
+
+function parseIco(bytes) {
+ assert.equal(bytes.readUInt16LE(0), 0);
+ assert.equal(bytes.readUInt16LE(2), 1);
+ const count = bytes.readUInt16LE(4);
+ const frames = [];
+ for (let index = 0; index < count; index += 1) {
+ const entry = 6 + index * 16;
+ const width = bytes.readUInt8(entry) || 256;
+ const height = bytes.readUInt8(entry + 1) || 256;
+ const length = bytes.readUInt32LE(entry + 8);
+ const offset = bytes.readUInt32LE(entry + 12);
+ frames.push({ bytes: bytes.subarray(offset, offset + length), height, width });
+ }
+ return frames;
+}
+
+test('a clean output directory receives the complete deterministic platform inventory', async () => {
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-derivatives-'));
+ const outputDirectory = join(temporaryRoot, 'generated');
+ const manifestPath = join(temporaryRoot, 'derivatives.json');
+
+ try {
+ await runGenerator(['--output', outputDirectory, '--manifest', manifestPath]);
+
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
+ const actual = manifest.assets.map(({ file, height, width }) => [file, width, height]);
+ assert.deepEqual(actual, expectedInventory);
+
+ const platformDirectories = await readdir(outputDirectory);
+ assert.deepEqual(platformDirectories.sort(), ['android', 'desktop', 'web']);
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('committed derivatives reproduce byte-for-byte and a changed output is detected as drift', async () => {
+ const module = await import('../scripts/generate-brand-derivatives.mjs');
+ assert.equal(typeof module.compareBrandDerivatives, 'function');
+
+ await module.compareBrandDerivatives({
+ expectedManifestPath: committedManifestPath,
+ expectedOutputDirectory: committedOutputDirectory,
+ });
+
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-drift-'));
+ try {
+ const outputDirectory = join(temporaryRoot, 'generated');
+ const manifestPath = join(temporaryRoot, 'derivatives.json');
+ await runGenerator(['--output', outputDirectory, '--manifest', manifestPath]);
+ const faviconPath = join(outputDirectory, 'web', 'favicon-16.png');
+ const changed = Buffer.from(await readFile(faviconPath));
+ changed[changed.length - 1] ^= 0xff;
+ await import('node:fs/promises').then(({ writeFile }) => writeFile(faviconPath, changed));
+
+ await assert.rejects(
+ module.compareBrandDerivatives({
+ expectedManifestPath: manifestPath,
+ expectedOutputDirectory: outputDirectory,
+ }),
+ /Brand derivative drift detected: web\/favicon-16\.png/,
+ );
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('--check validates the output and manifest paths supplied by the caller', async () => {
+ const temporaryRoot = await mkdtemp(join(tmpdir(), 'databreeze-brand-custom-check-'));
+ const outputDirectory = join(temporaryRoot, 'generated');
+ const manifestPath = join(temporaryRoot, 'derivatives.json');
+
+ try {
+ await runGenerator(['--output', outputDirectory, '--manifest', manifestPath]);
+ await runGenerator(['--check', '--output', outputDirectory, '--manifest', manifestPath]);
+
+ const faviconPath = join(outputDirectory, 'web', 'favicon-16.png');
+ const changed = Buffer.from(await readFile(faviconPath));
+ changed[changed.length - 1] ^= 0xff;
+ await import('node:fs/promises').then(({ writeFile }) => writeFile(faviconPath, changed));
+
+ await assert.rejects(
+ runGenerator(['--check', '--output', outputDirectory, '--manifest', manifestPath]),
+ /Brand derivative drift detected: web\/favicon-16\.png/,
+ );
+ } finally {
+ await rm(temporaryRoot, { force: true, recursive: true });
+ }
+});
+
+test('the derivative manifest links every output to an approved source and records safe fitted geometry', async () => {
+ const [manifest, sourceManifest] = await Promise.all([
+ readFile(committedManifestPath, 'utf8').then(JSON.parse),
+ readFile(join(brandDirectory, 'manifest.json'), 'utf8').then(JSON.parse),
+ ]);
+ const approved = new Map(sourceManifest.assets.map((source) => [source.file, source]));
+
+ assert.equal(
+ manifest.sourceManifestSha256,
+ hash(await readFile(join(brandDirectory, 'manifest.json'))),
+ );
+ for (const asset of manifest.assets) {
+ const source = approved.get(asset.source.file);
+ assert.ok(source, `${asset.file} must use an approved source`);
+ assert.equal(asset.source.sha256, source.sha256);
+ assert.equal(
+ asset.transform,
+ asset.outputMode === 'android-alpha-mask'
+ ? 'alpha-mask-from-approved-geometry'
+ : 'aspect-preserving-contain',
+ );
+ assert.ok(asset.safeZone.length > 0);
+ assert.ok(asset.fittedBox.x >= asset.contentBox.x);
+ assert.ok(asset.fittedBox.y >= asset.contentBox.y);
+ assert.ok(
+ asset.fittedBox.x + asset.fittedBox.width <= asset.contentBox.x + asset.contentBox.width,
+ );
+ assert.ok(
+ asset.fittedBox.y + asset.fittedBox.height <= asset.contentBox.y + asset.contentBox.height,
+ );
+
+ const sourceWidth = asset.source.crop?.width ?? source.width;
+ const sourceHeight = asset.source.crop?.height ?? source.height;
+ const sourceAspect = sourceWidth / sourceHeight;
+ const fittedAspectError =
+ sourceAspect >= asset.contentBox.width / asset.contentBox.height
+ ? Math.abs(asset.fittedBox.height - asset.fittedBox.width / sourceAspect)
+ : Math.abs(asset.fittedBox.width - asset.fittedBox.height * sourceAspect);
+ assert.ok(fittedAspectError <= 0.5, `${asset.file} must preserve the source aspect ratio`);
+ }
+});
+
+test('PNG and ICO outputs have declared dimensions, transparent clear space, and preserved brand colors', async () => {
+ const [{ default: sharp }, manifest] = await Promise.all([
+ import('sharp'),
+ readFile(committedManifestPath, 'utf8').then(JSON.parse),
+ ]);
+ const sourceColors = {
+ 'databreeze-mark-dark.png': [
+ [4, 9, 32],
+ [52, 78, 248],
+ ],
+ 'databreeze-wordmark-black.png': [[0, 0, 0]],
+ 'databreeze-wordmark-blue.png': [[52, 78, 248]],
+ };
+
+ for (const asset of manifest.assets) {
+ const bytes = await readFile(join(committedOutputDirectory, asset.file));
+ assert.equal(hash(bytes), asset.sha256);
+ const pngs =
+ asset.mediaType === 'image/x-icon'
+ ? parseIco(bytes)
+ : [{ bytes, width: asset.width, height: asset.height }];
+
+ if (asset.frames)
+ assert.deepEqual(
+ pngs.map(({ width }) => width),
+ asset.frames,
+ );
+ for (const frame of pngs) {
+ if (asset.frames) assert.equal(frame.width, frame.height);
+ assert.deepEqual(frame.bytes.subarray(0, 8), Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
+ const { data, info } = await sharp(frame.bytes)
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true });
+ assert.equal(info.width, frame.width);
+ assert.equal(info.height, frame.height);
+
+ let transparentPixelFound = false;
+ const visibleColors = new Set();
+ for (let offset = 0; offset < data.length; offset += 4) {
+ if (data[offset + 3] === 0) transparentPixelFound = true;
+ if (data[offset + 3] > 0)
+ visibleColors.add(`${data[offset]},${data[offset + 1]},${data[offset + 2]}`);
+ }
+ if (asset.file === 'web/social-card-1200x630.png') {
+ assert.equal(transparentPixelFound, false, `${asset.file} must be fully opaque`);
+ } else {
+ assert.ok(transparentPixelFound, `${asset.file} must retain transparent clear space`);
+ }
+ const expectedColors =
+ asset.outputMode === 'android-alpha-mask'
+ ? [[255, 255, 255]]
+ : sourceColors[asset.source.file];
+ assert.ok(
+ expectedColors.some((color) => visibleColors.has(color.join(','))),
+ `${asset.file} must retain an approved source color`,
+ );
+ }
+ }
+});
+
+test('visual signatures match the separately approved golden fixture', async () => {
+ const [manifest, golden] = await Promise.all([
+ readFile(committedManifestPath, 'utf8').then(JSON.parse),
+ readFile(goldenPath, 'utf8').then(JSON.parse),
+ ]);
+ assert.deepEqual(
+ Object.fromEntries(manifest.assets.map((asset) => [asset.file, asset.visualSha256])),
+ golden.assets,
+ );
+});
+
+test('plan validation blocks unsafe paths, invalid safe zones, and duplicate wordmark text policy', async () => {
+ const { validateDerivativePlan } = await import('../scripts/generate-brand-derivatives.mjs');
+ const [plan, sourceManifest] = await Promise.all([
+ readFile(join(brandDirectory, 'derivative-plan.json'), 'utf8').then(JSON.parse),
+ readFile(join(brandDirectory, 'manifest.json'), 'utf8').then(JSON.parse),
+ ]);
+
+ const traversal = cloneJson(plan);
+ traversal.assets[0].file = '../source/changed.png';
+ assert.throws(() => validateDerivativePlan(traversal, { sourceManifest }), /portable.*path/i);
+
+ const unsafeGeometry = cloneJson(plan);
+ unsafeGeometry.assets[0].contentBox.width = unsafeGeometry.assets[0].width;
+ assert.throws(
+ () => validateDerivativePlan(unsafeGeometry, { sourceManifest }),
+ /Content box exceeds output bounds/,
+ );
+
+ const duplicateText = cloneJson(plan);
+ const wordmark = duplicateText.assets.find((asset) => asset.containsWordmark);
+ wordmark.adjacentProductNamePolicy = 'allowed';
+ assert.throws(
+ () => validateDerivativePlan(duplicateText, { sourceManifest }),
+ /invalid adjacent product name policy|must forbid adjacent duplicate product text/i,
+ );
+});
+
+test('output target validation prevents any generated write beneath immutable sources', async () => {
+ const { validateOutputTargets } = await import('../scripts/generate-brand-derivatives.mjs');
+ assert.equal(typeof validateOutputTargets, 'function');
+ await assert.rejects(
+ validateOutputTargets({
+ manifestPath: join(brandDirectory, 'source', 'derivatives.json'),
+ outputDirectory: join(brandDirectory, 'generated'),
+ }),
+ /must not contain or overwrite immutable brand sources/,
+ );
+});
+
+test('the source hash gate rejects bytes that are not the approved canonical asset', async () => {
+ const { assertApprovedSourceBytes } = await import('../scripts/generate-brand-derivatives.mjs');
+ assert.equal(typeof assertApprovedSourceBytes, 'function');
+ assert.throws(
+ () =>
+ assertApprovedSourceBytes({
+ approvedSha256: 'A'.repeat(64),
+ bytes: Buffer.from('changed source'),
+ file: 'databreeze-wordmark-blue.png',
+ }),
+ /Approved source checksum mismatch: databreeze-wordmark-blue\.png/,
+ );
+});
diff --git a/packages/design-tokens/test/brand-platform-assets.test.mjs b/packages/design-tokens/test/brand-platform-assets.test.mjs
new file mode 100644
index 00000000..9fb22d25
--- /dev/null
+++ b/packages/design-tokens/test/brand-platform-assets.test.mjs
@@ -0,0 +1,201 @@
+import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
+import { readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { test } from 'node:test';
+import { fileURLToPath, URL } from 'node:url';
+import sharp from 'sharp';
+
+const packageDirectory = fileURLToPath(new URL('../', import.meta.url));
+const brandDirectory = join(packageDirectory, 'brand');
+const outputDirectory = join(brandDirectory, 'generated');
+const planPath = join(brandDirectory, 'derivative-plan.json');
+const manifestPath = join(brandDirectory, 'derivatives.json');
+const sourcePath = join(brandDirectory, 'source', 'databreeze-wordmark-blue.png');
+
+async function rgba(file) {
+ return sharp(file).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
+}
+
+function visibleBounds(data, width, height) {
+ const bounds = { maxX: -1, maxY: -1, minX: width, minY: height };
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ if (data[(y * width + x) * 4 + 3] === 0) continue;
+ bounds.minX = Math.min(bounds.minX, x);
+ bounds.minY = Math.min(bounds.minY, y);
+ bounds.maxX = Math.max(bounds.maxX, x);
+ bounds.maxY = Math.max(bounds.maxY, y);
+ }
+ }
+ if (bounds.maxX < 0 || bounds.maxY < 0) {
+ throw new Error('Image must contain visible pixels');
+ }
+ return bounds;
+}
+
+function foregroundBounds(data, width, height, background) {
+ const bounds = { maxX: -1, maxY: -1, minX: width, minY: height };
+ for (let y = 0; y < height; y += 1) {
+ for (let x = 0; x < width; x += 1) {
+ const offset = (y * width + x) * 4;
+ if (
+ data[offset] === background[0] &&
+ data[offset + 1] === background[1] &&
+ data[offset + 2] === background[2] &&
+ data[offset + 3] === background[3]
+ ) {
+ continue;
+ }
+ bounds.minX = Math.min(bounds.minX, x);
+ bounds.minY = Math.min(bounds.minY, y);
+ bounds.maxX = Math.max(bounds.maxX, x);
+ bounds.maxY = Math.max(bounds.maxY, y);
+ }
+ }
+ return bounds;
+}
+
+function linearChannel(value) {
+ const normalized = value / 255;
+ return normalized <= 0.04045 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
+}
+
+function contrast(left, right) {
+ const luminance = (color) =>
+ 0.2126 * linearChannel(color[0]) +
+ 0.7152 * linearChannel(color[1]) +
+ 0.0722 * linearChannel(color[2]);
+ const values = [luminance(left), luminance(right)].sort((a, b) => b - a);
+ return (values[0] + 0.05) / (values[1] + 0.05);
+}
+
+test('visible bounds reject an image with no visible pixels', () => {
+ assert.throws(() => visibleBounds(Buffer.alloc(4 * 4 * 4), 4, 4), /visible pixels/i);
+});
+
+test('navigation wordmarks have documented transparent clear space on every edge', async () => {
+ const [plan, manifest] = await Promise.all([
+ readFile(planPath, 'utf8').then(JSON.parse),
+ readFile(manifestPath, 'utf8').then(JSON.parse),
+ ]);
+ for (const name of ['black', 'blue']) {
+ const file = `web/navigation-wordmark-${name}-204x50.png`;
+ const assetPlan = plan.assets.find((asset) => asset.file === file);
+ const assetManifest = manifest.assets.find((asset) => asset.file === file);
+ const { data, info } = await rgba(join(outputDirectory, file));
+ const bounds = visibleBounds(data, info.width, info.height);
+ assert.deepEqual(assetPlan.contentBox, { x: 10, y: 5, width: 184, height: 40 });
+ assert.equal(assetPlan.safeZone, 'minimum-5px-vertical-and-20px-fitted-horizontal');
+ assert.ok(bounds.minX >= 20 && bounds.minY >= 5, `${file} leading clear space`);
+ assert.ok(bounds.maxX <= 183 && bounds.maxY <= 44, `${file} trailing clear space`);
+ assert.deepEqual(assetManifest.visibleBounds, bounds);
+ }
+});
+
+test('Android notification sources are white alpha masks with exact approved-mark geometry', async () => {
+ const plan = JSON.parse(await readFile(planPath, 'utf8'));
+ const source = plan.sources.blueMark;
+ for (const asset of plan.assets.filter((candidate) =>
+ candidate.file.startsWith('android/notification-'),
+ )) {
+ assert.equal(asset.outputMode, 'android-alpha-mask');
+ let reference = sharp(sourcePath)
+ .extract(source.crop)
+ .resize({
+ width: asset.contentBox.width,
+ height: asset.contentBox.height,
+ fit: 'contain',
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ kernel: sharp.kernel.lanczos3,
+ });
+ const resized = await reference.png().toBuffer();
+ const referenceCanvas = await sharp({
+ create: {
+ width: asset.width,
+ height: asset.height,
+ channels: 4,
+ background: { r: 0, g: 0, b: 0, alpha: 0 },
+ },
+ })
+ .composite([{ input: resized, left: asset.contentBox.x, top: asset.contentBox.y }])
+ .ensureAlpha()
+ .raw()
+ .toBuffer();
+ const { data: mask, info } = await rgba(join(outputDirectory, asset.file));
+ assert.equal(mask.length, referenceCanvas.length);
+ for (let offset = 0; offset < mask.length; offset += 4) {
+ assert.equal(mask[offset + 3], referenceCanvas[offset + 3], `${asset.file} alpha geometry`);
+ if (mask[offset + 3] > 0) {
+ assert.deepEqual([...mask.subarray(offset, offset + 3)], [255, 255, 255]);
+ }
+ }
+ assert.equal(info.width, asset.width);
+ assert.equal(info.height, asset.height);
+ }
+});
+
+test('social metadata uses the approved opaque dark background and unchanged blue wordmark', async () => {
+ const [plan, manifest, { data, info }] = await Promise.all([
+ readFile(planPath, 'utf8').then(JSON.parse),
+ readFile(manifestPath, 'utf8').then(JSON.parse),
+ rgba(join(outputDirectory, 'web', 'social-card-1200x630.png')),
+ ]);
+ const assetPlan = plan.assets.find((asset) => asset.file === 'web/social-card-1200x630.png');
+ const assetManifest = manifest.assets.find(
+ (asset) => asset.file === 'web/social-card-1200x630.png',
+ );
+ assert.deepEqual(assetPlan.backgroundColor, { red: 4, green: 9, blue: 32, alpha: 1 });
+ assert.deepEqual(assetManifest.backgroundColor, assetPlan.backgroundColor);
+ let blueFound = false;
+ for (let offset = 0; offset < data.length; offset += 4) {
+ assert.equal(data[offset + 3], 255, 'social image must be fully opaque');
+ if (data[offset] === 52 && data[offset + 1] === 78 && data[offset + 2] === 248) {
+ blueFound = true;
+ }
+ }
+ assert.deepEqual([...data.subarray(0, 4)], [4, 9, 32, 255]);
+ assert.ok(blueFound, 'social image must retain exact approved blue pixels');
+ assert.ok(contrast([52, 78, 248], [4, 9, 32]) >= 3);
+ const bounds = foregroundBounds(data, info.width, info.height, [4, 9, 32, 255]);
+ assert.deepEqual(assetManifest.visibleBounds, bounds);
+ assert.ok(bounds.minX >= 120 && bounds.maxX < info.width - 120);
+ assert.ok(bounds.minY >= 126 && bounds.maxY < info.height - 126);
+});
+
+test('visual approval provenance is explicit and independently anchored to approved source hashes', async () => {
+ const [plan, manifest, golden] = await Promise.all([
+ readFile(planPath, 'utf8').then(JSON.parse),
+ readFile(manifestPath, 'utf8').then(JSON.parse),
+ readFile(join(packageDirectory, 'test', 'fixtures', 'brand-visual-golden.json'), 'utf8').then(
+ JSON.parse,
+ ),
+ ]);
+ const expected = {
+ status: 'plan-approved',
+ reviewedOn: '2026-08-01',
+ reviewSource: 'approved Task 11 plan and DataBreeze brand specification',
+ specReference: 'docs/product/brand-and-experience.md#1-brand-continuity',
+ taskReference:
+ 'docs/plans/010-engineering-foundation.md#task-11-reproducible-brand-derivatives',
+ cropRationale:
+ 'The blue mark is the left 1155x1155 square of the approved blue wordmark; cropping removes only the adjacent DataBreeze letters and does not redraw geometry.',
+ sourceHashes: {
+ 'databreeze-mark-dark.png':
+ '5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4',
+ 'databreeze-wordmark-black.png':
+ '4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98',
+ 'databreeze-wordmark-blue.png':
+ 'B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D',
+ },
+ };
+ assert.deepEqual(plan.approval, expected);
+ assert.deepEqual(manifest.approval, expected);
+ assert.deepEqual(golden.approval, {
+ status: expected.status,
+ reviewedOn: expected.reviewedOn,
+ reviewSource: expected.reviewSource,
+ specReference: expected.specReference,
+ taskReference: expected.taskReference,
+ });
+});
diff --git a/packages/design-tokens/test/brand-sources.test.mjs b/packages/design-tokens/test/brand-sources.test.mjs
new file mode 100644
index 00000000..9675fb8c
--- /dev/null
+++ b/packages/design-tokens/test/brand-sources.test.mjs
@@ -0,0 +1,127 @@
+import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
+import { createHash } from 'node:crypto';
+import { mkdir, mkdtemp, readdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { test } from 'node:test';
+import { URL } from 'node:url';
+
+const brandDirectory = new URL('../brand/', import.meta.url);
+const sourceDirectory = new URL('source/', brandDirectory);
+const manifestUrl = new URL('manifest.json', brandDirectory);
+
+const approvedAssets = [
+ {
+ file: 'databreeze-mark-dark.png',
+ height: 1973,
+ intendedUse: 'Standalone application and product mark on dark backgrounds',
+ mediaType: 'image/png',
+ sha256: '5EE10842AD090F2BB980B51DDCF8BB4F8738C87B9659BE10387FE0B2D845B7A4',
+ width: 1974,
+ },
+ {
+ file: 'databreeze-wordmark-black.png',
+ height: 1155,
+ intendedUse: 'Monochrome DataBreeze wordmark on light backgrounds',
+ mediaType: 'image/png',
+ sha256: '4F37835E9648E7035DE9BCB6ADA05C1203A1C05A1D0DB81DF1D1AEA01D46FC98',
+ width: 4710,
+ },
+ {
+ file: 'databreeze-wordmark-blue.png',
+ height: 1155,
+ intendedUse: 'Primary DataBreeze wordmark on light backgrounds',
+ mediaType: 'image/png',
+ sha256: 'B2BB9353A2E2C42DAC8F68EC5BC30A9EB366F3C8139A46D4FDEC686264590D3D',
+ width: 4710,
+ },
+];
+
+const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
+
+async function readManifest() {
+ return JSON.parse(await readFile(manifestUrl, 'utf8'));
+}
+
+async function sourceFileNames(directory) {
+ const entries = await readdir(directory, { withFileTypes: true });
+ entries.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
+
+ for (const entry of entries) {
+ if (!entry.isFile()) {
+ throw new Error(`Unexpected brand source entry "${entry.name}": expected a regular file`);
+ }
+ }
+
+ return entries.map((entry) => entry.name);
+}
+
+async function createTemporarySourceDirectory() {
+ const root = await mkdtemp(join(tmpdir(), 'databreeze-brand-source-'));
+ const directory = join(root, 'source');
+ await mkdir(directory);
+ await Promise.all(
+ approvedAssets.map(({ file }) => writeFile(join(directory, file), Buffer.alloc(0))),
+ );
+ return { directory, root };
+}
+
+test('the manifest records the complete approved immutable source set', async () => {
+ const manifest = await readManifest();
+
+ assert.deepEqual(manifest, {
+ schemaVersion: 1,
+ assets: approvedAssets,
+ });
+});
+
+test('the source directory contains only the three canonical named PNGs', async () => {
+ const sourceFiles = await sourceFileNames(sourceDirectory);
+
+ assert.deepEqual(sourceFiles, approvedAssets.map(({ file }) => file).sort());
+});
+
+test('source-set validation rejects an unexpected nested directory', async () => {
+ const temporary = await createTemporarySourceDirectory();
+
+ try {
+ await mkdir(join(temporary.directory, 'legacy'));
+
+ await assert.rejects(
+ sourceFileNames(temporary.directory),
+ /Unexpected brand source entry "legacy": expected a regular file/,
+ );
+ } finally {
+ await rm(temporary.root, { force: true, recursive: true });
+ }
+});
+
+test('source-set validation rejects an unexpected symbolic link', async () => {
+ const temporary = await createTemporarySourceDirectory();
+
+ try {
+ const legacyDirectory = join(temporary.root, 'legacy');
+ await mkdir(legacyDirectory);
+ await symlink(legacyDirectory, join(temporary.directory, 'asset-4.png'), 'junction');
+
+ await assert.rejects(
+ sourceFileNames(temporary.directory),
+ /Unexpected brand source entry "asset-4\.png": expected a regular file/,
+ );
+ } finally {
+ await rm(temporary.root, { force: true, recursive: true });
+ }
+});
+
+for (const asset of approvedAssets) {
+ test(`${asset.file} retains its approved bytes and PNG dimensions`, async () => {
+ const bytes = await readFile(new URL(asset.file, sourceDirectory));
+
+ assert.deepEqual(bytes.subarray(0, pngSignature.length), pngSignature);
+ assert.equal(bytes.subarray(12, 16).toString('ascii'), 'IHDR');
+ assert.equal(bytes.readUInt32BE(16), asset.width);
+ assert.equal(bytes.readUInt32BE(20), asset.height);
+ assert.equal(createHash('sha256').update(bytes).digest('hex').toUpperCase(), asset.sha256);
+ });
+}
diff --git a/packages/design-tokens/test/fixtures/brand-visual-golden.json b/packages/design-tokens/test/fixtures/brand-visual-golden.json
new file mode 100644
index 00000000..3f4eb728
--- /dev/null
+++ b/packages/design-tokens/test/fixtures/brand-visual-golden.json
@@ -0,0 +1,36 @@
+{
+ "schemaVersion": 1,
+ "approval": {
+ "status": "plan-approved",
+ "reviewedOn": "2026-08-01",
+ "reviewSource": "approved Task 11 plan and DataBreeze brand specification",
+ "specReference": "docs/product/brand-and-experience.md#1-brand-continuity",
+ "taskReference": "docs/plans/010-engineering-foundation.md#task-11-reproducible-brand-derivatives"
+ },
+ "assets": {
+ "android/adaptive-foreground-432.png": "C16B2F8154035627EE4BC1970B6BFC21C557AE8D44F1A33F85DE3C3377A7E645",
+ "android/launcher-hdpi-72.png": "C850208368B49DD40EB5206145DC8044A0AD97AD5A15222B5AAA14458ACBD626",
+ "android/launcher-mdpi-48.png": "2249A9C233228CDF3E7CD648AD7E011F1369837C00BA34F4407B71FA2C134216",
+ "android/launcher-xhdpi-96.png": "D8C304EDE847F7F8B370F3477EBE155E951644F7406FE77FA44596CBB4497EBB",
+ "android/launcher-xxhdpi-144.png": "56AAE8DD4F4DDA38B333325F56D2CF0064E7866DF22C15C7564D294B8E08377E",
+ "android/launcher-xxxhdpi-192.png": "B83E179BC03B00A0C6B9ED6CD14B89BDBB792730799FD1890DC1B6769DE1367B",
+ "android/notification-hdpi-36.png": "73F2DA8D89A22CEEDF5CFC85303FE80114A5F7158AB5BC585FDCF0BFC5D885C7",
+ "android/notification-mdpi-24.png": "3B6BFDF2EB3696325AD644F268CD7DE9F8014B19C0DA2698BD19BE6E5984F717",
+ "android/notification-xhdpi-48.png": "BEEDF672DC8861EB6BAE1C47A5B669205C5B46B27B6870C7F78EAD6715519E56",
+ "android/notification-xxhdpi-72.png": "DFE4ED9EB98092732874F42629A5359259D9D8B4EC9307131EC625C991891290",
+ "android/notification-xxxhdpi-96.png": "B555B71BBBC56556AA0B650D1BE63296A2EF7CCFB3528D88FEE23F6EE6D708BF",
+ "desktop/application-256.png": "E38BD7B30191F57F9399D39972649B252AD8C30708FCB476A77C77A0A4E4E373",
+ "desktop/application.ico": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "desktop/installer.ico": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "desktop/notification-32.png": "38903201463667946F1232BF5235841B937208520E39568C564F0BC2E3F64ADD",
+ "desktop/updater.ico": "DFE2F8828A981E686B2EAF8BB7E2C960AB7575B88081D59B79469374021B0058",
+ "web/apple-touch-icon-180.png": "4049EC9E808406AB767E07EC5F11A37021453D1ED601E04ED2C910612B8248CB",
+ "web/favicon-16.png": "A7DC5A14885ABC87316C39E803D9C8BEE2FB560189C1096F2CC3E323CE175327",
+ "web/favicon-32.png": "C01AA26FD1B9DDC0CAE0175EB7AC7A16E0CF802C354AB1DE7A76D922EB17702C",
+ "web/install-icon-192.png": "B83E179BC03B00A0C6B9ED6CD14B89BDBB792730799FD1890DC1B6769DE1367B",
+ "web/install-icon-512.png": "F3FC6E0C0F32F40C26961A5570321D025540313693793C44B58B7B8718621792",
+ "web/navigation-wordmark-black-204x50.png": "468507ED94F13BABE083361984247E177C92C4514D03536862A2F0D102FD5688",
+ "web/navigation-wordmark-blue-204x50.png": "CD44FB5AD502B125863C26331FF3BB0C6E88804D6B6FC0261DE05E66400AFC62",
+ "web/social-card-1200x630.png": "C890808C04E3A41315B7471C2BA25B7F0438D26E37367308525D29F0AD1AF531"
+ }
+}
diff --git a/packages/design-tokens/turbo.json b/packages/design-tokens/turbo.json
new file mode 100644
index 00000000..009e3262
--- /dev/null
+++ b/packages/design-tokens/turbo.json
@@ -0,0 +1,12 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "build": {
+ "outputs": []
+ },
+ "test": {
+ "outputs": []
+ }
+ }
+}
diff --git a/packages/domain/README.md b/packages/domain/README.md
index 5246171f..aac7e69c 100644
--- a/packages/domain/README.md
+++ b/packages/domain/README.md
@@ -1,3 +1,84 @@
# Shared Domain Values
-Pure TypeScript value logic with no network, filesystem, database, UI-framework, or service-implementation dependencies.
+Pure TypeScript value logic with no network, filesystem, database, UI-framework, or
+service-implementation dependencies.
+
+## Public interfaces
+
+All imports are explicitly versioned. There is intentionally no unversioned package root.
+
+- `@databreeze/domain/permissions/v1` publishes the closed version-1 permission vocabulary,
+ the six initial immutable role bundles, explicit resource/channel applicability, and
+ deny-by-default lookup helpers.
+- `@databreeze/domain/tenant-scope/v1` publishes branded UUIDv4/UUIDv7 and UTC values,
+ complete organization/workspace/project scopes, and equality, containment, and narrowing
+ helpers.
+- `@databreeze/domain/authorization/v1` publishes a provider-bound evaluator for exact tenant
+ filters, authoritative resource/membership/policy resolution, and authorization decisions.
+- `@databreeze/domain/v1` aggregates the three version-1 interfaces.
+
+The package uses the public `@databreeze/contracts/v1` validator. It does not deep-import
+generated files or duplicate the canonical protocol schemas.
+
+## Initial roles
+
+The initial identifiers are `owner`, `admin`, `analyst`, `operator`, `approver`, and `viewer`.
+Their names remain Owner, Admin, Analyst, Operator, Approver, and Viewer. A role is only a
+permission bundle: it never establishes tenant membership, resource ownership, or a final
+authorization decision.
+
+Owner materializes every Admin permission plus ownership-transfer and billing permissions.
+Neither Owner nor Admin receives `approval.decision.create`. Approval, retention, legal-hold,
+data-mode, device, entitlement, separation-of-duties, and recent-MFA conditions remain
+independent policy gates. Request consumers cannot submit those results; the authority provider
+evaluates applicable policy from trusted application state.
+
+## Trusted authorization flow
+
+1. At server composition, inject an `AuthorizationAuthorityProviderV1` whose methods are backed
+ by the authenticated principal, scoped repositories, membership store, and policy engine.
+2. Give request handling only the frozen evaluator. Its sole method is `authorizeV1`; it has no
+ public filter, resource, membership, role, or policy minting API.
+3. Submit only the permission, channel, complete tenant filter, and resource selector. Extra
+ request fields are rejected before any authority lookup.
+4. The evaluator validates the permission's explicit resource/channel applicability, resolves
+ the authenticated principal, and sends the exact frozen tenant filter to the provider's
+ scoped resource lookup.
+5. It validates the returned resource and intrinsic organization/workspace/project identity,
+ then resolves membership and policy internally. Unknown or inactive roles, unavailable or
+ malformed authority results, unmet policy, scope mismatch, and identity mismatch all deny.
+
+Provider methods are captured when the evaluator is created, so later mutation cannot replace
+its authority. Provider results are always runtime-validated even when an adapter is typed.
+Each provider call has an independently cleaned-up deadline: 1 second by default, configurable
+per evaluator from 1 millisecond through 60 seconds with `providerCallTimeoutMs`. A provider
+timeout or exception fails closed as `AUTHORITY_UNAVAILABLE`; late provider settlement cannot
+resume the authorization flow.
+Clients may use published permission bundles and applicability as display hints, but
+authoritative enforcement belongs to a server or trusted worker with its own provider-backed
+evaluator.
+
+## Requirement traceability
+
+This package and its tests provide partial foundation coverage only:
+
+- `IAM-001`: branded UUIDv4/UUIDv7 identifiers and strict UTC timestamp parsing.
+- `IAM-002`: pure action, explicit channel/resource applicability, and scoped decision
+ primitives.
+- `IAM-003`: default denial and runtime/type-level rejection of caller-supplied authority facts.
+- `IAM-004`: versioned permissions and exactly six immutable initial role bundles.
+- `IAM-009`: exact provider-owned scoped lookup and resource-identity gates.
+- `IAM-019`: complete scope parsing, exact filters, ancestry containment, and non-broadening
+ narrowing.
+
+These requirements are not complete. This package does not implement authentication, IAM
+persistence, memberships, repository queries, API guards, custom roles, offline snapshots,
+policy engines, audit writes, or feature workflows.
+
+## Local commands
+
+```text
+corepack pnpm --filter @databreeze/domain typecheck
+corepack pnpm --filter @databreeze/domain test
+corepack pnpm --filter @databreeze/domain build
+```
diff --git a/packages/domain/package.json b/packages/domain/package.json
new file mode 100644
index 00000000..5db9d05e
--- /dev/null
+++ b/packages/domain/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "@databreeze/domain",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./v1": {
+ "types": "./src/v1.ts",
+ "import": "./dist/v1.js"
+ },
+ "./permissions/v1": {
+ "types": "./src/permissions/v1.ts",
+ "import": "./dist/permissions/v1.js"
+ },
+ "./tenant-scope/v1": {
+ "types": "./src/tenant-scope/v1.ts",
+ "import": "./dist/tenant-scope/v1.js"
+ },
+ "./authorization/v1": {
+ "types": "./src/authorization/v1.ts",
+ "import": "./dist/authorization/v1.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json && node test/built-public-api-smoke.mjs",
+ "test": "node --test test/**/*.test.mjs",
+ "typecheck": "tsc --noEmit --project tsconfig.json"
+ },
+ "dependencies": {
+ "@databreeze/contracts": "workspace:*"
+ }
+}
diff --git a/packages/domain/src/authorization/v1.ts b/packages/domain/src/authorization/v1.ts
new file mode 100644
index 00000000..72fb0ac9
--- /dev/null
+++ b/packages/domain/src/authorization/v1.ts
@@ -0,0 +1,504 @@
+import {
+ AUTHORIZATION_CHANNELS_V1,
+ PERMISSION_APPLICABILITY_V1,
+ RESOURCE_TYPES_V1,
+ isPermissionV1,
+ isRoleIdV1,
+ roleHasPermissionV1,
+ type AuthorizationChannelV1,
+ type InitialRoleIdV1,
+ type PermissionV1,
+ type ResourceTypeV1,
+} from '../permissions/v1.ts';
+import {
+ parseStableIdentifierV1,
+ parseTenantScopeV1,
+ tenantScopeContainsV1,
+ tenantScopesEqualV1,
+ type StableIdentifierV1,
+ type TenantScopeV1,
+} from '../tenant-scope/v1.ts';
+
+export { AUTHORIZATION_CHANNELS_V1 } from '../permissions/v1.ts';
+export type { AuthorizationChannelV1 } from '../permissions/v1.ts';
+
+/** Partial foundation coverage: IAM-002, IAM-003, IAM-004, IAM-009, and IAM-019. */
+
+export const AUTHORIZATION_SCHEMA_VERSION_V1 = 1 as const;
+
+export type AuthorizationDenialCodeV1 =
+ | 'AUTHORITY_INVALID'
+ | 'AUTHORITY_UNAVAILABLE'
+ | 'CHANNEL_NOT_ALLOWED'
+ | 'INACTIVE_MEMBERSHIP'
+ | 'INVALID_AUTHORIZATION_REQUEST'
+ | 'INVALID_RESOURCE_SELECTOR'
+ | 'POLICY_CONDITIONS_REQUIRED'
+ | 'RESOURCE_IDENTITY_MISMATCH'
+ | 'RESOURCE_OWNERSHIP_MISMATCH'
+ | 'RESOURCE_TYPE_MISMATCH'
+ | 'ROLE_PERMISSION_MISSING'
+ | 'TENANT_FILTER_INVALID'
+ | 'TENANT_FILTER_REQUIRED'
+ | 'TENANT_SCOPE_MISMATCH'
+ | 'UNKNOWN_CHANNEL'
+ | 'UNKNOWN_PERMISSION'
+ | 'UNKNOWN_ROLE';
+
+export type AuthorizationDecisionV1 =
+ | {
+ readonly allowed: true;
+ readonly permission: PermissionV1;
+ readonly tenantScope: TenantScopeV1;
+ }
+ | { readonly allowed: false; readonly code: AuthorizationDenialCodeV1 };
+
+export interface AuthorizationResourceSelectorV1 {
+ readonly resourceType: ResourceTypeV1;
+ readonly resourceId: StableIdentifierV1;
+}
+
+/** The only request-controlled inputs accepted by the evaluator. */
+export interface AuthorizationRequestV1 {
+ readonly permission: PermissionV1;
+ readonly channel: AuthorizationChannelV1;
+ readonly tenantFilter: TenantScopeV1;
+ readonly resource: AuthorizationResourceSelectorV1;
+}
+
+export interface AuthoritativeResourceV1 extends AuthorizationResourceSelectorV1 {
+ readonly tenantScope: TenantScopeV1;
+}
+
+export interface ScopedResourceLookupQueryV1 extends AuthorizationResourceSelectorV1 {
+ readonly tenantScope: TenantScopeV1;
+}
+
+export interface MembershipResolutionQueryV1 {
+ readonly principalId: StableIdentifierV1;
+ readonly resource: AuthoritativeResourceV1;
+}
+
+export interface EvaluatedMembershipV1 {
+ readonly roleId: InitialRoleIdV1;
+ readonly membershipScope: TenantScopeV1;
+ readonly membershipActive: true;
+}
+
+export interface PolicyEvaluationQueryV1 {
+ readonly principalId: StableIdentifierV1;
+ readonly permission: PermissionV1;
+ readonly channel: AuthorizationChannelV1;
+ readonly membership: EvaluatedMembershipV1;
+ readonly resource: AuthoritativeResourceV1;
+}
+
+export type AwaitableV1 = TValue | PromiseLike;
+
+/**
+ * Server-composed authority boundary. Implementations resolve every fact from authenticated,
+ * tenant-scoped application state; request bodies must never implement this port.
+ *
+ * Results remain `unknown` so runtime validation is mandatory even for typed adapters.
+ */
+export interface AuthorizationAuthorityProviderV1 {
+ readonly resolveAuthenticatedPrincipalV1: () => AwaitableV1;
+ readonly lookupResourceV1: (query: ScopedResourceLookupQueryV1) => AwaitableV1;
+ readonly resolveMembershipV1: (query: MembershipResolutionQueryV1) => AwaitableV1;
+ readonly evaluatePolicyV1: (query: PolicyEvaluationQueryV1) => AwaitableV1;
+}
+
+export interface ScopedAuthorizationEvaluatorV1 {
+ readonly authorizeV1: (request: unknown) => Promise;
+}
+
+export interface ScopedAuthorizationEvaluatorOptionsV1 {
+ /** Maximum time allowed for each call to the provider bound to this evaluator. */
+ readonly providerCallTimeoutMs: number;
+}
+
+interface ParsedMembershipV1 {
+ readonly roleId: string;
+ readonly membershipScope: TenantScopeV1;
+ readonly membershipActive: boolean;
+}
+
+type ParsedValueV1 =
+ | { readonly accepted: true; readonly value: TValue }
+ | { readonly accepted: false };
+
+const authorizationChannelSet = new Set(AUTHORIZATION_CHANNELS_V1);
+const resourceTypeSet = new Set(RESOURCE_TYPES_V1);
+const DEFAULT_PROVIDER_CALL_TIMEOUT_MS_V1 = 1_000;
+const MAX_PROVIDER_CALL_TIMEOUT_MS_V1 = 60_000;
+
+interface AuthorizationTimerRuntimeV1 {
+ readonly setTimeout: (callback: () => void, delayMs: number) => unknown;
+ readonly clearTimeout: (handle: unknown) => void;
+}
+
+const resourceScopeTypes: Readonly> =
+ Object.freeze({
+ 'approval-request': Object.freeze(['workspace', 'project'] as const),
+ artifact: Object.freeze(['workspace', 'project'] as const),
+ 'billing-account': Object.freeze(['organization'] as const),
+ device: Object.freeze(['organization'] as const),
+ job: Object.freeze(['workspace', 'project'] as const),
+ organization: Object.freeze(['organization'] as const),
+ project: Object.freeze(['project'] as const),
+ workspace: Object.freeze(['workspace'] as const),
+ });
+
+function isRecord(input: unknown): input is Record {
+ return typeof input === 'object' && input !== null && !Array.isArray(input);
+}
+
+function hasExactKeys(input: Record, expectedKeys: readonly string[]): boolean {
+ const actualKeys = Object.keys(input).sort();
+ const sortedExpected = [...expectedKeys].sort();
+ return (
+ actualKeys.length === sortedExpected.length &&
+ actualKeys.every((key, index) => key === sortedExpected[index])
+ );
+}
+
+function providerCallTimeoutMsV1(input: unknown): number {
+ if (input === undefined) {
+ return DEFAULT_PROVIDER_CALL_TIMEOUT_MS_V1;
+ }
+ if (!isRecord(input) || !hasExactKeys(input, ['providerCallTimeoutMs'])) {
+ throw new TypeError('Invalid authorization evaluator options');
+ }
+
+ const timeoutMs = input['providerCallTimeoutMs'];
+ if (
+ typeof timeoutMs !== 'number' ||
+ !Number.isInteger(timeoutMs) ||
+ timeoutMs < 1 ||
+ timeoutMs > MAX_PROVIDER_CALL_TIMEOUT_MS_V1
+ ) {
+ throw new TypeError('Invalid authorization provider call timeout');
+ }
+ return timeoutMs;
+}
+
+function timerRuntimeV1(): AuthorizationTimerRuntimeV1 {
+ const runtime = globalThis as unknown as Partial;
+ if (typeof runtime.setTimeout !== 'function' || typeof runtime.clearTimeout !== 'function') {
+ throw new TypeError('Authorization evaluator requires timer support');
+ }
+ return Object.freeze({
+ setTimeout: runtime.setTimeout.bind(globalThis),
+ clearTimeout: runtime.clearTimeout.bind(globalThis),
+ });
+}
+
+async function withProviderCallTimeoutV1(
+ operation: () => AwaitableV1,
+ timeoutMs: number,
+ timers: AuthorizationTimerRuntimeV1,
+): Promise {
+ let scheduled = false;
+ let timeoutHandle: unknown;
+ const timeout = new Promise((_resolve, reject) => {
+ timeoutHandle = timers.setTimeout(() => reject(new Error('AUTHORITY_TIMEOUT')), timeoutMs);
+ scheduled = true;
+ });
+
+ try {
+ return await Promise.race([Promise.resolve().then(operation), timeout]);
+ } finally {
+ if (scheduled) {
+ timers.clearTimeout(timeoutHandle);
+ }
+ }
+}
+
+function isAuthorizationChannelV1(input: unknown): input is AuthorizationChannelV1 {
+ return typeof input === 'string' && authorizationChannelSet.has(input);
+}
+
+function isResourceTypeV1(input: unknown): input is ResourceTypeV1 {
+ return typeof input === 'string' && resourceTypeSet.has(input);
+}
+
+function accepted(value: TValue): ParsedValueV1 {
+ return Object.freeze({ accepted: true, value });
+}
+
+function rejected(): ParsedValueV1 {
+ return Object.freeze({ accepted: false });
+}
+
+function deny(code: AuthorizationDenialCodeV1): AuthorizationDecisionV1 {
+ return Object.freeze({ allowed: false, code });
+}
+
+function parseResourceSelectorV1(input: unknown): ParsedValueV1 {
+ if (!isRecord(input) || !hasExactKeys(input, ['resourceId', 'resourceType'])) {
+ return rejected();
+ }
+
+ const resourceType = input['resourceType'];
+ const resourceId = parseStableIdentifierV1(input['resourceId']);
+ if (!isResourceTypeV1(resourceType) || !resourceId.accepted) {
+ return rejected();
+ }
+
+ return accepted(Object.freeze({ resourceType, resourceId: resourceId.value }));
+}
+
+function parsePrincipalV1(input: unknown): ParsedValueV1 {
+ if (!isRecord(input) || !hasExactKeys(input, ['principalId'])) {
+ return rejected();
+ }
+
+ const principalId = parseStableIdentifierV1(input['principalId']);
+ return principalId.accepted ? accepted(principalId.value) : rejected();
+}
+
+function parseAuthoritativeResourceV1(input: unknown): ParsedValueV1 {
+ if (!isRecord(input) || !hasExactKeys(input, ['resourceId', 'resourceType', 'tenantScope'])) {
+ return rejected();
+ }
+
+ const resourceType = input['resourceType'];
+ const resourceId = parseStableIdentifierV1(input['resourceId']);
+ const tenantScope = parseTenantScopeV1(input['tenantScope']);
+ if (!isResourceTypeV1(resourceType) || !resourceId.accepted || !tenantScope.accepted) {
+ return rejected();
+ }
+ if (!resourceScopeTypes[resourceType].includes(tenantScope.value.scopeType)) {
+ return rejected();
+ }
+
+ return accepted(
+ Object.freeze({
+ resourceType,
+ resourceId: resourceId.value,
+ tenantScope: tenantScope.value,
+ }),
+ );
+}
+
+function parseMembershipV1(input: unknown): ParsedValueV1 {
+ if (!isRecord(input) || !hasExactKeys(input, ['membershipActive', 'membershipScope', 'roleId'])) {
+ return rejected();
+ }
+
+ const roleId = input['roleId'];
+ const membershipActive = input['membershipActive'];
+ const membershipScope = parseTenantScopeV1(input['membershipScope']);
+ if (
+ typeof roleId !== 'string' ||
+ roleId.length === 0 ||
+ typeof membershipActive !== 'boolean' ||
+ !membershipScope.accepted
+ ) {
+ return rejected();
+ }
+
+ return accepted(
+ Object.freeze({ roleId, membershipScope: membershipScope.value, membershipActive }),
+ );
+}
+
+function parsePolicyResultV1(input: unknown): ParsedValueV1 {
+ if (!isRecord(input) || !hasExactKeys(input, ['satisfied'])) {
+ return rejected();
+ }
+
+ return typeof input['satisfied'] === 'boolean' ? accepted(input['satisfied']) : rejected();
+}
+
+function resourceIdentityIsCoherentV1(resource: AuthoritativeResourceV1): boolean {
+ if (resource.resourceType === 'organization') {
+ return (
+ resource.tenantScope.scopeType === 'organization' &&
+ resource.resourceId === resource.tenantScope.organizationId
+ );
+ }
+ if (resource.resourceType === 'workspace') {
+ return (
+ resource.tenantScope.scopeType === 'workspace' &&
+ resource.resourceId === resource.tenantScope.workspaceId
+ );
+ }
+ if (resource.resourceType === 'project') {
+ return (
+ resource.tenantScope.scopeType === 'project' &&
+ resource.resourceId === resource.tenantScope.projectId
+ );
+ }
+
+ return true;
+}
+
+function bindAuthorityMethodV1(
+ provider: AuthorizationAuthorityProviderV1,
+ key: TKey,
+): AuthorizationAuthorityProviderV1[TKey] {
+ const method = provider[key];
+ if (typeof method !== 'function') {
+ throw new TypeError(`Authorization authority provider is missing ${key}`);
+ }
+
+ return method.bind(provider) as AuthorizationAuthorityProviderV1[TKey];
+}
+
+export function createScopedAuthorizationEvaluatorV1(
+ provider: AuthorizationAuthorityProviderV1,
+ options?: ScopedAuthorizationEvaluatorOptionsV1,
+): ScopedAuthorizationEvaluatorV1 {
+ const providerCallTimeoutMs = providerCallTimeoutMsV1(options);
+ const timers = timerRuntimeV1();
+ const resolveAuthenticatedPrincipalV1 = bindAuthorityMethodV1(
+ provider,
+ 'resolveAuthenticatedPrincipalV1',
+ );
+ const lookupResourceV1 = bindAuthorityMethodV1(provider, 'lookupResourceV1');
+ const resolveMembershipV1 = bindAuthorityMethodV1(provider, 'resolveMembershipV1');
+ const evaluatePolicyV1 = bindAuthorityMethodV1(provider, 'evaluatePolicyV1');
+
+ async function authorizeV1(request: unknown): Promise {
+ if (!isRecord(request)) {
+ return deny('INVALID_AUTHORIZATION_REQUEST');
+ }
+ if (!Object.hasOwn(request, 'tenantFilter') || request['tenantFilter'] == null) {
+ return deny('TENANT_FILTER_REQUIRED');
+ }
+ if (!hasExactKeys(request, ['channel', 'permission', 'resource', 'tenantFilter'])) {
+ return deny('INVALID_AUTHORIZATION_REQUEST');
+ }
+
+ const permission = request['permission'];
+ if (!isPermissionV1(permission)) {
+ return deny('UNKNOWN_PERMISSION');
+ }
+
+ const channel = request['channel'];
+ if (!isAuthorizationChannelV1(channel)) {
+ return deny('UNKNOWN_CHANNEL');
+ }
+
+ const applicability = PERMISSION_APPLICABILITY_V1[permission];
+ if (!applicability.allowedChannels.includes(channel)) {
+ return deny('CHANNEL_NOT_ALLOWED');
+ }
+
+ const tenantFilter = parseTenantScopeV1(request['tenantFilter']);
+ if (!tenantFilter.accepted) {
+ return deny('TENANT_FILTER_INVALID');
+ }
+
+ const resourceSelector = parseResourceSelectorV1(request['resource']);
+ if (!resourceSelector.accepted) {
+ return deny('INVALID_RESOURCE_SELECTOR');
+ }
+ if (resourceSelector.value.resourceType !== applicability.resourceType) {
+ return deny('RESOURCE_TYPE_MISMATCH');
+ }
+
+ try {
+ const principal = parsePrincipalV1(
+ await withProviderCallTimeoutV1(
+ () => resolveAuthenticatedPrincipalV1(),
+ providerCallTimeoutMs,
+ timers,
+ ),
+ );
+ if (!principal.accepted) {
+ return deny('AUTHORITY_INVALID');
+ }
+
+ const lookupQuery: ScopedResourceLookupQueryV1 = Object.freeze({
+ resourceType: resourceSelector.value.resourceType,
+ resourceId: resourceSelector.value.resourceId,
+ tenantScope: tenantFilter.value,
+ });
+ const resource = parseAuthoritativeResourceV1(
+ await withProviderCallTimeoutV1(
+ () => lookupResourceV1(lookupQuery),
+ providerCallTimeoutMs,
+ timers,
+ ),
+ );
+ if (!resource.accepted) {
+ return deny('AUTHORITY_INVALID');
+ }
+ if (
+ resource.value.resourceType !== resourceSelector.value.resourceType ||
+ resource.value.resourceId !== resourceSelector.value.resourceId ||
+ !tenantScopesEqualV1(resource.value.tenantScope, tenantFilter.value)
+ ) {
+ return deny('RESOURCE_OWNERSHIP_MISMATCH');
+ }
+ if (!resourceIdentityIsCoherentV1(resource.value)) {
+ return deny('RESOURCE_IDENTITY_MISMATCH');
+ }
+
+ const membershipQuery: MembershipResolutionQueryV1 = Object.freeze({
+ principalId: principal.value,
+ resource: resource.value,
+ });
+ const membership = parseMembershipV1(
+ await withProviderCallTimeoutV1(
+ () => resolveMembershipV1(membershipQuery),
+ providerCallTimeoutMs,
+ timers,
+ ),
+ );
+ if (!membership.accepted) {
+ return deny('AUTHORITY_INVALID');
+ }
+ if (!isRoleIdV1(membership.value.roleId)) {
+ return deny('UNKNOWN_ROLE');
+ }
+ if (!membership.value.membershipActive) {
+ return deny('INACTIVE_MEMBERSHIP');
+ }
+ if (!tenantScopeContainsV1(membership.value.membershipScope, resource.value.tenantScope)) {
+ return deny('TENANT_SCOPE_MISMATCH');
+ }
+ if (!roleHasPermissionV1(membership.value.roleId, permission)) {
+ return deny('ROLE_PERMISSION_MISSING');
+ }
+
+ const evaluatedMembership: EvaluatedMembershipV1 = Object.freeze({
+ roleId: membership.value.roleId,
+ membershipScope: membership.value.membershipScope,
+ membershipActive: true,
+ });
+ const policyQuery: PolicyEvaluationQueryV1 = Object.freeze({
+ principalId: principal.value,
+ permission,
+ channel,
+ membership: evaluatedMembership,
+ resource: resource.value,
+ });
+ const policy = parsePolicyResultV1(
+ await withProviderCallTimeoutV1(
+ () => evaluatePolicyV1(policyQuery),
+ providerCallTimeoutMs,
+ timers,
+ ),
+ );
+ if (!policy.accepted) {
+ return deny('AUTHORITY_INVALID');
+ }
+ if (!policy.value) {
+ return deny('POLICY_CONDITIONS_REQUIRED');
+ }
+
+ return Object.freeze({
+ allowed: true,
+ permission,
+ tenantScope: resource.value.tenantScope,
+ });
+ } catch {
+ return deny('AUTHORITY_UNAVAILABLE');
+ }
+ }
+
+ return Object.freeze({ authorizeV1 });
+}
diff --git a/packages/domain/src/permissions/v1.ts b/packages/domain/src/permissions/v1.ts
new file mode 100644
index 00000000..6ee72030
--- /dev/null
+++ b/packages/domain/src/permissions/v1.ts
@@ -0,0 +1,265 @@
+/**
+ * Version 1 of the DataBreeze permission vocabulary.
+ *
+ * Partial foundation coverage: IAM-002, IAM-003, and IAM-004.
+ */
+
+export const PERMISSION_SCHEMA_VERSION_V1 = 1 as const;
+
+export const PERMISSIONS_V1 = Object.freeze({
+ ORGANIZATION_PROFILE_READ: 'organization.profile.read',
+ ORGANIZATION_SETTINGS_MANAGE: 'organization.settings.manage',
+ ORGANIZATION_OWNERSHIP_TRANSFER: 'organization.ownership.transfer',
+ WORKSPACE_SETTINGS_READ: 'workspace.settings.read',
+ WORKSPACE_SETTINGS_MANAGE: 'workspace.settings.manage',
+ PROJECT_RECORD_READ: 'project.record.read',
+ PROJECT_RECORD_MANAGE: 'project.record.manage',
+ ARTIFACT_RECORD_READ: 'artifact.record.read',
+ ARTIFACT_ORIGINAL_DOWNLOAD: 'artifact.original.download',
+ ARTIFACT_DERIVED_CREATE: 'artifact.derived.create',
+ JOB_EXECUTION_READ: 'job.execution.read',
+ JOB_EXECUTION_CREATE: 'job.execution.create',
+ JOB_EXECUTION_RUN: 'job.execution.run',
+ JOB_EXECUTION_CANCEL: 'job.execution.cancel',
+ APPROVAL_REQUEST_READ: 'approval.request.read',
+ APPROVAL_DECISION_CREATE: 'approval.decision.create',
+ BILLING_ACCOUNT_READ: 'billing.account.read',
+ BILLING_ACCOUNT_MANAGE: 'billing.account.manage',
+ DEVICE_IDENTITY_READ: 'device.identity.read',
+ DEVICE_IDENTITY_REVOKE: 'device.identity.revoke',
+} as const);
+
+export type PermissionV1 = (typeof PERMISSIONS_V1)[keyof typeof PERMISSIONS_V1];
+
+export const AUTHORIZATION_CHANNELS_V1 = Object.freeze([
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ 'sync',
+ 'stream',
+ 'shared-link',
+] as const);
+
+export type AuthorizationChannelV1 = (typeof AUTHORIZATION_CHANNELS_V1)[number];
+
+export const RESOURCE_TYPES_V1 = Object.freeze([
+ 'approval-request',
+ 'artifact',
+ 'billing-account',
+ 'device',
+ 'job',
+ 'organization',
+ 'project',
+ 'workspace',
+] as const);
+
+export type ResourceTypeV1 = (typeof RESOURCE_TYPES_V1)[number];
+
+export interface PermissionApplicabilityV1 {
+ readonly resourceType: ResourceTypeV1;
+ readonly allowedChannels: readonly AuthorizationChannelV1[];
+}
+
+function immutableApplicability(
+ resourceType: ResourceTypeV1,
+ allowedChannels: readonly AuthorizationChannelV1[],
+): PermissionApplicabilityV1 {
+ return Object.freeze({ resourceType, allowedChannels: Object.freeze([...allowedChannels]) });
+}
+
+/**
+ * Closed transport applicability for every v1 permission.
+ *
+ * A permission being present in a role bundle never implies that it is valid on every channel.
+ * New permissions and channels require a new versioned entry instead of inheriting access.
+ */
+export const PERMISSION_APPLICABILITY_V1: Readonly<
+ Record
+> = Object.freeze({
+ 'organization.profile.read': immutableApplicability('organization', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ ]),
+ 'organization.settings.manage': immutableApplicability('organization', ['api', 'web']),
+ 'organization.ownership.transfer': immutableApplicability('organization', ['api', 'web']),
+ 'workspace.settings.read': immutableApplicability('workspace', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ ]),
+ 'workspace.settings.manage': immutableApplicability('workspace', ['api', 'web']),
+ 'project.record.read': immutableApplicability('project', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ 'sync',
+ ]),
+ 'project.record.manage': immutableApplicability('project', ['api', 'web']),
+ 'artifact.record.read': immutableApplicability('artifact', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ 'sync',
+ 'shared-link',
+ ]),
+ 'artifact.original.download': immutableApplicability('artifact', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ ]),
+ 'artifact.derived.create': immutableApplicability('artifact', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'worker',
+ ]),
+ 'job.execution.read': immutableApplicability('job', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ 'sync',
+ 'stream',
+ ]),
+ 'job.execution.create': immutableApplicability('job', ['api', 'web', 'desktop', 'worker']),
+ 'job.execution.run': immutableApplicability('job', ['api', 'web', 'desktop', 'worker']),
+ 'job.execution.cancel': immutableApplicability('job', ['api', 'web', 'desktop']),
+ 'approval.request.read': immutableApplicability('approval-request', [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ ]),
+ 'approval.decision.create': immutableApplicability('approval-request', ['api', 'web', 'android']),
+ 'billing.account.read': immutableApplicability('billing-account', ['api', 'web']),
+ 'billing.account.manage': immutableApplicability('billing-account', ['api', 'web']),
+ 'device.identity.read': immutableApplicability('device', ['api', 'web']),
+ 'device.identity.revoke': immutableApplicability('device', ['api', 'web']),
+});
+
+export const INITIAL_ROLE_IDS_V1 = Object.freeze([
+ 'owner',
+ 'admin',
+ 'analyst',
+ 'operator',
+ 'approver',
+ 'viewer',
+] as const);
+
+export type InitialRoleIdV1 = (typeof INITIAL_ROLE_IDS_V1)[number];
+
+export interface InitialRoleBundleV1 {
+ readonly id: InitialRoleIdV1;
+ readonly name: 'Owner' | 'Admin' | 'Analyst' | 'Operator' | 'Approver' | 'Viewer';
+ readonly schemaVersion: typeof PERMISSION_SCHEMA_VERSION_V1;
+ readonly permissions: readonly PermissionV1[];
+}
+
+function immutableBundle(
+ id: InitialRoleIdV1,
+ name: InitialRoleBundleV1['name'],
+ permissions: readonly PermissionV1[],
+): InitialRoleBundleV1 {
+ return Object.freeze({
+ id,
+ name,
+ schemaVersion: PERMISSION_SCHEMA_VERSION_V1,
+ permissions: Object.freeze([...permissions]),
+ });
+}
+
+const adminPermissions = [
+ PERMISSIONS_V1.ORGANIZATION_PROFILE_READ,
+ PERMISSIONS_V1.ORGANIZATION_SETTINGS_MANAGE,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_READ,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_MANAGE,
+ PERMISSIONS_V1.PROJECT_RECORD_READ,
+ PERMISSIONS_V1.PROJECT_RECORD_MANAGE,
+ PERMISSIONS_V1.JOB_EXECUTION_READ,
+ PERMISSIONS_V1.DEVICE_IDENTITY_READ,
+ PERMISSIONS_V1.DEVICE_IDENTITY_REVOKE,
+] as const;
+
+const ownerPermissionSet = new Set([
+ ...adminPermissions,
+ PERMISSIONS_V1.ORGANIZATION_OWNERSHIP_TRANSFER,
+ PERMISSIONS_V1.BILLING_ACCOUNT_READ,
+ PERMISSIONS_V1.BILLING_ACCOUNT_MANAGE,
+]);
+const ownerPermissions = Object.freeze(
+ Object.values(PERMISSIONS_V1).filter((permission) => ownerPermissionSet.has(permission)),
+);
+
+export const INITIAL_ROLE_BUNDLES_V1: Readonly> =
+ Object.freeze({
+ owner: immutableBundle('owner', 'Owner', ownerPermissions),
+ admin: immutableBundle('admin', 'Admin', adminPermissions),
+ analyst: immutableBundle('analyst', 'Analyst', [
+ PERMISSIONS_V1.ORGANIZATION_PROFILE_READ,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_READ,
+ PERMISSIONS_V1.PROJECT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_ORIGINAL_DOWNLOAD,
+ PERMISSIONS_V1.ARTIFACT_DERIVED_CREATE,
+ PERMISSIONS_V1.JOB_EXECUTION_READ,
+ PERMISSIONS_V1.JOB_EXECUTION_CREATE,
+ PERMISSIONS_V1.JOB_EXECUTION_RUN,
+ PERMISSIONS_V1.JOB_EXECUTION_CANCEL,
+ ]),
+ operator: immutableBundle('operator', 'Operator', [
+ PERMISSIONS_V1.ORGANIZATION_PROFILE_READ,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_READ,
+ PERMISSIONS_V1.PROJECT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_DERIVED_CREATE,
+ PERMISSIONS_V1.JOB_EXECUTION_READ,
+ PERMISSIONS_V1.JOB_EXECUTION_RUN,
+ ]),
+ approver: immutableBundle('approver', 'Approver', [
+ PERMISSIONS_V1.ORGANIZATION_PROFILE_READ,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_READ,
+ PERMISSIONS_V1.PROJECT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_RECORD_READ,
+ PERMISSIONS_V1.JOB_EXECUTION_READ,
+ PERMISSIONS_V1.APPROVAL_REQUEST_READ,
+ PERMISSIONS_V1.APPROVAL_DECISION_CREATE,
+ ]),
+ viewer: immutableBundle('viewer', 'Viewer', [
+ PERMISSIONS_V1.ORGANIZATION_PROFILE_READ,
+ PERMISSIONS_V1.WORKSPACE_SETTINGS_READ,
+ PERMISSIONS_V1.PROJECT_RECORD_READ,
+ PERMISSIONS_V1.ARTIFACT_RECORD_READ,
+ PERMISSIONS_V1.JOB_EXECUTION_READ,
+ ]),
+ });
+
+const permissionSet = new Set(Object.values(PERMISSIONS_V1));
+const roleSet = new Set(INITIAL_ROLE_IDS_V1);
+
+export function isPermissionV1(value: unknown): value is PermissionV1 {
+ return typeof value === 'string' && permissionSet.has(value as PermissionV1);
+}
+
+export function isRoleIdV1(value: unknown): value is InitialRoleIdV1 {
+ return typeof value === 'string' && roleSet.has(value as InitialRoleIdV1);
+}
+
+export function roleHasPermissionV1(roleId: unknown, permission: unknown): boolean {
+ if (!isRoleIdV1(roleId) || !isPermissionV1(permission)) {
+ return false;
+ }
+
+ return INITIAL_ROLE_BUNDLES_V1[roleId].permissions.includes(permission);
+}
diff --git a/packages/domain/src/tenant-scope/v1.ts b/packages/domain/src/tenant-scope/v1.ts
new file mode 100644
index 00000000..b8353cea
--- /dev/null
+++ b/packages/domain/src/tenant-scope/v1.ts
@@ -0,0 +1,172 @@
+import {
+ parseV1Contract,
+ type Identifier,
+ type TenantScope,
+ type UtcTimestamp,
+} from '@databreeze/contracts/v1';
+
+/** Partial foundation coverage: IAM-001, IAM-009, and IAM-019. */
+
+declare const stableIdentifierV1Brand: unique symbol;
+declare const strictUtcTimestampV1Brand: unique symbol;
+
+export type StableIdentifierV1 = Identifier & {
+ readonly [stableIdentifierV1Brand]: 'StableIdentifierV1';
+};
+
+export type StrictUtcTimestampV1 = UtcTimestamp & {
+ readonly [strictUtcTimestampV1Brand]: 'StrictUtcTimestampV1';
+};
+
+export interface OrganizationTenantScopeV1 {
+ readonly scopeType: 'organization';
+ readonly organizationId: StableIdentifierV1;
+}
+
+export interface WorkspaceTenantScopeV1 {
+ readonly scopeType: 'workspace';
+ readonly organizationId: StableIdentifierV1;
+ readonly workspaceId: StableIdentifierV1;
+}
+
+export interface ProjectTenantScopeV1 {
+ readonly scopeType: 'project';
+ readonly organizationId: StableIdentifierV1;
+ readonly workspaceId: StableIdentifierV1;
+ readonly projectId: StableIdentifierV1;
+}
+
+export type TenantScopeV1 =
+ | OrganizationTenantScopeV1
+ | WorkspaceTenantScopeV1
+ | ProjectTenantScopeV1;
+
+export type ParseValueResultV1 =
+ | { readonly accepted: true; readonly value: TValue }
+ | { readonly accepted: false; readonly code: TCode };
+
+const identifierSchemaId = 'https://schemas.databreeze.dev/contracts/v1/identifier';
+const utcTimestampSchemaId = 'https://schemas.databreeze.dev/contracts/v1/utc-timestamp';
+const tenantScopeSchemaId = 'https://schemas.databreeze.dev/contracts/v1/tenant-scope';
+
+// UUIDv4 and UUIDv7 are random/time-sortable non-guessable identifiers used by DataBreeze.
+const nonGuessableUuidPattern =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[47][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
+
+function rejected(
+ code: TCode,
+): { readonly accepted: false; readonly code: TCode } {
+ return Object.freeze({ accepted: false, code });
+}
+
+function accepted(value: TValue): { readonly accepted: true; readonly value: TValue } {
+ return Object.freeze({ accepted: true, value });
+}
+
+export function parseStableIdentifierV1(
+ input: unknown,
+): ParseValueResultV1 {
+ const parsed = parseV1Contract(identifierSchemaId, input);
+ if (!parsed.accepted || !nonGuessableUuidPattern.test(parsed.value)) {
+ return rejected('INVALID_IDENTIFIER');
+ }
+
+ return accepted(parsed.value.toLowerCase() as StableIdentifierV1);
+}
+
+export function parseStrictUtcTimestampV1(
+ input: unknown,
+): ParseValueResultV1 {
+ const parsed = parseV1Contract(utcTimestampSchemaId, input);
+ if (!parsed.accepted) {
+ return rejected('INVALID_UTC_TIMESTAMP');
+ }
+
+ return accepted(parsed.value as StrictUtcTimestampV1);
+}
+
+function identifierFrom(input: unknown): StableIdentifierV1 | undefined {
+ const result = parseStableIdentifierV1(input);
+ return result.accepted ? result.value : undefined;
+}
+
+export function parseTenantScopeV1(
+ input: unknown,
+): ParseValueResultV1 {
+ const parsed = parseV1Contract(tenantScopeSchemaId, input);
+ if (!parsed.accepted) {
+ return rejected('INVALID_TENANT_SCOPE');
+ }
+
+ const organizationId = identifierFrom(parsed.value.organizationId);
+ if (organizationId === undefined) {
+ return rejected('INVALID_TENANT_SCOPE');
+ }
+
+ if (parsed.value.scopeType === 'organization') {
+ return accepted(Object.freeze({ scopeType: 'organization', organizationId }));
+ }
+
+ const workspaceId = identifierFrom(parsed.value.workspaceId);
+ if (workspaceId === undefined) {
+ return rejected('INVALID_TENANT_SCOPE');
+ }
+
+ if (parsed.value.scopeType === 'workspace') {
+ return accepted(Object.freeze({ scopeType: 'workspace', organizationId, workspaceId }));
+ }
+
+ const projectId = identifierFrom(parsed.value.projectId);
+ if (projectId === undefined) {
+ return rejected('INVALID_TENANT_SCOPE');
+ }
+
+ return accepted(Object.freeze({ scopeType: 'project', organizationId, workspaceId, projectId }));
+}
+
+export function tenantScopesEqualV1(left: TenantScopeV1, right: TenantScopeV1): boolean {
+ if (left.scopeType !== right.scopeType || left.organizationId !== right.organizationId) {
+ return false;
+ }
+
+ if (left.scopeType === 'organization' || right.scopeType === 'organization') {
+ return left.scopeType === right.scopeType;
+ }
+
+ if (left.workspaceId !== right.workspaceId) {
+ return false;
+ }
+
+ if (left.scopeType === 'workspace' || right.scopeType === 'workspace') {
+ return left.scopeType === right.scopeType;
+ }
+
+ return left.projectId === right.projectId;
+}
+
+export function tenantScopeContainsV1(container: TenantScopeV1, candidate: TenantScopeV1): boolean {
+ if (container.organizationId !== candidate.organizationId) {
+ return false;
+ }
+
+ if (container.scopeType === 'organization') {
+ return true;
+ }
+
+ if (candidate.scopeType === 'organization' || container.workspaceId !== candidate.workspaceId) {
+ return false;
+ }
+
+ if (container.scopeType === 'workspace') {
+ return true;
+ }
+
+ return candidate.scopeType === 'project' && container.projectId === candidate.projectId;
+}
+
+export function narrowTenantScopeV1(
+ current: TenantScopeV1,
+ candidate: TenantScopeV1,
+): TenantScopeV1 | undefined {
+ return tenantScopeContainsV1(current, candidate) ? candidate : undefined;
+}
diff --git a/packages/domain/src/v1.ts b/packages/domain/src/v1.ts
new file mode 100644
index 00000000..4d091b27
--- /dev/null
+++ b/packages/domain/src/v1.ts
@@ -0,0 +1,3 @@
+export * from './authorization/v1.ts';
+export * from './permissions/v1.ts';
+export * from './tenant-scope/v1.ts';
diff --git a/packages/domain/test/authorization-v1.test.mjs b/packages/domain/test/authorization-v1.test.mjs
new file mode 100644
index 00000000..8a61ebcf
--- /dev/null
+++ b/packages/domain/test/authorization-v1.test.mjs
@@ -0,0 +1,549 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import { createScopedAuthorizationEvaluatorV1 } from '../src/authorization/v1.ts';
+
+const ids = Object.freeze({
+ principal: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
+ organizationA: '018f0f8c-7b77-7abc-8def-0123456789ab',
+ organizationB: '018f0f8c-7b77-7abc-9def-0123456789ac',
+ workspaceA: '11111111-1111-4111-8111-111111111111',
+ workspaceB: '11111111-1111-4111-8111-111111111112',
+ projectA: '22222222-2222-4222-8222-222222222222',
+ projectB: '33333333-3333-4333-8333-333333333333',
+ resourceA: '44444444-4444-4444-8444-444444444444',
+ resourceB: '55555555-5555-4555-8555-555555555555',
+});
+
+const organizationA = Object.freeze({
+ scopeType: 'organization',
+ organizationId: ids.organizationA,
+});
+const organizationB = Object.freeze({
+ scopeType: 'organization',
+ organizationId: ids.organizationB,
+});
+const workspaceA = Object.freeze({
+ scopeType: 'workspace',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+});
+const workspaceB = Object.freeze({
+ scopeType: 'workspace',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceB,
+});
+const projectA = Object.freeze({
+ scopeType: 'project',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+ projectId: ids.projectA,
+});
+const projectB = Object.freeze({
+ scopeType: 'project',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+ projectId: ids.projectB,
+});
+
+function resourceFor(resourceType, overrides = {}) {
+ const defaults = {
+ organization: {
+ resourceType: 'organization',
+ resourceId: ids.organizationA,
+ tenantScope: organizationA,
+ },
+ workspace: {
+ resourceType: 'workspace',
+ resourceId: ids.workspaceA,
+ tenantScope: workspaceA,
+ },
+ project: {
+ resourceType: 'project',
+ resourceId: ids.projectA,
+ tenantScope: projectA,
+ },
+ artifact: {
+ resourceType: 'artifact',
+ resourceId: ids.resourceA,
+ tenantScope: projectA,
+ },
+ job: {
+ resourceType: 'job',
+ resourceId: ids.resourceA,
+ tenantScope: workspaceA,
+ },
+ 'approval-request': {
+ resourceType: 'approval-request',
+ resourceId: ids.resourceA,
+ tenantScope: projectA,
+ },
+ 'billing-account': {
+ resourceType: 'billing-account',
+ resourceId: ids.resourceA,
+ tenantScope: organizationA,
+ },
+ device: {
+ resourceType: 'device',
+ resourceId: ids.resourceA,
+ tenantScope: organizationA,
+ },
+ };
+ return Object.freeze({ ...defaults[resourceType], ...overrides });
+}
+
+function requestFor(permission, channel, resource, overrides = {}) {
+ return {
+ permission,
+ channel,
+ tenantFilter: resource.tenantScope,
+ resource: {
+ resourceType: resource.resourceType,
+ resourceId: resource.resourceId,
+ },
+ ...overrides,
+ };
+}
+
+function authorityProvider({
+ principalId = ids.principal,
+ resource = resourceFor('artifact'),
+ roleId = 'viewer',
+ membershipScope = resource.tenantScope,
+ membershipActive = true,
+ policyConditionsSatisfied = true,
+ principalResult,
+ membershipResult,
+ policyResult,
+ hangFrom,
+ throwFrom,
+} = {}) {
+ const calls = {
+ principal: 0,
+ lookup: 0,
+ membership: 0,
+ policy: 0,
+ lookupQuery: undefined,
+ };
+
+ const provider = Object.freeze({
+ async resolveAuthenticatedPrincipalV1() {
+ calls.principal += 1;
+ if (hangFrom === 'principal') return new Promise(() => {});
+ if (throwFrom === 'principal') throw new Error('principal unavailable');
+ return principalResult ?? { principalId };
+ },
+ async lookupResourceV1(query) {
+ calls.lookup += 1;
+ calls.lookupQuery = query;
+ if (hangFrom === 'lookup') return new Promise(() => {});
+ if (throwFrom === 'lookup') throw new Error('lookup unavailable');
+ return resource;
+ },
+ async resolveMembershipV1() {
+ calls.membership += 1;
+ if (hangFrom === 'membership') return new Promise(() => {});
+ if (throwFrom === 'membership') throw new Error('membership unavailable');
+ return membershipResult ?? { roleId, membershipScope, membershipActive };
+ },
+ async evaluatePolicyV1() {
+ calls.policy += 1;
+ if (hangFrom === 'policy') return new Promise(() => {});
+ if (throwFrom === 'policy') throw new Error('policy unavailable');
+ return policyResult ?? { satisfied: policyConditionsSatisfied };
+ },
+ });
+
+ return { provider, calls };
+}
+
+async function settleWithin(promise, timeoutMs) {
+ let guard;
+ try {
+ return await Promise.race([
+ promise,
+ new Promise((_, reject) => {
+ guard = globalThis.setTimeout(
+ () => reject(new Error('authorization did not settle')),
+ timeoutMs,
+ );
+ }),
+ ]);
+ } finally {
+ globalThis.clearTimeout(guard);
+ }
+}
+
+test('[IAM-002, IAM-003] evaluator owns authority and exposes no caller minting API', async () => {
+ const billing = resourceFor('billing-account');
+ const { provider, calls } = authorityProvider({ resource: billing, roleId: 'owner' });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+
+ assert.deepEqual(Object.keys(evaluator), ['authorizeV1']);
+ assert.deepEqual(
+ await evaluator.authorizeV1(requestFor('billing.account.manage', 'web', billing)),
+ {
+ allowed: true,
+ permission: 'billing.account.manage',
+ tenantScope: organizationA,
+ },
+ );
+
+ const callsBeforeExploit = { ...calls };
+ const fabricatedOwnerRequest = requestFor('billing.account.manage', 'web', billing, {
+ roleId: 'owner',
+ membershipActive: true,
+ membershipScope: organizationA,
+ policyConditionsSatisfied: true,
+ resourceOwnership: billing,
+ });
+ assert.deepEqual(await evaluator.authorizeV1(fabricatedOwnerRequest), {
+ allowed: false,
+ code: 'INVALID_AUTHORIZATION_REQUEST',
+ });
+ assert.deepEqual(calls, callsBeforeExploit);
+});
+
+test('[IAM-002, IAM-003] provider facts cannot be overridden by request claims', async () => {
+ const billing = resourceFor('billing-account');
+ const viewerAuthority = authorityProvider({ resource: billing, roleId: 'viewer' });
+ const viewerEvaluator = createScopedAuthorizationEvaluatorV1(viewerAuthority.provider);
+ assert.deepEqual(
+ await viewerEvaluator.authorizeV1(requestFor('billing.account.manage', 'web', billing)),
+ { allowed: false, code: 'ROLE_PERMISSION_MISSING' },
+ );
+
+ const blockedOwnerAuthority = authorityProvider({
+ resource: billing,
+ roleId: 'owner',
+ policyConditionsSatisfied: false,
+ });
+ const blockedOwnerEvaluator = createScopedAuthorizationEvaluatorV1(
+ blockedOwnerAuthority.provider,
+ );
+ assert.deepEqual(
+ await blockedOwnerEvaluator.authorizeV1(requestFor('billing.account.manage', 'web', billing)),
+ { allowed: false, code: 'POLICY_CONDITIONS_REQUIRED' },
+ );
+});
+
+test('[IAM-009, IAM-019] exact tenant filters are enforced before authoritative lookup', async () => {
+ const artifact = resourceFor('artifact');
+ const { provider, calls } = authorityProvider({ resource: artifact });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+
+ const missingFilter = requestFor('artifact.record.read', 'web', artifact);
+ delete missingFilter.tenantFilter;
+ assert.deepEqual(await evaluator.authorizeV1(missingFilter), {
+ allowed: false,
+ code: 'TENANT_FILTER_REQUIRED',
+ });
+ assert.equal(calls.lookup, 0);
+
+ assert.deepEqual(
+ await evaluator.authorizeV1(
+ requestFor('artifact.record.read', 'web', artifact, {
+ tenantFilter: { ...projectA, optionalWorkspaceId: undefined },
+ }),
+ ),
+ { allowed: false, code: 'TENANT_FILTER_INVALID' },
+ );
+ assert.equal(calls.lookup, 0);
+
+ assert.deepEqual(
+ await evaluator.authorizeV1(
+ requestFor('artifact.record.read', 'web', artifact, { tenantFilter: projectB }),
+ ),
+ { allowed: false, code: 'RESOURCE_OWNERSHIP_MISMATCH' },
+ );
+ assert.equal(calls.lookup, 1);
+ assert.ok(Object.isFrozen(calls.lookupQuery));
+ assert.deepEqual(calls.lookupQuery.tenantScope, projectB);
+});
+
+test('[IAM-009, IAM-019] organization, workspace, and project identities match their ancestry', async () => {
+ const cases = [
+ resourceFor('organization', { resourceId: ids.resourceB }),
+ resourceFor('workspace', { resourceId: ids.resourceB }),
+ resourceFor('project', { resourceId: ids.resourceB }),
+ ];
+
+ for (const resource of cases) {
+ const { provider } = authorityProvider({ resource, roleId: 'viewer' });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ const permission = {
+ organization: 'organization.profile.read',
+ workspace: 'workspace.settings.read',
+ project: 'project.record.read',
+ }[resource.resourceType];
+ assert.deepEqual(await evaluator.authorizeV1(requestFor(permission, 'web', resource)), {
+ allowed: false,
+ code: 'RESOURCE_IDENTITY_MISMATCH',
+ });
+ }
+});
+
+test('[IAM-002, IAM-003] unknown and inactive authoritative facts deny by default', async () => {
+ const artifact = resourceFor('artifact');
+ const cases = [
+ [{ roleId: 'custom-admin' }, 'artifact.record.read', 'web', 'UNKNOWN_ROLE'],
+ [{}, 'future.resource.read', 'web', 'UNKNOWN_PERMISSION'],
+ [{}, 'artifact.record.read', 'carrier-pigeon', 'UNKNOWN_CHANNEL'],
+ [{ membershipActive: false }, 'artifact.record.read', 'web', 'INACTIVE_MEMBERSHIP'],
+ ];
+
+ for (const [authorityOverrides, permission, channel, code] of cases) {
+ const { provider } = authorityProvider({ resource: artifact, ...authorityOverrides });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.deepEqual(await evaluator.authorizeV1(requestFor(permission, channel, artifact)), {
+ allowed: false,
+ code,
+ });
+ }
+});
+
+test('[IAM-002, IAM-003] every permission is restricted to its explicit channels', async () => {
+ const cases = {
+ 'organization.profile.read': ['viewer', 'organization', ['api', 'web', 'desktop', 'android']],
+ 'organization.settings.manage': ['admin', 'organization', ['api', 'web']],
+ 'organization.ownership.transfer': ['owner', 'organization', ['api', 'web']],
+ 'workspace.settings.read': [
+ 'viewer',
+ 'workspace',
+ ['api', 'web', 'desktop', 'android', 'worker'],
+ ],
+ 'workspace.settings.manage': ['admin', 'workspace', ['api', 'web']],
+ 'project.record.read': [
+ 'viewer',
+ 'project',
+ ['api', 'web', 'desktop', 'android', 'worker', 'sync'],
+ ],
+ 'project.record.manage': ['admin', 'project', ['api', 'web']],
+ 'artifact.record.read': [
+ 'viewer',
+ 'artifact',
+ ['api', 'web', 'desktop', 'android', 'worker', 'sync', 'shared-link'],
+ ],
+ 'artifact.original.download': ['analyst', 'artifact', ['api', 'web', 'desktop', 'android']],
+ 'artifact.derived.create': ['analyst', 'artifact', ['api', 'web', 'desktop', 'worker']],
+ 'job.execution.read': [
+ 'viewer',
+ 'job',
+ ['api', 'web', 'desktop', 'android', 'worker', 'sync', 'stream'],
+ ],
+ 'job.execution.create': ['analyst', 'job', ['api', 'web', 'desktop', 'worker']],
+ 'job.execution.run': ['operator', 'job', ['api', 'web', 'desktop', 'worker']],
+ 'job.execution.cancel': ['analyst', 'job', ['api', 'web', 'desktop']],
+ 'approval.request.read': ['approver', 'approval-request', ['api', 'web', 'desktop', 'android']],
+ 'approval.decision.create': ['approver', 'approval-request', ['api', 'web', 'android']],
+ 'billing.account.read': ['owner', 'billing-account', ['api', 'web']],
+ 'billing.account.manage': ['owner', 'billing-account', ['api', 'web']],
+ 'device.identity.read': ['admin', 'device', ['api', 'web']],
+ 'device.identity.revoke': ['admin', 'device', ['api', 'web']],
+ };
+ const allChannels = [
+ 'api',
+ 'web',
+ 'desktop',
+ 'android',
+ 'worker',
+ 'sync',
+ 'stream',
+ 'shared-link',
+ ];
+
+ for (const [permission, [roleId, resourceType, allowedChannels]] of Object.entries(cases)) {
+ for (const channel of allChannels) {
+ const resource = resourceFor(resourceType);
+ const { provider } = authorityProvider({
+ resource,
+ roleId,
+ membershipScope: organizationA,
+ });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ const decision = await evaluator.authorizeV1(requestFor(permission, channel, resource));
+
+ if (allowedChannels.includes(channel)) {
+ assert.deepEqual(decision, {
+ allowed: true,
+ permission,
+ tenantScope: resource.tenantScope,
+ });
+ } else {
+ assert.deepEqual(decision, { allowed: false, code: 'CHANNEL_NOT_ALLOWED' });
+ }
+ }
+ }
+});
+
+test('[IAM-002, IAM-003, IAM-004] representative actions use the six authoritative roles', async () => {
+ const cases = [
+ ['owner', 'organization', 'organization.settings.manage'],
+ ['owner', 'billing-account', 'billing.account.manage'],
+ ['owner', 'device', 'device.identity.revoke'],
+ ['admin', 'workspace', 'workspace.settings.manage'],
+ ['admin', 'project', 'project.record.manage'],
+ ['analyst', 'artifact', 'artifact.original.download'],
+ ['analyst', 'job', 'job.execution.create'],
+ ['operator', 'job', 'job.execution.run'],
+ ['approver', 'approval-request', 'approval.decision.create'],
+ ['viewer', 'artifact', 'artifact.record.read'],
+ ];
+
+ for (const [roleId, resourceType, permission] of cases) {
+ const resource = resourceFor(resourceType);
+ const { provider } = authorityProvider({
+ resource,
+ roleId,
+ membershipScope: organizationA,
+ });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.deepEqual(await evaluator.authorizeV1(requestFor(permission, 'web', resource)), {
+ allowed: true,
+ permission,
+ tenantScope: resource.tenantScope,
+ });
+ }
+});
+
+test('[IAM-002, IAM-009, IAM-019] authoritative memberships cannot expand tenant scope', async () => {
+ const artifact = resourceFor('artifact');
+ const deniedScopes = [organizationB, workspaceB, projectB];
+ for (const membershipScope of deniedScopes) {
+ const { provider } = authorityProvider({ resource: artifact, membershipScope });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.deepEqual(
+ await evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact)),
+ { allowed: false, code: 'TENANT_SCOPE_MISMATCH' },
+ );
+ }
+
+ for (const membershipScope of [organizationA, workspaceA, projectA]) {
+ const { provider } = authorityProvider({ resource: artifact, membershipScope });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.equal(
+ (await evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact))).allowed,
+ true,
+ );
+ }
+});
+
+test('[IAM-002, IAM-003] authority failures and malformed results fail closed', async () => {
+ const artifact = resourceFor('artifact');
+ for (const throwFrom of ['principal', 'lookup', 'membership', 'policy']) {
+ const { provider } = authorityProvider({ resource: artifact, throwFrom });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.deepEqual(
+ await evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact)),
+ { allowed: false, code: 'AUTHORITY_UNAVAILABLE' },
+ );
+ }
+
+ const malformedAuthorities = [
+ { principalResult: { principalId: 'request-user' } },
+ { resource: { ...artifact, clientSuppliedOwner: true } },
+ { membershipResult: { roleId: 'viewer', membershipScope: projectA } },
+ { policyResult: { satisfied: 'yes' } },
+ ];
+ for (const overrides of malformedAuthorities) {
+ const { provider } = authorityProvider({ ...overrides, roleId: 'viewer' });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider);
+ assert.deepEqual(
+ await evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact)),
+ { allowed: false, code: 'AUTHORITY_INVALID' },
+ );
+ }
+});
+
+for (const [hangFrom, expectedCalls] of [
+ ['principal', { principal: 1, lookup: 0, membership: 0, policy: 0 }],
+ ['lookup', { principal: 1, lookup: 1, membership: 0, policy: 0 }],
+ ['membership', { principal: 1, lookup: 1, membership: 1, policy: 0 }],
+ ['policy', { principal: 1, lookup: 1, membership: 1, policy: 1 }],
+]) {
+ test(`[IAM-002, IAM-003] ${hangFrom} authority timeout fails closed`, async () => {
+ const artifact = resourceFor('artifact');
+ const { provider, calls } = authorityProvider({ resource: artifact, hangFrom });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider, {
+ providerCallTimeoutMs: 10,
+ });
+
+ assert.deepEqual(
+ await settleWithin(
+ evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact)),
+ 250,
+ ),
+ { allowed: false, code: 'AUTHORITY_UNAVAILABLE' },
+ );
+ assert.deepEqual(
+ {
+ principal: calls.principal,
+ lookup: calls.lookup,
+ membership: calls.membership,
+ policy: calls.policy,
+ },
+ expectedCalls,
+ );
+ });
+}
+
+test('[IAM-002, IAM-003] authority deadline timers are cleared after settle', async () => {
+ const nativeSetTimeout = globalThis.setTimeout;
+ const nativeClearTimeout = globalThis.clearTimeout;
+ const activeTimers = new Set();
+ let scheduled = 0;
+ let cleared = 0;
+
+ globalThis.setTimeout = (callback, delay, ...arguments_) => {
+ scheduled += 1;
+ const handle = nativeSetTimeout(() => {
+ activeTimers.delete(handle);
+ callback(...arguments_);
+ }, delay);
+ activeTimers.add(handle);
+ return handle;
+ };
+ globalThis.clearTimeout = (handle) => {
+ cleared += 1;
+ activeTimers.delete(handle);
+ return nativeClearTimeout(handle);
+ };
+
+ try {
+ const artifact = resourceFor('artifact');
+ for (const [throwFrom, expectedDecision] of [
+ [undefined, { allowed: true, permission: 'artifact.record.read', tenantScope: projectA }],
+ ['membership', { allowed: false, code: 'AUTHORITY_UNAVAILABLE' }],
+ ]) {
+ const { provider } = authorityProvider({ resource: artifact, throwFrom });
+ const evaluator = createScopedAuthorizationEvaluatorV1(provider, {
+ providerCallTimeoutMs: 1_000,
+ });
+ assert.deepEqual(
+ await evaluator.authorizeV1(requestFor('artifact.record.read', 'web', artifact)),
+ expectedDecision,
+ );
+ }
+
+ assert.equal(scheduled, 7);
+ assert.equal(cleared, 7);
+ assert.equal(activeTimers.size, 0);
+ } finally {
+ globalThis.setTimeout = nativeSetTimeout;
+ globalThis.clearTimeout = nativeClearTimeout;
+ for (const handle of activeTimers) nativeClearTimeout(handle);
+ }
+});
+
+test('[IAM-002, IAM-003] unsafe authority timeout configuration fails at composition', () => {
+ const { provider } = authorityProvider();
+ for (const options of [
+ {},
+ { providerCallTimeoutMs: 0 },
+ { providerCallTimeoutMs: 1.5 },
+ { providerCallTimeoutMs: 60_001 },
+ { providerCallTimeoutMs: 1_000, extra: true },
+ ]) {
+ assert.throws(() => createScopedAuthorizationEvaluatorV1(provider, options), TypeError);
+ }
+});
diff --git a/packages/domain/test/built-public-api-smoke.mjs b/packages/domain/test/built-public-api-smoke.mjs
new file mode 100644
index 00000000..a2337153
--- /dev/null
+++ b/packages/domain/test/built-public-api-smoke.mjs
@@ -0,0 +1,15 @@
+import assert from 'node:assert/strict';
+
+const [aggregate, permissions, tenantScope, authorization] = await Promise.all([
+ import('@databreeze/domain/v1'),
+ import('@databreeze/domain/permissions/v1'),
+ import('@databreeze/domain/tenant-scope/v1'),
+ import('@databreeze/domain/authorization/v1'),
+]);
+
+assert.equal(aggregate.PERMISSION_SCHEMA_VERSION_V1, 1);
+assert.equal(aggregate.AUTHORIZATION_SCHEMA_VERSION_V1, 1);
+assert.equal(permissions.PERMISSION_SCHEMA_VERSION_V1, 1);
+assert.equal(typeof tenantScope.parseTenantScopeV1, 'function');
+assert.equal(typeof authorization.createScopedAuthorizationEvaluatorV1, 'function');
+await assert.rejects(import('@databreeze/domain'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });
diff --git a/packages/domain/test/permission-applicability-v1.test.mjs b/packages/domain/test/permission-applicability-v1.test.mjs
new file mode 100644
index 00000000..510594f9
--- /dev/null
+++ b/packages/domain/test/permission-applicability-v1.test.mjs
@@ -0,0 +1,74 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+import * as api from '../src/permissions/v1.ts';
+
+const { AUTHORIZATION_CHANNELS_V1, PERMISSION_APPLICABILITY_V1, PERMISSIONS_V1 } = api;
+
+const expectedChannels = Object.freeze({
+ 'organization.profile.read': ['api', 'web', 'desktop', 'android'],
+ 'organization.settings.manage': ['api', 'web'],
+ 'organization.ownership.transfer': ['api', 'web'],
+ 'workspace.settings.read': ['api', 'web', 'desktop', 'android', 'worker'],
+ 'workspace.settings.manage': ['api', 'web'],
+ 'project.record.read': ['api', 'web', 'desktop', 'android', 'worker', 'sync'],
+ 'project.record.manage': ['api', 'web'],
+ 'artifact.record.read': ['api', 'web', 'desktop', 'android', 'worker', 'sync', 'shared-link'],
+ 'artifact.original.download': ['api', 'web', 'desktop', 'android'],
+ 'artifact.derived.create': ['api', 'web', 'desktop', 'worker'],
+ 'job.execution.read': ['api', 'web', 'desktop', 'android', 'worker', 'sync', 'stream'],
+ 'job.execution.create': ['api', 'web', 'desktop', 'worker'],
+ 'job.execution.run': ['api', 'web', 'desktop', 'worker'],
+ 'job.execution.cancel': ['api', 'web', 'desktop'],
+ 'approval.request.read': ['api', 'web', 'desktop', 'android'],
+ 'approval.decision.create': ['api', 'web', 'android'],
+ 'billing.account.read': ['api', 'web'],
+ 'billing.account.manage': ['api', 'web'],
+ 'device.identity.read': ['api', 'web'],
+ 'device.identity.revoke': ['api', 'web'],
+});
+
+test('[IAM-002, IAM-003] every permission has an explicit closed channel policy', () => {
+ assert.deepEqual(Object.keys(PERMISSION_APPLICABILITY_V1), Object.values(PERMISSIONS_V1));
+ assert.deepEqual(
+ Object.fromEntries(
+ Object.entries(PERMISSION_APPLICABILITY_V1).map(([permission, policy]) => [
+ permission,
+ policy.allowedChannels,
+ ]),
+ ),
+ expectedChannels,
+ );
+
+ assert.ok(Object.isFrozen(AUTHORIZATION_CHANNELS_V1));
+ assert.ok(Object.isFrozen(PERMISSION_APPLICABILITY_V1));
+ for (const policy of Object.values(PERMISSION_APPLICABILITY_V1)) {
+ assert.ok(Object.isFrozen(policy));
+ assert.ok(Object.isFrozen(policy.allowedChannels));
+ }
+});
+
+test('[IAM-002, IAM-003] sensitive actions are closed to shared-link, stream, and sync', () => {
+ const sensitive = [
+ 'organization.settings.manage',
+ 'organization.ownership.transfer',
+ 'workspace.settings.manage',
+ 'project.record.manage',
+ 'artifact.derived.create',
+ 'job.execution.create',
+ 'job.execution.run',
+ 'job.execution.cancel',
+ 'approval.decision.create',
+ 'billing.account.manage',
+ 'device.identity.revoke',
+ ];
+
+ for (const permission of sensitive) {
+ for (const channel of ['shared-link', 'stream', 'sync']) {
+ assert.equal(
+ PERMISSION_APPLICABILITY_V1[permission].allowedChannels.includes(channel),
+ false,
+ );
+ }
+ }
+});
diff --git a/packages/domain/test/permissions-v1.test.mjs b/packages/domain/test/permissions-v1.test.mjs
new file mode 100644
index 00000000..a3e32b61
--- /dev/null
+++ b/packages/domain/test/permissions-v1.test.mjs
@@ -0,0 +1,171 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+async function loadPermissions() {
+ try {
+ return await import('../src/permissions/v1.ts');
+ } catch {
+ return undefined;
+ }
+}
+
+test('[IAM-004] publishes a closed versioned permission vocabulary', async () => {
+ const api = await loadPermissions();
+
+ assert.ok(api, 'the permissions/v1 module must exist');
+ assert.equal(api.PERMISSION_SCHEMA_VERSION_V1, 1);
+ assert.deepEqual(Object.values(api.PERMISSIONS_V1), [
+ 'organization.profile.read',
+ 'organization.settings.manage',
+ 'organization.ownership.transfer',
+ 'workspace.settings.read',
+ 'workspace.settings.manage',
+ 'project.record.read',
+ 'project.record.manage',
+ 'artifact.record.read',
+ 'artifact.original.download',
+ 'artifact.derived.create',
+ 'job.execution.read',
+ 'job.execution.create',
+ 'job.execution.run',
+ 'job.execution.cancel',
+ 'approval.request.read',
+ 'approval.decision.create',
+ 'billing.account.read',
+ 'billing.account.manage',
+ 'device.identity.read',
+ 'device.identity.revoke',
+ ]);
+ assert.ok(Object.isFrozen(api.PERMISSIONS_V1));
+});
+
+test('[IAM-004] maps exactly six immutable initial role bundles', async () => {
+ const api = await loadPermissions();
+ assert.ok(api);
+
+ assert.deepEqual(api.INITIAL_ROLE_IDS_V1, [
+ 'owner',
+ 'admin',
+ 'analyst',
+ 'operator',
+ 'approver',
+ 'viewer',
+ ]);
+
+ const expected = {
+ owner: [
+ 'organization.profile.read',
+ 'organization.settings.manage',
+ 'organization.ownership.transfer',
+ 'workspace.settings.read',
+ 'workspace.settings.manage',
+ 'project.record.read',
+ 'project.record.manage',
+ 'job.execution.read',
+ 'billing.account.read',
+ 'billing.account.manage',
+ 'device.identity.read',
+ 'device.identity.revoke',
+ ],
+ admin: [
+ 'organization.profile.read',
+ 'organization.settings.manage',
+ 'workspace.settings.read',
+ 'workspace.settings.manage',
+ 'project.record.read',
+ 'project.record.manage',
+ 'job.execution.read',
+ 'device.identity.read',
+ 'device.identity.revoke',
+ ],
+ analyst: [
+ 'organization.profile.read',
+ 'workspace.settings.read',
+ 'project.record.read',
+ 'artifact.record.read',
+ 'artifact.original.download',
+ 'artifact.derived.create',
+ 'job.execution.read',
+ 'job.execution.create',
+ 'job.execution.run',
+ 'job.execution.cancel',
+ ],
+ operator: [
+ 'organization.profile.read',
+ 'workspace.settings.read',
+ 'project.record.read',
+ 'artifact.record.read',
+ 'artifact.derived.create',
+ 'job.execution.read',
+ 'job.execution.run',
+ ],
+ approver: [
+ 'organization.profile.read',
+ 'workspace.settings.read',
+ 'project.record.read',
+ 'artifact.record.read',
+ 'job.execution.read',
+ 'approval.request.read',
+ 'approval.decision.create',
+ ],
+ viewer: [
+ 'organization.profile.read',
+ 'workspace.settings.read',
+ 'project.record.read',
+ 'artifact.record.read',
+ 'job.execution.read',
+ ],
+ };
+
+ assert.deepEqual(
+ Object.fromEntries(
+ Object.entries(api.INITIAL_ROLE_BUNDLES_V1).map(([roleId, bundle]) => [
+ roleId,
+ bundle.permissions,
+ ]),
+ ),
+ expected,
+ );
+
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.owner.name, 'Owner');
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.admin.name, 'Admin');
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.analyst.name, 'Analyst');
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.operator.name, 'Operator');
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.approver.name, 'Approver');
+ assert.equal(api.INITIAL_ROLE_BUNDLES_V1.viewer.name, 'Viewer');
+ assert.ok(Object.isFrozen(api.INITIAL_ROLE_IDS_V1));
+ assert.ok(Object.isFrozen(api.INITIAL_ROLE_BUNDLES_V1));
+ for (const bundle of Object.values(api.INITIAL_ROLE_BUNDLES_V1)) {
+ assert.ok(Object.isFrozen(bundle));
+ assert.ok(Object.isFrozen(bundle.permissions));
+ }
+
+ for (const permission of api.INITIAL_ROLE_BUNDLES_V1.admin.permissions) {
+ assert.equal(
+ api.INITIAL_ROLE_BUNDLES_V1.owner.permissions.includes(permission),
+ true,
+ `Owner must retain the Admin permission ${permission}`,
+ );
+ }
+});
+
+test('[IAM-004] role lookup denies unknown roles and permissions', async () => {
+ const api = await loadPermissions();
+ assert.ok(api);
+
+ assert.equal(api.roleHasPermissionV1('viewer', 'artifact.record.read'), true);
+ assert.equal(api.roleHasPermissionV1('viewer', 'billing.account.manage'), false);
+ assert.equal(api.roleHasPermissionV1('custom-admin', 'artifact.record.read'), false);
+ assert.equal(api.roleHasPermissionV1('owner', 'future.resource.read'), false);
+ assert.equal(api.isRoleIdV1('Owner'), false);
+ assert.equal(api.isPermissionV1('artifact.read'), false);
+});
+
+test('[IAM-003, IAM-004] administration roles do not bypass approval policy', async () => {
+ const api = await loadPermissions();
+ assert.ok(api);
+
+ assert.equal(api.roleHasPermissionV1('owner', 'approval.decision.create'), false);
+ assert.equal(api.roleHasPermissionV1('admin', 'approval.decision.create'), false);
+ assert.equal(api.roleHasPermissionV1('approver', 'approval.decision.create'), true);
+});
diff --git a/packages/domain/test/public-api-v1.test.mjs b/packages/domain/test/public-api-v1.test.mjs
new file mode 100644
index 00000000..0b5c291b
--- /dev/null
+++ b/packages/domain/test/public-api-v1.test.mjs
@@ -0,0 +1,38 @@
+import assert from 'node:assert/strict';
+import { existsSync, readFileSync } from 'node:fs';
+import path from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+
+test('[IAM-001, IAM-002, IAM-003, IAM-004, IAM-009, IAM-019 partial] publishes only stable versioned entry points', async () => {
+ const manifest = JSON.parse(readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
+ assert.deepEqual(Object.keys(manifest.exports), [
+ './v1',
+ './permissions/v1',
+ './tenant-scope/v1',
+ './authorization/v1',
+ ]);
+
+ for (const entry of Object.values(manifest.exports)) {
+ assert.ok(existsSync(path.resolve(packageDirectory, entry.types)));
+ assert.match(entry.import, /^\.\/dist\/.+\.js$/);
+ }
+
+ let aggregate;
+ try {
+ aggregate = await import('../src/v1.ts');
+ } catch {
+ aggregate = undefined;
+ }
+ assert.ok(aggregate, 'the source v1 aggregate must exist');
+ assert.equal(aggregate.PERMISSION_SCHEMA_VERSION_V1, 1);
+ assert.equal(aggregate.AUTHORIZATION_SCHEMA_VERSION_V1, 1);
+ assert.equal(typeof aggregate.parseTenantScopeV1, 'function');
+ assert.equal(typeof aggregate.createScopedAuthorizationEvaluatorV1, 'function');
+});
+
+test('[IAM-004] does not expose an unversioned package root', async () => {
+ await assert.rejects(import('@databreeze/domain'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });
+});
diff --git a/packages/domain/test/public-api-v1.type-test.ts b/packages/domain/test/public-api-v1.type-test.ts
new file mode 100644
index 00000000..529da937
--- /dev/null
+++ b/packages/domain/test/public-api-v1.type-test.ts
@@ -0,0 +1,55 @@
+import { createScopedAuthorizationEvaluatorV1 } from '@databreeze/domain/authorization/v1';
+import type {
+ AuthorizationAuthorityProviderV1,
+ AuthorizationRequestV1,
+ ScopedResourceLookupQueryV1,
+ ScopedAuthorizationEvaluatorOptionsV1,
+} from '@databreeze/domain/authorization/v1';
+import type { StableIdentifierV1, TenantScopeV1 } from '@databreeze/domain/tenant-scope/v1';
+
+declare const scope: TenantScopeV1;
+declare const stableId: StableIdentifierV1;
+
+const provider: AuthorizationAuthorityProviderV1 = {
+ resolveAuthenticatedPrincipalV1() {
+ return { principalId: stableId };
+ },
+ lookupResourceV1(query: ScopedResourceLookupQueryV1) {
+ return {
+ resourceType: query.resourceType,
+ resourceId: query.resourceId,
+ tenantScope: query.tenantScope,
+ };
+ },
+ resolveMembershipV1() {
+ return { roleId: 'viewer', membershipScope: scope, membershipActive: true };
+ },
+ evaluatePolicyV1() {
+ return { satisfied: true };
+ },
+};
+
+const evaluatorOptions: ScopedAuthorizationEvaluatorOptionsV1 = { providerCallTimeoutMs: 1_000 };
+const evaluator = createScopedAuthorizationEvaluatorV1(provider, evaluatorOptions);
+const request: AuthorizationRequestV1 = {
+ permission: 'artifact.record.read',
+ channel: 'web',
+ tenantFilter: scope,
+ resource: { resourceType: 'artifact', resourceId: stableId },
+};
+
+void evaluator.authorizeV1(request);
+
+const fabricatedAuthority: AuthorizationRequestV1 = {
+ ...request,
+ // @ts-expect-error -- request consumers cannot provide authoritative role facts.
+ roleId: 'owner',
+};
+
+const allowedEvaluatorKey: keyof typeof evaluator = 'authorizeV1';
+// @ts-expect-error -- the evaluator does not expose authority-minting methods.
+const forbiddenEvaluatorKey: keyof typeof evaluator = 'createEvaluatedContextV1';
+
+void allowedEvaluatorKey;
+void forbiddenEvaluatorKey;
+void fabricatedAuthority;
diff --git a/packages/domain/test/tenant-scope-v1.test.mjs b/packages/domain/test/tenant-scope-v1.test.mjs
new file mode 100644
index 00000000..216c2894
--- /dev/null
+++ b/packages/domain/test/tenant-scope-v1.test.mjs
@@ -0,0 +1,204 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+async function loadTenantScope() {
+ try {
+ return await import('../src/tenant-scope/v1.ts');
+ } catch {
+ return undefined;
+ }
+}
+
+const ids = Object.freeze({
+ organizationA: '018f0f8c-7b77-7abc-8def-0123456789ab',
+ organizationB: '018f0f8c-7b77-7abc-9def-0123456789ac',
+ workspaceA: '11111111-1111-4111-8111-111111111111',
+ workspaceB: '11111111-1111-4111-8111-111111111112',
+ projectA: '22222222-2222-4222-8222-222222222222',
+ projectB: '33333333-3333-4333-8333-333333333333',
+});
+
+const organizationA = Object.freeze({
+ scopeType: 'organization',
+ organizationId: ids.organizationA,
+});
+const organizationB = Object.freeze({
+ scopeType: 'organization',
+ organizationId: ids.organizationB,
+});
+const workspaceA = Object.freeze({
+ scopeType: 'workspace',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+});
+const workspaceB = Object.freeze({
+ scopeType: 'workspace',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceB,
+});
+const projectA = Object.freeze({
+ scopeType: 'project',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+ projectId: ids.projectA,
+});
+const projectB = Object.freeze({
+ scopeType: 'project',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+ projectId: ids.projectB,
+});
+
+function expectAccepted(result) {
+ assert.equal(result.accepted, true);
+ return result.value;
+}
+
+test('[IAM-001] accepts only non-guessable UUIDv4/v7 identifiers and strict UTC timestamps', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api, 'the tenant-scope/v1 module must exist');
+
+ assert.equal(api.parseStableIdentifierV1(ids.organizationA).accepted, true);
+ assert.equal(api.parseStableIdentifierV1(ids.workspaceA).accepted, true);
+ assert.deepEqual(api.parseStableIdentifierV1('00000000-0000-0000-0000-000000000000'), {
+ accepted: false,
+ code: 'INVALID_IDENTIFIER',
+ });
+ assert.deepEqual(api.parseStableIdentifierV1('6ba7b810-9dad-11d1-80b4-00c04fd430c8'), {
+ accepted: false,
+ code: 'INVALID_IDENTIFIER',
+ });
+ assert.deepEqual(api.parseStableIdentifierV1(123), {
+ accepted: false,
+ code: 'INVALID_IDENTIFIER',
+ });
+
+ assert.equal(api.parseStrictUtcTimestampV1('2026-08-01T12:34:56.123Z').accepted, true);
+ assert.deepEqual(api.parseStrictUtcTimestampV1('2026-08-01T19:34:56+07:00'), {
+ accepted: false,
+ code: 'INVALID_UTC_TIMESTAMP',
+ });
+ assert.deepEqual(api.parseStrictUtcTimestampV1('2026-08-01T12:34:56z'), {
+ accepted: false,
+ code: 'INVALID_UTC_TIMESTAMP',
+ });
+});
+
+test('[IAM-001, IAM-019] canonicalizes mixed-case UUID identities to lowercase', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api);
+
+ assert.deepEqual(api.parseStableIdentifierV1(ids.organizationA.toUpperCase()), {
+ accepted: true,
+ value: ids.organizationA,
+ });
+
+ const mixedCaseProject = {
+ scopeType: 'project',
+ organizationId: ids.organizationA.toUpperCase(),
+ workspaceId: ids.workspaceA.toUpperCase(),
+ projectId: ids.projectA.toUpperCase(),
+ };
+ const parsed = expectAccepted(api.parseTenantScopeV1(mixedCaseProject));
+ assert.deepEqual(parsed, projectA);
+ assert.equal(
+ api.tenantScopesEqualV1(parsed, expectAccepted(api.parseTenantScopeV1(projectA))),
+ true,
+ );
+});
+
+test('[IAM-019] parses only complete closed tenant ancestry', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api);
+
+ for (const scope of [organizationA, workspaceA, projectA]) {
+ const parsed = expectAccepted(api.parseTenantScopeV1(scope));
+ assert.deepEqual(parsed, scope);
+ assert.ok(Object.isFrozen(parsed));
+ }
+
+ for (const scope of [
+ undefined,
+ { scopeType: 'workspace', workspaceId: ids.workspaceA },
+ {
+ scopeType: 'project',
+ organizationId: ids.organizationA,
+ workspaceId: ids.workspaceA,
+ },
+ { ...workspaceA, projectId: ids.projectA },
+ ]) {
+ assert.deepEqual(api.parseTenantScopeV1(scope), {
+ accepted: false,
+ code: 'INVALID_TENANT_SCOPE',
+ });
+ }
+});
+
+test('[IAM-019] equality and containment require complete matching ancestry', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api);
+ const orgA = expectAccepted(api.parseTenantScopeV1(organizationA));
+ const orgB = expectAccepted(api.parseTenantScopeV1(organizationB));
+ const wsA = expectAccepted(api.parseTenantScopeV1(workspaceA));
+ const wsB = expectAccepted(api.parseTenantScopeV1(workspaceB));
+ const projA = expectAccepted(api.parseTenantScopeV1(projectA));
+ const projB = expectAccepted(api.parseTenantScopeV1(projectB));
+
+ assert.equal(api.tenantScopesEqualV1(wsA, wsA), true);
+ assert.equal(api.tenantScopesEqualV1(wsA, wsB), false);
+ assert.equal(api.tenantScopeContainsV1(orgA, wsA), true);
+ assert.equal(api.tenantScopeContainsV1(orgA, projA), true);
+ assert.equal(api.tenantScopeContainsV1(orgB, wsA), false);
+ assert.equal(api.tenantScopeContainsV1(wsA, projA), true);
+ assert.equal(api.tenantScopeContainsV1(wsA, projB), true);
+ assert.equal(api.tenantScopeContainsV1(wsA, wsB), false);
+ assert.equal(api.tenantScopeContainsV1(projA, projB), false);
+ assert.equal(api.tenantScopeContainsV1(projA, wsA), false);
+});
+
+test('[IAM-019] narrowing permits descendants but never parents or siblings', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api);
+ const orgA = expectAccepted(api.parseTenantScopeV1(organizationA));
+ const wsA = expectAccepted(api.parseTenantScopeV1(workspaceA));
+ const wsB = expectAccepted(api.parseTenantScopeV1(workspaceB));
+ const projA = expectAccepted(api.parseTenantScopeV1(projectA));
+ const projB = expectAccepted(api.parseTenantScopeV1(projectB));
+
+ assert.equal(api.narrowTenantScopeV1(orgA, wsA), wsA);
+ assert.equal(api.narrowTenantScopeV1(orgA, projA), projA);
+ assert.equal(api.narrowTenantScopeV1(wsA, projA), projA);
+ assert.equal(api.narrowTenantScopeV1(projA, projA), projA);
+ assert.equal(api.narrowTenantScopeV1(wsA, orgA), undefined);
+ assert.equal(api.narrowTenantScopeV1(wsA, wsB), undefined);
+ assert.equal(api.narrowTenantScopeV1(projA, projB), undefined);
+});
+
+test('[IAM-019] property: successful narrowing cannot broaden a scope', async () => {
+ const api = await loadTenantScope();
+ assert.ok(api);
+
+ const rawScopes = [];
+ for (let index = 1; index <= 16; index += 1) {
+ const organizationId = `${index.toString(16).padStart(8, '0')}-0000-4000-8000-000000000001`;
+ const workspaceId = `${index.toString(16).padStart(8, '0')}-0000-4000-8000-000000000002`;
+ const projectId = `${index.toString(16).padStart(8, '0')}-0000-4000-8000-000000000003`;
+ rawScopes.push(
+ { scopeType: 'organization', organizationId },
+ { scopeType: 'workspace', organizationId, workspaceId },
+ { scopeType: 'project', organizationId, workspaceId, projectId },
+ );
+ }
+ const scopes = rawScopes.map((scope) => expectAccepted(api.parseTenantScopeV1(scope)));
+
+ for (const current of scopes) {
+ for (const candidate of scopes) {
+ const narrowed = api.narrowTenantScopeV1(current, candidate);
+ assert.equal(narrowed !== undefined, api.tenantScopeContainsV1(current, candidate));
+ if (narrowed !== undefined) {
+ assert.equal(api.tenantScopesEqualV1(narrowed, candidate), true);
+ assert.equal(api.tenantScopeContainsV1(current, narrowed), true);
+ }
+ }
+ }
+});
diff --git a/packages/domain/tsconfig.build.json b/packages/domain/tsconfig.build.json
new file mode 100644
index 00000000..ffb43181
--- /dev/null
+++ b/packages/domain/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "outDir": "dist",
+ "rewriteRelativeImportExtensions": true,
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/domain/tsconfig.json b/packages/domain/tsconfig.json
new file mode 100644
index 00000000..28754dda
--- /dev/null
+++ b/packages/domain/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts", "test/**/*.ts"]
+}
diff --git a/packages/domain/turbo.json b/packages/domain/turbo.json
new file mode 100644
index 00000000..25e76c6b
--- /dev/null
+++ b/packages/domain/turbo.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "test": {
+ "outputs": []
+ }
+ }
+}
diff --git a/packages/i18n/README.md b/packages/i18n/README.md
index 40bdc280..ffe4ac97 100644
--- a/packages/i18n/README.md
+++ b/packages/i18n/README.md
@@ -1,3 +1,25 @@
# Internationalization
-Vietnamese-default and complete English catalogs, message identifiers, formatting rules, and cross-platform locale fixtures.
+`@databreeze/i18n/v1` is the canonical TypeScript foundation for DataBreeze product language. It provides a bounded bilingual vocabulary, strict text interpolation, locale negotiation, and validated `Intl` formatting helpers. Vietnamese (`vi-VN`) is the exact default and English (`en`) is complete for every published v1 key.
+
+This package provides partial foundation coverage for IAM-016, WEB-013, WEB-021, WEB-022, DSK-021, AND-017, and NCO-017. It does not claim that future screens or notification templates are already translated.
+
+## Public contract
+
+- `MESSAGE_CATALOGS_V1` and `MESSAGE_KEYS_V1` contain stable, versioned keys with identical placeholder schemas in both locales.
+- `negotiateLocaleV1` gives a supported user preference priority over `Accept-Language` and safely falls back to `vi-VN`.
+- `formatMessageV1` requires every declared `{name}` parameter, rejects undeclared parameters, bounds and NFC-normalizes safe Unicode text, and applies stricter syntax to identifier-like values such as correlation IDs. It performs literal text substitution without parsing HTML. Callers must render the returned string through their platform's text API, never an HTML injection API.
+- `formatRetryAfterSecondsV1` chooses an explicit singular or plural catalog variant through `Intl.PluralRules` and rejects negative, fractional, or unsafe integer values.
+- Date/time formatting always requires an explicit IANA time zone. Decimal, currency, percent, list, relative-time, and plural helpers reject unsupported locales and malformed values instead of silently changing business values.
+
+## Boundaries
+
+The package has no runtime dependencies and must not depend on UI frameworks, Web/Desktop shells, Android resources, persistence, IAM services, notification delivery, feature modules, remote translation systems, or provider adapters. Android will consume generated terminology and fixtures in its own implementation task; it does not import this TypeScript package.
+
+## Expanding the catalogs
+
+1. Add a stable key to both `vi-VN` and `en` in the same change. Prefer domain-neutral foundation language here; feature-specific copy belongs with the feature registration.
+2. Use natural professional Vietnamese, then complete English copy. Do not add a partial fallback, placeholder copy, HTML, controls, or bidirectional formatting characters.
+3. Declare every interpolation parameter and its `identifier`, `text`, or finite `number` type identically in both locales.
+4. Add behavior-focused tests for the new message or formatter case. Run the package tests, typecheck, build self-import, and root repository checks.
+5. Preserve existing v1 key semantics. Breaking key, placeholder, or meaning changes require a new versioned export rather than mutating a released contract.
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
new file mode 100644
index 00000000..22e5322a
--- /dev/null
+++ b/packages/i18n/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@databreeze/i18n",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./v1": {
+ "types": "./src/v1.ts",
+ "import": "./dist/v1.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json && node test/built-public-api-smoke.mjs",
+ "test": "node --test test/**/*.test.mjs",
+ "typecheck": "tsc --noEmit --project tsconfig.json"
+ }
+}
diff --git a/packages/i18n/src/catalogs-v1.ts b/packages/i18n/src/catalogs-v1.ts
new file mode 100644
index 00000000..89e268bf
--- /dev/null
+++ b/packages/i18n/src/catalogs-v1.ts
@@ -0,0 +1,301 @@
+export const I18N_SCHEMA_VERSION_V1 = 1 as const;
+
+export const SUPPORTED_LOCALES_V1 = Object.freeze(['vi-VN', 'en'] as const);
+export type SupportedLocaleV1 = (typeof SUPPORTED_LOCALES_V1)[number];
+export const DEFAULT_LOCALE_V1: SupportedLocaleV1 = 'vi-VN';
+
+export type MessageParameterTypeV1 = 'identifier' | 'number' | 'text';
+
+export interface CatalogMessageV1 {
+ readonly message: string;
+ readonly parameters: Readonly>;
+}
+
+function entry(
+ message: string,
+ parameters: Readonly> = {},
+): CatalogMessageV1 {
+ return Object.freeze({ message, parameters: Object.freeze({ ...parameters }) });
+}
+
+const vietnameseCatalogV1 = {
+ 'product.name': entry('DataBreeze'),
+ 'common.yes': entry('Có'),
+ 'common.no': entry('Không'),
+ 'common.notAvailable': entry('Không có sẵn'),
+ 'common.unknown': entry('Chưa xác định'),
+ 'action.add': entry('Thêm'),
+ 'action.approve': entry('Phê duyệt'),
+ 'action.assign': entry('Giao việc'),
+ 'action.cancel': entry('Hủy'),
+ 'action.close': entry('Đóng'),
+ 'action.confirm': entry('Xác nhận'),
+ 'action.continue': entry('Tiếp tục'),
+ 'action.create': entry('Tạo'),
+ 'action.delete': entry('Xóa'),
+ 'action.edit': entry('Chỉnh sửa'),
+ 'action.open': entry('Mở'),
+ 'action.reject': entry('Từ chối'),
+ 'action.retry': entry('Thử lại'),
+ 'action.save': entry('Lưu'),
+ 'action.search': entry('Tìm kiếm'),
+ 'action.submit': entry('Gửi'),
+ 'nav.home': entry('Trang chủ'),
+ 'nav.inbox': entry('Hộp thư đến'),
+ 'nav.datasets': entry('Bộ dữ liệu'),
+ 'nav.jobs': entry('Tác vụ'),
+ 'nav.reviews': entry('Nội dung cần xem xét'),
+ 'nav.approvals': entry('Nội dung cần phê duyệt'),
+ 'nav.reports': entry('Báo cáo'),
+ 'nav.devices': entry('Thiết bị'),
+ 'nav.audit': entry('Nhật ký kiểm toán'),
+ 'nav.settings': entry('Cài đặt'),
+ 'role.owner': entry('Chủ sở hữu'),
+ 'role.admin': entry('Quản trị viên'),
+ 'role.analyst': entry('Chuyên viên phân tích'),
+ 'role.operator': entry('Nhân viên vận hành'),
+ 'role.approver': entry('Người phê duyệt'),
+ 'role.viewer': entry('Người xem'),
+ 'scope.organization': entry('Tổ chức'),
+ 'scope.workspace': entry('Không gian làm việc'),
+ 'scope.project': entry('Dự án'),
+ 'dataMode.local.label': entry('Cục bộ'),
+ 'dataMode.local.description': entry(
+ 'Dữ liệu gốc được giữ trên thiết bị đã được cấp quyền; chỉ thông tin được chính sách cho phép mới được đồng bộ.',
+ ),
+ 'dataMode.hybrid.label': entry('Kết hợp'),
+ 'dataMode.hybrid.description': entry(
+ 'Dữ liệu gốc có thể ở lại trên thiết bị, còn dữ liệu có cấu trúc và kết quả đã chọn có thể đồng bộ.',
+ ),
+ 'dataMode.cloud.label': entry('Đám mây'),
+ 'dataMode.cloud.description': entry(
+ 'Dữ liệu gốc đã được phê duyệt có thể được lưu trữ và xử lý an toàn trên hạ tầng đám mây.',
+ ),
+ 'job.status.created': entry('Đã tạo'),
+ 'job.status.queued': entry('Đang chờ xử lý'),
+ 'job.status.waitingForDevice': entry('Đang chờ thiết bị'),
+ 'job.status.dispatched': entry('Đã chuyển đến bộ xử lý'),
+ 'job.status.running': entry('Đang chạy'),
+ 'job.status.needsReview': entry('Cần xem xét'),
+ 'job.status.awaitingApproval': entry('Đang chờ phê duyệt'),
+ 'job.status.succeeded': entry('Đã hoàn thành'),
+ 'job.status.partiallySucceeded': entry('Hoàn thành một phần'),
+ 'job.status.failed': entry('Không thành công'),
+ 'job.status.cancelRequested': entry('Đang yêu cầu hủy'),
+ 'job.status.cancelled': entry('Đã hủy'),
+ 'job.status.expired': entry('Đã hết hạn'),
+ 'review.status.open': entry('Cần xử lý'),
+ 'review.status.acknowledged': entry('Đã ghi nhận'),
+ 'review.status.inReview': entry('Đang xem xét'),
+ 'review.status.resolved': entry('Đã giải quyết'),
+ 'review.status.dismissed': entry('Đã bỏ qua'),
+ 'review.status.suppressed': entry('Đã tạm ẩn'),
+ 'approval.status.pending': entry('Đang chờ quyết định'),
+ 'approval.status.approved': entry('Đã phê duyệt'),
+ 'approval.status.rejected': entry('Đã từ chối'),
+ 'approval.status.expired': entry('Yêu cầu đã hết hạn'),
+ 'approval.status.invalidated': entry('Phê duyệt không còn hiệu lực'),
+ 'approval.status.cancelled': entry('Yêu cầu đã bị hủy'),
+ 'offline.available': entry('Có thể sử dụng khi ngoại tuyến'),
+ 'offline.working': entry('Đang làm việc ngoại tuyến'),
+ 'offline.changesQueued': entry('Thay đổi đang chờ đồng bộ'),
+ 'offline.requiresConnection': entry('Thao tác này cần kết nối mạng'),
+ 'sync.idle': entry('Không có thay đổi cần đồng bộ'),
+ 'sync.inProgress': entry('Đang đồng bộ'),
+ 'sync.complete': entry('Đồng bộ hoàn tất'),
+ 'sync.paused': entry('Đã tạm dừng đồng bộ'),
+ 'sync.conflict': entry('Có xung đột cần xử lý'),
+ 'sync.failed': entry('Đồng bộ chưa thành công'),
+ 'sync.waitingForNetwork': entry('Đang chờ kết nối mạng'),
+ 'sync.lastCompletedAt': entry('Đồng bộ gần nhất lúc {time}.', { time: 'text' }),
+ 'error.generic': entry('Đã xảy ra lỗi. Dữ liệu của bạn vẫn được giữ nguyên.'),
+ 'error.genericWithCorrelationId': entry('Đã xảy ra lỗi. Mã đối chiếu: {correlationId}.', {
+ correlationId: 'identifier',
+ }),
+ 'error.invalidRequest': entry('Yêu cầu không hợp lệ. Hãy kiểm tra thông tin và thử lại.'),
+ 'error.unauthorized': entry('Phiên đăng nhập không hợp lệ. Hãy đăng nhập lại.'),
+ 'error.forbidden': entry('Bạn không có quyền thực hiện thao tác này.'),
+ 'error.notFound': entry('Không tìm thấy mục này hoặc bạn không có quyền truy cập.'),
+ 'error.conflict': entry('Dữ liệu đã thay đổi. Hãy tải lại trước khi tiếp tục.'),
+ 'error.rateLimited': entry('Đang có quá nhiều yêu cầu. Hãy thử lại sau.'),
+ 'error.serviceUnavailable': entry(
+ 'Dịch vụ tạm thời không khả dụng. Dữ liệu của bạn vẫn được giữ nguyên.',
+ ),
+ 'error.networkUnavailable': entry(
+ 'Không có kết nối mạng. Các thay đổi được phép sẽ được lưu và đồng bộ sau.',
+ ),
+ 'error.sourceOffline': entry('Thiết bị chứa dữ liệu gốc hiện đang ngoại tuyến.'),
+ 'error.sessionExpired': entry('Phiên làm việc đã hết hạn. Hãy đăng nhập lại để tiếp tục.'),
+ 'retry.now': entry('Thử lại ngay'),
+ 'retry.later': entry('Hãy thử lại sau. Dữ liệu đã nhập vẫn được giữ nguyên.'),
+ 'retry.afterSeconds.one': entry('Thử lại sau {seconds} giây.', { seconds: 'number' }),
+ 'retry.afterSeconds.other': entry('Thử lại sau {seconds} giây.', { seconds: 'number' }),
+ 'module.folderAutopilot': entry('Folder Autopilot'),
+ 'module.spreadsheetAuditor': entry('Spreadsheet Auditor'),
+ 'module.quoteIntelligence': entry('Quote Intelligence'),
+ 'module.operationsCapture': entry('Operations Capture'),
+ 'module.invoiceLeakDetector': entry('Invoice Leak Detector'),
+ 'module.clientReportFactory': entry('Client Report Factory'),
+ 'module.privateDataAnalyst': entry('Private Data Analyst'),
+ 'module.migrationReady': entry('Migration Ready'),
+ 'module.dataQualityGuard': entry('Data Quality Guard'),
+ 'module.embeddedImporter': entry('Embedded Importer'),
+ 'accessibility.mainNavigation': entry('Điều hướng chính'),
+ 'accessibility.loading': entry('Đang tải nội dung'),
+ 'accessibility.requiredField': entry('Trường bắt buộc'),
+ 'accessibility.progressLabel': entry('Tiến độ: {current} trên {total}.', {
+ current: 'number',
+ total: 'number',
+ }),
+ 'status.ready': entry('Sẵn sàng'),
+ 'status.inProgress': entry('Đang thực hiện'),
+ 'status.completed': entry('Đã hoàn tất'),
+} as const;
+
+export type MessageKeyV1 = keyof typeof vietnameseCatalogV1;
+export type MessageCatalogV1 = Readonly>;
+
+const englishCatalogV1: MessageCatalogV1 = {
+ 'product.name': entry('DataBreeze'),
+ 'common.yes': entry('Yes'),
+ 'common.no': entry('No'),
+ 'common.notAvailable': entry('Not available'),
+ 'common.unknown': entry('Unknown'),
+ 'action.add': entry('Add'),
+ 'action.approve': entry('Approve'),
+ 'action.assign': entry('Assign'),
+ 'action.cancel': entry('Cancel'),
+ 'action.close': entry('Close'),
+ 'action.confirm': entry('Confirm'),
+ 'action.continue': entry('Continue'),
+ 'action.create': entry('Create'),
+ 'action.delete': entry('Delete'),
+ 'action.edit': entry('Edit'),
+ 'action.open': entry('Open'),
+ 'action.reject': entry('Reject'),
+ 'action.retry': entry('Retry'),
+ 'action.save': entry('Save'),
+ 'action.search': entry('Search'),
+ 'action.submit': entry('Submit'),
+ 'nav.home': entry('Home'),
+ 'nav.inbox': entry('Inbox'),
+ 'nav.datasets': entry('Datasets'),
+ 'nav.jobs': entry('Jobs'),
+ 'nav.reviews': entry('Reviews'),
+ 'nav.approvals': entry('Approvals'),
+ 'nav.reports': entry('Reports'),
+ 'nav.devices': entry('Devices'),
+ 'nav.audit': entry('Audit log'),
+ 'nav.settings': entry('Settings'),
+ 'role.owner': entry('Owner'),
+ 'role.admin': entry('Admin'),
+ 'role.analyst': entry('Analyst'),
+ 'role.operator': entry('Operator'),
+ 'role.approver': entry('Approver'),
+ 'role.viewer': entry('Viewer'),
+ 'scope.organization': entry('Organization'),
+ 'scope.workspace': entry('Workspace'),
+ 'scope.project': entry('Project'),
+ 'dataMode.local.label': entry('Local'),
+ 'dataMode.local.description': entry(
+ 'Original data stays on an approved device; only policy-approved information synchronizes.',
+ ),
+ 'dataMode.hybrid.label': entry('Hybrid'),
+ 'dataMode.hybrid.description': entry(
+ 'Original data can remain on a device while selected structured data and results synchronize.',
+ ),
+ 'dataMode.cloud.label': entry('Cloud'),
+ 'dataMode.cloud.description': entry(
+ 'Authorized original data can be stored and processed securely in cloud infrastructure.',
+ ),
+ 'job.status.created': entry('Created'),
+ 'job.status.queued': entry('Queued'),
+ 'job.status.waitingForDevice': entry('Waiting for device'),
+ 'job.status.dispatched': entry('Sent to processor'),
+ 'job.status.running': entry('Running'),
+ 'job.status.needsReview': entry('Needs review'),
+ 'job.status.awaitingApproval': entry('Awaiting approval'),
+ 'job.status.succeeded': entry('Succeeded'),
+ 'job.status.partiallySucceeded': entry('Partially succeeded'),
+ 'job.status.failed': entry('Failed'),
+ 'job.status.cancelRequested': entry('Cancellation requested'),
+ 'job.status.cancelled': entry('Cancelled'),
+ 'job.status.expired': entry('Expired'),
+ 'review.status.open': entry('Open'),
+ 'review.status.acknowledged': entry('Acknowledged'),
+ 'review.status.inReview': entry('In review'),
+ 'review.status.resolved': entry('Resolved'),
+ 'review.status.dismissed': entry('Dismissed'),
+ 'review.status.suppressed': entry('Suppressed'),
+ 'approval.status.pending': entry('Decision pending'),
+ 'approval.status.approved': entry('Approved'),
+ 'approval.status.rejected': entry('Rejected'),
+ 'approval.status.expired': entry('Request expired'),
+ 'approval.status.invalidated': entry('Approval invalidated'),
+ 'approval.status.cancelled': entry('Request cancelled'),
+ 'offline.available': entry('Available offline'),
+ 'offline.working': entry('Working offline'),
+ 'offline.changesQueued': entry('Changes are queued for sync'),
+ 'offline.requiresConnection': entry('This action requires a network connection'),
+ 'sync.idle': entry('No changes to synchronize'),
+ 'sync.inProgress': entry('Synchronizing'),
+ 'sync.complete': entry('Sync complete'),
+ 'sync.paused': entry('Sync paused'),
+ 'sync.conflict': entry('Conflict needs attention'),
+ 'sync.failed': entry('Sync did not complete'),
+ 'sync.waitingForNetwork': entry('Waiting for a network connection'),
+ 'sync.lastCompletedAt': entry('Last synchronized at {time}.', { time: 'text' }),
+ 'error.generic': entry('Something went wrong. Your data has been preserved.'),
+ 'error.genericWithCorrelationId': entry(
+ 'Something went wrong. Reference code: {correlationId}.',
+ {
+ correlationId: 'identifier',
+ },
+ ),
+ 'error.invalidRequest': entry('The request is invalid. Check the information and try again.'),
+ 'error.unauthorized': entry('Your sign-in is no longer valid. Sign in again.'),
+ 'error.forbidden': entry('You do not have permission to perform this action.'),
+ 'error.notFound': entry('This item was not found or you do not have access.'),
+ 'error.conflict': entry('The data changed. Reload before continuing.'),
+ 'error.rateLimited': entry('There are too many requests. Try again later.'),
+ 'error.serviceUnavailable': entry(
+ 'The service is temporarily unavailable. Your data has been preserved.',
+ ),
+ 'error.networkUnavailable': entry(
+ 'There is no network connection. Allowed changes will wait to sync.',
+ ),
+ 'error.sourceOffline': entry('The device containing the original data is offline.'),
+ 'error.sessionExpired': entry('Your session expired. Sign in again to continue.'),
+ 'retry.now': entry('Try again now'),
+ 'retry.later': entry('Try again later. Your entered data has been preserved.'),
+ 'retry.afterSeconds.one': entry('Try again in {seconds} second.', { seconds: 'number' }),
+ 'retry.afterSeconds.other': entry('Try again in {seconds} seconds.', { seconds: 'number' }),
+ 'module.folderAutopilot': entry('Folder Autopilot'),
+ 'module.spreadsheetAuditor': entry('Spreadsheet Auditor'),
+ 'module.quoteIntelligence': entry('Quote Intelligence'),
+ 'module.operationsCapture': entry('Operations Capture'),
+ 'module.invoiceLeakDetector': entry('Invoice Leak Detector'),
+ 'module.clientReportFactory': entry('Client Report Factory'),
+ 'module.privateDataAnalyst': entry('Private Data Analyst'),
+ 'module.migrationReady': entry('Migration Ready'),
+ 'module.dataQualityGuard': entry('Data Quality Guard'),
+ 'module.embeddedImporter': entry('Embedded Importer'),
+ 'accessibility.mainNavigation': entry('Main navigation'),
+ 'accessibility.loading': entry('Loading content'),
+ 'accessibility.requiredField': entry('Required field'),
+ 'accessibility.progressLabel': entry('Progress: {current} of {total}.', {
+ current: 'number',
+ total: 'number',
+ }),
+ 'status.ready': entry('Ready'),
+ 'status.inProgress': entry('In progress'),
+ 'status.completed': entry('Completed'),
+};
+
+export const MESSAGE_KEYS_V1 = Object.freeze(Object.keys(vietnameseCatalogV1) as MessageKeyV1[]);
+
+export const MESSAGE_CATALOGS_V1: Readonly> =
+ Object.freeze({
+ 'vi-VN': Object.freeze(vietnameseCatalogV1),
+ en: Object.freeze(englishCatalogV1),
+ });
diff --git a/packages/i18n/src/errors-v1.ts b/packages/i18n/src/errors-v1.ts
new file mode 100644
index 00000000..041c5d90
--- /dev/null
+++ b/packages/i18n/src/errors-v1.ts
@@ -0,0 +1,34 @@
+export type I18nErrorCodeV1 =
+ | 'EXTRA_PARAMETER'
+ | 'INVALID_ARGUMENT'
+ | 'INVALID_CURRENCY'
+ | 'INVALID_DATE'
+ | 'INVALID_LOCALE'
+ | 'INVALID_NUMBER'
+ | 'INVALID_PARAMETER'
+ | 'INVALID_TIME_ZONE'
+ | 'MISSING_MESSAGE'
+ | 'MISSING_PARAMETER';
+
+const ERROR_MESSAGES_V1: Readonly> = Object.freeze({
+ EXTRA_PARAMETER: 'The message received an undeclared parameter.',
+ INVALID_ARGUMENT: 'The internationalization argument is invalid.',
+ INVALID_CURRENCY: 'The currency code is not supported.',
+ INVALID_DATE: 'The date value is invalid.',
+ INVALID_LOCALE: 'The locale is not supported.',
+ INVALID_NUMBER: 'The numeric value is invalid or outside the supported range.',
+ INVALID_PARAMETER: 'The message parameter has an invalid value.',
+ INVALID_TIME_ZONE: 'An explicit supported time zone is required.',
+ MISSING_MESSAGE: 'The message key is not present in the catalog.',
+ MISSING_PARAMETER: 'A required message parameter is missing.',
+});
+
+export class I18nErrorV1 extends Error {
+ readonly code: I18nErrorCodeV1;
+
+ constructor(code: I18nErrorCodeV1) {
+ super(ERROR_MESSAGES_V1[code]);
+ this.name = 'I18nErrorV1';
+ this.code = code;
+ }
+}
diff --git a/packages/i18n/src/formatting-v1.ts b/packages/i18n/src/formatting-v1.ts
new file mode 100644
index 00000000..e45eaee6
--- /dev/null
+++ b/packages/i18n/src/formatting-v1.ts
@@ -0,0 +1,386 @@
+import type { SupportedLocaleV1 } from './catalogs-v1.ts';
+import { I18nErrorV1 } from './errors-v1.ts';
+import {
+ convertToStringIntrinsicV1,
+ createDateIntrinsicV1,
+ dateTimestampIntrinsicV1,
+ formatDateTimeIntrinsicV1,
+ formatListIntrinsicV1,
+ formatNumberIntrinsicV1,
+ formatRelativeTimeIntrinsicV1,
+ selectPluralIntrinsicV1,
+ supportedValuesIntrinsicV1,
+ trimStringIntrinsicV1,
+} from './intrinsics-v1.ts';
+import { assertSupportedLocaleV1 } from './locale-v1.ts';
+import { readClosedDataObjectV1 } from './safe-input-v1.ts';
+
+const FRACTION_KEYS_V1 = new Set([
+ 'locale',
+ 'maximumFractionDigits',
+ 'minimumFractionDigits',
+ 'useGrouping',
+]);
+const CURRENCY_KEYS_V1 = new Set([...FRACTION_KEYS_V1, 'currency', 'currencyDisplay']);
+const DATE_TIME_KEYS_V1 = new Set(['dateStyle', 'hour12', 'locale', 'timeStyle', 'timeZone']);
+const LIST_KEYS_V1 = new Set(['locale', 'style', 'type']);
+const RELATIVE_TIME_KEYS_V1 = new Set(['locale', 'numeric', 'style']);
+const PLURAL_KEYS_V1 = new Set(['locale', 'type']);
+const CURRENCY_CODES_V1 = new Set(supportedValuesIntrinsicV1('currency'));
+const MAX_LIST_ITEMS_V1 = 1_000;
+const RELATIVE_TIME_UNITS_V1 = new Set([
+ 'day',
+ 'days',
+ 'hour',
+ 'hours',
+ 'minute',
+ 'minutes',
+ 'month',
+ 'months',
+ 'quarter',
+ 'quarters',
+ 'second',
+ 'seconds',
+ 'week',
+ 'weeks',
+ 'year',
+ 'years',
+]);
+
+interface FractionOptionsV1 {
+ readonly locale: SupportedLocaleV1;
+ readonly minimumFractionDigits?: number;
+ readonly maximumFractionDigits?: number;
+ readonly useGrouping?: boolean;
+}
+
+export type DecimalFormatOptionsV1 = FractionOptionsV1;
+
+export interface CurrencyFormatOptionsV1 extends FractionOptionsV1 {
+ readonly currency: string;
+ readonly currencyDisplay?: 'code' | 'name' | 'narrowSymbol' | 'symbol';
+}
+
+export type PercentFormatOptionsV1 = FractionOptionsV1;
+
+export interface DateTimeFormatOptionsV1 {
+ readonly locale: SupportedLocaleV1;
+ readonly timeZone: string;
+ readonly dateStyle?: 'full' | 'long' | 'medium' | 'short';
+ readonly timeStyle?: 'full' | 'long' | 'medium' | 'short';
+ readonly hour12?: boolean;
+}
+
+export interface ListFormatOptionsV1 {
+ readonly locale: SupportedLocaleV1;
+ readonly type?: 'conjunction' | 'disjunction' | 'unit';
+ readonly style?: 'long' | 'narrow' | 'short';
+}
+
+export interface RelativeTimeFormatOptionsV1 {
+ readonly locale: SupportedLocaleV1;
+ readonly numeric?: 'always' | 'auto';
+ readonly style?: 'long' | 'narrow' | 'short';
+}
+
+export interface PluralFormatOptionsV1 {
+ readonly locale: SupportedLocaleV1;
+ readonly type?: 'cardinal' | 'ordinal';
+}
+
+function requiredLocale(options: Readonly>): SupportedLocaleV1 {
+ const locale = options['locale'];
+ assertSupportedLocaleV1(locale);
+ return locale;
+}
+
+function finiteNumber(value: unknown): number {
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
+ throw new I18nErrorV1('INVALID_NUMBER');
+ }
+ return value;
+}
+
+function optionalEnum(value: unknown, values: readonly T[]): T | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value !== 'string') {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ for (let index = 0; index < values.length; index += 1) {
+ if (value === values[index]) {
+ return value as T;
+ }
+ }
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+}
+
+function optionalBoolean(value: unknown): boolean | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value !== 'boolean') {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ return value;
+}
+
+function optionalFractionDigit(value: unknown): number | undefined {
+ if (value === undefined) {
+ return undefined;
+ }
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0 || value > 20) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ return value;
+}
+
+function snapshotDateV1(value: unknown): Date {
+ try {
+ const timestamp = typeof value === 'number' ? value : dateTimestampIntrinsicV1(value);
+ if (!Number.isFinite(timestamp)) {
+ throw new I18nErrorV1('INVALID_DATE');
+ }
+ const date = createDateIntrinsicV1(timestamp);
+ if (!Number.isFinite(dateTimestampIntrinsicV1(date))) {
+ throw new I18nErrorV1('INVALID_DATE');
+ }
+ return date;
+ } catch {
+ throw new I18nErrorV1('INVALID_DATE');
+ }
+}
+
+function snapshotStringListV1(value: unknown): readonly string[] {
+ try {
+ if (!Array.isArray(value)) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(value, 'length');
+ if (
+ lengthDescriptor === undefined ||
+ !Object.hasOwn(lengthDescriptor, 'value') ||
+ typeof lengthDescriptor.value !== 'number' ||
+ !Number.isInteger(lengthDescriptor.value) ||
+ lengthDescriptor.value < 0 ||
+ lengthDescriptor.value > MAX_LIST_ITEMS_V1
+ ) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const length = lengthDescriptor.value;
+ for (const key of Reflect.ownKeys(value)) {
+ if (key === 'length') {
+ continue;
+ }
+ if (typeof key !== 'string') {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const index = Number(key);
+ if (
+ !Number.isSafeInteger(index) ||
+ index < 0 ||
+ index >= length ||
+ convertToStringIntrinsicV1(index) !== key
+ ) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ }
+
+ const snapshot: string[] = [];
+ for (let index = 0; index < length; index += 1) {
+ const descriptor = Object.getOwnPropertyDescriptor(value, convertToStringIntrinsicV1(index));
+ if (
+ descriptor === undefined ||
+ !Object.hasOwn(descriptor, 'value') ||
+ typeof descriptor.value !== 'string'
+ ) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ snapshot.push(descriptor.value);
+ }
+ return snapshot;
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
+
+function numberFormatOptions(
+ input: unknown,
+ allowedKeys: ReadonlySet,
+): { readonly locale: SupportedLocaleV1; readonly options: Intl.NumberFormatOptions } {
+ const source = readClosedDataObjectV1(input, allowedKeys);
+ const locale = requiredLocale(source);
+ const minimumFractionDigits = optionalFractionDigit(source['minimumFractionDigits']);
+ const maximumFractionDigits = optionalFractionDigit(source['maximumFractionDigits']);
+ if (
+ minimumFractionDigits !== undefined &&
+ maximumFractionDigits !== undefined &&
+ minimumFractionDigits > maximumFractionDigits
+ ) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const useGrouping = optionalBoolean(source['useGrouping']);
+ return {
+ locale,
+ options: {
+ ...(minimumFractionDigits === undefined ? {} : { minimumFractionDigits }),
+ ...(maximumFractionDigits === undefined ? {} : { maximumFractionDigits }),
+ ...(useGrouping === undefined ? {} : { useGrouping }),
+ },
+ };
+}
+
+export function formatDateTimeV1(value: number | Date, input: DateTimeFormatOptionsV1): string {
+ const source = readClosedDataObjectV1(input, DATE_TIME_KEYS_V1);
+ const locale = requiredLocale(source);
+ const rawTimeZone = source['timeZone'];
+ if (typeof rawTimeZone !== 'string' || trimStringIntrinsicV1(rawTimeZone) === '') {
+ throw new I18nErrorV1('INVALID_TIME_ZONE');
+ }
+ const date = snapshotDateV1(value);
+ const dateStyle = optionalEnum(source['dateStyle'], ['full', 'long', 'medium', 'short']);
+ const timeStyle = optionalEnum(source['timeStyle'], ['full', 'long', 'medium', 'short']);
+ const hour12 = optionalBoolean(source['hour12']);
+ try {
+ return formatDateTimeIntrinsicV1(
+ locale,
+ {
+ timeZone: rawTimeZone,
+ dateStyle: dateStyle ?? 'medium',
+ timeStyle: timeStyle ?? 'short',
+ ...(hour12 === undefined ? {} : { hour12 }),
+ },
+ date,
+ );
+ } catch {
+ throw new I18nErrorV1('INVALID_TIME_ZONE');
+ }
+}
+
+export function formatDecimalV1(value: number, input: DecimalFormatOptionsV1): string {
+ const { locale, options } = numberFormatOptions(input, FRACTION_KEYS_V1);
+ const number = finiteNumber(value);
+ try {
+ return formatNumberIntrinsicV1(locale, { ...options, style: 'decimal' }, number);
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
+
+export function formatCurrencyV1(value: number, input: CurrencyFormatOptionsV1): string {
+ const source = readClosedDataObjectV1(input, CURRENCY_KEYS_V1);
+ const { locale, options } = numberFormatOptions(source, CURRENCY_KEYS_V1);
+ const currency = source['currency'];
+ if (
+ typeof currency !== 'string' ||
+ !/^[A-Z]{3}$/u.test(currency) ||
+ !CURRENCY_CODES_V1.has(currency)
+ ) {
+ throw new I18nErrorV1('INVALID_CURRENCY');
+ }
+ const currencyDisplay = optionalEnum(source['currencyDisplay'], [
+ 'code',
+ 'name',
+ 'narrowSymbol',
+ 'symbol',
+ ]);
+ const number = finiteNumber(value);
+ try {
+ return formatNumberIntrinsicV1(
+ locale,
+ {
+ ...options,
+ style: 'currency',
+ currency,
+ ...(currencyDisplay === undefined ? {} : { currencyDisplay }),
+ },
+ number,
+ );
+ } catch {
+ throw new I18nErrorV1('INVALID_CURRENCY');
+ }
+}
+
+export function formatPercentV1(value: number, input: PercentFormatOptionsV1): string {
+ const { locale, options } = numberFormatOptions(input, FRACTION_KEYS_V1);
+ const maximumFractionDigits =
+ options.maximumFractionDigits ?? Math.max(options.minimumFractionDigits ?? 0, 3);
+ const number = finiteNumber(value);
+ try {
+ return formatNumberIntrinsicV1(
+ locale,
+ {
+ ...options,
+ maximumFractionDigits,
+ style: 'percent',
+ },
+ number,
+ );
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
+
+export function formatListV1(values: readonly string[], input: ListFormatOptionsV1): string {
+ const source = readClosedDataObjectV1(input, LIST_KEYS_V1);
+ const locale = requiredLocale(source);
+ const snapshot = snapshotStringListV1(values);
+ const type = optionalEnum(source['type'], ['conjunction', 'disjunction', 'unit']);
+ const style = optionalEnum(source['style'], ['long', 'narrow', 'short']);
+ try {
+ return formatListIntrinsicV1(
+ locale,
+ {
+ ...(type === undefined ? {} : { type }),
+ ...(style === undefined ? {} : { style }),
+ },
+ snapshot,
+ );
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
+
+export function formatRelativeTimeV1(
+ value: number,
+ unit: Intl.RelativeTimeFormatUnit,
+ input: RelativeTimeFormatOptionsV1,
+): string {
+ const source = readClosedDataObjectV1(input, RELATIVE_TIME_KEYS_V1);
+ const locale = requiredLocale(source);
+ if (typeof unit !== 'string' || !RELATIVE_TIME_UNITS_V1.has(unit)) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const numeric = optionalEnum(source['numeric'], ['always', 'auto']);
+ const style = optionalEnum(source['style'], ['long', 'narrow', 'short']);
+ const number = finiteNumber(value);
+ try {
+ return formatRelativeTimeIntrinsicV1(
+ locale,
+ {
+ ...(numeric === undefined ? {} : { numeric }),
+ ...(style === undefined ? {} : { style }),
+ },
+ number,
+ unit,
+ );
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
+
+export function selectPluralCategoryV1(
+ value: number,
+ input: PluralFormatOptionsV1,
+): Intl.LDMLPluralRule {
+ const source = readClosedDataObjectV1(input, PLURAL_KEYS_V1);
+ const locale = requiredLocale(source);
+ const type = optionalEnum(source['type'], ['cardinal', 'ordinal']);
+ const number = finiteNumber(value);
+ try {
+ return selectPluralIntrinsicV1(locale, type === undefined ? {} : { type }, number);
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
diff --git a/packages/i18n/src/intrinsics-v1.ts b/packages/i18n/src/intrinsics-v1.ts
new file mode 100644
index 00000000..418d758f
--- /dev/null
+++ b/packages/i18n/src/intrinsics-v1.ts
@@ -0,0 +1,168 @@
+import { I18nErrorV1 } from './errors-v1.ts';
+
+type IntrinsicFunctionV1 = (...arguments_: never[]) => unknown;
+
+const reflectApplyV1 = Reflect.apply;
+const DateConstructorV1 = Date;
+const IntlObjectV1 = Intl;
+const LocaleConstructorV1 = Intl.Locale;
+const DateTimeFormatConstructorV1 = Intl.DateTimeFormat;
+const NumberFormatConstructorV1 = Intl.NumberFormat;
+const ListFormatConstructorV1 = Intl.ListFormat;
+const RelativeTimeFormatConstructorV1 = Intl.RelativeTimeFormat;
+const PluralRulesConstructorV1 = Intl.PluralRules;
+
+function captureMethodV1(target: object, key: PropertyKey): IntrinsicFunctionV1 {
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
+ if (descriptor === undefined || typeof descriptor.value !== 'function') {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ return descriptor.value as IntrinsicFunctionV1;
+}
+
+function captureGetterV1(target: object, key: PropertyKey): IntrinsicFunctionV1 {
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
+ if (descriptor === undefined || typeof descriptor.get !== 'function') {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- The getter is intentionally captured with its receiver supplied by Reflect.apply.
+ return descriptor.get as IntrinsicFunctionV1;
+}
+
+function applyIntrinsicV1(
+ intrinsic: IntrinsicFunctionV1,
+ thisArgument: unknown,
+ argumentsList: readonly unknown[],
+): TResult {
+ return reflectApplyV1(intrinsic, thisArgument, argumentsList) as TResult;
+}
+
+const getCanonicalLocalesV1 = captureMethodV1(Intl, 'getCanonicalLocales');
+const supportedValuesOfV1 = captureMethodV1(Intl, 'supportedValuesOf');
+const localeLanguageGetterV1 = captureGetterV1(
+ LocaleConstructorV1.prototype as unknown as object,
+ 'language',
+);
+const dateGetTimeV1 = captureMethodV1(DateConstructorV1.prototype, 'getTime');
+const dateTimeFormatGetterV1 = captureGetterV1(DateTimeFormatConstructorV1.prototype, 'format');
+const numberFormatGetterV1 = captureGetterV1(NumberFormatConstructorV1.prototype, 'format');
+const listFormatMethodV1 = captureMethodV1(ListFormatConstructorV1.prototype, 'format');
+const relativeTimeFormatMethodV1 = captureMethodV1(
+ RelativeTimeFormatConstructorV1.prototype as unknown as object,
+ 'format',
+);
+const pluralSelectMethodV1 = captureMethodV1(
+ PluralRulesConstructorV1.prototype as unknown as object,
+ 'select',
+);
+const stringConversionV1 = captureMethodV1(globalThis, 'String');
+const stringCharCodeAtV1 = captureMethodV1(String.prototype, 'charCodeAt');
+const stringNormalizeV1 = captureMethodV1(String.prototype, 'normalize');
+const stringReplaceV1 = captureMethodV1(String.prototype, 'replace');
+const stringTrimV1 = captureMethodV1(String.prototype, 'trim');
+const stringSplitV1 = captureMethodV1(String.prototype, 'split');
+
+export function canonicalizeLocalesIntrinsicV1(locales: readonly string[]): readonly string[] {
+ return applyIntrinsicV1(getCanonicalLocalesV1, IntlObjectV1, [locales]);
+}
+
+export function localeLanguageIntrinsicV1(tag: string): string {
+ const locale = new LocaleConstructorV1(tag);
+ return applyIntrinsicV1(localeLanguageGetterV1, locale, []);
+}
+
+export function supportedValuesIntrinsicV1(key: 'currency'): readonly string[] {
+ return applyIntrinsicV1(supportedValuesOfV1, IntlObjectV1, [key]);
+}
+
+export function dateTimestampIntrinsicV1(value: unknown): number {
+ return applyIntrinsicV1(dateGetTimeV1, value, []);
+}
+
+export function createDateIntrinsicV1(timestamp: number): Date {
+ return new DateConstructorV1(timestamp);
+}
+
+export function formatDateTimeIntrinsicV1(
+ locale: string,
+ options: Intl.DateTimeFormatOptions,
+ value: Date,
+): string {
+ const formatter = new DateTimeFormatConstructorV1(locale, options);
+ const format = applyIntrinsicV1<(date?: Date | number) => string>(
+ dateTimeFormatGetterV1,
+ formatter,
+ [],
+ );
+ return format(value);
+}
+
+export function formatNumberIntrinsicV1(
+ locale: string,
+ options: Intl.NumberFormatOptions,
+ value: number,
+): string {
+ const formatter = new NumberFormatConstructorV1(locale, options);
+ const format = applyIntrinsicV1<(number?: number | bigint) => string>(
+ numberFormatGetterV1,
+ formatter,
+ [],
+ );
+ return format(value);
+}
+
+export function formatListIntrinsicV1(
+ locale: string,
+ options: Intl.ListFormatOptions,
+ values: readonly string[],
+): string {
+ const formatter = new ListFormatConstructorV1(locale, options);
+ return applyIntrinsicV1(listFormatMethodV1, formatter, [values]);
+}
+
+export function formatRelativeTimeIntrinsicV1(
+ locale: string,
+ options: Intl.RelativeTimeFormatOptions,
+ value: number,
+ unit: Intl.RelativeTimeFormatUnit,
+): string {
+ const formatter = new RelativeTimeFormatConstructorV1(locale, options);
+ return applyIntrinsicV1(relativeTimeFormatMethodV1, formatter, [value, unit]);
+}
+
+export function selectPluralIntrinsicV1(
+ locale: string,
+ options: Intl.PluralRulesOptions,
+ value: number,
+): Intl.LDMLPluralRule {
+ const formatter = new PluralRulesConstructorV1(locale, options);
+ return applyIntrinsicV1(pluralSelectMethodV1, formatter, [value]);
+}
+
+export function stringCodeUnitAtIntrinsicV1(value: string, index: number): number {
+ return applyIntrinsicV1(stringCharCodeAtV1, value, [index]);
+}
+
+export function convertToStringIntrinsicV1(value: unknown): string {
+ return applyIntrinsicV1(stringConversionV1, undefined, [value]);
+}
+
+export function normalizeStringIntrinsicV1(value: string): string {
+ return applyIntrinsicV1(stringNormalizeV1, value, ['NFC']);
+}
+
+export function replaceStringIntrinsicV1(
+ value: string,
+ pattern: RegExp,
+ replacement: (substring: string, capture: string) => string,
+): string {
+ return applyIntrinsicV1(stringReplaceV1, value, [pattern, replacement]);
+}
+
+export function trimStringIntrinsicV1(value: string): string {
+ return applyIntrinsicV1(stringTrimV1, value, []);
+}
+
+export function splitStringIntrinsicV1(value: string, separator: string): readonly string[] {
+ return applyIntrinsicV1(stringSplitV1, value, [separator]);
+}
diff --git a/packages/i18n/src/locale-v1.ts b/packages/i18n/src/locale-v1.ts
new file mode 100644
index 00000000..f6ac5e47
--- /dev/null
+++ b/packages/i18n/src/locale-v1.ts
@@ -0,0 +1,175 @@
+import { DEFAULT_LOCALE_V1, SUPPORTED_LOCALES_V1, type SupportedLocaleV1 } from './catalogs-v1.ts';
+import { I18nErrorV1 } from './errors-v1.ts';
+import {
+ canonicalizeLocalesIntrinsicV1,
+ localeLanguageIntrinsicV1,
+ splitStringIntrinsicV1,
+ trimStringIntrinsicV1,
+} from './intrinsics-v1.ts';
+import { readClosedDataObjectV1 } from './safe-input-v1.ts';
+
+const NEGOTIATION_KEYS_V1 = new Set(['acceptLanguage', 'userLocale']);
+const Q_VALUE_V1 = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/u;
+
+interface CanonicalRangeV1 {
+ readonly canonical: string;
+ readonly locale: SupportedLocaleV1;
+ readonly specificity: number;
+}
+
+interface CandidateV1 {
+ readonly canonical: string;
+ readonly locale: SupportedLocaleV1 | '*';
+ readonly quality: number;
+ readonly order: number;
+ readonly specificity: number;
+}
+
+function canonicalSupportedRange(tag: string): CanonicalRangeV1 | undefined {
+ const trimmed = trimStringIntrinsicV1(tag);
+ if (trimmed === '' || trimmed.length > 255) {
+ return undefined;
+ }
+
+ try {
+ const canonicalLocales = canonicalizeLocalesIntrinsicV1([trimmed]);
+ if (canonicalLocales.length !== 1 || canonicalLocales[0] === undefined) {
+ return undefined;
+ }
+ const canonical = canonicalLocales[0];
+ const language = localeLanguageIntrinsicV1(canonical);
+ const locale = language === 'vi' ? 'vi-VN' : language === 'en' ? 'en' : undefined;
+ if (locale === undefined) {
+ return undefined;
+ }
+ return { canonical, locale, specificity: splitStringIntrinsicV1(canonical, '-').length };
+ } catch {
+ return undefined;
+ }
+}
+
+function parseCandidate(part: string, order: number): CandidateV1 | undefined {
+ const sections = splitStringIntrinsicV1(part, ';').map((section) =>
+ trimStringIntrinsicV1(section),
+ );
+ if (sections.length > 2 || sections[0] === '') {
+ return undefined;
+ }
+ let quality = 1;
+ if (sections.length === 2) {
+ const match = /^q=(.+)$/iu.exec(sections[1] ?? '');
+ if (match === null || !Q_VALUE_V1.test(match[1] ?? '')) {
+ return undefined;
+ }
+ quality = Number(match[1]);
+ }
+
+ if (sections[0] === '*') {
+ return { canonical: '*', locale: '*', quality, order, specificity: 0 };
+ }
+ const range = canonicalSupportedRange(sections[0] ?? '');
+ return range === undefined ? undefined : { ...range, quality, order };
+}
+
+function consolidateDuplicateRanges(candidates: readonly CandidateV1[]): readonly CandidateV1[] {
+ const bestByRange = new Map();
+ for (const candidate of candidates) {
+ const key = candidate.locale === '*' ? '*' : candidate.canonical;
+ const current = bestByRange.get(key);
+ if (
+ current === undefined ||
+ candidate.quality > current.quality ||
+ (candidate.quality === current.quality && candidate.order < current.order)
+ ) {
+ bestByRange.set(key, candidate);
+ }
+ }
+ return [...bestByRange.values()];
+}
+
+function mostSpecificExplicit(
+ candidates: readonly CandidateV1[],
+ locale: SupportedLocaleV1,
+): CandidateV1 | undefined {
+ return candidates
+ .filter((candidate) => candidate.locale === locale)
+ .sort(
+ (left, right) =>
+ right.specificity - left.specificity ||
+ right.quality - left.quality ||
+ left.order - right.order,
+ )[0];
+}
+
+function bestWildcard(candidates: readonly CandidateV1[]): CandidateV1 | undefined {
+ return candidates
+ .filter((candidate) => candidate.locale === '*')
+ .sort((left, right) => right.quality - left.quality || left.order - right.order)[0];
+}
+
+function negotiateHeader(header: unknown): SupportedLocaleV1 {
+ if (typeof header !== 'string' || header.length === 0 || header.length > 8_192) {
+ return DEFAULT_LOCALE_V1;
+ }
+
+ const candidates = consolidateDuplicateRanges(
+ splitStringIntrinsicV1(header, ',')
+ .slice(0, 64)
+ .map(parseCandidate)
+ .filter((candidate): candidate is CandidateV1 => candidate !== undefined),
+ );
+ const wildcard = bestWildcard(candidates);
+ const scores = SUPPORTED_LOCALES_V1.map((locale, localeOrder) => {
+ const explicit = mostSpecificExplicit(candidates, locale);
+ const candidate = explicit ?? wildcard;
+ return {
+ locale,
+ localeOrder,
+ quality: candidate?.quality ?? 0,
+ order: candidate?.order ?? Number.MAX_SAFE_INTEGER,
+ };
+ }).filter((score) => score.quality > 0);
+
+ scores.sort(
+ (left, right) =>
+ right.quality - left.quality ||
+ left.order - right.order ||
+ left.localeOrder - right.localeOrder,
+ );
+ return scores[0]?.locale ?? DEFAULT_LOCALE_V1;
+}
+
+export interface LocaleNegotiationInputV1 {
+ readonly userLocale?: unknown;
+ readonly acceptLanguage?: unknown;
+}
+
+export function negotiateLocaleV1(input?: unknown): SupportedLocaleV1 {
+ if (typeof input === 'string') {
+ return negotiateHeader(input);
+ }
+ if (input === undefined || input === null) {
+ return DEFAULT_LOCALE_V1;
+ }
+
+ let negotiation: Readonly>;
+ try {
+ negotiation = readClosedDataObjectV1(input, NEGOTIATION_KEYS_V1);
+ } catch {
+ return DEFAULT_LOCALE_V1;
+ }
+ const userLocale = negotiation['userLocale'];
+ if (typeof userLocale === 'string') {
+ const preferred = canonicalSupportedRange(userLocale);
+ if (preferred !== undefined) {
+ return preferred.locale;
+ }
+ }
+ return negotiateHeader(negotiation['acceptLanguage']);
+}
+
+export function assertSupportedLocaleV1(locale: unknown): asserts locale is SupportedLocaleV1 {
+ if (locale !== 'vi-VN' && locale !== 'en') {
+ throw new I18nErrorV1('INVALID_LOCALE');
+ }
+}
diff --git a/packages/i18n/src/messages-v1.ts b/packages/i18n/src/messages-v1.ts
new file mode 100644
index 00000000..cec34e53
--- /dev/null
+++ b/packages/i18n/src/messages-v1.ts
@@ -0,0 +1,105 @@
+import {
+ MESSAGE_CATALOGS_V1,
+ MESSAGE_KEYS_V1,
+ type MessageKeyV1,
+ type MessageParameterTypeV1,
+ type SupportedLocaleV1,
+} from './catalogs-v1.ts';
+import { I18nErrorV1 } from './errors-v1.ts';
+import {
+ convertToStringIntrinsicV1,
+ replaceStringIntrinsicV1,
+ selectPluralIntrinsicV1,
+} from './intrinsics-v1.ts';
+import { assertSupportedLocaleV1 } from './locale-v1.ts';
+import { readClosedDataObjectV1 } from './safe-input-v1.ts';
+import { sanitizeTextParameterV1 } from './text-v1.ts';
+
+const MESSAGE_KEY_SET_V1 = new Set(MESSAGE_KEYS_V1);
+const PLACEHOLDER_V1 = /\{([A-Za-z][A-Za-z0-9]*)\}/gu;
+
+function normalizeParameterValue(type: MessageParameterTypeV1, value: unknown): number | string {
+ if (type === 'number' && typeof value === 'number' && Number.isFinite(value)) {
+ return value;
+ }
+ if (type === 'identifier' || type === 'text') {
+ return sanitizeTextParameterV1(value, type);
+ }
+ throw new I18nErrorV1('INVALID_PARAMETER');
+}
+
+export function formatMessageV1(
+ locale: SupportedLocaleV1,
+ key: MessageKeyV1,
+ parameters: unknown = {},
+): string {
+ assertSupportedLocaleV1(locale);
+ if (typeof key !== 'string' || !MESSAGE_KEY_SET_V1.has(key)) {
+ throw new I18nErrorV1('MISSING_MESSAGE');
+ }
+ const catalogMessage = MESSAGE_CATALOGS_V1[locale][key];
+ const parameterNames = Object.keys(catalogMessage.parameters);
+ const parameterNameSet = new Set(parameterNames);
+ let safeParameters: Readonly>;
+ try {
+ safeParameters = readClosedDataObjectV1(parameters, parameterNameSet);
+ } catch (error) {
+ if (
+ error instanceof I18nErrorV1 &&
+ error.code === 'INVALID_ARGUMENT' &&
+ parameters !== null &&
+ typeof parameters === 'object'
+ ) {
+ let keys: PropertyKey[] = [];
+ try {
+ keys = Reflect.ownKeys(parameters);
+ } catch {
+ throw error;
+ }
+ if (
+ keys.some(
+ (parameterName) => !parameterNameSet.has(convertToStringIntrinsicV1(parameterName)),
+ )
+ ) {
+ throw new I18nErrorV1('EXTRA_PARAMETER');
+ }
+ }
+ throw error;
+ }
+
+ const normalizedParameters: Record = Object.create(null) as Record<
+ string,
+ number | string
+ >;
+ for (const parameterName of parameterNames) {
+ if (!Object.hasOwn(safeParameters, parameterName)) {
+ throw new I18nErrorV1('MISSING_PARAMETER');
+ }
+ normalizedParameters[parameterName] = normalizeParameterValue(
+ catalogMessage.parameters[parameterName]!,
+ safeParameters[parameterName],
+ );
+ }
+
+ return replaceStringIntrinsicV1(
+ catalogMessage.message,
+ PLACEHOLDER_V1,
+ (_placeholder, parameterName) =>
+ convertToStringIntrinsicV1(normalizedParameters[parameterName]),
+ );
+}
+
+export function formatRetryAfterSecondsV1(locale: SupportedLocaleV1, seconds: number): string {
+ assertSupportedLocaleV1(locale);
+ if (!Number.isFinite(seconds) || !Number.isSafeInteger(seconds) || seconds < 0) {
+ throw new I18nErrorV1('INVALID_NUMBER');
+ }
+ let category: Intl.LDMLPluralRule;
+ try {
+ category = selectPluralIntrinsicV1(locale, {}, seconds);
+ } catch {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const key = category === 'one' ? 'retry.afterSeconds.one' : 'retry.afterSeconds.other';
+ return formatMessageV1(locale, key, { seconds });
+}
diff --git a/packages/i18n/src/safe-input-v1.ts b/packages/i18n/src/safe-input-v1.ts
new file mode 100644
index 00000000..4fab4566
--- /dev/null
+++ b/packages/i18n/src/safe-input-v1.ts
@@ -0,0 +1,34 @@
+import { I18nErrorV1 } from './errors-v1.ts';
+
+export function readClosedDataObjectV1(
+ value: unknown,
+ allowedKeys: ReadonlySet,
+): Readonly> {
+ try {
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const prototype = Object.getPrototypeOf(value) as unknown;
+ if (prototype !== Object.prototype && prototype !== null) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+
+ const result: Record = Object.create(null) as Record;
+ for (const key of Reflect.ownKeys(value)) {
+ if (typeof key !== 'string' || !allowedKeys.has(key)) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
+ if (descriptor === undefined || !Object.hasOwn(descriptor, 'value')) {
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+ result[key] = descriptor.value;
+ }
+ return result;
+ } catch (error) {
+ if (error instanceof I18nErrorV1) {
+ throw error;
+ }
+ throw new I18nErrorV1('INVALID_ARGUMENT');
+ }
+}
diff --git a/packages/i18n/src/text-v1.ts b/packages/i18n/src/text-v1.ts
new file mode 100644
index 00000000..6a8b669a
--- /dev/null
+++ b/packages/i18n/src/text-v1.ts
@@ -0,0 +1,49 @@
+import { I18nErrorV1 } from './errors-v1.ts';
+import { normalizeStringIntrinsicV1, stringCodeUnitAtIntrinsicV1 } from './intrinsics-v1.ts';
+
+const MAX_IDENTIFIER_LENGTH_V1 = 128;
+const MAX_TEXT_LENGTH_V1 = 512;
+const UNSAFE_TEXT_V1 = /(?:\p{Cc}|[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069])/u;
+const IDENTIFIER_V1 = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
+
+function hasUnpairedSurrogate(value: string): boolean {
+ for (let index = 0; index < value.length; index += 1) {
+ const codeUnit = stringCodeUnitAtIntrinsicV1(value, index);
+ if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
+ if (index + 1 >= value.length) {
+ return true;
+ }
+ const next = stringCodeUnitAtIntrinsicV1(value, index + 1);
+ if (next < 0xdc00 || next > 0xdfff) {
+ return true;
+ }
+ index += 1;
+ } else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
+ return true;
+ }
+ }
+ return false;
+}
+
+export function sanitizeTextParameterV1(value: unknown, kind: 'identifier' | 'text'): string {
+ if (typeof value !== 'string' || hasUnpairedSurrogate(value)) {
+ throw new I18nErrorV1('INVALID_PARAMETER');
+ }
+
+ let normalized: string;
+ try {
+ normalized = normalizeStringIntrinsicV1(value);
+ } catch {
+ throw new I18nErrorV1('INVALID_PARAMETER');
+ }
+ const maximumLength = kind === 'identifier' ? MAX_IDENTIFIER_LENGTH_V1 : MAX_TEXT_LENGTH_V1;
+ if (
+ normalized.length === 0 ||
+ normalized.length > maximumLength ||
+ UNSAFE_TEXT_V1.test(normalized) ||
+ (kind === 'identifier' && !IDENTIFIER_V1.test(normalized))
+ ) {
+ throw new I18nErrorV1('INVALID_PARAMETER');
+ }
+ return normalized;
+}
diff --git a/packages/i18n/src/v1.ts b/packages/i18n/src/v1.ts
new file mode 100644
index 00000000..c8daaca6
--- /dev/null
+++ b/packages/i18n/src/v1.ts
@@ -0,0 +1,5 @@
+export * from './catalogs-v1.ts';
+export * from './errors-v1.ts';
+export * from './formatting-v1.ts';
+export * from './locale-v1.ts';
+export * from './messages-v1.ts';
diff --git a/packages/i18n/test/built-public-api-smoke.mjs b/packages/i18n/test/built-public-api-smoke.mjs
new file mode 100644
index 00000000..2717c331
--- /dev/null
+++ b/packages/i18n/test/built-public-api-smoke.mjs
@@ -0,0 +1,10 @@
+import assert from 'node:assert/strict';
+
+const api = await import('../dist/v1.js');
+
+assert.equal(api.I18N_SCHEMA_VERSION_V1, 1);
+assert.equal(api.DEFAULT_LOCALE_V1, 'vi-VN');
+assert.equal(api.negotiateLocaleV1('en-US'), 'en');
+assert.equal(api.formatMessageV1('vi-VN', 'action.save'), 'Lưu');
+assert.equal(api.formatRetryAfterSecondsV1('en', 1), 'Try again in 1 second.');
+assert.equal(typeof api.formatCurrencyV1, 'function');
diff --git a/packages/i18n/test/catalogs-v1.test.mjs b/packages/i18n/test/catalogs-v1.test.mjs
new file mode 100644
index 00000000..444f3872
--- /dev/null
+++ b/packages/i18n/test/catalogs-v1.test.mjs
@@ -0,0 +1,228 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+const REQUIRED_KEYS = Object.freeze([
+ 'product.name',
+ 'common.yes',
+ 'common.no',
+ 'common.notAvailable',
+ 'common.unknown',
+ 'action.add',
+ 'action.approve',
+ 'action.assign',
+ 'action.cancel',
+ 'action.close',
+ 'action.confirm',
+ 'action.continue',
+ 'action.create',
+ 'action.delete',
+ 'action.edit',
+ 'action.open',
+ 'action.reject',
+ 'action.retry',
+ 'action.save',
+ 'action.search',
+ 'action.submit',
+ 'nav.home',
+ 'nav.inbox',
+ 'nav.datasets',
+ 'nav.jobs',
+ 'nav.reviews',
+ 'nav.approvals',
+ 'nav.reports',
+ 'nav.devices',
+ 'nav.audit',
+ 'nav.settings',
+ 'role.owner',
+ 'role.admin',
+ 'role.analyst',
+ 'role.operator',
+ 'role.approver',
+ 'role.viewer',
+ 'scope.organization',
+ 'scope.workspace',
+ 'scope.project',
+ 'dataMode.local.label',
+ 'dataMode.local.description',
+ 'dataMode.hybrid.label',
+ 'dataMode.hybrid.description',
+ 'dataMode.cloud.label',
+ 'dataMode.cloud.description',
+ 'job.status.created',
+ 'job.status.queued',
+ 'job.status.waitingForDevice',
+ 'job.status.dispatched',
+ 'job.status.running',
+ 'job.status.needsReview',
+ 'job.status.awaitingApproval',
+ 'job.status.succeeded',
+ 'job.status.partiallySucceeded',
+ 'job.status.failed',
+ 'job.status.cancelRequested',
+ 'job.status.cancelled',
+ 'job.status.expired',
+ 'review.status.open',
+ 'review.status.acknowledged',
+ 'review.status.inReview',
+ 'review.status.resolved',
+ 'review.status.dismissed',
+ 'review.status.suppressed',
+ 'approval.status.pending',
+ 'approval.status.approved',
+ 'approval.status.rejected',
+ 'approval.status.expired',
+ 'approval.status.invalidated',
+ 'approval.status.cancelled',
+ 'offline.available',
+ 'offline.working',
+ 'offline.changesQueued',
+ 'offline.requiresConnection',
+ 'sync.idle',
+ 'sync.inProgress',
+ 'sync.complete',
+ 'sync.paused',
+ 'sync.conflict',
+ 'sync.failed',
+ 'sync.waitingForNetwork',
+ 'sync.lastCompletedAt',
+ 'error.generic',
+ 'error.genericWithCorrelationId',
+ 'error.invalidRequest',
+ 'error.unauthorized',
+ 'error.forbidden',
+ 'error.notFound',
+ 'error.conflict',
+ 'error.rateLimited',
+ 'error.serviceUnavailable',
+ 'error.networkUnavailable',
+ 'error.sourceOffline',
+ 'error.sessionExpired',
+ 'retry.now',
+ 'retry.later',
+ 'retry.afterSeconds.one',
+ 'retry.afterSeconds.other',
+ 'module.folderAutopilot',
+ 'module.spreadsheetAuditor',
+ 'module.quoteIntelligence',
+ 'module.operationsCapture',
+ 'module.invoiceLeakDetector',
+ 'module.clientReportFactory',
+ 'module.privateDataAnalyst',
+ 'module.migrationReady',
+ 'module.dataQualityGuard',
+ 'module.embeddedImporter',
+ 'accessibility.mainNavigation',
+ 'accessibility.loading',
+ 'accessibility.requiredField',
+ 'accessibility.progressLabel',
+ 'status.ready',
+ 'status.inProgress',
+ 'status.completed',
+]);
+
+function placeholders(message) {
+ return [...message.matchAll(/\{([A-Za-z][A-Za-z0-9]*)\}/gu)].map((match) => match[1]);
+}
+
+function assertDeeplyFrozen(value) {
+ assert.equal(Object.isFrozen(value), true);
+ for (const child of Object.values(value)) {
+ if (child !== null && typeof child === 'object') {
+ assertDeeplyFrozen(child);
+ }
+ }
+}
+
+test('[IAM-016, WEB-013, DSK-021, AND-017, NCO-017] catalogs contain the complete bounded v1 vocabulary without fallback gaps', async () => {
+ const { MESSAGE_CATALOGS_V1, MESSAGE_KEYS_V1 } = await import('../src/v1.ts');
+ const viKeys = Object.keys(MESSAGE_CATALOGS_V1['vi-VN']);
+ const enKeys = Object.keys(MESSAGE_CATALOGS_V1.en);
+
+ assert.deepEqual(MESSAGE_KEYS_V1, REQUIRED_KEYS);
+ assert.deepEqual(viKeys, REQUIRED_KEYS);
+ assert.deepEqual(enKeys, REQUIRED_KEYS);
+});
+
+test('catalog messages and placeholder schemas are equivalent across locales', async () => {
+ const { MESSAGE_CATALOGS_V1, MESSAGE_KEYS_V1 } = await import('../src/v1.ts');
+
+ for (const key of MESSAGE_KEYS_V1) {
+ const vi = MESSAGE_CATALOGS_V1['vi-VN'][key];
+ const en = MESSAGE_CATALOGS_V1.en[key];
+ assert.deepEqual(vi.parameters, en.parameters, `${key} must use the same parameter types`);
+ assert.deepEqual(
+ [...new Set(placeholders(vi.message))].sort(),
+ Object.keys(vi.parameters).sort(),
+ `${key} Vietnamese placeholders must be declared`,
+ );
+ assert.deepEqual(
+ [...new Set(placeholders(en.message))].sort(),
+ Object.keys(en.parameters).sort(),
+ `${key} English placeholders must be declared`,
+ );
+ }
+});
+
+test('catalogs are deeply immutable and resist mutation', async () => {
+ const { MESSAGE_CATALOGS_V1, MESSAGE_KEYS_V1 } = await import('../src/v1.ts');
+
+ assertDeeplyFrozen(MESSAGE_CATALOGS_V1);
+ assertDeeplyFrozen(MESSAGE_KEYS_V1);
+ const before = MESSAGE_CATALOGS_V1['vi-VN']['action.save'].message;
+ assert.throws(() => {
+ MESSAGE_CATALOGS_V1['vi-VN']['action.save'].message = 'Thay đổi';
+ }, TypeError);
+ assert.equal(MESSAGE_CATALOGS_V1['vi-VN']['action.save'].message, before);
+});
+
+test('catalog copy is normalized, non-empty, plain text, and free of placeholders for unfinished work', async () => {
+ const { MESSAGE_CATALOGS_V1, MESSAGE_KEYS_V1 } = await import('../src/v1.ts');
+ const forbiddenText =
+ /(?:<|>|\p{Cc}|[\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]|\b(?:TODO|TBD|FIXME|lorem ipsum)\b)/iu;
+
+ for (const locale of ['vi-VN', 'en']) {
+ for (const key of MESSAGE_KEYS_V1) {
+ const message = MESSAGE_CATALOGS_V1[locale][key].message;
+ assert.equal(message, message.normalize('NFC'), `${locale}:${key} must be NFC`);
+ assert.equal(message.trim().length > 0, true, `${locale}:${key} must not be empty`);
+ assert.doesNotMatch(message, forbiddenText, `${locale}:${key} must remain safe plain text`);
+ }
+ }
+});
+
+test('Vietnamese foundation copy is primary professional copy rather than an English fallback', async () => {
+ const { MESSAGE_CATALOGS_V1, MESSAGE_KEYS_V1 } = await import('../src/v1.ts');
+ const canonicalNames = new Set([
+ 'product.name',
+ ...MESSAGE_KEYS_V1.filter((key) => key.startsWith('module.')),
+ ]);
+
+ for (const key of MESSAGE_KEYS_V1) {
+ if (!canonicalNames.has(key)) {
+ assert.notEqual(
+ MESSAGE_CATALOGS_V1['vi-VN'][key].message,
+ MESSAGE_CATALOGS_V1.en[key].message,
+ `${key} must not silently fall back to English`,
+ );
+ }
+ }
+ assert.equal(MESSAGE_CATALOGS_V1['vi-VN']['role.approver'].message, 'Người phê duyệt');
+ assert.match(MESSAGE_CATALOGS_V1['vi-VN']['dataMode.hybrid.description'].message, /dữ liệu/u);
+ assert.equal(MESSAGE_CATALOGS_V1['vi-VN']['sync.complete'].message, 'Đồng bộ hoàn tất');
+ assert.equal(
+ MESSAGE_CATALOGS_V1['vi-VN']['approval.status.cancelled'].message,
+ 'Yêu cầu đã bị hủy',
+ );
+ assert.match(
+ MESSAGE_CATALOGS_V1['vi-VN']['dataMode.local.description'].message,
+ /thiết bị đã được cấp quyền/u,
+ );
+ assert.match(
+ MESSAGE_CATALOGS_V1['vi-VN']['dataMode.cloud.description'].message,
+ /Dữ liệu gốc đã được phê duyệt/u,
+ );
+ assert.equal(
+ MESSAGE_CATALOGS_V1['vi-VN']['error.networkUnavailable'].message,
+ 'Không có kết nối mạng. Các thay đổi được phép sẽ được lưu và đồng bộ sau.',
+ );
+});
diff --git a/packages/i18n/test/formatting-v1.test.mjs b/packages/i18n/test/formatting-v1.test.mjs
new file mode 100644
index 00000000..5f812b30
--- /dev/null
+++ b/packages/i18n/test/formatting-v1.test.mjs
@@ -0,0 +1,147 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+function visibleSpaces(value) {
+ return value.replace(/[\u00a0\u202f]/gu, ' ');
+}
+
+test('[WEB-013] formats one instant in an explicit time zone without using the host time zone', async () => {
+ const { formatDateTimeV1 } = await import('../src/v1.ts');
+ const instant = Date.parse('2026-08-01T17:30:00.000Z');
+ const vietnamese = visibleSpaces(
+ formatDateTimeV1(instant, {
+ locale: 'vi-VN',
+ timeZone: 'Asia/Ho_Chi_Minh',
+ dateStyle: 'short',
+ timeStyle: 'short',
+ }),
+ );
+ const englishUtc = visibleSpaces(
+ formatDateTimeV1(instant, {
+ locale: 'en',
+ timeZone: 'UTC',
+ dateStyle: 'short',
+ timeStyle: 'short',
+ hour12: false,
+ }),
+ );
+
+ assert.match(vietnamese, /2\/8\/(?:26|2026)/u);
+ assert.match(vietnamese, /00:30/u);
+ assert.match(englishUtc, /8\/1\/(?:26|2026)/u);
+ assert.match(englishUtc, /17:30/u);
+});
+
+test('formats decimal, VND and other currencies, and percentages without changing input values', async () => {
+ const { formatCurrencyV1, formatDecimalV1, formatPercentV1 } = await import('../src/v1.ts');
+ const amount = 1234.5;
+
+ assert.equal(visibleSpaces(formatDecimalV1(amount, { locale: 'vi-VN' })), '1.234,5');
+ assert.equal(visibleSpaces(formatDecimalV1(amount, { locale: 'en' })), '1,234.5');
+ assert.match(
+ visibleSpaces(
+ formatCurrencyV1(amount, {
+ locale: 'vi-VN',
+ currency: 'VND',
+ minimumFractionDigits: 1,
+ maximumFractionDigits: 1,
+ }),
+ ),
+ /1\.234,5.*₫/u,
+ );
+ assert.match(
+ visibleSpaces(formatCurrencyV1(amount, { locale: 'en', currency: 'USD' })),
+ /\$1,234\.50/u,
+ );
+ assert.match(visibleSpaces(formatPercentV1(0.125, { locale: 'vi-VN' })), /12,5.*%/u);
+ assert.equal(amount, 1234.5);
+});
+
+test('formats lists, relative time, and plural categories for both locales', async () => {
+ const { formatListV1, formatRelativeTimeV1, selectPluralCategoryV1 } = await import(
+ '../src/v1.ts'
+ );
+
+ const viList = formatListV1(['Web', 'Máy tính', 'Android'], { locale: 'vi-VN' });
+ const enList = formatListV1(['Web', 'Desktop', 'Android'], { locale: 'en' });
+ assert.match(viList, /Web.*Máy tính.*Android/u);
+ assert.match(enList, /Web.*Desktop.*and.*Android/u);
+ assert.match(formatRelativeTimeV1(-2, 'day', { locale: 'vi-VN' }), /2 ngày trước/u);
+ assert.match(formatRelativeTimeV1(-2, 'day', { locale: 'en' }), /2 days ago/u);
+ assert.equal(selectPluralCategoryV1(1, { locale: 'en' }), 'one');
+ assert.equal(selectPluralCategoryV1(2, { locale: 'en' }), 'other');
+ assert.equal(selectPluralCategoryV1(1, { locale: 'vi-VN' }), 'other');
+});
+
+test('accepts every plural relative-time unit admitted by the public TypeScript type', async () => {
+ const { formatRelativeTimeV1 } = await import('../src/v1.ts');
+ const cases = [
+ ['years', '2 years ago'],
+ ['quarters', '2 quarters ago'],
+ ['months', '2 months ago'],
+ ['weeks', '2 weeks ago'],
+ ['days', '2 days ago'],
+ ['hours', '2 hours ago'],
+ ['minutes', '2 minutes ago'],
+ ['seconds', '2 seconds ago'],
+ ];
+
+ for (const [unit, expected] of cases) {
+ assert.equal(formatRelativeTimeV1(-2, unit, { locale: 'en' }), expected);
+ }
+});
+
+test('rejects invalid locale, time zone, date, currency, numeric values, units, and option keys', async () => {
+ const { formatCurrencyV1, formatDateTimeV1, formatDecimalV1, formatRelativeTimeV1, I18nErrorV1 } =
+ await import('../src/v1.ts');
+ const cases = [
+ [() => formatDecimalV1(1, { locale: 'fr' }), 'INVALID_LOCALE'],
+ [() => formatDecimalV1(Infinity, { locale: 'en' }), 'INVALID_NUMBER'],
+ [() => formatDateTimeV1(Number.NaN, { locale: 'en', timeZone: 'UTC' }), 'INVALID_DATE'],
+ [() => formatDateTimeV1(8.64e15 + 1, { locale: 'en', timeZone: 'UTC' }), 'INVALID_DATE'],
+ [() => formatDateTimeV1(0, { locale: 'en' }), 'INVALID_TIME_ZONE'],
+ [() => formatDateTimeV1(0, { locale: 'en', timeZone: 'Moon/Base' }), 'INVALID_TIME_ZONE'],
+ [() => formatCurrencyV1(1, { locale: 'en', currency: 'usd' }), 'INVALID_CURRENCY'],
+ [() => formatRelativeTimeV1(1, 'fortnight', { locale: 'en' }), 'INVALID_ARGUMENT'],
+ [() => formatDecimalV1(1, { locale: 'en', rawProviderOption: true }), 'INVALID_ARGUMENT'],
+ ];
+
+ for (const [operation, code] of cases) {
+ assert.throws(operation, (error) => error instanceof I18nErrorV1 && error.code === code);
+ }
+});
+
+test('rejects impossible fraction ranges and accessor-backed formatter options safely', async () => {
+ const { formatDecimalV1, I18nErrorV1 } = await import('../src/v1.ts');
+ let getterCalls = 0;
+ const options = {};
+ Object.defineProperty(options, 'locale', {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ return 'en';
+ },
+ });
+
+ assert.throws(
+ () =>
+ formatDecimalV1(1, {
+ locale: 'en',
+ minimumFractionDigits: 3,
+ maximumFractionDigits: 2,
+ }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_ARGUMENT',
+ );
+ assert.throws(
+ () => formatDecimalV1(1, options),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_ARGUMENT',
+ );
+ assert.equal(getterCalls, 0);
+
+ const { proxy, revoke } = Proxy.revocable({}, {});
+ revoke();
+ assert.throws(
+ () => formatDecimalV1(1, proxy),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_ARGUMENT',
+ );
+});
diff --git a/packages/i18n/test/intrinsic-capture-v1.test.mjs b/packages/i18n/test/intrinsic-capture-v1.test.mjs
new file mode 100644
index 00000000..cb6e5ea6
--- /dev/null
+++ b/packages/i18n/test/intrinsic-capture-v1.test.mjs
@@ -0,0 +1,317 @@
+import assert from 'node:assert/strict';
+import { inspect } from 'node:util';
+import test from 'node:test';
+
+function replaceProperty(target, key, replacement) {
+ const original = Object.getOwnPropertyDescriptor(target, key);
+ assert.notEqual(original, undefined, `missing intrinsic ${String(key)}`);
+ Object.defineProperty(target, key, replacement(original));
+ return () => Object.defineProperty(target, key, original);
+}
+
+function replaceValue(target, key, value) {
+ return replaceProperty(target, key, (original) => ({ ...original, value }));
+}
+
+function restoreAll(restorations) {
+ for (let index = restorations.length - 1; index >= 0; index -= 1) {
+ restorations[index]();
+ }
+}
+
+function hostileFunction(calls, marker) {
+ return function hostileIntrinsic() {
+ calls.push(marker);
+ throw new Error(marker);
+ };
+}
+
+test('rejects every lone surrogate without leaking input and accepts valid astral text', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+ const high = String.fromCharCode(0xd800);
+ const low = String.fromCharCode(0xdc00);
+ const invalidValues = [
+ `${high}surrogate-marker-leading-high`,
+ `surrogate-marker-${high}-middle-high`,
+ `surrogate-marker-trailing-high${high}`,
+ `${low}surrogate-marker-leading-low`,
+ `surrogate-marker-${low}-middle-low`,
+ `surrogate-marker-trailing-low${low}`,
+ ];
+
+ for (const time of invalidValues) {
+ assert.throws(
+ () => formatMessageV1('en', 'sync.lastCompletedAt', { time }),
+ (error) => {
+ assert.equal(error instanceof I18nErrorV1, true);
+ assert.equal(error.code, 'INVALID_PARAMETER');
+ assert.doesNotMatch(inspect(error), /surrogate-marker/u);
+ return true;
+ },
+ );
+ }
+ assert.equal(
+ formatMessageV1('en', 'sync.lastCompletedAt', { time: 'Launch \u{1f680}' }),
+ 'Last synchronized at Launch 🚀.',
+ );
+});
+
+test('uses captured locale canonicalization, constructor, and language getter intrinsics', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const LocaleIntrinsic = Intl.Locale;
+
+ try {
+ restorations.push(
+ replaceValue(Intl, 'getCanonicalLocales', hostileFunction(calls, 'canonical-marker')),
+ );
+ restorations.push(
+ replaceValue(Intl, 'Locale', hostileFunction(calls, 'locale-constructor-marker')),
+ );
+ restorations.push(
+ replaceProperty(LocaleIntrinsic.prototype, 'language', (original) => ({
+ ...original,
+ get: hostileFunction(calls, 'locale-language-marker'),
+ })),
+ );
+
+ assert.equal(negotiateLocaleV1('en-US-u-ca-gregory'), 'en');
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('uses captured Date and DateTimeFormat constructor and method intrinsics', async () => {
+ const { formatDateTimeV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const DateIntrinsic = Date;
+ const DateTimeFormatIntrinsic = Intl.DateTimeFormat;
+
+ try {
+ restorations.push(
+ replaceValue(globalThis, 'Date', hostileFunction(calls, 'date-constructor-marker')),
+ );
+ restorations.push(
+ replaceValue(
+ DateIntrinsic.prototype,
+ 'getTime',
+ hostileFunction(calls, 'date-get-time-marker'),
+ ),
+ );
+ restorations.push(
+ replaceValue(Intl, 'DateTimeFormat', hostileFunction(calls, 'date-time-constructor-marker')),
+ );
+ restorations.push(
+ replaceProperty(DateTimeFormatIntrinsic.prototype, 'format', (original) => ({
+ ...original,
+ get: hostileFunction(calls, 'date-time-format-marker'),
+ })),
+ );
+
+ assert.match(formatDateTimeV1(0, { locale: 'en', timeZone: 'UTC' }), /1970/u);
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('uses captured NumberFormat constructor and format getter intrinsics', async () => {
+ const { formatDecimalV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const NumberFormatIntrinsic = Intl.NumberFormat;
+
+ try {
+ restorations.push(
+ replaceValue(Intl, 'NumberFormat', hostileFunction(calls, 'number-constructor-marker')),
+ );
+ restorations.push(
+ replaceProperty(NumberFormatIntrinsic.prototype, 'format', (original) => ({
+ ...original,
+ get: hostileFunction(calls, 'number-format-marker'),
+ })),
+ );
+
+ assert.equal(formatDecimalV1(1234.5, { locale: 'en' }), '1,234.5');
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('uses captured ListFormat constructor and format method intrinsics', async () => {
+ const { formatListV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const ListFormatIntrinsic = Intl.ListFormat;
+
+ try {
+ restorations.push(
+ replaceValue(Intl, 'ListFormat', hostileFunction(calls, 'list-constructor-marker')),
+ );
+ restorations.push(
+ replaceValue(
+ ListFormatIntrinsic.prototype,
+ 'format',
+ hostileFunction(calls, 'list-format-marker'),
+ ),
+ );
+
+ assert.equal(formatListV1(['Web', 'Android'], { locale: 'en' }), 'Web and Android');
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('uses captured RelativeTimeFormat constructor and format method intrinsics', async () => {
+ const { formatRelativeTimeV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const RelativeTimeFormatIntrinsic = Intl.RelativeTimeFormat;
+
+ try {
+ restorations.push(
+ replaceValue(
+ Intl,
+ 'RelativeTimeFormat',
+ hostileFunction(calls, 'relative-constructor-marker'),
+ ),
+ );
+ restorations.push(
+ replaceValue(
+ RelativeTimeFormatIntrinsic.prototype,
+ 'format',
+ hostileFunction(calls, 'relative-format-marker'),
+ ),
+ );
+
+ assert.equal(formatRelativeTimeV1(-2, 'day', { locale: 'en' }), '2 days ago');
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('uses captured PluralRules constructor and select method for both plural APIs', async () => {
+ const { formatRetryAfterSecondsV1, selectPluralCategoryV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const PluralRulesIntrinsic = Intl.PluralRules;
+
+ try {
+ restorations.push(
+ replaceValue(Intl, 'PluralRules', hostileFunction(calls, 'plural-constructor-marker')),
+ );
+ restorations.push(
+ replaceValue(
+ PluralRulesIntrinsic.prototype,
+ 'select',
+ hostileFunction(calls, 'plural-select-marker'),
+ ),
+ );
+
+ assert.equal(selectPluralCategoryV1(1, { locale: 'en' }), 'one');
+ assert.equal(formatRetryAfterSecondsV1('en', 2), 'Try again in 2 seconds.');
+ assert.deepEqual(calls, []);
+ } finally {
+ restoreAll(restorations);
+ }
+});
+
+test('describes invalid retry seconds without claiming every failure is non-finite', async () => {
+ const { formatRetryAfterSecondsV1, I18nErrorV1 } = await import('../src/v1.ts');
+
+ for (const seconds of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1]) {
+ assert.throws(
+ () => formatRetryAfterSecondsV1('en', seconds),
+ (error) =>
+ error instanceof I18nErrorV1 &&
+ error.code === 'INVALID_NUMBER' &&
+ error.message === 'The numeric value is invalid or outside the supported range.',
+ );
+ }
+});
+
+test('preserves valid i18n outputs after global String and prototype methods are replaced', async () => {
+ const { formatListV1, formatMessageV1, formatRetryAfterSecondsV1, negotiateLocaleV1 } =
+ await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const StringIntrinsic = String;
+ let actual;
+
+ try {
+ for (const method of ['replace', 'replaceAll', 'startsWith', 'includes']) {
+ restorations.push(
+ replaceValue(
+ StringIntrinsic.prototype,
+ method,
+ hostileFunction(calls, `string-${method}-marker`),
+ ),
+ );
+ }
+ restorations.push(
+ replaceValue(globalThis, 'String', hostileFunction(calls, 'string-constructor-marker')),
+ );
+
+ actual = {
+ error: formatMessageV1('en', 'error.generic'),
+ list: formatListV1(['Web', 'Android'], { locale: 'en' }),
+ message: formatMessageV1('en', 'accessibility.progressLabel', { current: 2, total: 5 }),
+ negotiated: negotiateLocaleV1('en-US-u-ca-gregory'),
+ plural: formatRetryAfterSecondsV1('en', 1),
+ };
+ } finally {
+ restoreAll(restorations);
+ }
+
+ assert.deepEqual(actual, {
+ error: 'Something went wrong. Your data has been preserved.',
+ list: 'Web and Android',
+ message: 'Progress: 2 of 5.',
+ negotiated: 'en',
+ plural: 'Try again in 1 second.',
+ });
+ assert.deepEqual(calls, []);
+});
+
+test('keeps extra-parameter errors stable when global String conversion is replaced', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+ const calls = [];
+ const restorations = [];
+ const StringIntrinsic = String;
+ const parameters = { [Symbol('extra-parameter-marker')]: 'hidden' };
+ let caught;
+
+ try {
+ restorations.push(
+ replaceValue(
+ StringIntrinsic.prototype,
+ 'replace',
+ hostileFunction(calls, 'string-replace-marker'),
+ ),
+ );
+ restorations.push(
+ replaceValue(globalThis, 'String', hostileFunction(calls, 'string-constructor-marker')),
+ );
+ try {
+ formatMessageV1('en', 'action.save', parameters);
+ } catch (error) {
+ caught = error;
+ }
+ } finally {
+ restoreAll(restorations);
+ }
+
+ assert.equal(caught instanceof I18nErrorV1, true);
+ assert.equal(caught.code, 'EXTRA_PARAMETER');
+ assert.doesNotMatch(
+ inspect(caught),
+ /(?:extra-parameter|string-(?:constructor|replace))-marker/u,
+ );
+ assert.deepEqual(calls, []);
+});
diff --git a/packages/i18n/test/locale-v1.test.mjs b/packages/i18n/test/locale-v1.test.mjs
new file mode 100644
index 00000000..4e5f6513
--- /dev/null
+++ b/packages/i18n/test/locale-v1.test.mjs
@@ -0,0 +1,94 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+test('[IAM-016] defaults exactly to Vietnamese for absent, empty, unsupported, and malformed preferences', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ for (const input of [
+ undefined,
+ null,
+ '',
+ ' ',
+ 'fr-FR',
+ 'en;q=bogus',
+ 'en;q=1.1',
+ 'en;q=.5',
+ 'en;q=0',
+ 42,
+ [],
+ ]) {
+ assert.equal(negotiateLocaleV1(input), 'vi-VN', `input ${String(input)} must fail safely`);
+ }
+});
+
+test('canonicalizes supported language and region variants', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ for (const input of ['vi', 'VI', 'vi-vn', 'vi-VN', 'vi-US']) {
+ assert.equal(negotiateLocaleV1(input), 'vi-VN');
+ }
+ for (const input of ['en', 'EN', 'en-us', 'en-GB']) {
+ assert.equal(negotiateLocaleV1(input), 'en');
+ }
+});
+
+test('uses quality weights, stable source order, and the highest duplicate weight', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ assert.equal(negotiateLocaleV1('en;q=0.8, vi-VN;q=0.9'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('en;q=0.9, vi;q=0.9'), 'en');
+ assert.equal(negotiateLocaleV1('en;q=0.2, en-US;q=0.8, vi;q=0.7'), 'en');
+ assert.equal(negotiateLocaleV1('en;q=0, en-US;q=0.8, vi;q=0.7'), 'en');
+ assert.equal(negotiateLocaleV1('fr;q=1, en;q=0.5'), 'en');
+});
+
+test('applies wildcard policy without reviving an explicitly excluded locale', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ assert.equal(negotiateLocaleV1('*;q=0.5'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('vi;q=0, *;q=0.5'), 'en');
+ assert.equal(negotiateLocaleV1('vi;q=0, en;q=0, *;q=1'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('*;q=0, en;q=0.4'), 'en');
+});
+
+test('gives a supported explicit user preference priority over Accept-Language', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ assert.equal(negotiateLocaleV1({ userLocale: 'EN-gb', acceptLanguage: 'vi;q=1' }), 'en');
+ assert.equal(negotiateLocaleV1({ userLocale: 'fr', acceptLanguage: 'en;q=0.8' }), 'en');
+ assert.equal(negotiateLocaleV1({ userLocale: '', acceptLanguage: 'en' }), 'en');
+});
+
+test('does not execute hostile locale accessors and falls back safely for hostile objects', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+ let getterCalls = 0;
+ const hostileAccessor = {};
+ Object.defineProperties(hostileAccessor, {
+ userLocale: {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ throw new Error('must not run');
+ },
+ },
+ acceptLanguage: {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ return 'en';
+ },
+ },
+ });
+ const hostileProxy = new Proxy(
+ {},
+ {
+ getOwnPropertyDescriptor() {
+ throw new Error('must fail closed');
+ },
+ },
+ );
+
+ assert.equal(negotiateLocaleV1(hostileAccessor), 'vi-VN');
+ assert.equal(getterCalls, 0);
+ assert.equal(negotiateLocaleV1(hostileProxy), 'vi-VN');
+});
diff --git a/packages/i18n/test/messages-v1.test.mjs b/packages/i18n/test/messages-v1.test.mjs
new file mode 100644
index 00000000..b853e0bc
--- /dev/null
+++ b/packages/i18n/test/messages-v1.test.mjs
@@ -0,0 +1,85 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+
+test('interpolates declared string and numeric parameters deterministically', async () => {
+ const { formatMessageV1 } = await import('../src/v1.ts');
+
+ assert.equal(
+ formatMessageV1('vi-VN', 'error.genericWithCorrelationId', { correlationId: 'corr-123' }),
+ 'Đã xảy ra lỗi. Mã đối chiếu: corr-123.',
+ );
+ assert.equal(
+ formatMessageV1('en', 'retry.afterSeconds.other', { seconds: 15 }),
+ 'Try again in 15 seconds.',
+ );
+ assert.equal(
+ formatMessageV1('vi-VN', 'accessibility.progressLabel', { current: 2, total: 5 }),
+ 'Tiến độ: 2 trên 5.',
+ );
+});
+
+test('[WEB-021, NCO-017] requires every declared parameter and rejects extras', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+
+ assert.throws(
+ () => formatMessageV1('en', 'retry.afterSeconds.other', {}),
+ (error) => error instanceof I18nErrorV1 && error.code === 'MISSING_PARAMETER',
+ );
+ assert.throws(
+ () => formatMessageV1('en', 'retry.afterSeconds.other', { seconds: 2, undeclared: 'no' }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'EXTRA_PARAMETER',
+ );
+ assert.throws(
+ () => formatMessageV1('en', 'action.save', { unexpected: 'no' }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'EXTRA_PARAMETER',
+ );
+});
+
+test('rejects wrong parameter types, non-finite numbers, missing keys, and unsupported locales', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+
+ const cases = [
+ [
+ () => formatMessageV1('en', 'retry.afterSeconds.other', { seconds: '2' }),
+ 'INVALID_PARAMETER',
+ ],
+ [
+ () => formatMessageV1('en', 'retry.afterSeconds.other', { seconds: Number.NaN }),
+ 'INVALID_PARAMETER',
+ ],
+ [() => formatMessageV1('en', 'missing.key', {}), 'MISSING_MESSAGE'],
+ [() => formatMessageV1('fr', 'action.save', {}), 'INVALID_LOCALE'],
+ ];
+ for (const [operation, code] of cases) {
+ assert.throws(operation, (error) => error instanceof I18nErrorV1 && error.code === code);
+ }
+});
+
+test('performs literal text interpolation without interpreting HTML', async () => {
+ const { formatMessageV1 } = await import('../src/v1.ts');
+ const marker = '& customer';
+
+ assert.equal(
+ formatMessageV1('en', 'sync.lastCompletedAt', { time: marker }),
+ `Last synchronized at ${marker}.`,
+ );
+});
+
+test('rejects accessor-backed parameter bags without invoking them', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+ let getterCalls = 0;
+ const parameters = {};
+ Object.defineProperty(parameters, 'seconds', {
+ enumerable: true,
+ get() {
+ getterCalls += 1;
+ return 5;
+ },
+ });
+
+ assert.throws(
+ () => formatMessageV1('en', 'retry.afterSeconds.other', parameters),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_ARGUMENT',
+ );
+ assert.equal(getterCalls, 0);
+});
diff --git a/packages/i18n/test/public-api-v1.test.mjs b/packages/i18n/test/public-api-v1.test.mjs
new file mode 100644
index 00000000..3855f98b
--- /dev/null
+++ b/packages/i18n/test/public-api-v1.test.mjs
@@ -0,0 +1,41 @@
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+const packageDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+
+async function loadSourceApi() {
+ try {
+ return await import('../src/v1.ts');
+ } catch {
+ return undefined;
+ }
+}
+
+test('publishes the versioned bilingual foundation API', async () => {
+ const api = await loadSourceApi();
+
+ assert.ok(api, 'the i18n v1 source entry point must exist');
+ assert.equal(api.I18N_SCHEMA_VERSION_V1, 1);
+ assert.equal(api.DEFAULT_LOCALE_V1, 'vi-VN');
+ assert.deepEqual(api.SUPPORTED_LOCALES_V1, ['vi-VN', 'en']);
+ assert.equal(typeof api.negotiateLocaleV1, 'function');
+ assert.equal(typeof api.formatMessageV1, 'function');
+ assert.equal(typeof api.formatRetryAfterSecondsV1, 'function');
+ assert.equal(typeof api.formatDateTimeV1, 'function');
+ assert.equal(typeof api.formatDecimalV1, 'function');
+ assert.equal(typeof api.formatCurrencyV1, 'function');
+ assert.equal(typeof api.formatPercentV1, 'function');
+ assert.equal(typeof api.formatListV1, 'function');
+ assert.equal(typeof api.formatRelativeTimeV1, 'function');
+ assert.equal(typeof api.selectPluralCategoryV1, 'function');
+});
+
+test('exposes only the versioned entry point', async () => {
+ const manifest = JSON.parse(readFileSync(path.join(packageDirectory, 'package.json'), 'utf8'));
+
+ assert.deepEqual(Object.keys(manifest.exports), ['./v1']);
+ await assert.rejects(import('@databreeze/i18n'), { code: 'ERR_PACKAGE_PATH_NOT_EXPORTED' });
+});
diff --git a/packages/i18n/test/review-regressions-v1.test.mjs b/packages/i18n/test/review-regressions-v1.test.mjs
new file mode 100644
index 00000000..55f84bae
--- /dev/null
+++ b/packages/i18n/test/review-regressions-v1.test.mjs
@@ -0,0 +1,150 @@
+import assert from 'node:assert/strict';
+import { inspect } from 'node:util';
+import test from 'node:test';
+
+test('canonicalizes full BCP 47 locales and ignores structurally malformed ranges', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ assert.equal(negotiateLocaleV1('en-US-u-ca-gregory'), 'en');
+ assert.equal(negotiateLocaleV1({ userLocale: 'EN-us-u-CA-gregory', acceptLanguage: 'vi' }), 'en');
+ assert.equal(negotiateLocaleV1('en-US-u-ca-gregory;q=0.8,vi;q=0.7'), 'en');
+ assert.equal(negotiateLocaleV1('en-1a'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('en-1a;q=1,vi;q=0.5'), 'vi-VN');
+});
+
+test('scores explicit locale ranges before wildcard fallback', async () => {
+ const { negotiateLocaleV1 } = await import('../src/v1.ts');
+
+ assert.equal(negotiateLocaleV1('vi;q=0.1,*;q=0.9,en;q=0.4'), 'en');
+ assert.equal(negotiateLocaleV1('vi;q=0,*;q=0.9'), 'en');
+ assert.equal(negotiateLocaleV1('vi;q=0,en;q=0,*;q=1'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('en;q=0.2,en-US;q=0.8,vi;q=0.7'), 'en');
+ assert.equal(negotiateLocaleV1('en;q=0.9,en-US;q=0.2,vi;q=0.5'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('vi;q=0.8,en;q=0.8'), 'vi-VN');
+ assert.equal(negotiateLocaleV1('en;q=0.8,vi;q=0.8'), 'en');
+});
+
+test('reads Date instances through the built-in intrinsic and bounds hostile failures', async () => {
+ const { formatDateTimeV1, I18nErrorV1 } = await import('../src/v1.ts');
+ let overrideCalls = 0;
+ class HostileDate extends Date {
+ getTime() {
+ overrideCalls += 1;
+ throw new Error('date-marker-must-not-run');
+ }
+ }
+
+ assert.match(
+ formatDateTimeV1(new HostileDate('2026-08-01T17:30:00.000Z'), {
+ locale: 'en',
+ timeZone: 'UTC',
+ }),
+ /2026/u,
+ );
+ assert.equal(overrideCalls, 0);
+
+ const hostileProxy = new Proxy(new Date(0), {
+ get() {
+ throw new Error('date-proxy-marker');
+ },
+ });
+ const { proxy: revokedDate, revoke } = Proxy.revocable(new Date(0), {});
+ revoke();
+ for (const value of [hostileProxy, revokedDate]) {
+ assert.throws(
+ () => formatDateTimeV1(value, { locale: 'en', timeZone: 'UTC' }),
+ (error) => {
+ assert.equal(error instanceof I18nErrorV1, true);
+ assert.equal(error.code, 'INVALID_DATE');
+ assert.doesNotMatch(inspect(error), /date-(?:proxy-)?marker/u);
+ return true;
+ },
+ );
+ }
+});
+
+test('snapshots list items without holes, accessors, extra keys, or caller iterators', async () => {
+ const { formatListV1, I18nErrorV1 } = await import('../src/v1.ts');
+ let accessorCalls = 0;
+ const accessorList = ['safe'];
+ Object.defineProperty(accessorList, '0', {
+ enumerable: true,
+ get() {
+ accessorCalls += 1;
+ throw new Error('list-accessor-marker');
+ },
+ });
+ const sparse = new Array(2);
+ sparse[1] = 'Android';
+ const extra = ['Web'];
+ extra.metadata = 'must not survive';
+ const symbolExtra = ['Web'];
+ symbolExtra[Symbol('hidden')] = 'must not survive';
+
+ for (const value of [accessorList, sparse, extra, symbolExtra]) {
+ assert.throws(
+ () => formatListV1(value, { locale: 'en' }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_ARGUMENT',
+ );
+ }
+ assert.equal(accessorCalls, 0);
+
+ let iteratorCalls = 0;
+ class HostileList extends Array {
+ [Symbol.iterator]() {
+ iteratorCalls += 1;
+ throw new Error('list-iterator-marker');
+ }
+ }
+ const list = new HostileList();
+ list.push('Web', 'Android');
+ assert.match(formatListV1(list, { locale: 'en' }), /Web.*and.*Android/u);
+ assert.equal(iteratorCalls, 0);
+});
+
+test('normalizes safe text and rejects unsafe or unbounded interpolation', async () => {
+ const { formatMessageV1, I18nErrorV1 } = await import('../src/v1.ts');
+
+ assert.equal(
+ formatMessageV1('vi-VN', 'sync.lastCompletedAt', { time: 'Nguye\u0302\u0303n A\u0301nh' }),
+ 'Đồng bộ gần nhất lúc Nguyễn Ánh.',
+ );
+ assert.equal(
+ formatMessageV1('en', 'error.genericWithCorrelationId', { correlationId: 'corr-123_ABC.9' }),
+ 'Something went wrong. Reference code: corr-123_ABC.9.',
+ );
+
+ for (const correlationId of [
+ 'corr 123',
+ 'corr\n123',
+ 'corr\u202e123',
+ `corr-${String.fromCharCode(0xd800)}`,
+ 'x'.repeat(129),
+ ]) {
+ assert.throws(
+ () => formatMessageV1('en', 'error.genericWithCorrelationId', { correlationId }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_PARAMETER',
+ );
+ }
+ assert.throws(
+ () => formatMessageV1('en', 'sync.lastCompletedAt', { time: 'x'.repeat(513) }),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_PARAMETER',
+ );
+});
+
+test('selects grammatically correct retry messages with Intl plural rules', async () => {
+ const { formatRetryAfterSecondsV1, I18nErrorV1 } = await import('../src/v1.ts');
+
+ assert.equal(formatRetryAfterSecondsV1('en', 0), 'Try again in 0 seconds.');
+ assert.equal(formatRetryAfterSecondsV1('en', 1), 'Try again in 1 second.');
+ assert.equal(formatRetryAfterSecondsV1('en', 2), 'Try again in 2 seconds.');
+ assert.equal(formatRetryAfterSecondsV1('vi-VN', 0), 'Thử lại sau 0 giây.');
+ assert.equal(formatRetryAfterSecondsV1('vi-VN', 1), 'Thử lại sau 1 giây.');
+ assert.equal(formatRetryAfterSecondsV1('vi-VN', 2), 'Thử lại sau 2 giây.');
+ for (const seconds of [-1, 1.5, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER + 1]) {
+ assert.throws(
+ () => formatRetryAfterSecondsV1('en', seconds),
+ (error) => error instanceof I18nErrorV1 && error.code === 'INVALID_NUMBER',
+ );
+ }
+});
diff --git a/packages/i18n/tsconfig.build.json b/packages/i18n/tsconfig.build.json
new file mode 100644
index 00000000..ffb43181
--- /dev/null
+++ b/packages/i18n/tsconfig.build.json
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "declaration": true,
+ "outDir": "dist",
+ "rewriteRelativeImportExtensions": true,
+ "rootDir": "src"
+ },
+ "include": ["src/**/*.ts"]
+}
diff --git a/packages/i18n/tsconfig.json b/packages/i18n/tsconfig.json
new file mode 100644
index 00000000..28754dda
--- /dev/null
+++ b/packages/i18n/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": true,
+ "noEmit": true
+ },
+ "include": ["src/**/*.ts", "test/**/*.ts"]
+}
diff --git a/packages/i18n/turbo.json b/packages/i18n/turbo.json
new file mode 100644
index 00000000..25e76c6b
--- /dev/null
+++ b/packages/i18n/turbo.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://turbo.build/schema.json",
+ "extends": ["//"],
+ "tasks": {
+ "test": {
+ "outputs": []
+ }
+ }
+}
diff --git a/packages/provider-ports/README.md b/packages/provider-ports/README.md
new file mode 100644
index 00000000..7370aba3
--- /dev/null
+++ b/packages/provider-ports/README.md
@@ -0,0 +1,72 @@
+# Provider Ports
+
+Pure, provider-neutral TypeScript contracts for replaceable DataBreeze infrastructure adapters.
+This package defines boundaries only; it contains no provider implementation, network call,
+credential value, persistence, framework, or SDK dependency.
+
+## Public interface
+
+`@databreeze/provider-ports/v1` exports common provider contracts plus ports for:
+
+- S3-compatible object storage;
+- transactional email and Android push;
+- OCR and structured AI assistance;
+- optional DataBreeze subscription billing;
+- telemetry export; and
+- opaque secret-handle resolution.
+
+Every port shares only descriptor and health operations. State leaves through provider-specific,
+closed, content-safe contracts: object manifests, delivery-suppression manifests, subscription
+migration manifests, or secrets portability metadata. Stateless OCR, AI, and telemetry adapters do
+not invent an arbitrary export record. A descriptor declares the complete operation set for its
+provider kind plus idempotency, cancellation, timeouts, retry limits, data regions,
+retention/training behavior, failover/degraded behavior, and coherent exit metadata.
+
+Common helpers validate and freeze closed metadata, reuse the canonical contract timestamp parser,
+enforce cancellation/deadlines/idempotency, and create errors only through a redacting factory with
+allowlisted operations and code-derived message keys. Raw provider causes are neither accessed nor
+retained. A composition-owned issuer/resolver capability creates and resolves secret references;
+references and handles expose no identifier fields or raw public ID and redact string, JSON, and
+diagnostic inspection. Provenance predicates and assertions validate genuine capabilities,
+issuers, references, and capability membership without revealing identifier metadata.
+
+Object storage is resumable and bounded-memory: begin, upload a validated copy-isolated 8-64 MiB
+part, complete, or abort. Factory-issued uploads bind their immutable plan; uploaded-part receipts
+bind their upload and part metadata; completion accepts only the exact ordered, contiguous receipt
+set and derives total length and digest from the bound plan. Plans support immutable objects through
+20 GiB with declared whole-object and per-part SHA-256 digests. Email and push expose explicit typed
+recipient-suppression operations; durable notification policy remains owned by NCO.
+
+There is intentionally no unversioned package root. Provider-specific identifiers may appear only
+as opaque external references returned by an adapter; they never replace DataBreeze domain IDs or
+become the only representation of customer state.
+
+## Payment boundary
+
+`PaymentsProviderPortV1` is restricted to hosted checkout/portal, subscription upsert, verified
+subscription webhooks, reconciliation, and a schema-validated migration manifest for DataBreeze's
+own organization subscriptions. It has no customer charge, capture, refund, transfer, withholding,
+reversal, settlement, raw payment credential, or arbitrary provider-state operation. Built-in
+Free/Development/Admin-granted entitlement operation remains provider-independent; a missing
+payment adapter must not block it.
+
+## Forbidden dependencies
+
+- Provider/cloud SDKs and concrete adapters. The sole dependency is the generated canonical
+ DataBreeze contract validator used for timestamps.
+- Service/application implementations, databases, queues, filesystems, or UI frameworks.
+- Raw secrets, API keys, payment credentials, or provider response bodies in errors.
+- Product workflows, entitlement authority, storage authority, notification durability, OCR/AI
+ truth decisions, content-safe telemetry policy, and adapter failover orchestration.
+
+Concrete adapters, provider webhook persistence, telemetry allowlists/redaction, billing workflow,
+object-placement authority, and provider selection/failover execution are deferred to their owning
+plans. The ports deliberately preserve those boundaries rather than implementing them here.
+
+## Local commands
+
+```text
+corepack pnpm --filter @databreeze/provider-ports test
+corepack pnpm --filter @databreeze/provider-ports typecheck
+corepack pnpm --filter @databreeze/provider-ports build
+```
diff --git a/packages/provider-ports/package.json b/packages/provider-ports/package.json
new file mode 100644
index 00000000..32591ab1
--- /dev/null
+++ b/packages/provider-ports/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@databreeze/provider-ports",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "exports": {
+ "./v1": {
+ "types": "./src/v1.ts",
+ "import": "./dist/v1.js"
+ }
+ },
+ "scripts": {
+ "build": "tsc --project tsconfig.build.json && node test/built-public-api-smoke.mjs",
+ "test": "node --test test/**/*.test.mjs",
+ "typecheck": "tsc --noEmit --project tsconfig.json"
+ },
+ "dependencies": {
+ "@databreeze/contracts": "workspace:*"
+ }
+}
diff --git a/packages/provider-ports/src/common-v1.ts b/packages/provider-ports/src/common-v1.ts
new file mode 100644
index 00000000..a7100309
--- /dev/null
+++ b/packages/provider-ports/src/common-v1.ts
@@ -0,0 +1,989 @@
+import { parseV1Contract } from '@databreeze/contracts/v1';
+
+export const PROVIDER_PORT_SCHEMA_VERSION_V1 = 1 as const;
+
+export type ProviderKindV1 =
+ | 'object-storage'
+ | 'email'
+ | 'push'
+ | 'ocr'
+ | 'ai'
+ | 'payments'
+ | 'telemetry'
+ | 'secrets';
+
+export const PROVIDER_OPERATIONS_BY_KIND_V1 = Object.freeze({
+ 'object-storage': Object.freeze([
+ 'begin-multipart-upload',
+ 'upload-part',
+ 'complete-multipart-upload',
+ 'abort-multipart-upload',
+ 'read-range',
+ 'verify-digest',
+ 'apply-retention',
+ 'delete-verified',
+ 'create-read-grant',
+ 'export-object-manifest',
+ ] as const),
+ email: Object.freeze([
+ 'send-template',
+ 'verify-delivery-webhook',
+ 'suppress-recipient',
+ 'export-suppression-manifest',
+ ] as const),
+ push: Object.freeze([
+ 'send-push',
+ 'verify-delivery-webhook',
+ 'suppress-recipient',
+ 'export-suppression-manifest',
+ ] as const),
+ ocr: Object.freeze(['extract'] as const),
+ ai: Object.freeze(['generate-structured'] as const),
+ payments: Object.freeze([
+ 'create-hosted-subscription-checkout',
+ 'create-subscription-portal',
+ 'upsert-databreeze-subscription',
+ 'verify-subscription-webhook',
+ 'reconcile-databreeze-subscription',
+ 'export-subscription-migration',
+ ] as const),
+ telemetry: Object.freeze(['export-telemetry-batch'] as const),
+ secrets: Object.freeze(['resolve-handle', 'revoke-handle', 'describe-portability'] as const),
+} satisfies Readonly>);
+
+export type ProviderOperationV1 =
+ | (typeof PROVIDER_OPERATIONS_BY_KIND_V1)[ProviderKindV1][number]
+ | 'contract-validation';
+
+export type ProviderErrorCodeV1 =
+ | 'INVALID_REQUEST'
+ | 'AUTHENTICATION_FAILED'
+ | 'AUTHORIZATION_DENIED'
+ | 'NOT_FOUND'
+ | 'CONFLICT'
+ | 'RATE_LIMITED'
+ | 'QUOTA_EXCEEDED'
+ | 'TIMEOUT'
+ | 'ABORTED'
+ | 'UNAVAILABLE'
+ | 'POLICY_DENIED'
+ | 'UNSUPPORTED'
+ | 'INTEGRITY_FAILED'
+ | 'UNKNOWN';
+
+export type ProviderIdempotencyV1 = 'required' | 'supported' | 'not_applicable';
+export type ProviderCancellationV1 = 'cooperative' | 'supported' | 'not_supported';
+export type ProviderHealthStatusV1 = 'healthy' | 'degraded' | 'unavailable';
+export type ProviderContentRetentionV1 = 'none' | 'transient' | 'durable' | 'provider_policy';
+export type ProviderTrainingUseV1 = 'prohibited' | 'policy_controlled' | 'not_applicable';
+export type ProviderFailoverV1 = 'none' | 'manual' | 'automatic';
+export type ProviderDegradedBehaviorV1 =
+ | 'fail_closed'
+ | 'queue'
+ | 'local_fallback'
+ | 'in_app_only'
+ | 'read_only';
+export type ProviderStatePortabilityV1 = 'none' | 'manifest' | 'full';
+export type ProviderCredentialRevocationV1 = 'not_applicable' | 'supported' | 'manual';
+
+export interface ProviderCapabilityV1 {
+ readonly operation: ProviderOperationV1;
+ readonly idempotency: ProviderIdempotencyV1;
+ readonly cancellation: ProviderCancellationV1;
+ readonly timeoutMs: number;
+ readonly maxAttempts: number;
+}
+
+export interface ProviderDataHandlingV1 {
+ readonly regions: readonly string[];
+ readonly contentRetention: ProviderContentRetentionV1;
+ readonly maximumRetentionSeconds?: number;
+ readonly trainingUse: ProviderTrainingUseV1;
+}
+
+export interface ProviderResilienceV1 {
+ readonly failover: ProviderFailoverV1;
+ readonly degradedBehavior: ProviderDegradedBehaviorV1;
+}
+
+export interface ProviderExitV1 {
+ readonly statePortability: ProviderStatePortabilityV1;
+ readonly exportFormat: string;
+ readonly credentialRevocation: ProviderCredentialRevocationV1;
+}
+
+export interface ProviderDescriptorInputV1 {
+ readonly kind: K;
+ readonly adapterKey: string;
+ readonly capabilities: readonly ProviderCapabilityV1[];
+ readonly dataHandling: ProviderDataHandlingV1;
+ readonly resilience: ProviderResilienceV1;
+ readonly exit: ProviderExitV1;
+}
+
+export interface ProviderDescriptorV1
+ extends ProviderDescriptorInputV1 {
+ readonly schemaVersion: typeof PROVIDER_PORT_SCHEMA_VERSION_V1;
+}
+
+export class ProviderContractErrorV1 extends Error {
+ public readonly code = 'INVALID_DESCRIPTOR' as const;
+
+ public constructor() {
+ super('Provider contract is invalid.');
+ this.name = 'ProviderContractErrorV1';
+ Object.freeze(this);
+ }
+
+ public toJSON(): Readonly<{ name: string; code: 'INVALID_DESCRIPTOR' }> {
+ return Object.freeze({ name: this.name, code: this.code });
+ }
+}
+
+type UnknownRecord = Record;
+
+const providerKinds = new Set(
+ Object.keys(PROVIDER_OPERATIONS_BY_KIND_V1) as ProviderKindV1[],
+);
+const providerOperations = new Set([
+ ...Object.values(PROVIDER_OPERATIONS_BY_KIND_V1).flat(),
+ 'contract-validation',
+]);
+const idempotencyValues = new Set([
+ 'required',
+ 'supported',
+ 'not_applicable',
+]);
+const cancellationValues = new Set([
+ 'cooperative',
+ 'supported',
+ 'not_supported',
+]);
+const contentRetentionValues = new Set([
+ 'none',
+ 'transient',
+ 'durable',
+ 'provider_policy',
+]);
+const trainingUseValues = new Set([
+ 'prohibited',
+ 'policy_controlled',
+ 'not_applicable',
+]);
+const failoverValues = new Set(['none', 'manual', 'automatic']);
+const degradedBehaviorValues = new Set([
+ 'fail_closed',
+ 'queue',
+ 'local_fallback',
+ 'in_app_only',
+ 'read_only',
+]);
+const statePortabilityValues = new Set(['none', 'manifest', 'full']);
+const credentialRevocationValues = new Set([
+ 'not_applicable',
+ 'supported',
+ 'manual',
+]);
+const errorCodes = new Set([
+ 'INVALID_REQUEST',
+ 'AUTHENTICATION_FAILED',
+ 'AUTHORIZATION_DENIED',
+ 'NOT_FOUND',
+ 'CONFLICT',
+ 'RATE_LIMITED',
+ 'QUOTA_EXCEEDED',
+ 'TIMEOUT',
+ 'ABORTED',
+ 'UNAVAILABLE',
+ 'POLICY_DENIED',
+ 'UNSUPPORTED',
+ 'INTEGRITY_FAILED',
+ 'UNKNOWN',
+]);
+const nonRetryableErrorCodes = new Set([
+ 'INVALID_REQUEST',
+ 'AUTHENTICATION_FAILED',
+ 'AUTHORIZATION_DENIED',
+ 'NOT_FOUND',
+ 'ABORTED',
+ 'POLICY_DENIED',
+ 'UNSUPPORTED',
+ 'INTEGRITY_FAILED',
+]);
+const healthReasonCodes = new Set([
+ 'AUTHENTICATION_FAILED',
+ 'DEGRADED_CAPACITY',
+ 'POLICY_RESTRICTED',
+ 'UPSTREAM_RATE_LIMITED',
+ 'UPSTREAM_UNAVAILABLE',
+]);
+const errorMessageKeys: Readonly> = Object.freeze({
+ INVALID_REQUEST: 'provider.invalid_request',
+ AUTHENTICATION_FAILED: 'provider.authentication_failed',
+ AUTHORIZATION_DENIED: 'provider.authorization_denied',
+ NOT_FOUND: 'provider.not_found',
+ CONFLICT: 'provider.conflict',
+ RATE_LIMITED: 'provider.rate_limited',
+ QUOTA_EXCEEDED: 'provider.quota_exceeded',
+ TIMEOUT: 'provider.timeout',
+ ABORTED: 'provider.aborted',
+ UNAVAILABLE: 'provider.unavailable',
+ POLICY_DENIED: 'provider.policy_denied',
+ UNSUPPORTED: 'provider.unsupported',
+ INTEGRITY_FAILED: 'provider.integrity_failed',
+ UNKNOWN: 'provider.unknown',
+});
+const UTC_TIMESTAMP_SCHEMA_ID = 'https://schemas.databreeze.dev/contracts/v1/utc-timestamp';
+
+function isObject(value: unknown): value is object {
+ return value !== null && typeof value === 'object';
+}
+
+function readClosedRecord(
+ value: unknown,
+ allowedKeys: readonly string[],
+): UnknownRecord | undefined {
+ if (!isObject(value)) return undefined;
+ const allowed = new Set(allowedKeys);
+ const result: UnknownRecord = Object.create(null) as UnknownRecord;
+ try {
+ if (Array.isArray(value)) return undefined;
+ for (const key of Reflect.ownKeys(value)) {
+ if (typeof key !== 'string' || !allowed.has(key)) return undefined;
+ const descriptor = Reflect.getOwnPropertyDescriptor(value, key);
+ if (descriptor === undefined || !('value' in descriptor)) return undefined;
+ result[key] = descriptor.value;
+ }
+ } catch {
+ return undefined;
+ }
+ return result;
+}
+
+function readArray(value: unknown, maximum = 100): readonly unknown[] | undefined {
+ let descriptors: Record;
+ try {
+ if (!Array.isArray(value)) return undefined;
+ descriptors = Object.getOwnPropertyDescriptors(value) as Record;
+ } catch {
+ return undefined;
+ }
+ const lengthDescriptor = descriptors['length'];
+ if (
+ lengthDescriptor === undefined ||
+ !('value' in lengthDescriptor) ||
+ !Number.isSafeInteger(lengthDescriptor.value) ||
+ lengthDescriptor.value < 0 ||
+ lengthDescriptor.value > maximum
+ ) {
+ return undefined;
+ }
+ const length = lengthDescriptor.value as number;
+ const result: unknown[] = [];
+ for (const key of Reflect.ownKeys(descriptors)) {
+ if (typeof key !== 'string') return undefined;
+ if (key === 'length') continue;
+ if (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= length) return undefined;
+ }
+ for (let index = 0; index < length; index += 1) {
+ const descriptor = descriptors[String(index)];
+ if (descriptor === undefined || !('value' in descriptor)) return undefined;
+ result.push(descriptor.value);
+ }
+ return result;
+}
+
+function isSafeToken(value: unknown, maximum = 200): value is string {
+ return (
+ typeof value === 'string' &&
+ value.length > 0 &&
+ value.length <= maximum &&
+ value.trim() === value &&
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value)
+ );
+}
+
+function isRegion(value: unknown): value is string {
+ return typeof value === 'string' && /^(?:local|global|[a-z]{2}(?:-[a-z0-9]+)+)$/.test(value);
+}
+
+function isPositiveInteger(value: unknown, maximum = Number.MAX_SAFE_INTEGER): value is number {
+ return Number.isSafeInteger(value) && (value as number) > 0 && (value as number) <= maximum;
+}
+
+function isUtcTimestamp(value: unknown): value is string {
+ return parseV1Contract(UTC_TIMESTAMP_SCHEMA_ID, value).accepted;
+}
+
+interface ComparableUtcTimestampV1 {
+ readonly epochSecond: number;
+ readonly fractionalSecond: string;
+}
+
+const comparableUtcTimestampPattern =
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?Z$/;
+
+function comparableUtcTimestamp(value: string): ComparableUtcTimestampV1 | undefined {
+ const match = comparableUtcTimestampPattern.exec(value);
+ if (match === null) return undefined;
+ const year = Number(match[1]);
+ const month = Number(match[2]);
+ const day = Number(match[3]);
+ const hour = Number(match[4]);
+ const minute = Number(match[5]);
+ const second = Number(match[6]);
+ const calendar = new Date(0);
+ calendar.setUTCHours(0, 0, 0, 0);
+ calendar.setUTCFullYear(year, month - 1, day);
+ calendar.setUTCHours(hour, minute, Math.min(second, 59), 0);
+ const epochSecond = calendar.getTime() / 1_000 + (second === 60 ? 1 : 0);
+ if (!Number.isSafeInteger(epochSecond)) return undefined;
+ return { epochSecond, fractionalSecond: match[7] ?? '' };
+}
+
+function compareUtcTimestamps(left: string, right: string): number | undefined {
+ const leftValue = comparableUtcTimestamp(left);
+ const rightValue = comparableUtcTimestamp(right);
+ if (leftValue === undefined || rightValue === undefined) return undefined;
+ if (leftValue.epochSecond !== rightValue.epochSecond) {
+ return leftValue.epochSecond < rightValue.epochSecond ? -1 : 1;
+ }
+ const fractionalLength = Math.max(
+ leftValue.fractionalSecond.length,
+ rightValue.fractionalSecond.length,
+ );
+ const leftFraction = leftValue.fractionalSecond.padEnd(fractionalLength, '0');
+ const rightFraction = rightValue.fractionalSecond.padEnd(fractionalLength, '0');
+ if (leftFraction === rightFraction) return 0;
+ return leftFraction < rightFraction ? -1 : 1;
+}
+
+function deepFreeze(value: T): T {
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
+ for (const child of Object.values(value)) deepFreeze(child);
+ Object.freeze(value);
+ }
+ return value;
+}
+
+function sameStringSet(left: ReadonlySet, right: ReadonlySet): boolean {
+ return left.size === right.size && [...left].every((value) => right.has(value));
+}
+
+export function defineProviderDescriptorV1(
+ input: ProviderDescriptorInputV1,
+): ProviderDescriptorV1 {
+ const root = readClosedRecord(input, [
+ 'kind',
+ 'adapterKey',
+ 'capabilities',
+ 'dataHandling',
+ 'resilience',
+ 'exit',
+ ]);
+ if (
+ root === undefined ||
+ !providerKinds.has(root['kind'] as ProviderKindV1) ||
+ !isSafeToken(root['adapterKey'])
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+ const kind = root['kind'] as K;
+ const rawCapabilities = readArray(root['capabilities'], 32);
+ if (rawCapabilities === undefined || rawCapabilities.length === 0) {
+ throw new ProviderContractErrorV1();
+ }
+ const capabilities: ProviderCapabilityV1[] = [];
+ const operations = new Set();
+ for (const rawCapability of rawCapabilities) {
+ const capability = readClosedRecord(rawCapability, [
+ 'operation',
+ 'idempotency',
+ 'cancellation',
+ 'timeoutMs',
+ 'maxAttempts',
+ ]);
+ if (
+ capability === undefined ||
+ !providerOperations.has(capability['operation'] as ProviderOperationV1) ||
+ operations.has(capability['operation'] as string) ||
+ !idempotencyValues.has(capability['idempotency'] as ProviderIdempotencyV1) ||
+ !cancellationValues.has(capability['cancellation'] as ProviderCancellationV1) ||
+ !isPositiveInteger(capability['timeoutMs'], 300_000) ||
+ !isPositiveInteger(capability['maxAttempts'], 20)
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+ operations.add(capability['operation'] as string);
+ capabilities.push({
+ operation: capability['operation'] as ProviderOperationV1,
+ idempotency: capability['idempotency'] as ProviderIdempotencyV1,
+ cancellation: capability['cancellation'] as ProviderCancellationV1,
+ timeoutMs: capability['timeoutMs'],
+ maxAttempts: capability['maxAttempts'],
+ });
+ }
+ if (!sameStringSet(operations, new Set(PROVIDER_OPERATIONS_BY_KIND_V1[kind]))) {
+ throw new ProviderContractErrorV1();
+ }
+
+ const dataHandling = readClosedRecord(root['dataHandling'], [
+ 'regions',
+ 'contentRetention',
+ 'maximumRetentionSeconds',
+ 'trainingUse',
+ ]);
+ const rawRegions =
+ dataHandling === undefined ? undefined : readArray(dataHandling['regions'], 32);
+ if (
+ dataHandling === undefined ||
+ rawRegions === undefined ||
+ rawRegions.length === 0 ||
+ rawRegions.some((region) => !isRegion(region)) ||
+ new Set(rawRegions).size !== rawRegions.length ||
+ !contentRetentionValues.has(dataHandling['contentRetention'] as ProviderContentRetentionV1) ||
+ !trainingUseValues.has(dataHandling['trainingUse'] as ProviderTrainingUseV1)
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+ const retention = dataHandling['contentRetention'] as ProviderContentRetentionV1;
+ const maximumRetentionSeconds = dataHandling['maximumRetentionSeconds'];
+ if (
+ (retention === 'none' && maximumRetentionSeconds !== undefined) ||
+ (retention === 'transient' && !isPositiveInteger(maximumRetentionSeconds, 31_536_000)) ||
+ ((retention === 'durable' || retention === 'provider_policy') &&
+ maximumRetentionSeconds !== undefined &&
+ !isPositiveInteger(maximumRetentionSeconds))
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+ const trainingUse = dataHandling['trainingUse'] as ProviderTrainingUseV1;
+ if (
+ (kind === 'ai' && trainingUse === 'not_applicable') ||
+ (kind !== 'ai' && trainingUse !== 'not_applicable')
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+
+ const resilience = readClosedRecord(root['resilience'], ['failover', 'degradedBehavior']);
+ const exit = readClosedRecord(root['exit'], [
+ 'statePortability',
+ 'exportFormat',
+ 'credentialRevocation',
+ ]);
+ if (
+ resilience === undefined ||
+ exit === undefined ||
+ !failoverValues.has(resilience['failover'] as ProviderFailoverV1) ||
+ !degradedBehaviorValues.has(resilience['degradedBehavior'] as ProviderDegradedBehaviorV1) ||
+ !statePortabilityValues.has(exit['statePortability'] as ProviderStatePortabilityV1) ||
+ !isSafeToken(exit['exportFormat']) ||
+ !credentialRevocationValues.has(exit['credentialRevocation'] as ProviderCredentialRevocationV1)
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+ const statePortability = exit['statePortability'] as ProviderStatePortabilityV1;
+ const statefulKind = new Set([
+ 'object-storage',
+ 'email',
+ 'push',
+ 'payments',
+ 'secrets',
+ ]).has(kind);
+ if (
+ (statePortability === 'none' && exit['exportFormat'] !== 'not-applicable') ||
+ (statePortability !== 'none' && exit['exportFormat'] === 'not-applicable') ||
+ (statefulKind && statePortability === 'none') ||
+ (!statefulKind && statePortability !== 'none') ||
+ (kind === 'secrets' && exit['credentialRevocation'] === 'not_applicable')
+ ) {
+ throw new ProviderContractErrorV1();
+ }
+
+ return deepFreeze({
+ schemaVersion: PROVIDER_PORT_SCHEMA_VERSION_V1,
+ kind,
+ adapterKey: root['adapterKey'],
+ capabilities,
+ dataHandling: {
+ regions: rawRegions as string[],
+ contentRetention: retention,
+ ...(maximumRetentionSeconds === undefined
+ ? {}
+ : { maximumRetentionSeconds: maximumRetentionSeconds as number }),
+ trainingUse,
+ },
+ resilience: {
+ failover: resilience['failover'] as ProviderFailoverV1,
+ degradedBehavior: resilience['degradedBehavior'] as ProviderDegradedBehaviorV1,
+ },
+ exit: {
+ statePortability,
+ exportFormat: exit['exportFormat'],
+ credentialRevocation: exit['credentialRevocation'] as ProviderCredentialRevocationV1,
+ },
+ });
+}
+
+export interface ProviderAbortSignalV1 {
+ readonly aborted: boolean;
+}
+
+export interface ProviderInvocationContextInputV1 {
+ readonly operation: ProviderOperationV1;
+ readonly operationId: string;
+ readonly correlationId: string;
+ readonly deadlineAt: string;
+ readonly timeoutMs: number;
+ readonly idempotencyKey?: string;
+ readonly abortSignal: ProviderAbortSignalV1;
+}
+
+export type ProviderInvocationContextV1 = ProviderInvocationContextInputV1;
+
+const invocationContexts = new WeakSet