diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf8ad766..7c53b43b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Semantic Versioning where the repository publishes a release. - Added an hourly organization commercial-readiness coordinator that discovers writable repositories, honors enabled dedicated writer leases and fully paginated live writer runs, refetches exact repository/workflow/run/PR state before dispatch, rotates bounded review-repair and opt-in NVIDIA OpenCode product-development targets, fails nonzero on fleet-wide inspection or dispatch outages, retains three-day JSON receipts, and keeps the existing 15-minute merge scheduler authoritative. - Added a dedicated OriginWeave hourly caller that invokes the product-neutral central scheduler with the exact repository, protected `main` branch, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, job-scoped OIDC, and only the established scheduler credentials. +- Added the CWL Ecosystem Integration Contract v1 with CloudEvents-profiled event and command envelopes, OpenAPI/AsyncAPI/JSON Schema compatibility rules, UUIDv7 identifiers, temporal and provenance semantics, purpose-bound PII dereferencing, and stdlib-only contract tests for cross-repository adoption. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. diff --git a/docs/doctoring/ecosystem-integration-standards.md b/docs/doctoring/ecosystem-integration-standards.md new file mode 100644 index 000000000..f9923b86e --- /dev/null +++ b/docs/doctoring/ecosystem-integration-standards.md @@ -0,0 +1,32 @@ +# Doctoring: CWL ecosystem integration standards + +This record supports the organization-level integration profile. Product-domain scientific decisions remain in the owning repository's doctoring/research documentation. + +## Design implications + +- **CloudEvents 1.0** provides a vendor-neutral event data model. CWL uses CloudEvents `specversion: "1.0"` and keeps organization metadata under the event `data` object so the profile is explicit and contract-testable. +- **OpenAPI 3.2.0** is the published OpenAPI baseline for new shared synchronous interfaces. +- **AsyncAPI 3.1.0** is the current published baseline for repositories that publish asynchronous channel contracts. +- **JSON Schema Draft 2020-12** is the baseline for shared JSON contracts. +- **RFC 9457** defines Problem Details for HTTP APIs and supersedes RFC 7807. +- **RFC 9562** defines UUIDs; UUIDv7 supplies a time-ordered Unix-millisecond layout appropriate for organization-level event/command/correlation identifiers when time-ordering is useful. +- **W3C Trace Context** defines stable cross-service tracing headers. The 2021 Recommendation is used as the production baseline rather than making a draft-level successor a hard dependency. The CWL v1 envelope profile pins `traceparent` version `00`, rejects the Recommendation's forbidden all-zero trace and parent identifiers, and accepts only flags `00` or `01` because senders MUST leave every reserved flag bit unset. +- **W3C PROV-O** supplies a standard vocabulary for provenance entities, activities, and agents when products expose provenance graphs. + +## APA 7th references + +AsyncAPI Initiative. (2026). *AsyncAPI specification (Version 3.1.0).* https://www.asyncapi.com/docs/reference/specification/v3.1.0 + +Cloud Native Computing Foundation. (2022). *CloudEvents specification (Version 1.0.2).* https://github.com/cloudevents/spec/tree/v1.0.2 + +Davis, K., Peabody, B., & Leach, P. (2024). *Universally unique identifiers (UUIDs)* (RFC 9562). Internet Engineering Task Force. https://doi.org/10.17487/RFC9562 + +Lebo, T., Sahoo, S., & McGuinness, D. (Eds.). (2013). *PROV-O: The PROV ontology.* World Wide Web Consortium. https://www.w3.org/TR/prov-o/ + +Nottingham, M., Wilde, E., & Dalal, S. (2023). *Problem details for HTTP APIs* (RFC 9457). Internet Engineering Task Force. https://doi.org/10.17487/RFC9457 + +OpenAPI Initiative. (2025). *OpenAPI specification (Version 3.2.0).* Linux Foundation. https://spec.openapis.org/oas/v3.2.0.html + +World Wide Web Consortium. (2021). *Trace Context.* https://www.w3.org/TR/trace-context/ + +Wright, A., Andrews, H., Hutton, B., & Dennis, G. (2022). *JSON Schema: A media type for describing JSON documents (Draft 2020-12).* JSON Schema. https://json-schema.org/draft/2020-12/json-schema-core diff --git a/docs/integration/CWL_ECOSYSTEM_INTEGRATION_CONTRACT.md b/docs/integration/CWL_ECOSYSTEM_INTEGRATION_CONTRACT.md new file mode 100644 index 000000000..0417afd27 --- /dev/null +++ b/docs/integration/CWL_ECOSYSTEM_INTEGRATION_CONTRACT.md @@ -0,0 +1,329 @@ +# CWL Ecosystem Integration Contract v1 + +Status: **Proposed organization contract** + +Owner: `ContextualWisdomLab/.github` + +Scope: interoperability rules shared by independently deployable ContextualWisdomLab products. Domain data models remain owned by the repository that owns the product responsibility. + +## 1. Purpose + +ContextualWisdomLab products must remain useful as standalone software while composing cleanly into a larger ecosystem. This contract defines the minimum API, event, command, provenance, identity-reference, temporal, privacy, and compatibility rules needed to connect independently versioned repositories without creating a distributed monolith. + +The central `.github` repository owns this organization-wide transport and evidence profile. Leaf repositories own their domain semantics, persistence, business authorization, and release lifecycle. + +## 2. Non-goals + +This contract does **not**: + +- make `.github` the source of truth for product-domain data; +- permit one service to read or mutate another service's application database directly; +- require every product to run as a network service; +- require raw personally identifiable information to be masked when an authorized workflow legitimately needs it; +- authorize an event or command merely because it validates against a schema; +- replace repository-local PRD, TRD, ADR, API, data model, security, or operability documentation; +- force experimental repositories to claim production maturity. + +## 3. Composition modes + +A CWL product may integrate through one or more of these modes: + +1. **In-process package** — small, stateless, versioned calculation or transformation libraries. +2. **Independent service** — products with their own persistence, security boundary, scaling model, or lifecycle. +3. **Offline scientific worker** — research, validation, simulation, model fitting, and parity or recovery work. +4. **Build and operations tool** — CI, SAST, schema, release, provenance, and governance tooling. + +A consumer must integrate through a versioned public interface. Git submodules and direct database access are not stable public interfaces. + +## 4. Authority and ownership + +### 4.1 Source-of-truth rule + +Each business fact has exactly one authoritative owner. Other products may keep explicitly non-authoritative projections or immutable references. + +Examples: + +- identity credentials and federation belong to the identity product; +- mailbox, calendar, or file originals remain with the customer/provider that owns them; +- a domain system of record owns its business facts; +- a psychometric calculation engine owns computation artifacts, not the host application's user lifecycle; +- a catalog owns ontology/catalog records, not another product's operational entities; +- inferred lineage remains inference and must not silently become an authoritative audit fact. + +### 4.2 No cross-service SQL + +Independent services **MUST NOT** read or mutate another product's application tables. They integrate through: + +- OpenAPI-described synchronous APIs; +- AsyncAPI-described channels where asynchronous messaging is used; +- CloudEvents-profiled domain events; +- explicitly versioned package interfaces; +- immutable artifact references. + +### 4.3 Persistence + +A service that owns durable state owns its migrations, backups, recovery, retention, and access controls. A consuming repository must not recreate a shadow authoritative copy merely to simplify integration. + +## 5. Synchronous API profile + +New shared HTTP APIs SHOULD use **OpenAPI 3.2.0**. + +API rules: + +- problem responses use **RFC 9457 Problem Details**; +- write operations use an `Idempotency-Key` where safe replay matters; +- distributed tracing propagates the stable W3C `traceparent`/`tracestate` contract; +- tenant, actor, purpose, and decision context are explicit and authorization-bound; +- authentication identifies the caller; it does not by itself authorize the requested domain action; +- opaque public identifiers are preferred over sequential database identifiers; +- API clients are generated or contract-tested from the owning service's published interface where practical. + +A synchronous request that changes state carries, either in authenticated request context, headers, or a versioned request body: + +```text +tenant_reference +actor_reference +purpose_code +decision_reference (when applicable) +idempotency_key +correlation_id +provenance_reference (when the request is evidence-backed) +``` + +## 6. Asynchronous event profile + +CWL domain events use the **CloudEvents 1.0** data model. The organization profile is machine-readable in `schemas/cwl-event-envelope-v1.schema.json`. + +The CloudEvents core attributes are the transport identity: + +```json +{ + "specversion": "1.0", + "id": "019c2d5d-f20a-7f8a-8d8e-4a5f6f5d5a41", + "source": "https://github.com/ContextualWisdomLab/example-product", + "type": "org.contextualwisdomlab.example.record.published.v1", + "subject": "record_reference:example_123", + "time": "2026-08-15T11:20:00Z", + "datacontenttype": "application/json", + "data": {} +} +``` + +CWL metadata lives under `data.metadata` rather than creating unconstrained top-level CloudEvents extensions. This gives the organization a versioned, contract-testable profile without redefining the CloudEvents data model. + +Required CWL metadata: + +```text +event_version +tenant_id +subject_reference +purpose_code +occurred_at +recorded_at +correlation_id +provenance_reference +data_classification +``` + +Optional but standardized metadata: + +```text +available_at +causation_id +decision_reference +actor_reference +traceparent +``` + +Semantic invariants that JSON Schema alone does not express: + +- CloudEvents `id` is the same logical identifier as the event receipt identifier and is a UUIDv7. +- CloudEvents `time` equals `data.metadata.occurred_at`. +- CloudEvents `subject` equals `data.metadata.subject_reference`. +- `recorded_at` is system-observation time; `occurred_at` is business/event time. +- `available_at`, when present, is when the evidence became usable by the receiving analytical or decision workflow. +- `causation_id`, when present, references the immediate predecessor command/event that caused this event. +- `correlation_id` remains stable across one business workflow. +- `provenance_reference` points to evidence or provenance owned by an authorized source, not an unverified free-text explanation. +- validation is necessary but never sufficient for authorization. + +## 7. Command profile + +Asynchronous commands use `schemas/cwl-command-envelope-v1.schema.json`. Commands are intent, not fact. + +A command includes: + +```text +command_id +command_type +command_version +tenant_id +actor_reference +subject_reference +purpose_code +requested_at +idempotency_key +correlation_id +provenance_reference +data_classification +payload +``` + +`causation_id`, `decision_reference`, and `traceparent` are standardized optional fields. + +Command handlers MUST: + +1. authenticate the caller; +2. authorize tenant, actor, subject, purpose, action, and resource; +3. enforce idempotency; +4. validate the current domain state; +5. execute or fail closed; +6. emit a domain event only after the authoritative state transition commits; +7. retain sufficient audit/provenance evidence for the owning product's policy. + +## 8. Temporal semantics + +CWL integrations distinguish at least: + +- **occurred_at** — when the business event happened or became valid; +- **recorded_at** — when the owning system recorded or observed it; +- **available_at** — when the evidence was available to a downstream analytical/decision workflow, when materially different. + +A temporal/analytical product MUST prevent future-information leakage by applying its own knowledge-cutoff rule to `available_at` rather than backdating later evidence to the historical event date. + +Products with stronger temporal requirements may add valid-time/system-time interval contracts in their own schema without redefining these shared meanings. + +## 9. Identity, PII, and privacy + +CWL does **not** adopt blanket PII masking as an interoperability strategy. + +Instead: + +- broadcast/event payloads default to opaque subject references and the minimum attributes required for the receiving purpose; +- a service with a legitimate purpose may dereference authorized PII through the authoritative API; +- authorization is purpose-bound, tenant-bound, resource-bound, and audited; +- raw credentials, session tokens, API keys, and authentication secrets never belong in domain events; +- raw PII in asynchronous payloads requires an explicit repository-local threat/privacy decision, topic ACLs, encryption, retention rules, and access/export logging; +- model traces receive only the minimum data needed for the approved operation; +- de-identification or pseudonymization is used where analytically sufficient, but never misrepresented as eliminating re-identification risk. + +## 10. Provenance and traceability + +W3C PROV-O is the semantic reference for representing provenance relationships where a graph representation is needed. The shared envelope uses `provenance_reference` so products can link to their own provenance store without copying the entire graph onto the message bus. + +The minimum provenance chain for a derived artifact SHOULD identify: + +```text +source evidence +→ transformation or computation +→ software/model version +→ output artifact +→ verification/review state +``` + +The provenance record must distinguish authoritative facts, inferred relationships, model judgments, and operator approvals. + +## 11. Schema and compatibility rules + +- JSON contracts use **JSON Schema Draft 2020-12**. +- Event transport follows CloudEvents **specversion `1.0`**. +- Async channel descriptions use **AsyncAPI 3.1.0** when a repository publishes asynchronous interfaces. +- Synchronous APIs use **OpenAPI 3.2.0** for new shared contracts unless a consumer compatibility constraint is documented. +- HTTP error payloads use **RFC 9457**. +- New organization-level event/command/correlation identifiers use **RFC 9562 UUIDv7** where time-ordered globally unique identifiers are beneficial. +- Stable distributed-trace propagation follows the W3C Trace Context Recommendation. The v1 JSON envelopes deliberately accept version `00` traceparents only, reject all-zero trace IDs and all-zero parent IDs, and accept only flags `00` or `01` because senders MUST set every reserved flag bit to zero. Future Trace Context versions require a new profile revision rather than silent widening. +- Breaking contracts require a new major version or a parallel endpoint/channel/schema; consumers are not silently migrated. +- Producers and consumers both maintain contract tests for interfaces they depend on. +- A producer does not declare a breaking interface safe solely because its own tests pass. +- Schema registry paths and artifact digests are immutable for released versions. + +## 12. Event type naming + +Event types use: + +```text +org.contextualwisdomlab....v +``` + +Example: + +```text +org.contextualwisdomlab.psychometrics.assessment_result.published.v1 +``` + +Commands use: + +```text +org.contextualwisdomlab....v +``` + +Example: + +```text +org.contextualwisdomlab.identity.account.provision.v1 +``` + +Each segment after `org.contextualwisdomlab` uses lowercase `snake_case`; empty segments, hyphenated segments, and version zero are invalid. + +Event names describe facts that have happened. Command names describe requested actions. + +## 13. Database projection rules + +Transport schemas are not database schemas. + +When a receiving service persists a projection: + +- its database remains normalized to at least third normal form for authoritative relational data unless an accepted ADR documents a deliberate read-model exception; +- owned database objects use descriptive `snake_case` names containing at least two words; +- external event identifiers, source references, and schema versions are stored separately from business-domain primary keys; +- raw transport envelopes may be retained only under an explicit audit/retention policy; +- a projection is labelled non-authoritative unless that service is the domain owner. + +## 14. Security and compliance evidence + +Integration features are designed for SOC 2 and CSAP evidence readiness without claiming certification. + +Every production integration SHOULD be able to demonstrate: + +- caller/service identity and authorization boundary; +- least-privilege credentials; +- data classification and purpose; +- encryption in transit and at rest where applicable; +- retention and deletion behavior; +- access/export auditability; +- schema/version compatibility evidence; +- dependency/SBOM/provenance evidence; +- retry, duplicate, timeout, and degraded-mode behavior; +- rollback/recovery procedure; +- exact-source release evidence. + +## 15. Repository adoption sequence + +A leaf repository adopting v1 should: + +1. document its bounded context and source-of-truth ownership; +2. identify existing inbound/outbound APIs, commands, events, packages, and artifacts; +3. map them to the v1 envelopes without changing domain ownership; +4. add provider and consumer contract tests; +5. add OpenAPI/AsyncAPI/JSON Schema artifacts only for interfaces it actually implements; +6. document PII classification and dereference policy; +7. document degraded, retry, idempotency, recovery, and version-migration behavior; +8. update its ADR/Architecture/Traceability/CHANGELOG; +9. roll out one bounded integration at a time. + +## 16. Organization-level acceptance + +An inter-repository integration is not complete until evidence exists for the same released contract version on both sides: + +```text +producer schema ++ producer test ++ consumer test ++ authorization test ++ idempotency/retry test ++ provenance evidence ++ degraded/recovery test ++ exact released artifact identities +``` + +Queued, skipped, stale-head, predecessor-version, or model-only evidence does not satisfy the contract. diff --git a/docs/integration/adr/0001-cwl-inter-repository-contract.md b/docs/integration/adr/0001-cwl-inter-repository-contract.md new file mode 100644 index 000000000..e532153af --- /dev/null +++ b/docs/integration/adr/0001-cwl-inter-repository-contract.md @@ -0,0 +1,106 @@ +# ADR-0001: Organization-wide inter-repository integration contract + +- Status: Proposed +- Date: 2026-08-15 +- Decision owner: ContextualWisdomLab organization control plane +- Scope: Cross-repository public interfaces + +## Context + +ContextualWisdomLab contains independently useful products with different ownership, persistence, release, and security boundaries. Without a shared integration contract, the ecosystem can drift toward direct database coupling, duplicated domain truth, inconsistent event metadata, non-auditable model handoffs, and incompatible identity/tenant/time semantics. + +The repository name alone is not an architectural boundary. The correct integration owner is determined by product responsibility, source-of-truth ownership, reuse boundary, existing implementation, and consuming repositories. + +## Considered options + +### A. Copy shared domain models into every repository + +Rejected. This creates divergent truth, synchronized release requirements, and a distributed monolith. + +### B. Make every reusable component an HTTP microservice + +Rejected. Stateless libraries and scientific kernels do not gain value from forced network boundaries, and the result would add failure domains and operational cost. + +### C. Federated composition with a shared transport/evidence profile + +Accepted. + +- stateless libraries may remain packages; +- products with independent state/lifecycle remain services; +- scientific validation may run as offline workers; +- build/security tooling remains tooling; +- cross-service interfaces use versioned API/event/command/artifact contracts; +- domain ownership never moves merely because another repository consumes a fact. + +## Decision + +Adopt `docs/integration/CWL_ECOSYSTEM_INTEGRATION_CONTRACT.md` and the v1 JSON Schemas as the organization profile. + +The profile uses: + +- OpenAPI 3.2.0 for new shared synchronous APIs; +- AsyncAPI 3.1.0 for published asynchronous channel contracts; +- CloudEvents 1.0 for domain-event transport; +- JSON Schema Draft 2020-12 for JSON payload contracts; +- RFC 9457 for HTTP problem details; +- RFC 9562 UUIDv7 for organization-level time-ordered identifiers where appropriate; +- W3C Trace Context Recommendation version `00` grammar for the v1 envelope, rejecting forbidden all-zero trace and parent identifiers and requiring every reserved trace-flag bit to remain zero; +- W3C PROV-O as the provenance semantic reference. + +CWL-specific metadata is placed inside `data.metadata` for domain events rather than inventing unrestricted top-level CloudEvents extensions. + +## Consequences + +### Positive + +- standalone products remain independently deployable; +- consumers can rely on shared tenant, purpose, causation, temporal, and provenance semantics; +- raw PII need not be broadcast merely to connect services; +- authorized services may still retrieve necessary PII from the authoritative owner; +- provider and consumer contract tests become a concrete merge/release gate; +- ecosystem diagrams and connectors can be generated from public contracts instead of repository-name assumptions. + +### Costs + +- producers must publish and version interfaces; +- consumers must maintain contract tests; +- adapters are required for legacy interfaces; +- some duplicated metadata is deliberate, such as CloudEvents `time`/`subject` mirrored by explicit CWL business metadata, and implementations must test semantic equality. + +## Failure and recovery + +If a producer emits a non-conforming or unsupported major version, consumers fail closed or quarantine the message according to their local operability contract. They must not coerce unknown fields into an older semantic meaning. + +Rollback keeps the prior compatible interface available until all required consumers have migrated or a documented compatibility window ends. + +## Security and governance impact + +Schema validation is not authorization. Services still enforce tenant, actor, purpose, subject, action, resource, and credential policy. + +Event buses default to opaque references and data minimization. Raw PII may be processed under a legitimate authorized purpose, but its asynchronous replication requires explicit security/privacy controls. + +Direct cross-service application-database access is prohibited. + +## Verification + +The central repository includes stdlib-only contract tests that validate: + +- required profile artifacts exist and are parseable; +- complete positive examples satisfy the supported JSON Schema subset used by the v1 profiles; +- unknown top-level properties, invalid trace identifiers, and nonzero reserved trace flags fail closed; +- CloudEvents `specversion` is pinned to `1.0`; +- JSON Schemas use Draft 2020-12; +- examples contain the required CWL metadata; +- UUIDv7, bounded event/command type names, and W3C Trace Context version `00` constraints are enforced; +- documentation names the current authoritative external standards. + +Leaf repositories add producer/consumer contract tests when they adopt the profile. Before release, published schemas are also checked with a conforming JSON Schema Draft 2020-12 implementation in the release pipeline. + +## Supersession + +A future ADR may supersede this decision only with: + +- explicit migration and rollback rules; +- compatibility impact on existing consumers; +- updated machine-readable schemas; +- updated standards/doctoring evidence. diff --git a/schemas/cwl-command-envelope-v1.schema.json b/schemas/cwl-command-envelope-v1.schema.json new file mode 100644 index 000000000..f6ec14da8 --- /dev/null +++ b/schemas/cwl-command-envelope-v1.schema.json @@ -0,0 +1,98 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contextualwisdomlab.github.io/schemas/cwl-command-envelope-v1.schema.json", + "title": "CWL Command Envelope v1", + "description": "ContextualWisdomLab profile for asynchronous commands.", + "type": "object", + "additionalProperties": false, + "required": [ + "command_id", + "command_type", + "command_version", + "tenant_id", + "actor_reference", + "subject_reference", + "purpose_code", + "requested_at", + "idempotency_key", + "correlation_id", + "provenance_reference", + "data_classification", + "payload" + ], + "properties": { + "command_id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "command_type": { + "type": "string", + "pattern": "^org\\.contextualwisdomlab\\.[a-z0-9]+(?:_[a-z0-9]+)*(?:\\.[a-z0-9]+(?:_[a-z0-9]+)*){2,}\\.v[1-9][0-9]*$", + "maxLength": 255 + }, + "command_version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "maxLength": 32 + }, + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "actor_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "subject_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "purpose_code": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{1,63}$" + }, + "requested_at": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" + }, + "idempotency_key": { + "type": "string", + "minLength": 16, + "maxLength": 256 + }, + "correlation_id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "causation_id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "decision_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "provenance_reference": { + "type": "string", + "format": "uri-reference", + "minLength": 1, + "maxLength": 2048 + }, + "data_classification": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{1,63}$" + }, + "traceparent": { + "type": "string", + "pattern": "^00-(?!0{32}-)[0-9a-f]{32}-(?!0{16}-)[0-9a-f]{16}-0[01]$" + }, + "payload": { + "type": "object" + } + } +} diff --git a/schemas/cwl-event-envelope-v1.schema.json b/schemas/cwl-event-envelope-v1.schema.json new file mode 100644 index 000000000..22c9d1069 --- /dev/null +++ b/schemas/cwl-event-envelope-v1.schema.json @@ -0,0 +1,152 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://contextualwisdomlab.github.io/schemas/cwl-event-envelope-v1.schema.json", + "title": "CWL Event Envelope v1", + "description": "ContextualWisdomLab profile for CloudEvents 1.0 domain events.", + "type": "object", + "additionalProperties": false, + "required": [ + "specversion", + "id", + "source", + "type", + "subject", + "time", + "datacontenttype", + "data" + ], + "properties": { + "specversion": { + "const": "1.0" + }, + "id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "source": { + "type": "string", + "format": "uri-reference", + "minLength": 1, + "maxLength": 2048 + }, + "type": { + "type": "string", + "pattern": "^org\\.contextualwisdomlab\\.[a-z0-9]+(?:_[a-z0-9]+)*(?:\\.[a-z0-9]+(?:_[a-z0-9]+)*){2,}\\.v[1-9][0-9]*$", + "maxLength": 255 + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "time": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" + }, + "datacontenttype": { + "const": "application/json" + }, + "dataschema": { + "type": "string", + "format": "uri-reference", + "maxLength": 2048 + }, + "data": { + "type": "object", + "additionalProperties": false, + "required": [ + "metadata", + "payload" + ], + "properties": { + "metadata": { + "type": "object", + "additionalProperties": false, + "required": [ + "event_version", + "tenant_id", + "subject_reference", + "purpose_code", + "occurred_at", + "recorded_at", + "correlation_id", + "provenance_reference", + "data_classification" + ], + "properties": { + "event_version": { + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$", + "maxLength": 32 + }, + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "subject_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "purpose_code": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{1,63}$" + }, + "occurred_at": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" + }, + "recorded_at": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" + }, + "available_at": { + "type": "string", + "format": "date-time", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$" + }, + "correlation_id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "causation_id": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "decision_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "actor_reference": { + "type": "string", + "minLength": 1, + "maxLength": 512 + }, + "provenance_reference": { + "type": "string", + "format": "uri-reference", + "minLength": 1, + "maxLength": 2048 + }, + "data_classification": { + "type": "string", + "pattern": "^[a-z][a-z0-9_]{1,63}$" + }, + "traceparent": { + "type": "string", + "pattern": "^00-(?!0{32}-)[0-9a-f]{32}-(?!0{16}-)[0-9a-f]{16}-0[01]$" + } + } + }, + "payload": { + "type": "object" + } + } + } + } +} diff --git a/schemas/examples/cwl-command-envelope-v1.example.json b/schemas/examples/cwl-command-envelope-v1.example.json new file mode 100644 index 000000000..23d3a98f8 --- /dev/null +++ b/schemas/examples/cwl-command-envelope-v1.example.json @@ -0,0 +1,20 @@ +{ + "command_id": "019c2d5d-f20a-7a8a-8d8e-4a5f6f5d5a50", + "command_type": "org.contextualwisdomlab.identity.account.provision.v1", + "command_version": "1.0.0", + "tenant_id": "tenant:example", + "actor_reference": "worker:opaque_4J8V", + "subject_reference": "person:opaque_9AT2", + "purpose_code": "workforce_provisioning", + "requested_at": "2026-08-15T11:21:00Z", + "idempotency_key": "provision-person-opaque_9AT2-20260815", + "correlation_id": "019c2d5d-f20a-7d8a-9d8e-4a5f6f5d5a42", + "causation_id": "019c2d5d-f20a-7f8a-8d8e-4a5f6f5d5a41", + "decision_reference": "employment_decision:opaque_4NK1", + "provenance_reference": "urn:cwl:provenance:prov_4NN2", + "data_classification": "restricted_hr", + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "payload": { + "requested_capability": "organization_access" + } +} diff --git a/schemas/examples/cwl-event-envelope-v1.example.json b/schemas/examples/cwl-event-envelope-v1.example.json new file mode 100644 index 000000000..5c5c38c72 --- /dev/null +++ b/schemas/examples/cwl-event-envelope-v1.example.json @@ -0,0 +1,31 @@ +{ + "specversion": "1.0", + "id": "019c2d5d-f20a-7f8a-8d8e-4a5f6f5d5a41", + "source": "https://github.com/ContextualWisdomLab/psychometrics-commons", + "type": "org.contextualwisdomlab.psychometrics.assessment_result.published.v1", + "subject": "assessment_result:result_7YQ9T", + "time": "2026-08-15T11:20:00Z", + "datacontenttype": "application/json", + "dataschema": "https://example.invalid/schemas/assessment-result-v1.schema.json", + "data": { + "metadata": { + "event_version": "1.0.0", + "tenant_id": "tenant:example", + "subject_reference": "assessment_result:result_7YQ9T", + "purpose_code": "selection_validation", + "occurred_at": "2026-08-15T11:20:00Z", + "recorded_at": "2026-08-15T11:20:01Z", + "available_at": "2026-08-15T11:20:01Z", + "correlation_id": "019c2d5d-f20a-7d8a-9d8e-4a5f6f5d5a42", + "causation_id": "019c2d5d-f20a-7c8a-8d8e-4a5f6f5d5a40", + "actor_reference": "worker:opaque_4J8V", + "provenance_reference": "urn:cwl:provenance:prov_3NK5", + "data_classification": "restricted_hr", + "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + }, + "payload": { + "result_reference": "assessment_result:result_7YQ9T", + "instrument_version": "1.4.0" + } + } +} diff --git a/tests/test_cwl_integration_contract.py b/tests/test_cwl_integration_contract.py new file mode 100644 index 000000000..32b8e6e06 --- /dev/null +++ b/tests/test_cwl_integration_contract.py @@ -0,0 +1,313 @@ +"""Contract tests for the CWL organization integration profile. + +These tests intentionally use only the Python standard library so that the +central contract can be verified without adding a runtime dependency. +""" + +from __future__ import annotations + +import copy +import json +import re +from datetime import datetime, timezone +from pathlib import Path +from urllib.parse import urlsplit + +ROOT = Path(__file__).resolve().parents[1] +EVENT_SCHEMA = ROOT / "schemas" / "cwl-event-envelope-v1.schema.json" +COMMAND_SCHEMA = ROOT / "schemas" / "cwl-command-envelope-v1.schema.json" +EVENT_EXAMPLE = ROOT / "schemas" / "examples" / "cwl-event-envelope-v1.example.json" +COMMAND_EXAMPLE = ROOT / "schemas" / "examples" / "cwl-command-envelope-v1.example.json" +CONTRACT = ROOT / "docs" / "integration" / "CWL_ECOSYSTEM_INTEGRATION_CONTRACT.md" +DOCTORING = ROOT / "docs" / "doctoring" / "ecosystem-integration-standards.md" +RFC3339_DATE_TIME = re.compile( + r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?" + r"(?:Z|[+-]\d{2}:\d{2})$" +) +RFC3339_LEAP_SECOND_DATES = frozenset( + { + "1972-06-30", + "1972-12-31", + "1973-12-31", + "1974-12-31", + "1975-12-31", + "1976-12-31", + "1977-12-31", + "1978-12-31", + "1979-12-31", + "1981-06-30", + "1982-06-30", + "1983-06-30", + "1985-06-30", + "1987-12-31", + "1989-12-31", + "1990-12-31", + "1992-06-30", + "1993-06-30", + "1994-06-30", + "1995-12-31", + "1997-06-30", + "1998-12-31", + "2005-12-31", + "2008-12-31", + "2012-06-30", + "2015-06-30", + "2016-12-31", + } +) + + +def _load(path: Path) -> dict: + """Load a JSON contract fixture from *path*.""" + + return json.loads(path.read_text(encoding="utf-8")) + + +def _assert_uri_reference(value: str, path: str) -> None: + """Assert that *value* is a bounded URI-reference without whitespace.""" + + assert not any(character.isspace() for character in value), path + parsed = urlsplit(value) + assert parsed.scheme or parsed.path, path + + +def _assert_date_time(value: str, path: str) -> None: + """Assert strict RFC 3339 lexical form plus a real offset-aware instant.""" + + assert RFC3339_DATE_TIME.fullmatch(value), path + normalized = value[:-1] + "+00:00" if value.endswith("Z") else value + if normalized[17:19] == "60": + parsed = datetime.fromisoformat(normalized[:17] + "59" + normalized[19:]) + utc = parsed.astimezone(timezone.utc) + assert ( + utc.hour == 23 + and utc.minute == 59 + and utc.date().isoformat() in RFC3339_LEAP_SECOND_DATES + ), path + return + parsed = datetime.fromisoformat(normalized) + assert parsed.tzinfo is not None, path + + +def _assert_profile_instance(instance: object, schema: dict, path: str = "$") -> None: + """Validate the JSON Schema subset used by the two CWL v1 envelopes.""" + + if "const" in schema: + assert instance == schema["const"], path + + expected_type = schema.get("type") + if expected_type == "object": + assert isinstance(instance, dict), path + properties = schema.get("properties", {}) + required = set(schema.get("required", ())) + assert required <= set(instance), path + if schema.get("additionalProperties") is False: + assert set(instance) <= set(properties), path + for name, value in instance.items(): + if name in properties: + _assert_profile_instance(value, properties[name], f"{path}.{name}") + return + + if expected_type == "string": + assert isinstance(instance, str), path + assert len(instance) >= schema.get("minLength", 0), path + if "maxLength" in schema: + assert len(instance) <= schema["maxLength"], path + if "pattern" in schema: + assert re.fullmatch(schema["pattern"], instance), path + if schema.get("format") == "date-time": + _assert_date_time(instance, path) + elif schema.get("format") == "uri-reference": + _assert_uri_reference(instance, path) + + +def _assert_invalid(instance: object, schema: dict) -> None: + """Assert that *instance* is rejected by the CWL profile validator.""" + + try: + _assert_profile_instance(instance, schema) + except (AssertionError, ValueError): + return + raise AssertionError("profile validator unexpectedly accepted invalid instance") + + +def _traceparent_schema(schema: dict) -> dict: + """Return the traceparent property from either envelope schema.""" + + if "traceparent" in schema["properties"]: + return schema["properties"]["traceparent"] + return schema["properties"]["data"]["properties"]["metadata"]["properties"]["traceparent"] + + +def test_shared_schemas_use_json_schema_2020_12() -> None: + """Shared JSON schemas must remain on the organization Draft 2020-12 baseline.""" + + for path in (EVENT_SCHEMA, COMMAND_SCHEMA): + schema = _load(path) + assert schema["$schema"] == "https://json-schema.org/draft/2020-12/schema" + assert schema["$id"].startswith("https://contextualwisdomlab.github.io/schemas/") + + +def test_examples_validate_against_declared_profiles() -> None: + """The published positive examples must satisfy their complete CWL profiles.""" + + _assert_profile_instance(_load(EVENT_EXAMPLE), _load(EVENT_SCHEMA)) + _assert_profile_instance(_load(COMMAND_EXAMPLE), _load(COMMAND_SCHEMA)) + + +def test_event_profile_pins_cloudevents_1_0_and_uuidv7() -> None: + """The event profile must bind CloudEvents 1.0 and UUIDv7 identifiers.""" + + schema = _load(EVENT_SCHEMA) + example = _load(EVENT_EXAMPLE) + + assert schema["properties"]["specversion"]["const"] == "1.0" + assert example["specversion"] == "1.0" + uuid_pattern = schema["properties"]["id"]["pattern"] + assert re.fullmatch(uuid_pattern, example["id"]) + assert example["datacontenttype"] == "application/json" + + +def test_event_example_contains_required_cwl_metadata() -> None: + """The event example must cover every required CWL metadata field.""" + + schema = _load(EVENT_SCHEMA) + example = _load(EVENT_EXAMPLE) + metadata_schema = schema["properties"]["data"]["properties"]["metadata"] + metadata = example["data"]["metadata"] + + assert set(metadata_schema["required"]) <= set(metadata) + assert example["time"] == metadata["occurred_at"] + assert example["subject"] == metadata["subject_reference"] + assert metadata["tenant_id"] + assert metadata["purpose_code"] + assert metadata["provenance_reference"] + + +def test_command_example_contains_required_control_context() -> None: + """Commands must include authorization context, idempotency, and provenance.""" + + schema = _load(COMMAND_SCHEMA) + example = _load(COMMAND_EXAMPLE) + + assert set(schema["required"]) <= set(example) + assert len(example["idempotency_key"]) >= 16 + for name in ("command_id", "correlation_id", "causation_id"): + assert re.fullmatch(schema["properties"][name]["pattern"], example[name]) + + +def test_traceparent_profile_rejects_forbidden_identifiers_and_flags() -> None: + """Trace Context v1 must reject invalid IDs and nonzero reserved flag bits.""" + + valid = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + invalid = ( + "ff-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", + "00-00000000000000000000000000000000-00f067aa0ba902b7-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-0000000000000000-01", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-02", + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-ff", + ) + + for schema_path in (EVENT_SCHEMA, COMMAND_SCHEMA): + pattern = _traceparent_schema(_load(schema_path))["pattern"] + assert re.fullmatch(pattern, valid) + assert all(re.fullmatch(pattern, candidate) is None for candidate in invalid) + + +def test_type_profile_rejects_empty_or_punctuated_segments() -> None: + """Event and command types must use non-empty lowercase snake-case segments.""" + + valid = "org.contextualwisdomlab.identity.account.provision.v1" + invalid = ( + "org.contextualwisdomlab.identity..provision.v1", + "org.contextualwisdomlab.identity.account-.provision.v1", + "org.contextualwisdomlab.identity.account.provision.v0", + ) + + for schema_path, property_name in ( + (EVENT_SCHEMA, "type"), + (COMMAND_SCHEMA, "command_type"), + ): + pattern = _load(schema_path)["properties"][property_name]["pattern"] + assert re.fullmatch(pattern, valid) + assert all(re.fullmatch(pattern, candidate) is None for candidate in invalid) + + +def test_profiles_reject_unknown_properties_and_invalid_trace_context() -> None: + """Negative examples must fail closed rather than drift beyond v1.""" + + event_schema = _load(EVENT_SCHEMA) + event = _load(EVENT_EXAMPLE) + event["unexpected"] = True + _assert_invalid(event, event_schema) + + command_schema = _load(COMMAND_SCHEMA) + command = copy.deepcopy(_load(COMMAND_EXAMPLE)) + command["traceparent"] = ( + "00-00000000000000000000000000000000-00f067aa0ba902b7-01" + ) + _assert_invalid(command, command_schema) + + +def test_date_time_profile_rejects_non_rfc3339_lexical_forms() -> None: + """All event and command timestamps reject broad ISO 8601 alternatives.""" + + event_schema = _load(EVENT_SCHEMA) + command_schema = _load(COMMAND_SCHEMA) + + event = copy.deepcopy(_load(EVENT_EXAMPLE)) + event["time"] = "2026-08-15 10:00:00+00:00" + _assert_invalid(event, event_schema) + + metadata_fields = ("occurred_at", "recorded_at", "available_at") + for field_name in metadata_fields: + candidate = copy.deepcopy(_load(EVENT_EXAMPLE)) + candidate["data"]["metadata"][field_name] = "2026-08-15 10:00:00+00:00" + _assert_invalid(candidate, event_schema) + + command = copy.deepcopy(_load(COMMAND_EXAMPLE)) + command["requested_at"] = "2026-08-15 10:00:00+00:00" + _assert_invalid(command, command_schema) + + for value in ( + "2026-08-15T10:00:00Z", + "2026-08-15T10:00:00.123456789+09:00", + ): + _assert_date_time(value, "$.timestamp") + + +def test_date_time_profile_handles_rfc3339_leap_seconds() -> None: + """Accept an announced leap second and reject impossible placements.""" + + _assert_date_time("2016-12-31T23:59:60Z", "$.timestamp") + _assert_date_time("2017-01-01T00:59:60+01:00", "$.timestamp") + for value in ( + "2016-12-31T23:58:60Z", + "2017-01-01T00:00:60Z", + "2017-01-01T00:59:60Z", + ): + try: + _assert_date_time(value, "$.timestamp") + except (AssertionError, ValueError): + continue + raise AssertionError(f"impossible leap second accepted: {value}") + + +def test_documented_external_contract_baselines_are_present() -> None: + """The canonical contract and doctoring file must name every pinned baseline.""" + + contract_text = CONTRACT.read_text(encoding="utf-8") + doctoring_text = DOCTORING.read_text(encoding="utf-8") + + for token in ( + "OpenAPI 3.2.0", + "AsyncAPI 3.1.0", + "CloudEvents 1.0", + "JSON Schema Draft 2020-12", + "RFC 9457", + "RFC 9562", + "W3C Trace Context", + "W3C PROV-O", + ): + assert token in contract_text + assert token in doctoring_text