diff --git a/schemas/README.md b/schemas/README.md new file mode 100644 index 0000000..0a97c20 --- /dev/null +++ b/schemas/README.md @@ -0,0 +1,21 @@ +# ProofedMesh Schemas + +Versionierte JSON-Schemas für die kanonischen Business-, Datenquellen- und Senken-Formate. Jedes Schema trägt am Objekt-Root `schema.name` und `schema.version`, damit Attestierungen die genutzte Fassung mit-signieren können (siehe [`docs/architecture/v1.1.md`](../docs/architecture/v1.1.md#user-content-61-fachliche-integritätsprüfung--schema-versionierung-verbindlich)). + +**Versionspolitik:** Breaking Changes bekommen eine neue Hauptversion (`v1.0.0` → `v2.0.0`). Nicht-breaking Erweiterungen eine Minor- oder Patch-Version. Alte Versionen werden niemals verändert — sie werden parallel vorgehalten, solange Attestierungen aus ihrer Laufzeit noch verifizierbar sein müssen (Default-Retention 1 Jahr, siehe PII-Retention-ADR). + +## Vorhandene Schemas + +| Pfad | Rolle | Verwendet in | +| ----------------------------------------------- | ------------------------------ | -------------------------------------------------- | +| `expense/canonical/v1.0.0.json` | Kanonische Business-Form | Approval-Attestierung (`subject`, `materials`), Agent-A-Transform-Output | +| `ds/expense/project_assignment/v1.0.0.json` | Datenquellen-Rückgabe | Query Attestation von `ds:expense-db-app` / `project_assignments` | +| `sink/db/expense/v1.0.0.json` | Senken-Payload | Transform-Attestierung `expense.map_to_sink` → `sink:expense-db-app` | + +## Namensschema + +- `expense/canonical` — kanonische (senken-unabhängige) Business-Form +- `ds//` — Datenquellen-Rückgabe +- `sink//` — Senken-Payload + +Pfad: `/.../v...json`. `$id` enthält die absolute URL unter `https://basicstack.de/schemas/...` (identisch zum Dateipfad). diff --git a/schemas/ds/expense/project_assignment/v1.0.0.json b/schemas/ds/expense/project_assignment/v1.0.0.json new file mode 100644 index 0000000..58b4d04 --- /dev/null +++ b/schemas/ds/expense/project_assignment/v1.0.0.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/ds/expense/project_assignment/v1.0.0.json", + "title": "DataSource Response: project_assignments (ds:expense-db-app) v1.0.0", + "description": "Rückgabe-Schema der Datenquelle ds:expense-db-app, Resource project_assignments. Der Datenquellen-Adapter signiert eine Query Attestation mit diesem Schema als result-Teil.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "data_source", "resource", "filters", "rows", "returned_at"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "ds.expense.project_assignment" }, + "version": { "const": "1.0.0" } + } + }, + "data_source": { "const": "ds:expense-db-app" }, + "resource": { "const": "project_assignments" }, + "filters": { + "type": "object", + "additionalProperties": false, + "required": ["employee_id"], + "properties": { + "employee_id": { "type": "string", "pattern": "^EMP-[0-9]+$" }, + "active_on": { "type": "string", "format": "date" } + } + }, + "returned_at": { "type": "string", "format": "date-time" }, + "rows": { + "type": "array", + "maxItems": 500, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["project_code", "project_display_name", "employee_id", "valid_from"], + "properties": { + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "project_display_name": { "type": "string", "minLength": 1, "maxLength": 200 }, + "employee_id": { "type": "string", "pattern": "^EMP-[0-9]+$" }, + "role": { "type": "string", "maxLength": 100 }, + "default_cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "valid_from": { "type": "string", "format": "date" }, + "valid_to": { "type": ["string", "null"], "format": "date" } + } + } + } + } +} diff --git a/schemas/expense/canonical/v1.0.0.json b/schemas/expense/canonical/v1.0.0.json new file mode 100644 index 0000000..325b0e6 --- /dev/null +++ b/schemas/expense/canonical/v1.0.0.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/expense/canonical/v1.0.0.json", + "title": "Expense (canonical) v1.0.0", + "description": "Kanonische Reisekostenstruktur. Bindet die Benutzerfreigabe. Technische Senken-Mappings dürfen die hier definierten Felder nicht in ihrer geschäftlichen Bedeutung ändern — insbesondere total_amount, currency und expense_items[].amount.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "workflow_id", "employee_id", "trip", "currency", "total_amount", "expense_items", "project_assignment"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "expense.canonical" }, + "version": { "const": "1.0.0" } + } + }, + "workflow_id": { + "type": "string", + "pattern": "^WF-[0-9]{4}-[0-9]{4,}$", + "description": "Prozess-ID, konstant über die gesamte Reisekostenabrechnung." + }, + "employee_id": { + "type": "string", + "pattern": "^EMP-[0-9]+$", + "description": "Interne Mitarbeiter-ID. Nicht die SAP-Personalnummer — die wird senken-seitig zugeordnet." + }, + "trip": { + "type": "object", + "additionalProperties": false, + "required": ["start_date", "end_date", "destination"], + "properties": { + "start_date": { "type": "string", "format": "date" }, + "end_date": { "type": "string", "format": "date" }, + "destination": { "type": "string", "minLength": 1, "maxLength": 200 }, + "purpose": { "type": "string", "maxLength": 500 } + } + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 Währungscode (z.B. EUR)." + }, + "total_amount": { + "type": "string", + "pattern": "^-?[0-9]+\\.[0-9]{2}$", + "description": "Summe aller expense_items, als Dezimal-String mit zwei Nachkommastellen (JSON Number vermeidet Rundungsfehler)." + }, + "expense_items": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["type", "amount", "incurred_on"], + "properties": { + "type": { + "type": "string", + "enum": ["HOTEL", "MEALS", "TRANSPORT_FLIGHT", "TRANSPORT_RAIL", "TRANSPORT_CAR", "PARKING", "COMMUNICATION", "CONFERENCE_FEE", "OTHER"] + }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "incurred_on": { "type": "string", "format": "date" }, + "description": { "type": "string", "maxLength": 500 }, + "receipt_ref": { + "type": "string", + "pattern": "^pii://[A-Za-z0-9/._-]+$", + "description": "Referenz in den PII-Store (verschlüsselter S3-Bucket), nicht der Beleg selbst." + } + } + } + }, + "project_assignment": { + "type": "object", + "additionalProperties": false, + "required": ["project_code", "cost_center", "source_attestation_ref"], + "properties": { + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "source_attestation_ref": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 der Query Attestation (ds:expense-db-app / project_assignments), aus der dieses Projekt stammt. Zwingend, damit Verification nachweisen kann, dass das gewählte Projekt tatsächlich in der dem Benutzer vorgelegten Liste war." + } + } + } + } +} diff --git a/schemas/sink/db/expense/v1.0.0.json b/schemas/sink/db/expense/v1.0.0.json new file mode 100644 index 0000000..73c45bf --- /dev/null +++ b/schemas/sink/db/expense/v1.0.0.json @@ -0,0 +1,84 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://basicstack.de/schemas/sink/db/expense/v1.0.0.json", + "title": "Sink Payload: sink:expense-db-app, action expense.post v1.0.0", + "description": "Senken-Payload für die eigene DB-App (sink:expense-db-app). Jede Transform-Attestierung von agent-sink-mapping erzeugt dieses Format aus einem expense.canonical-Dokument. Der Senken-Adapter prüft diesen Payload gegen Verification + OPA, legt dann einen Datensatz in der expenses-Tabelle an.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "workflow_id", "approval_id", "decision_id", "verification_ref", "canonical_payload_hash", "expense"], + "properties": { + "schema": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { "const": "sink.db.expense" }, + "version": { "const": "1.0.0" } + } + }, + "workflow_id": { "type": "string", "pattern": "^WF-[0-9]{4}-[0-9]{4,}$" }, + "approval_id": { "type": "string", "pattern": "^APR-[0-9]{4}-[0-9]{4,}$" }, + "decision_id": { "type": "string", "pattern": "^DEC-[0-9]{4}-[0-9]{4,}$" }, + "verification_ref": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 der Verifikationsantwort vom trust-api." + }, + "canonical_payload_hash": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 des originalen expense.canonical-Dokuments, über das die Benutzerfreigabe signiert hat." + }, + "expense": { + "type": "object", + "additionalProperties": false, + "required": ["employee_id", "project_code", "cost_center", "currency", "total_amount", "trip", "items"], + "properties": { + "employee_id": { "type": "string", "pattern": "^EMP-[0-9]+$" }, + "project_code": { "type": "string", "pattern": "^P-[0-9]+$" }, + "cost_center": { "type": "string", "pattern": "^CC-[0-9]+$" }, + "currency": { "type": "string", "pattern": "^[A-Z]{3}$" }, + "total_amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "trip": { + "type": "object", + "additionalProperties": false, + "required": ["start_date", "end_date", "destination"], + "properties": { + "start_date": { "type": "string", "format": "date" }, + "end_date": { "type": "string", "format": "date" }, + "destination": { "type": "string", "maxLength": 200 }, + "purpose": { "type": "string", "maxLength": 500 } + } + }, + "items": { + "type": "array", + "minItems": 1, + "maxItems": 200, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["type", "amount", "incurred_on", "gl_account"], + "properties": { + "type": { + "type": "string", + "enum": ["HOTEL", "MEALS", "TRANSPORT_FLIGHT", "TRANSPORT_RAIL", "TRANSPORT_CAR", "PARKING", "COMMUNICATION", "CONFERENCE_FEE", "OTHER"] + }, + "amount": { "type": "string", "pattern": "^-?[0-9]+\\.[0-9]{2}$" }, + "incurred_on": { "type": "string", "format": "date" }, + "description": { "type": "string", "maxLength": 500 }, + "gl_account": { + "type": "string", + "pattern": "^[0-9]{4,6}$", + "description": "Sachkonto in unserer DB-App (nicht SAP). Agent-sink-mapping bezieht es aus einer account_assignments-Query, deren Attestierung in der Transform-Attestierung unter read_materials verlinkt sein muss." + }, + "receipt_ref": { + "type": "string", + "pattern": "^pii://[A-Za-z0-9/._-]+$" + } + } + } + } + } + } + } +}